宣言型ダイアログとポップオーバー コントロール

Invoker Commands API を使用すると、HTML ボタンから <dialog> 要素と [popover] 要素の表示を直接切り替えることができます。カスタム JavaScript イベント リスナーは必要ありません。

commandfor(ターゲット ID)属性と command(アクション)属性を <button> に適用すると、ブラウザは開閉状態の変更、フォーカス管理、ユーザー補助バインディング(aria-expanded など)を自動的に処理します。この宣言型アプローチは、脆弱なボイラープレート コードを排除し、HTML 解析直後にインタラクションが機能するようにし、堅牢でネイティブにアクセス可能なユーザー エクスペリエンスを保証するため、推奨されます。

宣言型ポップオーバーの実装

ポップオーバーは、1 つのボタンを使用して開閉を切り替えることができます。

<!-- MANDATORY: The commandfor attribute links the invoker to the ID of the target element so the browser knows what to control. -->
<!-- MANDATORY: The command attribute specifies the action to perform. Use 'toggle-popover' to handle both open and close states automatically. -->
<button commandfor="my-popover" command="toggle-popover">
  Toggle Popover
</button>

<!-- MANDATORY: The target element must have the popover attribute to be controlled as a popover. -->
<div id="my-popover" popover>
  <p>Popover content goes here.</p>
</div>

開閉を別々のボタンで操作する必要がある場合は、show-popover コマンドと hide-popover コマンドを使用できます。

<!-- MANDATORY: Use 'show-popover' to explicitly open the popover. It will not close the popover if clicked again. -->
<button commandfor="my-explicit-popover" command="show-popover">
  Show Popover
</button>

<div id="my-explicit-popover" popover="manual">
  <p>This popover is explicitly opened and closed by separate buttons.</p>

  <!-- MANDATORY: Use 'hide-popover' to explicitly close the targeted popover. -->
  <button commandfor="my-explicit-popover" command="hide-popover">
    Hide Popover
  </button>
</div>

宣言型モーダル ダイアログの実装

ポップオーバーとは異なり、モーダル ダイアログでは通常、開くボタンと閉じるボタンが別々に使用されます。show-modal コマンドは、ダイアログをモーダルとして開く必要がある場合に使用します。

<!-- MANDATORY: Use command="show-modal" to trigger the dialog as a modal, trapping focus and preventing interaction with the rest of the page. -->
<!-- MANDATORY: The commandfor attribute connects this button to the dialog ID. -->
<button commandfor="confirm-dialog" command="show-modal">
  Open Confirmation
</button>

<dialog id="confirm-dialog">
  <p>Are you sure you want to proceed?</p>

  <!-- MANDATORY: Use command="close" to dismiss the dialog safely. -->
  <button commandfor="confirm-dialog" command="close">
    Cancel
  </button>
</dialog>

代替戦略

Invoker Commands と Popover はまだ広くサポートされていないため、古いブラウザのフォールバックとしてポリフィルを使用する必要があります。

Invoker Commands のポリフィル

必須: HTMLButtonElement プロトタイプの commandForElement プロパティを確認して、機能検出のサポートをチェックします。window オブジェクトや document オブジェクトは確認しないでください。ネイティブ機能がない場合にのみ、ポリフィルを動的にインポートする必要があります。ポリフィルを無条件に読み込まないでください。必須: ネイティブの「command」イベントはバブルしないため、ターゲット要素で直接「command」イベントをリッスンします。

オプション 1: バンドラを使用する npm(npm install invokers-polyfill)を介してポリフィルをインストールします。この方法は、バンドラ(Vite や Webpack など)またはインポート マップを使用するプロジェクトに適しています。その他の設定については、以下の CDN オプションを使用してください。

// MANDATORY: Feature detect 'commandForElement' on HTMLButtonElement.prototype.
// Conditionally load the invokers-polyfill only in browsers lacking native support.
if (!('commandForElement' in HTMLButtonElement.prototype)) {
  import('invokers-polyfill');
}

オプション 2: CDN を使用する バンドラのないプロジェクトの場合は、<script type="module"> 内の CDN からポリフィルを直接動的にインポートします。

<script type="module">
  // MANDATORY: Feature detect 'commandForElement' on HTMLButtonElement.prototype.
  // Conditionally load the invokers-polyfill from a CDN only in browsers lacking native support.
  if (!('commandForElement' in HTMLButtonElement.prototype)) {
    import('https://esm.run/invokers-polyfill');
  }
</script>

Invokers ポリフィルの制限事項 必須: このポリフィルは、ネイティブ ブラウザのようにコマンドボタンの ARIA 状態(aria-expanded など)を処理しません。サイトに完全にアクセスできるように、これらの状態を自分で処理することを強くおすすめします。

Invoker Commands API がサポートされていない場合、command イベントは発生しません。すべての最新のブラウザで完全なサポートを受けるには、npm install または CDN を介して https://github.com/keithamus/invokers-polyfill の invokers-polyfill を使用することをおすすめします。

このポリフィルは、カスタム アクション(-- で始まる)を完全にサポートし、ネイティブ API とまったく同じように command イベントをディスパッチします。

動的インポート(パフォーマンスの最適化)

最適なパフォーマンスを得るには、ブラウザが API をネイティブにサポートしていない場合にのみポリフィルを読み込む必要があります。これにより、最新のブラウザを使用しているユーザーの帯域幅を節約し、スクリプトの実行時間を短縮できます。

注: このポリフィルは、カスタム コマンドの ARIA 状態(aria-pressed や aria-expanded など)を管理しません。サイトにアクセスできるようにするには、イベント リスナーでこれらの状態を手動で同期する必要があります。

// 1. Conditionally load the polyfill
const hasNativeSupport = 'commandForElement' in HTMLButtonElement.prototype;

if (!hasNativeSupport) {
  // Wrap in an async IIFE to avoid top-level await issues in older browsers
  (async () => {
    try {
      await import('https://esm.run/invokers-polyfill');
    } catch (err) {
      console.error('Error loading fallback:', err);
    }
  })();
}

// 2. Manually manage ARIA states in your listener
document.getElementById('action-target').addEventListener('command', (event) => {
  const command = event.command;
  const target = event.target;
  const source = event.source; // The button that triggered the command

  if (command === '--spin') {
    const isSpun = target.classList.toggle('is-spun');
    
    // Polyfill tip: Manually update ARIA to match the new state
    source?.setAttribute('aria-pressed', isSpun);
  }
});

手動フォールバック(従来のパターン)

ポリフィルを使用しない場合は、イベント委任を組み合わせてイベントをディスパッチし、コマンド レジストリを使用してアクションを処理できます。これは、従来の JavaScript 開発でよく使用されるアーキテクチャ パターンであり、効率性とスケーラビリティに優れています。

// 1. **Optional:** Define a registry of requested actions for cleaner logic
const commandRegistry = {
  '--spin': (target) => target.classList.toggle('is-spun'),
  '--grow': (target) => target.classList.toggle('is-grown'),
  '--reset': (target) => target.classList.remove('is-spun', 'is-grown'),
};

// 2. If CommandEvent doesn't exist, we assume no native support and provide the fallback
if (!globalThis.CommandEvent) {
  globalThis.CommandEvent = class CommandEvent extends Event {
    constructor(type, { source, command, ...options } = {}) {
      super(type, options);
      this.source = source;
      this.command = command;
    }
  }
}

// 3. The fallback: Dispatch events manually if native support is missing
  document.addEventListener('click', (event) => {
    const button = event.composedPath().find((el) => el.matches?.("button[commandfor]"));
    if (!button) return;

    const target = document.getElementById(button.getAttribute('commandfor'));
    const command = button.getAttribute('command');

    if (target && command) {
      target.dispatchEvent(new CommandEvent('command', { 
        command, 
        source: button,
      }));
    }
  });

// 4. **Mandatory:** Register the unified listener directly on the target element
document.getElementById('action-target').addEventListener('command', (event) => {
  const command = event.command;
  const target = event.target;
  const action = commandRegistry[command];

  if (action) {
    action(target);
  }
 });

Popover のフォールバックとブラウザ サポート

Popover API は主にプログレッシブ エンハンスメントですが、その定義動作である最上位レイヤのプロモーション、ライトディスミス、popovertarget 呼び出しには、CSS のみの同等の機能がありません。古いブラウザでは、ポリフィルが必要です。ポリフィルを配布したくない場合は、手動でフォールバックする必要があります。

ポリフィル: 古いブラウザで popover 属性をサポートするには、条件付きで @oddbird/popover-polyfill を読み込みます。必須: HTMLElement.prototype の popover プロパティをチェックして機能を検出し、ネイティブ サポートがない場合にのみポリフィルを読み込みます。無条件に読み込まないでください。

バンドラまたはインポート マップを使用する場合:

// MANDATORY: Feature detect 'popover' on HTMLElement.prototype.
if (!("popover" in HTMLElement.prototype)) {
  import("@oddbird/popover-polyfill");
}

バンドラを使用しない場合は、<script type="module"> 内の CDN からインポートします。

<script type="module">
  if (!("popover" in HTMLElement.prototype)) {
    import("https://unpkg.com/@oddbird/popover-polyfill@latest/dist/popover.min.js");
  }
</script>

スタイリングに関する注意事項: ポリフィルは実際の :popover-open 疑似クラスを定義できないため、代わりに .\:popover-open クラスを適用します。必須: :is() または :where() を使用して 2 つを結合します。そうしないと、:popover-open がないブラウザではルール全体が破棄されます。

[popover]:is(:popover-open, .\:popover-open) {
  display: block;
}

または、ポリフィルを使用しないレガシー フォールバックの場合は、position: fixed を使用して getBoundingClientRect() で座標を手動で計算するか、ユースケースで許容される場合は inset: auto でデフォルトの配置を使用します。