Dialog

概要

モーダル ダイアログと非モーダル ダイアログには、ネイティブの <dialog> 要素を使用します。アクセシビリティ セマンティクス、フォーカス処理、最上位レイヤのレンダリング、Esc 処理が提供されます。カスタム オーバーレイでは、これらを手動で再構築する必要があります。

オーバーレイのタイプを選択する

最初に一致したルールを選択します。不明な場合は、ルール 1 を使用してください。

ルール 1: オーバーレイがユーザーの操作を中断し、ページをブロックする(デフォルト)

確認、フォーム、ログイン、破壊的なアクション、および特に指定のない「モーダル」、「ダイアログ」、「ライトボックス」、「ポップアップ」のリクエストに使用します。

<dialog> をモーダルとして開くには、command="show-modal" を使用して宣言的に開くか(宣言型ダイアログとポップオーバー コントロールを参照)、showModal() を使用してスクリプトから開きます。

<button type="button" command="show-modal" commandfor="confirm">Delete</button>

<!-- MANDATORY: no role="dialog" or aria-modal; the native modal already provides them -->
<dialog id="confirm" aria-labelledby="confirm-title">
  <h2 id="confirm-title">Delete this item?</h2>
  <form method="dialog">
    <button value="cancel" autofocus>Cancel</button>
    <button value="delete">Delete</button>
  </form>
</dialog>

ブラウザは、不活性ページ、フォーカス コンテインメント、最上位レイヤ、::backdrop、Esc、戻るジェスチャーによる閉じ、呼び出し元へのフォーカスの戻りを提供します。

  • role="dialog"、aria-modal、フォーカス トラップ、inert の切り替え、Esc ハンドラは追加しないでください。
  • show() または open 属性を使用して開かないでください。
  • 推奨: 以下の CSS を使用して、ページが背後でスクロールされないようにします。詳しくは、モーダル ダイアログの下のページがスクロールされないようにするをご覧ください。
  • html または body で overflow: hidden または position: fixed を使用してスクロールをロックしないでください。
/* MANDATORY (both rules): without them, scrolling over the backdrop or inside the dialog scrolls the page behind it */
dialog:modal {
  overscroll-behavior: contain; /* stops scroll gestures in the dialog chaining to the page */
}
dialog::backdrop {
  overflow: hidden; /* ::backdrop is not a scroll container by default, so it would ignore overscroll-behavior */
  overscroll-behavior: contain;
}

メニューやツールチップには使用しないでください(ルール 3 を参照)。

ルール 2: パネルが開いている間、ユーザーはページで作業を続ける

検索バー、インスペクタ、パレット、作成ペインなど、意味的にはダイアログであるものに使用します。

ポップオーバーの閉じ方(auto はライトディスミス、manual はライトディスミスなし)が設定された最上位レイヤのパネルには <dialog popover> を使用します。dialog.show() は、パネルを最上位レイヤに配置してはならない場合にのみ使用します。

<button type="button" popovertarget="find">Find</button>
<dialog id="find" popover="manual" aria-labelledby="find-title">
  <h2 id="find-title">Find</h2>
  <input type="search" aria-label="Search">
  <button type="button" popovertarget="find" popovertargetaction="hide">Close</button>
</dialog>

ページはインタラクティブなままで、不活性化されません。

  • すべきこと: 閉じるボタンを表示する。
  • showModal() はページをブロックするため、使用しないでください。

確認をブロックするためには使用しないでください。ルール 1 を参照してください。

ルール 3: オーバーレイはダイアログではない

メニュー、プルダウン、ツールチップ、ホバーカード、トーストに使用します。

<dialog> ではなく、popover 属性を持つセマンティック要素を使用します。

<button type="button" popovertarget="menu">Actions</button>
<ul id="menu" popover>
  <li><a href="/rename">Rename</a></li>
</ul>

アラームの解除方法を選択

デフォルト値は、ダイアログの開き方によって異なります。closedby が存在しないか無効な場合、showModal() は closerequest として動作し、他の方法で開いた場合は none として動作します。

  • Esc キーと戻るジェスチャーのみ(モーダルのデフォルト): 何も設定しないか、closedby="closerequest" を設定します。
  • 外側をクリックして閉じることも可能にする: closedby="any" を使用します。
  • デベロッパーのみが閉じる場合(保存されていない変更があるフォームなど): closedby="none" を使用して、確認パスを追加します。下記の「終了」を参照してください。

ガイドライン

ユーザー補助機能用の名前と初期フォーカス

  • 推奨: すべてのダイアログに名前を付け、aria-labelledby で見出しを指定します。表示されるタイトルがない場合は aria-label を使用します。
  • 推奨: autofocus は、プライマリ コントロールまたは最初のフィールドに配置します。
  • 推奨: 破壊的な確認では、安全なアクション(キャンセルや保持など)に autofocus を配置します。
  • 閉じる際にフォーカスを戻すコードを作成しないでください。ブラウザは、呼び出し元に返します。

導入

結び

  • <form method="dialog"> ボタンまたは command="close" で閉じるようにします。
  • closedby は慎重に選択してください。any(ライト ディスミスと Esc)、closerequest(Esc のみ、モーダルのデフォルト)、none(デベロッパーが制御するクローズのみ)から選択します。
  • command="request-close" または requestClose() は、未保存の変更ロジックを尊重する必要がある [キャンセル] ボタンに使用します。最初に cancel を起動し、closedby を無視します。
  • closedby はプログレッシブ エンハンスメントとして扱います。Esc とモバイルの戻るジェスチャーでは、すでにこのボタンなしでモーダルを閉じることができます。

close へのフック

  • 推奨: close リスナーで dialog.returnValue を読み取って、どのボタンが使用されたかを確認し、dialog.returnValue = '' でリセットします。Esc などのクローズ リクエストでは値が設定されず、以前の値をクリアするかどうかはブラウザによって異なるため、リセットしないと古い値が漏洩する可能性があります。'' を「選択せずに閉じた」として扱います。
  • cancel を使用して Esc と request-close をインターセプトしてください。ただし、ユーザー アクティベーションなしで Esc を繰り返し押す場合は、preventDefault() に依存しないでください。
  • closedby="none" を使用して、ダーティ フォームの明示的な確認ステップを追加する。
  • cancel が阻止されたときにダイアログが開いたままになっている理由をユーザーに伝える: 視覚的なメッセージを表示し、フォーカスを受け取るコントロール([破棄] など)から aria-describedby を使用して参照することで、フォーカスが移動したときにスクリーン リーダーが読み上げるようにします。この属性がないと、フォーカスが新しいボタンに移動したときに、そのボタンの名前のみが読み上げられます。
  • 避けるべきこと: 確認によってコントロールがポインタの下に移動するようにする。メッセージのスペースを予約し(display や hidden ではなく visibility で非表示にする)、既存のボタンが移動しない場所に破壊的なボタンを追加します(たとえば、右揃えの行の先頭)。これにより、同じ場所を 2 回クリックしても、ユーザーの変更が破棄されることはありません。

非モーダル ダイアログ

  • 推奨: 最上位レイヤのパネルには <dialog popover>(auto または manual)を使用し、最上位レイヤが不要な場合は dialog.show() を使用します。
  • 推奨事項: すべての非モーダル ダイアログに閉じるボタンを表示します。

背景とスタイリング

ユーザー補助

  • 禁止事項: ネイティブ モーダルに role="dialog"、aria-modal、手動フォーカス トラップ、inert の切り替えを追加する。
  • 推奨: 閉じるコントロールには、ユーザー補助機能用の名前を持つネイティブの <button> を使用します。
  • 推奨: ダイアログのタイトルには、一貫した見出しレベルを使用します。

代替戦略

closedby(dialog-closedby)

closedby がなくても、モーダルは Esc と戻るジェスチャーで閉じます。外側のクリックだけが失われるため、これはグレースフル デグラデーションです。closedby="none" は同じように無視されます。Esc は cancel を呼び出し、ダイアログを閉じます。未保存の変更のガードは、closedby="none" だけでなく cancel ハンドラにも保持します。これにより、closedby がない場合も保護されます。Baseline ターゲットでライトディスミスが必要な場合は、closedby がない場所に以下を追加します。

if (!('closedBy' in HTMLDialogElement.prototype)) {
  // Test the pointer position, not event.target: the <dialog> is also the target for its own
  // padding and the gaps between its children, so a target check would close on those clicks
  const outside = (e, dialog) => {
    const r = dialog.getBoundingClientRect();
    return e.clientX < r.left || e.clientX > r.right || e.clientY < r.top || e.clientY > r.bottom;
  };
  for (const dialog of document.querySelectorAll('dialog[closedby="any"]')) {
    let pressedOutside = false;
    dialog.addEventListener('pointerdown', (e) => { pressedOutside = outside(e, dialog); });
    // Require the press and the release outside, so a text-selection drag that ends on the backdrop does not dismiss
    dialog.addEventListener('pointerup', (e) => { if (pressedOutside && outside(e, dialog)) dialog.close(); });
  }
}

overscroll-behavior

スクロール コンテインメントはプログレッシブ エンハンスメントとして扱います。機能検出は行わないでください。@supports は、スクロール不可のコンテナが無視する場合でも、プロパティがサポートされていると報告します。適用されていない場合でも、ダイアログのコンテンツがオーバーフローするとスクロールが表示され、showModal() によってページが不活性になります。html または body で overflow: hidden または position: fixed にフォールバックしないでください。ルート スクロールバーが削除され、レイアウトがシフトします。

起動元コマンド(command / commandfor)

ベースライン ターゲットに Invoker Commands が含まれていない場合は、同じマークアップを維持し、条件付きでポリフィルを読み込みます(プログレッシブ エンハンスメントと同じエクスペリエンス)。インターフェースを検出し、無条件に読み込まないようにします。

// MANDATORY: feature detect on HTMLButtonElement.prototype and load only when native support is missing
if (!('commandForElement' in HTMLButtonElement.prototype)) {
  import('invokers-polyfill'); // npm install invokers-polyfill
}

ルール 2 と 3 のみがポップオーバーを使用します。

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 でデフォルトの配置を使用します。