本セッションの登壇者
セッション動画
「PopoverAPI、HTMLだけでアクセシブルなポップアップUI」というタイトルで発表します。よろしくお願いします。
小林大輔といいます。サイボウズ株式会社で製品のデザインシステムの構築に関わっています。その他、社内のアクセシビリティに関する啓発や改善活動にも携わっています。また副業として今年初めに『Webアプリケーションアクセシビリティ』という本を執筆しました。興味のある方はぜひお手に取っていただければと思います。

今回のテーマはポップオーバーです。皆さんは「ポップオーバー」と聞くと、どんなUIを思い浮かべるでしょうか?
今、画面には、今回紹介するPopover APIで実現できるUIが表示されています。吹き出しによるメッセージ、メニュー、カレンダーピッカーのようなUIが並んでいます。これらのUIはどういう特徴を持つでしょうか?

ポップオーバーを定義づける特徴とは
Popover APIはOpen UIというグループから提唱されたものです。Open UIは「典型的なポップオーバーは3つの特徴を持つ」と言っています。
1つ目は、画面の一番上のレイヤーに表示されるということです。
2つ目は、一時的に表示されるということです。ポップオーバーの外をクリックするなど、特定の操作によってポップオーバーがすぐに閉じるということです。
3つ目はひとつずつ表示されるということです。新しいポップオーバーが開くと、もともと開いていた古いポップオーバーは閉じます。
こういった特徴はHTMLだけで実装するのは難しくて、CSSやJavaScriptを駆使する必要があったんですけれども、Popover APIで簡単に実装できるようになりました。

早速デモをお見せしたいと思います。
今、画面にボタンが2つあって、ボタンを押すとポップオーバーが表示されます。

ポップオーバーの外をクリックするとポップオーバーが閉じます。

ポップオーバーを開いて、もう1個開くと、最初のポップオーバーが閉じます。これらの開閉は全部HTMLだけで実装されています。

3つの属性で思いどおりのポップオーバーを実現
ここからは実装の解説をしていきたいと思います。Popover APIの仕様では新たに3つの属性が追加されています。まず、ポップオーバーであるということを示すpopover
属性です。必要に応じてポップオーバーの基本的な振る舞いを定める属性でもあります。これについては後で解説したいと思います。
それからpopovertarget
とpopovertargetaction
というのは、ポップオーバーを開閉するボタンにつける属性です。popovertarget
は、開閉の対象となるポップオーバーのIDを指定して関連していることを示す属性です。popovertargetaction
は、ボタンを押したときにポップオーバーをどうするのかという操作を指定する属性です。開く(show)、閉じる(close)、開閉する(toggle)の3つから選ぶことができます。

今お見せしたデモのソースコードの一部を表示しています。ポップオーバーを開くbutton
要素があって、その下にポップオーバー自身であるdiv
要素が置かれていることがわかります。div
要素にはpopover
属性がついていて、これがポップオーバーであるということを示しています。
button
要素にはpopovertarget
とpopovertargetaction
の2つの属性が指定されていて、popovertarget
にはそのポップオーバーのIDが指定されています。popovertargetaction
にはtoggle
という値が指定されていて、これはボタンを押すごとに開く閉じるを繰り返すという意味です。

Popover APIで実装することで、先ほど紹介した一番上に表示される、一時的に表示される、ひとつずつ表示されるという3つの特徴を簡単に実現できます。
まず、一番上に表示されるということについては、Popover APIで実装したポップオーバーは、Top layerに配置されます。Top layerというのは、フルスクリーンモードの要素やモーダルなdialog
要素と同じスタイルです。
次に、一時的に表示されるということについては、Popover APIの中にはlight dismissという簡易非表示の仕様があって、いろいろな操作で簡単に閉じることができるようになっています。ポップオーバーの外をクリックしても閉じますし、Escキーを押しても閉じることができます。
最後にひとつずつ表示されることについては、デモでお見せした通り、特別な実装をしなくても一度にひとつずつポップオーバーを表示できます。

Popover APIで細かな制御も可能に
今、3つの特徴を紹介しましたが、場合によっては一部の特徴が必要ない場合もあると思います。
たとえば、枠外をクリックしても閉じる必要がないポップオーバーもあるでしょう。複数のポップオーバーが連携するサブメニューみたいな場合には、同時に表示する場合もあると思います。そういった場合はどういうふうに実装すればいいでしょうか?
実はpopover
属性には”auto”
と”manual”
という2つの値があって、”manual”
を指定することで、一時的に表示する機能とひとつずつ表示する機能をオフにできます。これを駆使することによって、例外的なポップオーバーもPopover APIを使って簡単に実装できるようになっています。単にpopover
とした場合はデフォルトの”auto”
になるので、3つ全部の属性を兼ね備えたポップオーバーが生成されます。

現在のサポート状況としては、Popover APIがいよいよすべてのモダンブラウザで実装されつつあるフェーズです。
Firefoxではフラグをオンにすることで機能できるという状況で、Safariは17から対応されます。多くのブラウザで使えるようになってほしいと思っています。

常にキーボードのみで操作できる
ここまでPopover APIの概要についてお話してきたんですけれども、ここからはPopover APIのアクセシビリティについてのお話をしてみたいと思います。Popover APIを利用することでアクセシビリティに大きく3つのメリットがあると考えています。1つ目はキーボード操作がサポートされること。2つ目は支援技術に役割や状態が通知されること。3つ目はDOMの順序が正しくなることです。ひとつずつ解説します。

Popover APIを使用すると、スペースキーで簡単にポップオーバーを開閉できます。これはbutton
要素のデフォルトの挙動です。Popover APIのpopovertarget
属性とpopovertargetaction
属性をサポートしているのがbutton
要素だけだからです。Popover APIに制約をかけて対象をキーボードでもクリックでも開けるbutton
要素に限定することで、Popover APIを使えば必ずキーボードで開閉できることが保証されています。
あとは先ほど紹介したEscキーです。light dismissの機能があるのでPopover APIはEscキーで閉じることができます。キー操作としては、もう少しきめ細かいこともサポートされています。

まず、開いたときにポップオーバー内にフォーカスを当てることができます。通常はボタンにとどまるのですけれども、ポップオーバーの中にautofocus
属性がつけられた要素があると、その要素にフォーカスが移動します。たとえば、入力欄にこの属性をつけておくことで、簡単にすぐ入力を始められます。Escキーで閉じるとポップオーバーを開いた元のボタンにフォーカスを戻すということまでやってくれます。行き帰りがすごくスムーズになって、キーボード操作がやりやすくなるということがPopover APIのメリットです。

スクリーンリーダーフレンドリーな機能
次に支援技術への通知についてですが、Popover APIを使うとポップオーバーに付与すべき基本的なロールやステートをサポートしてくれます。まず、ボタンについては先ほどの説明の通り、button
要素にしか基本的に使えないのでbutton
ロールがサポートされます。その上で開閉状態もサポートしてくれます。具体的には、ポップオーバーが開いているときにはExpandedステートがtrue
になり、それ以外はfalse
になります。なので、たとえばスクリーンリーダーでこのボタンに差し掛かると「ボタンだよ」ということと、ポップオーバーが開いているか、閉じているかという情報を読み上げてくれます。
ポップオーバー自体に明確なロールがない場合で、さっきみたいにdiv
要素で作った場合には、group
ロールが付与されることになっています。スクリーンリーダーでポップオーバーの中に入ると「グループ」と言ってくれて、外に出ると「グループの外」みたいに読み上げてくれるので、ここからここまでがグループでポップオーバーの中なんだということがユーザーにわかりやすいメリットがあります。このように、支援技術についてもかなりサポートがされています。

最後のメリット、DOMの順序が正しくなるということについて説明をしておきます。皆さんにひとつ考えてほしいんですけれども、皆さんがポップオーバーを実装する場合、DOM上のどこに配置するでしょうか?今、画面に2つのHTMLが書かれています。左側のHTMLは、<div popover/>
がbody
要素の末尾に配置されています。body
要素の先頭にあるポップオーバーを開閉するボタンとは距離が空いているという感じです。右側のHTMLは<div popover/>
が開閉ボタンの直後に配置されています。実はどちらの方法にもメリットとデメリットがあるのですが、アクセシビリティに関する問題が起きるのは得てして末尾に配置された場合です。

ポップオーバーをbody
の末尾に配置すると何が起こるかというと、基本的にスクリーンリーダーでポップオーバーを見つけられなくなります。スクリーンリーダーはHTML順に要素を読んでいくので、ボタンを押してすぐ下にポップオーバーが開いた場合に、ユーザーはボタンの下にポップオーバーがあると認識するわけですけれども、実際にはそこではなく、ページの末尾にあるのでポップオーバーが見つからないという状況になります。
これについてはポップオーバー自体やポップオーバーの中にフォーカスを当てれば解決はできるのですが、そうするとまた別の問題が起きます。ボタンを押すとフォーカスがポップオーバーに移動して中身を読むわけですけれども、そこからポップオーバーの外に出ると、ユーザー体験としてはいきなりページの末尾に移動することになります。自分が今いる場所はどこだろう、元のトグルボタンに戻るためにはどうすればいいんだろうとすごく混乱してしまうので、アクセシビリティを考えるとbody
の末尾にポップオーバーを置くべきではないと言われています。

一方、ポップオーバーをボタンの直後に配置した場合は、ポップオーバーが一番上に表示されないという問題が起きます。特にスタッキングコンテキストで囲まれると、ポップオーバーが見切れてうまく表示されないという状況が起きます。いずれにしても、ポップオーバーの実装にはアクセシビリティの問題や重ね合わせの問題が常につきまとうということです。

Popover APIを使うとどうなるかというと、この2つの問題を両方解決できるんです。先ほどお伝えした通り、Popover APIを使うと、ポップオーバーは他の要素より常に上に表示されます。ボタンの次の要素をポップオーバーにしておきさえすれば、それだけで重ね合わせの問題も、読み上げの順序の問題も両方解決できます。とても優れたユーザー体験になると言えます。

誰にでも使えるポップオーバーをHTMLだけで簡単に!
まとめです。Popover APIを使うと典型的なポップオーバーをHTMLだけで簡単に実装することができます。また、Popover APIはアクセシビリティも考慮されているので、今後、使っていっていただければいいかなと思います。

発表は以上となります。ご清聴ありがとうございました。
In the bitlife Life Simulator, you will play a simulation game where you must make life-changing choices. For instance, you could wed the love of your life, start a family, and further your education.