Movatterモバイル変換


[0]ホーム

URL:


created_at
updated_at
tags
toc

Dialog と Popover #5

Intro

このあたりから、まだ議論中の話が多いため、今後変わる可能性が高い点に注意。

popup が紆余曲折を経てpopover 属性になり、2023/3 に Safari が TP166 で実装した。そのまま Safari 17 に入ることを 2023/6 の WWDC で発表したあたりから、popover の実装は各ブラウザで一気に話が進む。

そして、2024/4 ごろに発表された Baseline 2024 にpopover がエントリしたことで、2024 年は全ブラウザで互換性を高めていくことに合意し、作業を進めていくことになる。俗に言う「元年」というやつと言えるだろう。

今回は、このpopover の議論と、仕様が完成していくまでをまとめる。

Popover 属性の完成

popover は属性になり、任意の要素を Popover できるようになった。

しかし、現実世界では、すでにpopover という属性を独自に使っているサイトもいくつかあったので、ブラウザに実装したところ壊れるサイトも報告された。

具体的には Angular UI がpopover 属性を独自に使っていたのだが、標準のpopover は開くまではdisplay: none がデフォルトであるため、ブラウザが対応した瞬間、当該要素が消えてしまうという問題だ。

他にも、Chrome の Stable リリースに伴い、同じように壊れるサイトがいくつか報告される。

ここで筆者は、「もしかしたらまた名前が変わるかもしれない」と思ったりもしたが、今回は壊れたサイトが少なかったため、サイト側を直して close する方が選ばれた。本来の互換性の考え方からは強引と言えるが「独自の属性はdata- をつけるのがルールであり、それを守っていないサイトまでカバーできない」という理由で切り捨てる形になった。確かにそれを擁護すると、たとえどんな名前に変えても、どこかしらのサイトは壊れることになるため、落とし所だったのかもしれない。

そうした作業を経て、ブラウザの実装も着々と進み、今では全ブラウザが一応 Ship している状態になり、Baseline の Newly Available に登録された。

Dialog と Popover の違い

まずは、前半で解説した<dialog> 要素とpopover 属性の違いについて、整理しておく。

どちらも、Top Layer に「ポコッ」と浮かび上がる UI を作ることができる点では類似しているが、それぞれの用途はかなり異なる。

もっとも注目すべき点はRole だ。

role=dialog

ARIA には 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> 側でサポートすべきかという議論はまだ進行中だ。

逆に、Close Watcher を無効にし、一切 Light Dismiss 的な挙動をしない<dialog> の提案についても議論はある。

このあたりは、議論がある程度まとまったら追記したいと思う。

Popover Target

JS にはpopover を開閉する API が用意されている。

そして、これは HTML だけで宣言的に記述できるようにもなっていた。

<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 だ。

しかし、このように追加していくとキリがないため、よりジェネリックな方向に進めるために提案されたのが Invoker だ。

最初は属性名も Invoker だったが、今はcommand という属性名になっている。

ややこしいが、仕様(概念)名は 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 にも逆輸入され、現在はpopovercommand で開けるようにしていく方針になっている。(つまり、いずれ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> の座標などを渡されない限りは、「画面の真ん中」や「四隅」といった、絶対値指定できる場所くらいしか、配置のしようがない。

Top Layer の真ん中に表示された Popover

そこで、Anchor という概念を導入し、「開いた<button> を Anchor として、開かれた側はその Anchor の右上に表示する」といった指定ができるようにした。これが Anchor Positioning だ。

button を anchor としその右下に Popover を表示

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> に対して右下に表示されるというものだ。

button を anchor として右下に popover を表示する

そして、何よりもこの Anchor 属性が独立していることで、Popover ではない要素でも Anchor 関係を指定し、配置することが可能なのだ。これは、従来の Flex, Grid などに並んで、新しい CSS の設計に影響する、重要な要素であると筆者は考えている。

Position Area Grid

Anchor に対して相対配置する場合は、Anchor となる要素の周囲のどこかに配置することになるため、先ほどの「左上」や「右下」などの指定は頻出することになる。

このケースに対して、anchor() を用いて指定する代わりに「Anchor の周囲のどこに配置するか」を指定できるように導入されたのが Position Area Grid だ。

Anchor を中心に 9 個に分割したグリッド

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 がはみ出して表示されることになる。

anchor で右下に表示した popover がはみ出す

これを避けるために、フォールバックという仕組みを導入し、「はみ出る場合は、別のスタイルに切り替える」という指定をするためのposition-try-options が提案された。

これは後にposition-try-fallbacks と名前を変え、例えばflip-inline を指定すれば Inline 方向(つまり左右)を逆にするようフォールバックできる。

[popover] {  top: anchor(end);  left: anchor(right);  position-try-fallbacks: flip-inline;}

左でははみ出す popover を anchor の右にフォールバック表示

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 を、適切に表示する上で重宝する。

CSS Anchor Name

このように、要素の配置に革新をもたらすであろう Anchor だが、HTML 属性であるにも関わらず、明らかにスタイルマターなユースケースであるために、「セマンティクスは何か」という議論がされることになった。

そこで、HTML の Anchor 属性は一旦保留し、同様の機能を CSS 側で定義したのが、anchor-nameposition-anchor だ。

指定は以下のように、HTML で定義していた関係を CSS Variable で定義した名前を用いて紐づける仕様になっている。

<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 になる。

GitHub のアカウントをマウスオーバーすると開くプロフィールのポップオーバー

このような 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 を元に確認している。

Links



[8]ページ先頭

©2009-2025 Movatter.jp