Search code examples
javascriptfirefox

How can one avoid the 5-second "lack of user activation" timeout for the file picker in Firefox?


I'm trying to avoid the dreaded " picker was blocked due to lack of user activation" error when clicking on an <input type="file"> element. I know you think you're seen this question before many times, but wait. This one is different.

Most/all other questions are about dispatching a click event on a file input element programmatically (.click()), but having it fail to open a picker for lack of user activation (i.e., a physical click).

In JavaScript can I make a "click" event fire programmatically for a file input element?

How can I programmatically open the file picker with JavaScript?

Firefox can't override "<input> picker was blocked due to lack of user activation" in about:config

`<input>` picker was blocked due to lack of user activation when programmatically trying to open File Load dialog

This question involves physically clicking the file input element, but checking interactively if the user really wants to select a file now.

  1. The user clicks a button (or something that looks like a button) to load a file.
  2. If there are unsaved changes, the program prompts the user with a warning. The user can cancel the load.
  3. If not cancelled, the program opens a file picker. The user selects a file.
  4. The program replaces the current file buffer contents with the selected file.

I've implemented (a simplified version of) that ideal flow with the first "button" below. However, there's a problem. If the user waits more than 5 seconds to decide that it's okay to load the file, the picker refuses to open with the "lack of user activation" error logged in the console.

Less ideally, I've implemented an alternate flow with the second "button" below.

  1. The user clicks a button (or something that looks like a button) to load a file.
  2. The program opens a file picker. The user selects a file.
  3. If there are unsaved changes, the program prompts the user with a warning. The user can cancel the load.
  4. If not cancelled, the program replaces the current file buffer contents with the selected file.

( function () {
  'use strict';

  function onChange1( e ) {
    alert( 'contents overwritten' );
  }

  function onClick1( e ) {
    if (
      !confirm(
        'There are unsaved changes.\n' +
        'Is it OK to discard them?' )
    ) {
      e.preventDefault();
    }
  }

  function onChange2() {
    if (
      confirm(
        'There are unsaved changes.\n' +
        'Is it OK to discard them?' )
    ) {
      alert( 'contents overwritten' );
    }
  }

  function onContentLoaded() {
    var
      input1 = document.getElementById( 'input1' ),
      input2 = document.getElementById( 'input2' );

    input1.addEventListener( 'click', onClick1 );
    input1.addEventListener( 'change', onChange1 );

    input2.addEventListener( 'change', onChange2 );
  }
  document.addEventListener( 'DOMContentLoaded', onContentLoaded, { once: true } );
} () );
div {
  background-color: #eee;
  display: inline-block;
}
input {
  display: none;
}
label {
  line-height: 2;
  padding: 0.5em;
}
<!DOCTYPE html><html lang="en"><head>
  <title>Stack Overflow QA</title>
  <link rel="stylesheet" href="page.css">
  <script src="page.js"></script>
</head><body>
  <div><label><input id="input1" type="file">Open File 1 ...</label></div>
  <div><label><input id="input2" type="file">Open File 2 ...</label></div>
</body></html>

I really prefer the first flow, so I'd like to find a way around the 5-second timeout. I'm specifically interested in a Firefox solution, even if it might apply to other browsers, as well.


Solution

  • Thanks to @GuyIncognito's comment, here's my (minimal) solution.

    The OK of the confirmation dialog is the user activation which is passed to the file picker. This solution follows the desired flow in the question.

    ( function () {
      'use strict';
    
      function onChange() {
        // just a status update, nothing real
        alert( 'contents overwritten' );
      }
    
      function onClickDialog( e ) {
        if ( e.target.id === 'OK' ) {
          // pass the click along
          document.getElementById( 'input' ).click();
        }
        // re-hide the dialog
        document.getElementById( 'modal' ).classList = 'hidden';
      }
    
      function onClickFile() {
        // unhide the dialog
        document.getElementById( 'modal' ).classList = '';
      }
    
      function onContentLoaded() {
        document.getElementById( 'button' ).addEventListener( 'click', onClickFile );
        document.getElementById( 'OK' ).addEventListener( 'click', onClickDialog );
        document.getElementById( 'Cancel' ).addEventListener( 'click', onClickDialog );
        document.getElementById( 'input' ).addEventListener( 'change', onChange )
      }
      document.addEventListener( 'DOMContentLoaded', onContentLoaded, { once: true } );
    } () );
    button {
      font-size: inherit;
    }
    input,
    .hidden {
      display: none;
    }
    #modal {
      background-color: #0001;
      height: 100%;
      left: 0;
      position: absolute;
      top: 0;
      width: 100%;
    }
    #dialog {
      background-color: #fff;
      display: inline-block;
      left: 50%;
      padding: 1em;
      position: relative;
      top:50%;
      transform: translate( -50%, -50% );
    }
    <!DOCTYPE html><html lang="en"><head>
      <title>Stack Overflow QA</title>
      <link rel="stylesheet" href="page.css">
      <script src="page.js"></script>
    </head><body>
      <div id="modal" class="hidden"><div id="dialog">
        <p>There are unsaved changes.</p>
        <p>Is it OK to discard them?</p>
        <button id="OK">OK</button> <button id="Cancel">Cancel</button>
      </div></div>
      <button id="button">Open File ...</button>
      <input id="input" type="file">
    </body></html>