- created_at
- updated_at
- tags
- toc
Dialog と Popover #5
Intro
このあたりから、まだ議論中の話が多いため、今後変わる可能性が高い点に注意。
popup
が紆余曲折を経てpopover
属性になり、2023/3 に Safari が TP166 で実装した。そのまま Safari 17 に入ることを 2023/6 の WWDC で発表したあたりから、popover
の実装は各ブラウザで一気に話が進む。
- Release Notes for Safari Technology Preview 166
- News from WWDC23: WebKit Features in Safari 17 beta
そして、2024/4 ごろに発表された Baseline 2024 にpopover
がエントリしたことで、2024 年は全ブラウザで互換性を高めていくことに合意し、作業を進めていくことになる。俗に言う「元年」というやつと言えるだろう。
- Popover API lands in Baseline
今回は、このpopover
の議論と、仕様が完成していくまでをまとめる。
Popover 属性の完成
popover
は属性になり、任意の要素を Popover できるようになった。
しかし、現実世界では、すでにpopover
という属性を独自に使っているサイトもいくつかあったので、ブラウザに実装したところ壊れるサイトも報告された。
- popover attribute may not be web compatible · Issue #9042 · whatwg/html
具体的には Angular UI がpopover
属性を独自に使っていたのだが、標準のpopover
は開くまではdisplay: none
がデフォルトであるため、ブラウザが対応した瞬間、当該要素が消えてしまうという問題だ。
- Some sites are reporting compat issues with popover [40270593] - Chromium
他にも、Chrome の Stable リリースに伴い、同じように壊れるサイトがいくつか報告される。
ここで筆者は、「もしかしたらまた名前が変わるかもしれない」と思ったりもしたが、今回は壊れたサイトが少なかったため、サイト側を直して close する方が選ばれた。本来の互換性の考え方からは強引と言えるが「独自の属性はdata-
をつけるのがルールであり、それを守っていないサイトまでカバーできない」という理由で切り捨てる形になった。確かにそれを擁護すると、たとえどんな名前に変えても、どこかしらのサイトは壊れることになるため、落とし所だったのかもしれない。
そうした作業を経て、ブラウザの実装も着々と進み、今では全ブラウザが一応 Ship している状態になり、Baseline の Newly Available に登録された。
- Baseline Newly Available
Dialog と Popover の違い
まずは、前半で解説した<dialog>
要素とpopover
属性の違いについて、整理しておく。
どちらも、Top Layer に「ポコッ」と浮かび上がる UI を作ることができる点では類似しているが、それぞれの用途はかなり異なる。
もっとも注目すべき点はRole だ。 ARIA には Dialog という Role が以前から定義されており、もし(概念上の)Dialog を自前で実装するのであれば 動きとしては、ここまで散々解説してきたような、role=dialog
<div role="dialog">
のように指定することで「これは Dialog だ」ということを明示し、UA に伝える必要があった。<div role="dialog"> <form> <label>Are you sure you want to continue?</label> <button type="submit">OK</button> </form></div>
inert
,::backdrop
, フォーカス管理, Top Layer などをネイティブで対応したshowModal
などの利点はもちろんあるが、セマンティクスの面で言えば、「<dialog>
はrole=dialog
に対応するネイティブの要素」という点も重要だ。
popover
一方、popover
は属性であり、それゆえに任意の要素を Popover することができる。
<div popover>Hello Popover</div>
popover
属性それ自体は、Role に関するセマンティクスを提供せず、付与した要素が持っている Role なりが、そのまま使用される。それを Top Layer に表示し、Light Dismiss できて、JS だけでなくpopovertarget
で操作でき、Anchor Positioning で配置できる。
popover
する要素のセマンティクスは別途考えないといけないわけだが、逆を言えば、どんなセマンティクスを付与された要素も、それをpopover
できるというメリットがある。今後「ポコっと浮かび上がる<selectmenu>
のような要素の標準化」を考える時が来ても、「ポコっと浮かび上がる」の部分は丸っと Popover に移譲できるため、Open UI が考えている様々な提案仕様にも、応用して仕様を整理できることが期待される。
「動きを与えるだけ」といえばそれまでだが、それでも Declarative に「この要素は Popover できる」ことを宣言でき、その API が標準化されることで、ブラウザは「今なにかが Popover した」といった事実を知ることができる。
これは、単に<div>
をz-index: 9999
などとしていた従来の実装と比べれば、格段に高い精度で UA に伝わり、ユーザにとっても UX の向上になると期待できるのだ。
Light Dismiss
<dialog>
とpopover
のもう 1 つ重要な違いは、Light Dismiss の存在だ。
特に Modal Dialog は、基本的にユーザをブロックすることに重きを置いているが、反対にpopover
は極力ユーザの邪魔にならないような挙動を求められる。そのため、<dialog>
同様に ESC などはもちろん、「戻る」ボタンや、Backdrop のクリック、他の Popover が開いた時など、よりカジュアルに閉じるような実装が可能になっている。
ここで使われているのが、#3 で解説した Close Watcher であり、必ずしも Modal だけがターゲットではない点が、"Modal Close Watcher" ではなく "Close Watcher" になった理由の 1 つでもある。
<dialog>
をpopover
する
<dialog>
はrole="dialog"
であることが重要だという話をした。
そこに対して、show()
/showModal()
で Modal として出すかどうかという使い分けをするのだが、実際には「role=dialog
を Popover で出したい」というユースケースもある。
この場合は「<dialog>
をpopover
する」という合わせ技も使える。
<dialog popover> <!-- ... --> <p>Ask me anything if you need help</p> <input type="text"> <button id="ask">ask</button> <!-- ... --> <button id="close">x</button></dialog>
これにより、特に Light Dismiss の恩恵で、カジュアルに閉じる<dialog>
が出せるため、non-Modal Dialog で応用できる。
一方で、Modal Dialog を Light Dismiss したいがために、popover
で実装し、::backdrop
を暗くすることで Modal っぽく実装するというのは、あまり良い実装ではないとされている。
<button popovertarget="foo">Click me</button><dialog popover id="foo">I'm a dialog!</dialog><style>dialog[popover]::backdrop { background-color: black;}</style>
ちなみに、showModal
したものも Light Dismiss したいというユースケースを<dialog>
側でサポートすべきかという議論はまだ進行中だ。
- Add light dismiss functionality to
<dialog>
· Issue #9373 · whatwg/html
逆に、Close Watcher を無効にし、一切 Light Dismiss 的な挙動をしない<dialog>
の提案についても議論はある。
- Support disabling CloseWatcher integration in
<dialog>
· Issue #10592 · whatwg/html
このあたりは、議論がある程度まとまったら追記したいと思う。
Popover Target
JS にはpopover
を開閉する API が用意されている。
showPopover()
hidePopover()
togglePopover()
そして、これは HTML だけで宣言的に記述できるようにもなっていた。
popovertarget
popovertargetaction
show
hide
toggle
<button popovertarget="foo" popovertargetaction="show"> Show a popover</button><article popover="auto" id="foo"> This is a popover article! <button popovertarget="foo" popovertargetaction="hide">Close</button></article>
基本的に Popover は明示的に閉じられるようにするのがプラクティスなので、x
アイコンを button としてpopover
の右上に表示し、それをaction=hide
にするのがプラクティスだろう。
Invoker
このpopovertarget
と同じように、開く閉じるの宣言的な実装を<dialog>
でも実現したいという要望が出た。最初はやはりdialogmodaltarget
だ。
dialogmodaltarget
attribute · Issue #834 · mozilla/standards-positions
しかし、このように追加していくとキリがないため、よりジェネリックな方向に進めるために提案されたのが Invoker だ。
- Intent to Prototype: Invokers
最初は属性名も Invoker だったが、今はcommand
という属性名になっている。
- [invokers] bikeshed the attribute names. · Issue #998 · openui/open-ui
ややこしいが、仕様(概念)名は Invoker で、属性名が Command という解釈だ。
<button commandfor="foo" command="show"> Show dialog</button><dialog id="foo"> This is a dialog <button commandfor="foo" command="hide">Close</button></dialog>
この仕様はpopover
にも逆輸入され、現在はpopover
もcommand
で開けるようにしていく方針になっている。(つまり、いずれpopovertarget
は消えるかもしれないので、これから実装する場合は最新の議論に注意したい)
<button commandfor="foo" command="show"> Show Popover</button><div id="foo" popover> This is a Popover <button commandfor="foo" command="hide">Close</button></div>
Anchor Positioning
Anchoring はpopover
と同時に策定されていた、今後かなり重要になる仕様の 1 つだ。
まず、先ほどの例を考えてみる。
<button commandfor="foo" command="show"> Show Popover</button><div id="foo" popover> This is a Popover <button commandfor="foo" command="hide">Close</button></div>
このとき、Popover した<div>
を<button>
の右下に表示したいとしよう。
通常の DOM であれば、<button>
を基準にし、<div>
を相対的に配置すれば良いが、今<div popover>
は Top Layer に、<button>
はその backdrop に表示されているため、2 つを相対的に配置することができないのだ。
<div popover>
が表示されている Top Layer には、他の DOM が何もない状態なので、何らかの方法で<div popover>
の座標などを渡されない限りは、「画面の真ん中」や「四隅」といった、絶対値指定できる場所くらいしか、配置のしようがない。
そこで、Anchor という概念を導入し、「開いた<button>
を Anchor として、開かれた側はその Anchor の右上に表示する」といった指定ができるようにした。これが Anchor Positioning だ。
Anchor Attributes
当初、Anchor は HTML 属性で指定する提案がされていた。
<button id="button" commandfor="foo" command="show"> Show Popover</button><div id="foo" anchor="button" popover> This is a Popover <button commandfor="foo" command="hide">Close</button></div><style>[popover] { top: anchor(bottom); left: anchor(right);}</style>
この場合、<div popover>
の Anchor を<button>
としている。
top: anchor(bottom)
は、自分の上端が Anchor の下端になるという指定だ。left: anchor(right)
は自身の左端を Anchor の右端にしているため、結果、<button>
に対して右下に表示されるというものだ。
そして、何よりもこの Anchor 属性が独立していることで、Popover ではない要素でも Anchor 関係を指定し、配置することが可能なのだ。これは、従来の Flex, Grid などに並んで、新しい CSS の設計に影響する、重要な要素であると筆者は考えている。
Position Area Grid
Anchor に対して相対配置する場合は、Anchor となる要素の周囲のどこかに配置することになるため、先ほどの「左上」や「右下」などの指定は頻出することになる。
このケースに対して、anchor()
を用いて指定する代わりに「Anchor の周囲のどこに配置するか」を指定できるように導入されたのが Position Area Grid だ。
position-anchor
を指定した要素にposition-area
で、表示したい位置を指定する。(もともとはinset-area
という名前だったが、指定する側からすれば Position なのでこの名前に変更された。)
先ほどの「右下」をposition-anchor
を使い、ロジカルプロパティで指定するなら、以下のようになる。
[popover] { position-anchor: --anchor; position-area: inline-end block-end;}
Anchor Position の fallback
先の例で、<div popover>
を表示するための余白が<button>
の右下に無かった場合、Popover がはみ出して表示されることになる。
これを避けるために、フォールバックという仕組みを導入し、「はみ出る場合は、別のスタイルに切り替える」という指定をするためのposition-try-options
が提案された。
これは後にposition-try-fallbacks
と名前を変え、例えばflip-inline
を指定すれば Inline 方向(つまり左右)を逆にするようフォールバックできる。
[popover] { top: anchor(end); left: anchor(right); position-try-fallbacks: flip-inline;}
Block 方向(つまり上下)であればflip-block
でフォールバックできる。
より細かくスタイルを指定したい場合は、フォールバックの先を@position-try
に複数指定し、それを並べて順に試行させることができる。複数適用できる場合の優先順位はposition-try-order
に指定可能だ。
[popover] { ... position-try-fallbacks: --first, --second, flip-inline, flip-block; position-try-order: most-height;}@position-try --first { top: anchor(end); right: anchor(self-start);}@position-try --end { /* .... */}
これで、どんな要素に Invoke されても、Top Layer 上に表示されつつ、さらに画面内にしっかりと収まるようなpopover
が表示できるのだ。
特に縦長になりがちな Menu 系の UI を、適切に表示する上で重宝する。 このように、要素の配置に革新をもたらすであろう Anchor だが、HTML 属性であるにも関わらず、明らかにスタイルマターなユースケースであるために、「セマンティクスは何か」という議論がされることになった。 そこで、HTML の Anchor 属性は一旦保留し、同様の機能を CSS 側で定義したのが、 指定は以下のように、HTML で定義していた関係を CSS Variable で定義した名前を用いて紐づける仕様になっている。 まず ここから、さらに余白を広げたいといった場合は、CSS Anchor Name
anchor-name
とposition-anchor
だ。<style>button { anchor-name: --anchor;}[popover] { position-anchor: --anchor; position: absolute; top: anchor(bottom); left: anchor(center);}</style><button>button</button><div id="message" popover> popover</div>
<button>
側でanchor-name
に CSS Variable で名前をつけ、popover
側はposition-anchor
でその名前を指定し、anchor()
で相対位置を指定する。translate
などで位置を調整することになるだろう。
Invoker Relationship
スタイルマターであるとはいえ、Anchor を指定するのに CSS でいちいち名前をつけて紐づけるというのは、Invoker と Popover の関連が HTML だけで完結できず、同時に CSS でも関連づけないと不整合が起こる点で不便だ。
特に動的に関連付けを変更したいような場合には、<div style="position-anchor: --anchor">
などと HTML 側でいじる必要が出るため、あまり使いやすい API ではない。
議論を進めた結果、「少なくとも Invoker と Popover の間には、暗黙的な Anchor の関係を見出しても良いだろう」ということになり、先のように<button commandfor>
で開いた<div popover>
との間には、暗黙的に Anchor の関係がある、ということになったのだ。これを Invoker Relationship と呼ぶ。
つまり、以下の例は HTML のanchor
属性も、CSS のanchor-name
もないが、anchor()
を使った配置ができていることに注目したい。
<style>[popover] { top: anchor(bottom); left: anchor(center);}</style><button id="button" commandfor="foo" command="show"> Show Popover</button><div id="foo" popover> This is a Popover <button commandfor="foo" command="hide">Close</button></div>
HTML Anchor Attributes の今後
Invoker Relationship が暗黙的に作られる場合は、Anchor 名を明示する必要がなくなった。それでもなお、動的に<popover>
を作り、JS で開くような場合は不便がある。
例えば、GitHub のリンクをマウスオーバーした場合、Issue なら概要、User ならプロフィールが Popover されるだろう。プロフィールの場合は、アイコンが Anchor になる。
このような Popover は、画面中のすべてのアカウントごとに<div popover>
を作っておくのではなく、1 つ用意してその内容を書き換えながら再利用する実装が多い。
つまり、Popover (プロフィール)側は変わらないが、Anchor (アイコン)側が動的に変わっていくわけだが、HTML のcommand
で開いているわけではなくonmouseover
などをフックして JS で開くため、暗黙的な Invoker Relationship も生成されない。
この実装として、<div popover>
側にposition-anchor: --user-icon
を指定しておいたとすると。
<style>[popover] { position-anchor: --user-icon; position: absolute; bottom: anchor(top); left: anchor(right);}</style>
JS でmouseover
された要素を、動的にanchor-name: --user-icon
に変えていく必要がある。以下のようなイメージだ。
document.querySelectorAll("img.icon").forEach((img) => { img.addEventListener("mouseover", (e) => { e.target.style.anchorName = "--user-icon" // 終わったら消す })})
対象のイベントで、動的にanchor-name
を変更し、終わったらanchor-name
を消す必要がある。消し忘れると、他とかぶって意図しない場所に表示される可能性もある。名前を変えても良いが、その場合は Popover 側の管理も必要になる。
もし HTML で Anchor 要素が使えるのであれば、<a>
が持つ ID を指定して、<div popover>
側を変えればよくなる。
<style>[popover] { position-anchor: --anchor; position: absolute; top: anchor(bottom); left: anchor(center);}</style><script>document.querySelectorAll("img.icon").forEach((img) => { img.addEventListener("mouseover", (e) => { $popover.anchor = e.target.id })})</script>
すでにある HTML に、Popover を後から追加する上でも、この方が実装は容易であり、管理しやすいため、筆者としてはこの機能が戻ってくることを心待ちにしている。
次回は Popover や Dialog につきものの、アニメーション関連の仕様について触れる。
DEMO
動作するデモを以下に用意した。執筆時点では Chrome Canary 131 を元に確認している。
- Popover Labs | labs.jxck.io
Links
- Popover API (Explainer) | Open UI
- Popover=hint (Explainer) | Open UI
- Invoker Commands (Explainer) | Open UI
- Dialogs and popovers seem similar. How are they different? | hidde.blog
- Semantics and the popover attribute: which role to use when? | hidde.blog
- Positioning anchored popovers | hidde.blog
- On popover accessibility: what the browser does and doesn't do | hidde.blog