Chrome、Edge で <input type="file"> の Cancel イベントがキャンセルできない

<dialog>要素内の <input type="file"> のファイル選択ウインドウでキャンセルをクリックすると、ファイル選択ウインドウだけでなく、親の <dialog> 要素まで閉じられてしまいました。

やりたいこと

モーダルの中でファイル選択ができるように HTML で <dialog> 要素の中に <input type="file"> 要素を配置したかったのですが、 Chromium ブラウザ (Chrome、Edge) で想定外の事象が起こりました。 <input type="file"> をクリックし、表示されるファイル選択ウインドウでキャンセルをクリックすると、ファイル選択ウインドウだけでなく、親の <dialog> 要素まで閉じられてしまいました。

環境

OS
Microsoft Windows 11 22H2
ブラウザ
Google Chrome 113.0.5672.127, Microsoft Edge 113.0.1774.42

事象

以下のような HTML を用意します。 <dialog> 要素の中に <input type="file"> 要素が含まれていて <dialog> 要素を表示・非表示するためのボタンを用意しています。

<button id="open_modal" type="button">モーダルを開く</button>
<dialog>
  <input type="file" />
  <button id="close_modal" type="button">モーダルを閉じる</button>
</dialog>

<script>
  document.querySelector("#open_modal").addEventListener("click", () => {
    document.querySelector("dialog").showModal();
  });

  document.querySelector("#close_modal").addEventListener("click", () => {
    document.querySelector("dialog").close();
  });
</script>

上記 HTML を Chrome または Edge で開き、「モーダルを開く」ボタンを押してモーダルを表示します。 モーダルを表示

次に「ファイルを選択」 (Chrome の場合。Edge では「ファイルの選択」)ボタンを押して、ファイル選択ウインドウを表示させます。 ファイル選択ウインドウ

ファイル選択ウインドウの「キャンセル」ボタンを押してファイル選択ウインドウを閉じます。するとモーダルも一緒に閉じられてしまいます。 モーダルが閉じる

同じ操作を FireFox で行うと問題なくファイル選択ウインドウのみが閉じられます。

調査

しばらく考えて <input type="file"> はファイル選択ウインドウで「キャンセル」ボタンを押されたときに何らかのイベントをディスパッチするのでは?と考えが至りました。そしてそのイベントがバブリングされ <dialog> がそのイベントをリッスンして <dialog> 自体も閉じてしまうのではと推測しました。

MDN で HTMLDialogElement インターフェースを調べると HTMLDialogElement: cancel イベント - Web API | MDN が見つかりました。

<input type="file">cancel イベントをディスパッチするのではと思って MDN を調べましたが見当たらなかったのでとりあえず試してみます。

先ほどの HTML の JavaScript に <input type="file">cancel イベントをリッスンするリスナーを追加します。

<script>
  document.querySelector("#open_modal").addEventListener("click", () => {
    document.querySelector("dialog").showModal();
  });

  document.querySelector("#close_modal").addEventListener("click", () => {
    document.querySelector("dialog").close();
  });

  document.querySelector("input[type='file']").addEventListener("cancel", (e) => {
    console.dir(e);
  });
</script>

同じ操作を行ってみるとばっちりイベントを捕まえられました。

{
  isTrusted: true,
  bubbles: true,
  cancelBubble: false
  cancelable: false,
  composed: false,
  currentTarget: null,
  defaultPrevented: false,
  eventPhase: 0,
  returnValue: true,
  srcElement: input,
  target: input,
  timeStamp: 6548.400000095367,
  type: "cancel",
}

そして MDN では見つけられなかったものの本家 HTML Living Standard では <input type="file">cancel イベントを発火すると書かれていました。

Fired at dialog elements when they are canceled by the user (e.g., by pressing the Escape key), or at input elements in the File state when the user does not change their selection - HTML Standard

さて次に <dialog> のほうでもリッスンしてみます。

<script>
  document.querySelector("#open_modal").addEventListener("click", () => {
    document.querySelector("dialog").showModal();
  });

  document.querySelector("#close_modal").addEventListener("click", () => {
    document.querySelector("dialog").close();
  });

  /*document.querySelector("input[type='file']").addEventListener("cancel", (e) => {
    console.dir(e);
  });*/

  document.querySelector("dialog").addEventListener("cancel", (e) => {
    console.dir(e);
  });
</script>

同じ操作を行うと、以下のイベントを捕まえました。やはりバブリングしてきているようです。

{
  isTrusted: true,
  bubbles: true,
  cancelBubble: false
  cancelable: false,
  composed: false,
  currentTarget: null,
  defaultPrevented: false,
  eventPhase: 0,
  returnValue: true,
  srcElement: input,
  target: input,
  timeStamp: 17014.199999809265,
  type: "cancel",
}

ただ FireFox では同様にイベントはバブリングしてきていましたが、<dialog> は開いたままです。つまり Chromium 系ブラウザでは <dialog> は子要素の cancel イベントをリッスンして自身のキャンセル処理を実施しているように見えます。

Chromium 系ブラウザのバグと言ってよい気がしますが、とりあえず対処するには <input type="file">cancel イベントのバブリングを止めれば良さそうです。というわけで止めてみます。 stopPropagation() のみで大丈夫なはずですが、stopImmediatePropagation() もつけておきます。

<script>
  document.querySelector("#open_modal").addEventListener("click", () => {
    document.querySelector("dialog").showModal();
  });

  document.querySelector("#close_modal").addEventListener("click", () => {
    document.querySelector("dialog").close();
  });

  document.querySelector("input[type='file']").addEventListener("cancel", (e) => {
    e.stopPropagation();
    e.stopImmediatePropagation();
    console.dir(e);
  });

  document.querySelector("dialog").addEventListener("cancel", (e) => {
    console.dir(e);
  });
</script>

同じ操作を行います…。ダメでした。しかも <dialog> につけたほうの console.dir(e) は出力されていないのでバブリングは止まっているはずです。そうするともうイベント以外の世界で何かが起こっているので対処のしようがなさそうです…。

そしてここまで来て Chromium Bug Trucker を検索してみると直近でバグがあがっていました。1446591 - chromium - An open-source project to help move the web forward. - Monorail

暫定対応

さて Chromium のバグ修正はされると期待するとして目の前の問題をどうするかです。 <dialog> の中に <input type="file"> を置けないわけなので、<dialog> を諦めるか <input type="file"> を諦めるかです。 ブラウザでファイルアップロードをしようとすると <input type="file"> を使うしかないのでモーダルを自作するしかないか…と諦めモードで MDN を漁っていたら Window.showOpenFilePicker() - Web API | MDN なるものを見つけました。

まだ実験機能扱いですが、Chrome や Edge では使えます。<input type="file">cancel イベント問題が発生しているのは Chrome、Edge なので Chrome、Edge の場合は Window.showOpenFilePicker() を使い、それ以外のブラウザでは <input type="file"> を使用するのがよさそうです。

ただし、iOS や Android の Chrome で <input type="file">cancel イベント問題が発生するかは未検証なので厳密にはもう少し検証が必要です。

showOpenFilePicker" | Can I use… Support tables for HTML5, CSS3, etc

今回は PC 利用のみだったのでこのまま進めます。少し長くなりますが以下に全量を示します。

<button id="open_modal" type="button">モーダルを開く</button>
<dialog>
  <button id="close_modal" type="button">モーダルを閉じる</button>
</dialog>

<script>
  /** 
   * Chromium 以外向け
   * <input type="file"> 要素の作成とイベントハンドラ設定 
   */
  const inputFile = document.createElement("input");
  inputFile.setAttribute("type", "file");
  inputFile.addEventListener("change", (e) => {
    const file = e.target.files[0];
    /** ファイルに対する処理 */
  });

  /**
   *  Chromium 向け
   *  window.showOpenFilePicker 要素の作成とイベントハンドラ設定
   */
  const buttonFile = document.createElement("button");
  buttonFile.setAttribute("type", "button");
  buttonFile.addEventListener("click", async () => {
    try{
      const [handler] = await window.showOpenFilePicker();
      const file = await handler.getFile();
      /** ファイルに対する処理 */
    } catch (e) {
      /** ファイル選択ウインドウを閉じると DOMException が発生 */
    }
  });
  
  /** Chrome, Edge の場合とそれ以外の場合で配置する要素を変える */
  document.querySelector("dialog")
    .insertAdjacentElement("afterbegin", 
      navigator.userAgent.indexOf("Chrome") != -1 
        ? buttonFile
        : inputFile
      );

  document.querySelector("#open_modal").addEventListener("click", () => {
    document.querySelector("dialog").showModal();
  });

  document.querySelector("#close_modal").addEventListener("click", () => {
    document.querySelector("dialog").close();
  });
</script>

これで無事に Chrome、Edge、FireFox 全てでファイル選択ウインドウをキャンセルで閉じてもモーダルが閉じることはなくなりました。

あとがき

思わぬバグに引っかかりましたが、個人的にはアクセシビリティや開発効率の観点で、 既存の HTML 要素で賄えるものは極力既存のものを使いたいので早く Fix して欲しいなと思います。 ※ 2023/05/19 に上記 Chromium bug trucker に更新があり Chrome 115 で Fix されるようです。