🔥

Invoker Commands APIのイベントのまとめ

2025/03/13に公開

<dialog>、Popover、Invoker CommandsがHTML標準で整いつつあります。JavaScriptを書かずにHTMLだけでインタラクティブなUIを作ることができるようになっていっているわけですが、それでもイベントをフックして何かしらの処理を実行したいことはあります。これらのUIのイベントはいくつか種類があるので、それぞれのUIや呼び出し方法に対応するイベントをまとめてみました。

前提

<dialog>

<dialog>はHTML標準のダイアログUIです。このダイアログUIはモードレスダイアログとモーダルダイアログの2つのモードがあります。モードレスダイアログはopen属性を付与することで表示されますが、モーダルダイアログはshowModal()メソッドを呼び出すことで表示されます。つまりこれまではモーダルダイアログを開く手段はJavaScriptを使うしかありませんでした。

<button type="button" id="open-dialog">Open Dialog</button>
<dialog id="dialog">
  <h1>Dialog</h1>
  <button id="close-dialog">Close Dialog</button>
</dialog>
document.getElementById("open-dialog").addEventListener("click", () => {
  document.getElementById("dialog").showModal();
});

document.getElementById("close-dialog").addEventListener("click", () => {
  document.getElementById("dialog").close();
});

Popover

一方、JavaScript不要でポップオーバーUIを制御できる仕組みがHTML標準になりました。popover属性を持つ要素は初期で非表示の状態になり、popovertarget属性をもつ<button>要素をクリックすることで表示されます。

<button type="button" popovertarget="popover">Open Popover</button>
<div popover id="popover">...</div>

このポップオーバーUIの特徴は「モードレス」の展開要素ということです。このポップオーバー以外をクリックしたりフォーカスを移動したりすることができます(そのとき同時にポップオーバーを閉じるかどうかはpopover属性にautomanualを設定することで変更できます)。

しかし、このポップオーバーUIはあくまでも「モードレス」であり、<dialog>popover属性を付与してもモーダルダイアログには当然なりません。PopoverのAPIではモーダルダイアログは作れないということです

Invoker Commands

そして登場したのがInvoker Commandsです。Invoker Commandsは<button>要素にcommand属性を付与することで、<dialog>やPopoverを制御することができます。command属性はいくつかの規定のコマンドを設定することができ、そのなかにshow-modalというコマンドがあります。このコマンドを<button>要素に設定することで、<dialog>をモーダルダイアログとして表示することができます。もちろんJavaScriptも不要です。ヤッタネ!

<button type="button" command="show-modal" commandfor="dialog">
  Open Dialog
</button>
<dialog id="dialog">
  <h1>Dialog</h1>
  <button command="close" commandfor="dialog">Close Dialog</button>
</dialog>

対応するイベント

JavaScript不要でポップオーバーUIもモーダルダイアログも実装できるようになりましたが、イベントをフックして何かしらの処理を行いたい場合はたまにあります。そこで、それぞれのUIに対応するイベントをまとめてみます。

イベント名 イベントオブジェクト バブリング <dialog> Popover Invoker(呼び出し元)ボタン
command CommandEvent ❌️ しない ✅️ Invokerからの呼び出し時 ✅️ Invokerからの呼び出し時 ❌️ 自らは発火しない
beforetoggle ToggleEvent ❌️ しない ✅️ 表示または非表示の直前 ✅️ 表示または非表示の直前 ❌️ 自らは発火しない
toggle ToggleEvent ❌️ しない ✅️ 表示または非表示の直後 ✅️ 表示または非表示の直後 ❌️ 自らは発火しない
close Event ❌️ しない ✅️ 非表示の直後 ❌️ 発火しない ❌️ 自らは発火しない
click(おまけ) PointerEvent ✅️ する ❌️ 発火しない ❌️ 発火しない ✅️ 当然発火する

CodeSandboxにサンプルコードを書いたので百聞は一見に如かず、コンソールを見ながらイベントの発火を確認してみてください。

発火の順番

<dialog> + command="show-modal"

  1. command
  2. beforetoggle
  3. click(Invokerボタン)
  4. toggle

<dialog> + ESCキーで閉じる

  1. beforetoggle
  2. toggle
  3. close

Popover (command="toggle-popover")

  1. command
  2. beforetoggle
  3. click(Invokerボタン)
  4. toggle

Popover(Light Dismiss)

  1. beforetoggle
  2. toggle

Popover (command="show-popover")

  1. command
  2. beforetoggle
  3. click(Invokerボタン)
  4. toggle

Popover (command="hide-popover")

  1. command
  2. beforetoggle
  3. click(Invokerボタン)
  4. toggle

イベントの使い分け

commandイベント

まず、commandイベントはInvoker(呼び出し元)ボタンに発火するのではなく、対象のポップオーバーUIやダイアログUIに対して発火します。そのためaddEventListenerの対象に注意が必要です。

CommandEventcommand属性に設定された値をevent.commandで、呼び出し元をevent.sourceで取得できます。つまり、複数の呼び出し元があったとしてもこれらの情報を使って処理を分岐させることができます。

target.addEventListener("command", (event) => {
  if (event.command === "show-modal") {
    console.log(event.source); // 呼び出し元
  }
});

command属性には--で始まるカスタムコマンドを設定することができます。

<button type="button" command="--custom-command" commandfor="foobar">
  Custom Command
</button>
target.addEventListener("command", (event) => {
  if (event.command === "--custom-command") {
    // カスタムコマンドの処理
  }
});

フック処理で言えば後述するbeforetoggle/toggleイベントの方が有用なケースが多いので、基本的にはcommandイベントはカスタムコマンドの実装で利用することになると思います。

beforetoggle/toggleイベント

beforetoggle/toggleイベントは、Invoker Commands APIとは関係なく、他の方法で呼び出されても発火します。showModal()で明示的に表示したりESCキーで閉じたときにも発火します。そのため、UIそのものの状態を監視するのに適しています。どのUIでも共通して発火するので実装漏れを防ぐためにも、基本的にはこのイベントを使うことを最初に検討するといいかもしれません。

ToggleEventnewStateoldStateというプロパティを持ち、それぞれ表示・非表示の状態を表します。

target.addEventListener("beforetoggle", (event) => {
  console.log(event.newState); // "open" もしくは "closed"
});

closeイベント

<dialog>にはcloseイベントがありますが、Popoverにはないので注意が必要です<dialog>が閉じられたときに発火します。<dialog>close()メソッドを呼び出したときにも発火します。

clickイベント

呼び出し元をクリックしたりエンターキーなどで実行をしたときに発火しますが、このボタンからの起点に限定されるので開閉のフック処理に使うことはあまり適さないと思います。

まとめ

  • フック処理は実装漏れを防ぐためにも、どのUI・どの呼び出しからでも共通で発火するbeforetoggle/toggleイベントを前提に検討することよさそう
  • commandイベントはカスタムコマンドの実装が主な用途
  • closeイベントは<dialog>にのみにしか発火しないし、showイベントやopenイベントもないので勘で実装しないように注意
GitHubで編集を提案

Discussion