3 ポイント 投稿者 GN⁺ 2024-12-08 | 1件のコメント | WhatsAppで共有
  • HTML `` 要素は、ブラウザネイティブのモーダル・非モーダルダイアログを作成し、モーダルではページの他の領域を inert 状態にして操作をブロックする
  • 開閉方法の基本は showModal()show()close() で、``、Invoker Commands API、Popover API による宣言的な制御も可能
  • closedby 属性は、ユーザーがダイアログを閉じられる経路を anycloserequestnone に分け、開き方によってデフォルト動作が異なる
  • アクセシビリティでは初期フォーカスと明示的な閉じるボタンが重要で、showModal() で開いたダイアログはデフォルトで最初のフォーカス可能要素へのフォーカスと Esc キーでのクローズを提供する
  • スタイリングでは :modal:open::backdrop を活用し、アニメーションには displayoverlay@starting-styletransition-behavior: allow-discrete などの離散的なトランジション処理が必要

`` の役割とモーダル動作

  • HTML `` 要素は、モーダルおよび非モーダルダイアログを作成するための要素
  • モーダルダイアログは他の UI 要素との相互作用を防ぎ、ページの残りの部分を inert 状態にする
  • 非モーダルダイアログは、開いている状態でもページの他の部分と引き続き相互作用できる

開いた状態と閉じるポリシー

  • `` はグローバル属性を含むが、tabindex 属性は使用してはいけない
    • `` 自体はインタラクティブな要素ではなく、フォーカスを受けない
    • 内部コンテンツと閉じるボタンはフォーカスを受け、操作できる
  • open 属性は、ダイアログがアクティブで操作可能であることを示す
    • open 属性がない場合、ユーザーには表示されない
    • ダイアログを表示する際は、open 属性より .show() または .showModal() の使用が推奨される
    • open 属性で開かれた `` は非モーダル
    • 非モーダルダイアログの開閉状態を open 属性のトグルで変更することはできるが、推奨されない
  • closedby 属性は、ユーザーがどの方法でダイアログを閉じられるかを指定する
    • any: 3 つの方法すべてで閉じられる
      • ダイアログ外のクリックやタップなどの light dismiss
      • Esc キー、モバイルの戻る操作、dismiss ジェスチャーなどのプラットフォーム固有の動作
      • HTMLDialogElement.close() を呼び出すボタンや `` の送信など、開発者が指定した方法
    • closerequest: プラットフォーム固有の動作と開発者指定の方法で閉じられる
    • none: 開発者指定の方法でのみ閉じられる
    • 有効な closedby 値がない場合、showModal() で開かれたときは closerequest のように、それ以外では none のように動作する

JavaScript と宣言的制御

  • JavaScript で `` の表示とクローズを直接制御できる
    • showModal(): モーダルダイアログを表示
    • show(): 非モーダルダイアログを表示
    • close(): ダイアログを閉じる
    • 内のmethod="dialog" で送信して閉じることもできる
    • モーダルダイアログは Esc キーでも閉じられる
  • Invoker Commands API は、ボタン属性だけでモーダルダイアログを開閉できるようにする
    • `` に commandforcommand 属性を指定する
    • ダイアログで使用できるコマンドは "show-modal""close""request-close"
Open dialog

  This dialog was opened using an invoker command.

  Close

  • Popover API は、非モーダルダイアログを宣言的に開閉・トグルできるようにする
    • `` に popover 属性を追加してポップオーバーにする
    • ボタンや入力要素に popovertargetpopovertargetaction を指定する
    • ポップオーバーダイアログは非モーダルなので、外側をクリックして閉じられる
    • popover 値を指定しない場合はデフォルト値 "auto" が使われ、light dismiss が有効になる
    • popover="manual" は light dismiss を無効にし、閉じるボタンなど別の方法が必要になる
    • popovertargetaction を省略するとデフォルト値 toggle が使われる

フォーム送信とクローズ処理

  • すべての `` には閉じるメカニズムが必要であり、物理キーボードのないデバイスでも動作しなければならない
  • 閉じる方法には複数ある
    • 内のmethod="dialog" を設定して送信する
    • light dismiss が有効な状態でダイアログ外をクリックする
    • 許可されたダイアログで Esc キーを押す
    • HTMLDialogElement.close() を呼び出す
  • `` または送信ボタンの formmethod="dialog" はダイアログを閉じる
    • フォームコントロールの状態は保存されるが、送信はされない
    • returnValue はアクティブなボタンの値に設定される
  • 必須入力値のあるフォームでは、ユーザーエージェントは値が提供されるまで通常送信によるダイアログのクローズを防ぐ
    • 閉じるボタンに formnovalidate を使うとフォーム検証を回避できる
    • JavaScript で dialog.close() を呼び出して閉じることもできる

フォーカスとアクセシビリティ

  • showModal() で `` を開くと、デフォルトで最初のネストされたフォーカス可能要素にフォーカスが設定される
  • 特定のダイアログで最も適切な初期フォーカス位置を指定するには autofocus 属性を使える
    • すぐに操作する要素がない場合は、閉じるボタンに autofocus を置くことが推奨される
    • 動的にレンダリングされるダイアログのように初期フォーカス位置が不確かな場合は、`` 自体が適切な初期フォーカス位置になることがある
  • すべてのユーザーが閉じられるよう、確認・キャンセル・閉じるボタンなどの 明示的なボタン を含める方法が最も堅牢
  • showModal() で呼び出されたダイアログはデフォルトで Esc キーで閉じる
    • 非モーダルダイアログはデフォルトでは Esc キーで閉じない
    • キーボードユーザーはモーダルダイアログが Esc キーで閉じることを期待する
    • 複数のモーダルダイアログが開いている場合、Esc キーは最後に表示されたダイアログだけを閉じるべきで、`` を使うとブラウザがこの動作を提供する
  • ネイティブの `` は、他の要素で作られたカスタムダイアログで手動実装しなければならないユーザビリティおよびアクセシビリティ機能を提供する
  • ブラウザは `` を ARIA role="dialog" を使用するカスタムダイアログに近い形で公開する
    • showModal() で呼び出された `` は暗黙的に aria-modal="true" を持つ
    • show()open 属性、デフォルトの display 変更で表示された `` は aria-modal="false" として公開される
    • モーダルダイアログ実装では、`` とそのコンテンツ以外のすべてが inert としてレンダリングされるべきであり、showModal() 使用時にはブラウザがこの動作を提供する

CSS スタイリングと backdrop

  • `` は通常の要素と同様に要素名で選択できる
  • 状態ベースのスタイリングには :modal:open 擬似クラスを使用できる
  • モーダルダイアログの背景は ::backdrop 擬似要素でスタイリングできる
    • showModal() でダイアログを表示したとき、`` の背後に現れる backdrop に適用される
    • inert 状態にある背後のコンテンツをぼかしたり、暗くしたり、覆い隠したりできる

例で見る使用パターン

  • Invoker Commands API の例では、command="show-modal" ボタンでダイアログを開き、command="close" ボタンで閉じる
    • “Open dialog” ボタンで開ける
    • “Close” ボタンまたは Esc キーで閉じられる
  • Popover API の例では、popoverpopovertargetpopovertargetaction で非モーダルダイアログを開閉する
    • “Close” ボタン、Esc キー、ダイアログ外の選択で閉じられる
    • popover="manual" を使うと light dismiss は無効になる
  • open 属性の例では、ページ読み込み時点ですでに開いている HTML のみの非モーダルダイアログを作る
    • `` の “OK” ボタンで閉じられる
    • 閉じた後に再び開く方法は提供されない
    • 非モーダルダイアログの表示には HTMLDialogElement.show() の使用が好ましい
  • モーダルダイアログの例では、.showModal() で開き close() で閉じる
    • ::backdrop でグラデーション背景をスタイリングする
    • ダイアログが開いている間、ダイアログ外部は inert 状態になり、ドキュメントと相互作用できない
  • returnValue の例では、フォームとボタン値でダイアログの戻り値を扱う
    • デフォルトの returnValue は空文字列、またはダイアログ内フォームを送信したボタンの値
    • “Confirm” ボタンは選択値を close() に渡して戻り値として使用する
    • Esc キーで閉じると returnValue は更新されず、close イベントも発生しないため、`` テキストは更新されない

`` アニメーション

  • 非表示の は `display: none`、表示中のdisplay: block になる
  • 表示状態が変わるとき、top layer とアクセシビリティツリーに追加または削除される
  • `` をアニメーションさせるには display プロパティがアニメーション可能である必要がある
    • 対応ブラウザでは display を離散アニメーションとして扱う
    • none から block に変わるときは 0% 時点で block に切り替わり、アニメーション全体を通して表示される
    • block から none に変わるときは 100% 時点で none に切り替わり、アニメーション全体を通して表示される
  • CSS transition によるアニメーション

    • CSS transition で `` をアニメーションさせるには、次の機能が必要
    • @starting-style: ダイアログが開くたびにトランジション開始値を提供する
    • display transition: トランジション中、ダイアログを表示する display 値を維持する
    • overlay transition: top layer からの削除をトランジション完了まで遅延させる
    • transition-behavior: allow-discrete: 通常はアニメーションしない displayoverlay の離散トランジションを有効化する
    • :open 擬似クラスをサポートしないブラウザでは、dialog[open] 属性セレクターで開いた状態をスタイリングできる
    • `` は表示されるたびに display: none から display: block に変わるため、各エントリトランジションで @starting-style から dialog:open スタイルへ遷移する
  • keyframe アニメーション

    • CSS keyframe アニメーションは transition とは異なる制約を持つ
    • @starting-style は提供しない
    • display 値を keyframe 内に含める
    • allow-discrete を明示的に有効化する必要はない
    • overlay を keyframe に設定する必要もない
    • backdrop のフェードアウトは、ダイアログが閉じるときに backdrop が DOM から即座に削除されるためアニメーションできない

技術要約

  • コンテンツカテゴリ: flow content、sectioning root
  • 許可されるコンテンツ: flow content
  • 開始タグと終了タグはいずれも必須で、タグ省略はない
  • 許可される親: flow content を受け入れるすべての要素
  • 暗黙的 ARIA ロール: dialog
  • 許可される ARIA ロール: alertdialog
  • DOM インターフェース: HTMLDialogElement
  • 仕様: HTML # the-dialog-element

1件のコメント

 
GN⁺ 2024-12-08
Hacker Newsのコメント
  • インタラクティブなHTML要素には、ファイルピッカー、カラーピッカー、日付/時刻ピッカー、数値スライダー、テキストフィールドの候補オプション、展開可能な要約/詳細、FAQ、コントロール付きメディアプレーヤーのようなものがある
    こうした要素は軽量でセマンティックなのがよく、同じnameを共有する<details>では、1つを開くと前の項目が閉じる挙動も可能

    • 最近は結局、ブラウザやバージョンごとに挙動の予測可能性が十分ではないため、こうしたものはツールキットで置き換えられることが多い
    • コントロール付きのメディアプレーヤーはブラウザごとに見た目がまったく異なり、JavaScriptなしではスタイリングもできない
      もっとよく実装されていてほしい
  • 2019年から**<dialog>**を使っているが、FirefoxとSafariがさらに数年対応していなかった時期でも、Googleのpolyfillの品質が高く、業務用SaaSの本番環境で問題なく使えた
    一番残念だったのは、この要素がほとんどスタイルされていなかったこと。非常に基本的な太い黒のピクセル境界線と角ばった角しかなく、ブラウザがOSのダイアログ/ウィンドウ配置慣例に従って、ネイティブのmacOS、iOS、Windowsのダイアログのように見せてくれることを期待していたのとは違っていた
    開いた要素は別の最上位レイヤーにあるのだから、ブラウザのビューポート外にも出せることを期待したが、その期待も外れた。ユーザーの立場では、文書内容を完全に覆う動かせないポップアップやモーダルダイアログを望んでいないからだ
    ブラウザベンダーはalert()prompt()confirm()がJavaScript/メインスレッドをブロックするので使ってほしくないようだが、それと同じくらい簡単で効果的で、UIデザイン能力を要求しない代替APIは出せていない。あらゆる場面で<dialog>のほうが優れていると説得しようとするより、Promiseベースのノンブロッキングな alert/prompt/confirm APIを提供してほしい

    • ブラウザのビューポート外に出せて、システムダイアログのようにスタイルされる機能は、ユーザーやブラウザ開発者よりも開発者のほうが求めそうな機能だ
      新機能が昔のalert()系のように動かなくなったのは、単なる残念な偶然ではなく利点でもある
      ただし<dialog>のデフォルトスタイルにはもう少し配慮が必要で、DOM外のシステムダイアログのように100%見えて動作しなくても、ブラウザのデフォルトスタイルと調和する標準スタイルがあるだけでもずっとよかったはずだ
      PWAのように、通常のページより権限の強いWebアプリであれば、ウィンドウのスタイリングやシステムと相互作用する別機能のような、この方向性はより受け入れられるかもしれない
    • ベンダーが<dialog>をネイティブのブラウザウィンドウのように見せると、line of deathを曖昧にしかねない
      悪意あるWebサイトがページ中央に「ブラウザを更新」ポップアップを表示し、それらしいChromeダウンロードページへリダイレクトして、改ざんされた実行ファイルをダウンロードさせることが容易になる
      https://textslashplain.com/2017/01/14/the-line-of-death/
    • ブラウザウィンドウ全体のフォーカスを奪うモーダルはよい考えではない
      ユーザーは複数のタブを開いており、別のタブにダイアログを完了するのに必要な情報があるかもしれない
      実際のダイアログにどれだけの視覚的制御権を与えるかについても、非常に慎重であるべきだ。特にOSのように見せると、今でも偽のブラウザウイルス警告にだまされる人が多いのに、本物らしく見えて必ず操作しなければならないダイアログは、攻撃の成功率を大きく高めうる
    • ほとんどスタイリングされていない点が、組み込みブラウザコンポーネントの広範な利用を妨げる主因に見える
      働いていた会社のほとんどにはある程度一貫したデザイン言語があり、純粋なHTML/CSSを好む人が何と言おうと、組み込みブラウザコンポーネントをそのまま使うとたいてい素人っぽくちぐはぐに見えた
      この問題が解決されない限り、広く使われるのは難しい
    • alert()prompt()confirm()の代替を、await Prompts.alert("This is an alert message!");await Prompts.confirm(...)await Prompts.prompt(...)のように動作させるアイデアを試している
      デモ: https://tools.simonwillison.net/prompts-js
      コードはo1が作成: https://chatgpt.com/share/67539c28-4df0-8006-b021-4f468e011f...
  • 「The HTML dialog element API is a mess」という記事を書いた: https://lapcatsoftware.com/articles/2024/2/1.html

    • Web標準が素晴らしく、よく設計され、適切に管理されていると言う人は多くないだろう
      それでも価値は標準であることにある。Webはどこでも動くという点で優れており、どうしても雑多にならざるを得ない
      この文脈では、<dialog>は特に社内管理ツールでは成功だと思う。最新のフロントエンド流行を気にせず、画面スペースを節約しながらメインビューの上にコンテンツをモーダルオーバーレイとして表示したいだけだからだ
    • 同じダイアログをモーダルで2回開くことは、現実にはほとんどない気がする
      ダイアログを開くにはユーザー入力が必要で、モーダルダイアログはユーザー入力を妨げるので、これが起こるにはダイアログ内の入力が再びそのダイアログを開かなければならない
      非同期処理でダイアログを開くなら何が開いていて何が閉じているかを追跡する必要があり、Qtのような環境でも似たようなことは起こる
    • <dialog>が、複数回開いてもブラウザごとに警告が出ず、カスタマイズ可能なawait confirm/promptの強化版になってくれることを期待していた
      だが実際には、装飾されたdivに近い
    • Google ChromeはURLPatternも作ったし、ChromeとSafariがサポートしないでいてほしかった
      Compression Streams APIは悪くなかったが、とても小さなAPIだった
      このパターンを見ると、Googleはユーザー体験と開発者体験の扱いがあまりうまくないように思える
      追記として標準の立場を調べたら、両方ともURLPatternをサポートしていた
  • 実装とは別に、正しい方向への一歩だったと思う
    現在は<select>を強化したような提案も進んでいる: https://open-ui.org/components/combobox.explainer/
    トースト通知向けには、すでにブラウザにPopover APIが入っている: https://mdn.github.io/dom-examples/popover-api/
    ツールチップのためのpopover hint提案もある: https://open-ui.org/components/popover-hint.research.explain...

  • **<dialog>**要素は気に入っており、特に組み込みで標準化されたアクセシビリティ配慮が良い
    Safari 15.4未満がサポート対象から外れれば、polyfillなしで使える日を楽しみにしている
    ただ最大の不満はJavaScript依存であることだ。サイト訪問者のほぼ全員がJavaScriptを有効にして来るだろうが、あえてそれを不要にできることに満足感を覚えることもある
    なぜCSSや対象ボタンでダイアログの開いた状態を制御できないのかわからず、もし自分が間違っているなら知りたい

    • なので主にpopover=""属性を付けたカスタム要素を使っている
      ボタンで簡単に対象にでき、クライアント側JavaScriptが不要で、外側をクリックすれば閉じることもできる。<dialog>には標準ではない動作だ
      アクセシビリティについては自信がないが、隠しラベル/チェックボックス/フォーム要素で作っていた昔のやり方よりは悪くなさそうで、ずっと単純でハックっぽさも少ない
    • これはinvokersで可能になる予定だ
      https://css-tricks.com/invoker-commands-additional-ways-to-w...
  • HTMLが、1つのファイル内に複数のページを定義して一度に1つずつ表示できる**<page>のようなタグ**という概念をサポートするとよいと思う
    ダイアログのようには見えず、各PAGEが選択状態に応じて同じページ内のヘッダー、サイドバー、フッターのような共通セクションを引き上げられるとよい
    今でもdivを隠したり表示したりする方法で実現できるが、特定のHTMLタグでこうしたアプローチを支援してくれると助かる

    • 最近、印刷用スタイルシートの分野ではそのようなことができる改善があった気がするが、画面表示向けにはまだないと認識している
  • 今日使っていて、回避できない問題があった
    ダイアログ内にフォームがあると、入力にフォーカスした状態でEnterを押すか、送信ボタンにフォーカスした状態でSpaceを押したとき、フォーム送信とともにダイアログが閉じる
    これをきれいに防ぐ方法が見つからなかった。通常フォームはページを再読み込みするので一般的な問題ではないかもしれないが、ここではhtmxを使っていた

    • 最後の文が正しい可能性が高い。基本的にフォームはネットワークリクエストを送る
      <dialog>内のフォームでiframeを更新するエディタを使っているが、対象のiframeが再読み込みされるだけでダイアログは閉じない
      Enterを押したときにダイアログが閉じるケースも再現できなかった。submitと同じはずで、submitでもダイアログは閉じない
      ちなみにボタンがダイアログを閉じられることは昨日知った
    • preventDefaultstopPropagationを使えばよいのでは?
    • HTMX側にバグ報告をしたほうがよいかもしれない
  • 新しく最適化したビルドに入れるCookie同意マネージャーを探していたが、オープンソースの選択肢が100KBを超えているのが気に入らず、自分で作った: https://github.com/replete/biscuitman
    できるだけ小さく書くために**<dialog>**に依存し、CSSルールをいくつか書くだけでスタイルなしでもネイティブに動作する
    結局、IE11や本当に古いブラウザバージョン向けまでコンパイルするビルドツールまで書くことになった
    <dialog>は概ねうまく動作し、古いブラウザではCSSの小技が少し必要だが扱いにくくはない。Webプラットフォームへの悪くない追加要素だが、この仕事を20年やってきて、数年ごとにカスタムの複数選択コントロールを作り直したいとは思わない。ネイティブコントロールが良い

  • ほとんどのサンプルの一般的な閉じる動作が Android版Firefox では動作しない

    • 昨日プロジェクトで使ったときは、閉じるボタンの autofocus 属性が動作しなかった
      結局、show() を呼ぶたびに $('#thatModal *[autofocus]').focus() のような1行を追加した
      MDNによれば、本来はデフォルトで意図どおりに動作するはず
    • 「ほとんどのサンプルの一般的な閉じる動作」とは何を指しているのか、もう少し説明してもらえる? 私が見たサンプルにはすべて閉じるボタンにイベントリスナーを付けるJavaScriptの断片があり、Firefox for Androidでも問題なく動作した
    • Windows 10のChromeでも同じ
      JavaScriptで追加したリスナーだけが正しく動作しているようだ