Popover API - JavaScript不要、HTMLのみでポップオーバーUI
HTML Standardにpopover
属性をはじめとしたPopover APIが正式にマージされました。Open UIによって提案されていた[1]APIで、名前がPopoverなのかPopupなのか紆余曲折の末、やっとHTML Standardとなります。
現段階で実装されているブラウザは少ないですが、簡易サンプルを作ったので体験しながら読んでいただくといいかもしれません。
簡易サンプル
4つの属性
まずはHTMLの属性が新しく追加されているところに注目しましょう。
以上の3つが追加されました。popover
属性はポップオーバーする要素自体に。ポップオーバーターゲット属性はそれを展開するボタン[2]に対して設定します。
<div popover="auto" id="p1">ポップオーバーUI</div>
<button type="button" popovertarget="p1">開閉ボタン</button>
たったこれだけで、ポップオーバーUIが動作します。JavaScriptは不要です。ポップオーバーターゲット属性のidがマッチしていれば、どんなに離れた場所にある要素でも動作するので、遠隔操作版の<details>
・<summary>
と言ってもいいもしれません。
popover
属性が付与された要素は、デフォルトで非表示(display: none
状態)となり、展開された場合にはトップレイヤーに表示されます。トップレイヤーとは、簡単に言うと、z-index
をどんなに大きな数にしても超えられない表示レイヤーで、<dialog>
もここに表示されます。
ポップオーバーUIを制御するトリガー側の属性は2種類あります。
-
popovertarget
属性: 対象のポップオーバーUIのIDを指定します。 -
popovertargetaction
属性: アクションを種類を指定します。toggle
、show
、hide
のいずれかです。デフォルトはtoggle
なので、この属性は省略することが出来ます。
筆者の感想としては「本気でJavaScript不要にしたかったんだな[3]」ということが伝わってきます。
簡易非表示(Light Dismiss)
今回新たに簡易非表示(英語原版ではLight Dismiss)[4]という概念が登場しました。
そんなに難しいものではなく
- ESCキーで閉じる
- ポップオーバーUIの外側をクリックすると閉じる
という単純な機能ですね。外側をクリックは一般的によく採用されてきたパターンですし、ESCキーも同様にキーボード操作でも容易に閉じることができるでアクセシビリティも担保できています。
この簡易非表示は、popover="auto"
というように属性値にauto
を指定したときのみに自動的に有効になります。これもJavaScriptを使わずに有効になるのはとても有用です。
複数表示と自動非表示
popover
属性の値はauto
とmanual
を受け取りますが、この2つの違いは先に説明した簡易非表示以外もあります。
auto
は開いたときに他のポップオーバーUIを閉じます。一方、manual
は他のポップオーバーを閉じません。表にまとめると、
値 | 複数表示 | 他のポップオーバーUIを | 簡易非表示 |
---|---|---|---|
auto |
できない | 閉じる | 有効 |
manual |
できる | 閉じない | 無効 |
ということになります。目的合わせて使い分けるといいでしょう。
JavaScriptで利用するには
ここまでJavaScript不要と紹介していますが、使えないわけではありません。DOM APIとしてもきちんと操作可能なAPIを提供しているので、これを利用してより目的にあった実装をすることも可能です。
メソッド
3つのメソッドを使うことができます。
<div popover="auto" id="p1">ポップオーバーUI</div>
const element = document.querySelector("#p1");
element.showPopover();
element.hidePopover();
element.togglePopover();
// togglePopoverはfocusPreviousElementオプションを受け取りますが、詳細は調査中です
element.togglePopover(true);
イベント
イベントは2種類です。イベントオブジェクトは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で独自の実装をするために設けられたものとも考えることができます。うまく扱っていきたいところです。
アクセシビリティ
気になるアクセシビリティオブジェクトのマッピングですが、セマンティクスへの影響(つまりroleへの上書き)は基本的にないことになっています。しかし、popover
属性を付与した時点で非表示(display:none
)になるので、その時点ではアクセシビリティツリーから消えることを意味するので、その点は留意が必要です。表示された時点でセマンティクスが変化することはありません。
一方、ポップオーバーターゲット属性を付与したボタン側にはexpanded
ステートがアクセシビリティオブジェクトに公開されます[5]。つまりaria-expanded
を付与させたのと同様になるということですね。ここも自動的に制御されるのでJavaScript不要で、かゆいところに手が届いています。
読み上げに関する工夫
上記のようにセマンティクスの影響がないことや、あくまでexpanded
での関係になるので、モーダルダイアログと異なりフォーカスが移動することはありません。また、読み上げの順番に影響を与えることもありません。この場合にはいくつかの工夫が必要となるでしょう。
DOMの順番で工夫する
HTMLの記述の順番をボタン→ポップオーバーUIのとすることで、スクリーンリーダーのカーソルが自然と開いた要素に移動できるようにします。
<button type="button" popovertarget="p1">開閉ボタン</button>
<div popover="auto" id="p1">ポップオーバーUI</div>
ライブリージョンを利用する
ポップオーバーUIが表示されたタイミングで読み上げます。ただし、再度読みげたり詳細な読み上げ操作をするには、その要素に移動しないといけないので、場合によってはあまり上手い方法とは言えません。
<div popover="auto" id="p1" aria-live="polite">ポップオーバーUI</div>
<button type="button" popovertarget="p1">開閉ボタン</button>
他にも、autofocus
属性を使って強制的にフォーカス移動させる方法も考えることができますが[6]、トグルボタンに戻ってくる手段がJavaScriptを使う以外におそらくないため有効な方法とは言えません。そもそもdialog
要素のモードレスダイアログでフォーカスを移動させることの問題点が指摘されています。基本的にはフォーカスの強制移動は避けて、それでもどうしても必要な場合は十分にアクセシビリティを考慮して採用するべきでしょう。
CSS擬似クラス
CSSでは擬似クラスが2つ適用されます。
-
:open
: 表示時に適用されるクラス -
:closed
: 非表示時に適用されるクラス
基本的には非表示状態はdisplay: none
となるので積極的な活用方法はないように思いますが、transition
などをうまく利用したアニメーションを実装する場合には必要になるかも知れません。
dialog
要素での利用
結論から言うと、モーダルダイアログに利用することはできません。なぜかと言うと、ポップオーバーターゲット属性からJavaScript(DOM API)を使用せずに呼び出す場合、showModal
に相当する呼び出しができないからです。
<dialog popover="auto" id="m1">ダイアログ</dialog>
<!-- showModalが呼び出されるわけではない -->
<button type="button" popovertarget="m1">開閉ボタン</button>
さらに、明確に併用できないことがDOM APIで厳密に規定されました。popover
属性をもつdialog
要素がshowModal
を呼び出すとDOMException
エラーとなります[7]。
<dialog popover="auto" id="m1">ダイアログ</dialog>
const dialog = document.querySelector("#m1");
dialog.showModal();
// => DOMException例外が投げられます。
Popover APIはあくまでもモードレスなUIにのみ利用できる、という位置づけとなるようです。設計時、実装時ともに気をつけないといけません。
ブラウザ実装状況
2023年2月19日時点でGoogle Chrome Canaryのみで動作確認できていました。しかし、2023年3月に属性の名前や役割を変えるなどの仕様変更がありました。その影響か動作確認できるブラウザがありません。まだまだ仕様変更が起こる可能性があります。また、MDN(MDNへの追加のIssue)やCan I Useにも情報が作られていないので、徐々に公開されるのを待ちましょう。
Discussion