🎈

Popover API - JavaScript不要、HTMLのみでポップオーバーUI

2023/02/19に公開約6,700字
更新履歴

Git履歴

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属性: アクションを種類を指定します。toggleshowhideのいずれかです。デフォルトはtoggleなので、この属性は省略することが出来ます。

筆者の感想としては「本気でJavaScript不要にしたかったんだな[3]」ということが伝わってきます。

簡易非表示(Light Dismiss)

今回新たに簡易非表示(英語原版ではLight Dismiss[4]という概念が登場しました。

そんなに難しいものではなく

  • ESCキーで閉じる
  • ポップオーバーUIの外側をクリックすると閉じる

という単純な機能ですね。外側をクリックは一般的によく採用されてきたパターンですし、ESCキーも同様にキーボード操作でも容易に閉じることができるでアクセシビリティも担保できています。

この簡易非表示は、popover="auto"というように属性値にautoを指定したときのみに自動的に有効になります。これもJavaScriptを使わずに有効になるのはとても有用です。

複数表示と自動非表示

popover属性の値はautomanualを受け取りますが、この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種類です。イベントオブジェクトはnewStateoldStateというプロパティを持ちます。

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月に属性の名前や役割を変えるなどの仕様変更がありました。その影響か動作確認できるブラウザがありません。まだまだ仕様変更が起こる可能性があります。また、MDNMDNへの追加のIssue)やCan I Useにも情報が作られていないので、徐々に公開されるのを待ちましょう。

脚注
  1. Popover API (Explainer) ↩︎

  2. button要素と、type=submittype=buttontype=resettype=imageをもつinput要素 ↩︎

  3. 実際、提案のゴールには “Avoid the need for any Javascript for most common cases.”(ほとんどの場合、Javascriptは必要ありません。)と記載されています ↩︎

  4. 簡易非表示という言葉は、日本語翻訳のページに合わせています ↩︎

  5. 2023年2月現在 Chrome Canaryにて観測 ↩︎

  6. 実際、autofocus属性の仕様もポップオーバーと併用できる点が追記されています。差分 ↩︎

  7. 2023年2月現在 Chrome Canaryでは未実装 ↩︎

GitHubで編集を提案

Discussion

ログインするとコメントできます