Popover API - JavaScript不要、HTMLのみでポップオーバーUI
更新履歴
- 2023-09-18 3/10〜9/18までの更新に対応する修正
- 2023-03-10 属性の破壊的変更による修正
- 2023-02-19 初稿
HTML Standardにpopover
属性をはじめとしたPopover APIが正式にマージされました。Open UIによって提案されていた[1]APIで、名前がPopoverなのかPopupなのか紆余曲折の末、やっとHTML Standardとなります。
現段階で実装されているブラウザは少ないですが、簡易サンプルを作ったので体験しながら読んでいただくといいかもしれません。
CodeSandboxの簡易サンプル
3つの属性
まずはHTMLの属性が新しく追加されているところに注目しましょう。
以上の3つが追加されました。popover
属性はポップオーバーする要素自体に。popovertarget
属性とpopovertargetaction
属性はそれを展開するボタン[2]に対して設定します。つまり、a
要素やdiv
要素には設定できません。これはアクセシビリティの観点から革新的と言えます。ついにdivボタンが消える…!
<button type="button" popovertarget="p1">開閉ボタン</button>
<div popover id="p1">ポップオーバーUI</div>
たったこれだけで、ポップオーバーUIが動作します。JavaScriptは不要です。popovertarget
属性のidがマッチしていれば、<details>
・<summary>
と同様にJavaScriptからメソッドを呼び出すことなく実装できます。
popover
属性が付与された要素は、デフォルトで非表示(display: none
状態)となり、展開された場合にはトップレイヤーに表示されます。トップレイヤーとは、簡単に言うと、z-index
をどんなに大きな数にしても超えられない表示レイヤーで、<dialog>
もここに表示されます。popover
属性の値は「空」かauto
かmanual
で、デフォルトはauto
です。「空」はauto
と同等なので、値なしで<div popover>
と書くこともできます。
ポップオーバーUIを制御するトリガー側の属性は2種類あります。
-
popovertarget
属性: 対象のポップオーバーUIのIDを指定します。 -
popovertargetaction
属性: アクションを種類を指定します。toggle
、show
、hide
のいずれかです。デフォルトはtoggle
なので、この属性は省略することが出来ます。
筆者の感想としては「本気でJavaScript不要にしたかったんだな[3]」ということが伝わってきます。
簡易非表示(Light Dismiss)
今回新たに簡易非表示(英語原版ではLight Dismiss)[4]という概念が登場しました。
そんなに難しいものではなく
- ESCキーで閉じる
- ポップオーバーUIの外側をクリックすると閉じる
という単純な機能ですね。外側をクリックは一般的によく採用されてきたパターンですし、ESCキーも同様にキーボード操作でも容易に閉じることができるでアクセシビリティも担保できています。
この簡易非表示(Light Dismiss) は、popover
属性がauto
のときに有効になります(つまり値なしでもOK)。これもJavaScriptを使わずに有効になるのはとても有用です。
auto/manual
popover
属性の値はauto
とmanual
を受け取りますが、この2つの違いは先に説明した簡易非表示以外もあります。
auto
は開いたときに他のポップオーバーUIを閉じます。一方、manual
は他のポップオーバーを閉じません。表にまとめると、
値 | 複数表示 | 他のポップオーバーUIを | 簡易非表示(Light Dismiss) |
---|---|---|---|
auto |
できない | 閉じる | 有効 |
manual |
できる | 閉じない | 無効 |
ということになります。目的合わせて使い分けるといいでしょう。
JavaScriptで利用するには
ここまでJavaScript不要と紹介していますが、使えないわけではありません。DOM APIとしてもきちんと操作可能なAPIを提供しているので、これを利用してより目的にあった実装をすることも可能です。
メソッド
3つのメソッドを使うことができます。
<div popover id="p1">ポップオーバーUI</div>
const element = document.querySelector("#p1");
// ポップオーバーUIを表示する
element.showPopover();
// ポップオーバーUIを非表示にする
element.hidePopover();
// 表示・非表示を切り替える(戻り値は開閉の結果)
const isShow = element.togglePopover();
// 強制的に表示・非表示を切り替える
element.togglePopover(true);
popover
属性を持たない要素をshowPopover
などで呼び出すと例外(NotSupportedError
)を投げます。
イベント
イベントはbeforetoggle
とtoggle
の2種類です。イベントオブジェクトはToggleEvent
インターフェイスでnewState
とoldState
というプロパティを持ちます。
element.addEventListener("beforeToggle", (ev) => {
console.log(ev.newState); // "open" もしくは "closed"
console.log(ev.oldState); // "open" もしくは "closed"
// beforeToggleイベント内ではpreventDefaultで開閉を抑制することができます。
ev.preventDefault();
});
element.addEventListener("toggle", (ev) => {
console.log(ev.newState); // "open" もしくは "closed"
console.log(ev.oldState); // "open" もしくは "closed"
});
popover="manual"
は簡易非表示を無効にしていることなどから、JavaScriptで独自の実装をするために設けられたものとも考えることができます。うまく扱っていきたいところです。
アクセシビリティ
アクセシビリティオブジェクトのマッピング
気になるアクセシビリティオブジェクトのマッピングですが、以下のような変化が起こります[5]。
要素 | ロール | ステート |
---|---|---|
popover 属性をもつ要素 |
group |
|
popovertarget 属性をもつ要素 |
button (変化なし[6]) |
expanded=false/true |
またpopover
属性を付与した時点で非表示(display:none
)になりアクセシビリティツリーから消えることを意味するので、その点は留意が必要です。
読み上げに関する工夫
以下のように言及されています。
Whenever possible ensure the popover element is placed immediately after its triggering element in the DOM. Doing so will help ensure that the popover is exposed in a logical programmatic reading order for users of assistive technology, such as screen readers.
これは、可能な限りポップオーバー要素を、それをトリガーする要素の直後にDOM内に配置するようにすることを推奨しています。このようにすることで、スクリーンリーダーなどの支援技術を使用するユーザーにとって、論理的なプログラム読み取り順序で公開されることを確保するのに役立ちます。
<button type="button" popovertarget="p1">開閉ボタン</button>
<div popover id="p1">ポップオーバーUI</div>
フォーカスの移動
基本的にトリガー要素からポップオーバー要素にフォーカスは移動しません。しかしポップオーバーの要素内にautofocus
属性をもつフォーカス可能要素があれば移動します。
<button type="button" popovertarget="p1">開閉ボタン</button>
<div popover id="p1">
<button autofocus>開くと同時にフォーカスが当たる要素</button>
</div>
この場合、ポップオーバーが閉じられたとき、フォーカスがポップオーバー要素の内側にある場合はトリガー要素にフォーカスが戻ります(ポップオーバー要素の外側の場合はそのままフォーカスは移動しません)。
ただしpopover="manual"
の場合は、ポップオーバーが閉じてもフォーカスは戻りません[7]。これは、JavaScriptで独自の実装をすることを想定しているためと考えられます。
CSS擬似クラス
Popover APIのページには擬似クラスについて言及はありませんが、デフォルトCSSの定義と、擬似クラスのページで仕様を確認することができます。
CSSでは擬似クラスが1つ適用されます。
-
:popover-open
: 表示時に適用されるクラス
dialog
要素での利用
結論から言うと、モーダルダイアログに利用することはできません。popovertarget
属性はshowModal
メソッドに相当する呼び出しができないからです。
<!-- showModalが呼び出されるわけではない -->
<button type="button" popovertarget="m1">開閉ボタン</button>
<dialog popover id="m1">ダイアログ</dialog>
またpopover
属性をもち、既にdialog
要素がポップオーバー要素として開いている状態でshowModal
を呼び出すとDOMException
エラーとなります[8]。
Popover APIは簡易非表示(Light Dismiss)が便利なため、モーダルダイアログでも利用したいところですがshowModal
メソッドが呼び出せないため、簡易非表示(Light Dismiss)が有効になりません。実装の際は注意が必要です。
id
属性を使わない実装
Element.popoverTargetElement
にポップオーバー要素のノードを渡すことで、id
属性を使わずに実装することができます。
<button type="button">開閉ボタン</button>
<div popover>ポップオーバーUI</div>
const button = document.querySelector("button");
button.popoverTargetElement = button.nextElementSibling;
これにより、不要なid
値を生成することなく実装することができます。Reactなどの実装でもuseRef
を使って同様のことができるでしょう。
ブラウザ実装状況
2023年9月現在でChromeとEdgeが対応しています。Safariはv17
から対応予定です。(HTMLElement API: popover | Can I use)
ポリフィル
Popover Attribute Polyfillが有志によって提供されています。SafariとFirefox向けに利用してみるとよいでしょう。
npm install @oddbird/popover-polyfill
-
button
要素と、type=submit
・type=button
・type=reset
・type=image
をもつinput
要素 ↩︎ -
実際、提案のゴールには “Avoid the need for any Javascript for most common cases.”(ほとんどの場合、Javascriptは必要ありません。)と記載されています ↩︎
-
簡易非表示という言葉は、日本語翻訳のページに合わせています ↩︎
-
2023年9月現在Chromeにて観測 ↩︎
-
暗黙的に
button
ロールになるbutton
要素とinput
要素にしかpopovertarget
属性は許可されないため ↩︎ -
ただし、ポップオーバーがアクセシビリティツリー上から消えるのでフォーカスは浮いた状態となります。 ↩︎
-
The showModal() method stepsのステップ4参照。2023年9月現在Chromeでは未実装 ↩︎
Discussion
2023年4月27日現在、
chrome://flags
から 「Experimental Web Platform features」のフラグをオンにすることで動作確認できました!参考: https://utilitybend.com/blog/open-ui-and-the-popover-api
上記記事では「Canaryで」と書いてありましたが、安定版のChrome 112で確認できました。
おお!情報ありがとうございます。
MDNやCan I useも更新されはじめたっぽいですね。
助かります🙏
Safari 17 (beta) で
popover
属性のサポートが表明されましたね。Firefox は、2024年4月16日リリースの Firefox 125 で対応しました。