ポップアップ: 復活!

Open UI イニシアチブの目標は、デベロッパーが優れたユーザー エクスペリエンスを実現しやすくすることです。そのために、デベロッパーが直面する、より問題の多いパターンへの取り組みを進めています。これは、プラットフォームに組み込まれたより優れた API とコンポーネントを提供することで実現できます。

そうした問題領域の一つがポップアップです。Open UI では「ポップオーバー」と呼びます。

ポップオーバーは長い間、二極化の評判を呼んでいました。これは部分的には、ビルドとデプロイの方法に起因しています。効果的なパターンとしては簡単ではありませんが、上手に使うと、ユーザーを特定のものに誘導したり、サイトのコンテンツを認知してもらうことで、大きな価値を生むことができます。

ポップオーバーを作成する際は、多くの場合、次の 2 つの大きな懸念事項があります。

  • 他のコンテンツよりも上の適切な場所に配置する方法。
  • ユーザー補助機能(キーボード操作に対応する、フォーカス可能など)にする方法。

組み込みの Popover API にはさまざまな目標があり、デベロッパーがこのパターンを簡単に構築できるようにするという総合的な目標があります。主な目標は次のとおりです。

  • 要素とその子孫がドキュメントの他の部分の上に簡単に表示されるようにします。
  • アクセスしやすくする。
  • ほとんどの一般的な動作(簡易的な拒否、シングルトン、スタッキングなど)では JavaScript を必要としません。

ポップアップの完全な仕様については、OpenUI のサイトをご覧ください。

ブラウザの互換性

組み込みの Popover API は現在どこで使用できるでしょうか。Chrome Canary では「試験運用版ウェブ プラットフォーム機能」によりサポート指定することもできます。

このフラグを有効にするには、Chrome Canary を開いて chrome://flags にアクセスします。次に、[試験運用版のウェブ プラットフォーム機能] を有効にします設定されます。

これを本番環境でテストしたいデベロッパー向けのオリジン トライアルもあります。

最後に、API 用に開発中のポリフィルがあります。github.com/oddbird/popup-polyfill でリポジトリを確認してください。

ポップアップのサポート状況は、以下で確認できます。

const supported = HTMLElement.prototype.hasOwnProperty("popover");

現在のソリューション

コンテンツを何よりも優先して宣伝するために、現在できることは何ですか。ブラウザでサポートされている場合は、HTML ダイアログ要素を使用できます。「モーダル」で使用する必要がありますフォームに入力します。そのためには JavaScript を使用する必要があります。

Dialog.showModal();

アクセシビリティに関する考慮事項がいくつかあります。たとえば、Safari バージョン 15.4 より前のユーザーを対象とする場合は、a11y-dialog を使用することをおすすめします。

また、ポップオーバー、アラート、ツールチップ ベースの多くのライブラリのいずれかを使用することもできます。これらの多くは同じように機能する傾向があります。

  • ポップオーバーを表示するために、本文にコンテナを追加します。
  • 他の何よりも上になるようにスタイルを設定する。
  • 要素を作成してコンテナに追加し、ポップオーバーを表示します。
  • これを非表示にするには、DOM からポップオーバー要素を削除します。

そのためには、追加の依存関係が必要となり、デベロッパーにとってはより多くの判断が求められます。また、必要なものをすべて提供するサービスを見つけるには、調査も必要です。Popover API は、ツールチップを含むさまざまなシナリオに対応することを目的としています。目標は、これらすべての一般的なシナリオをカバーし、デベロッパーが新たな決定を下す必要がなくなり、デベロッパーがエクスペリエンスの構築に集中できるようにすることです。

最初のポップアップ

これだけで十分です。

<div id="my-first-popover" popover>Popover Content!</div>
<button popovertoggletarget="my-first-popover">Toggle Popover</button>

ここでは何が起きているのでしょうか。

  • このポップオーバー要素は、コンテナなどに入れる必要はありません。デフォルトでは非表示になっています。
  • JavaScript を記述しなくても表示させることができます。これは popovertoggletarget 属性で処理されます。
  • これが表示されると、最上位のレイヤにプロモートされます。つまり、ビューポートの document の上にプロモートされます。z-index を管理したり、ポップオーバーが DOM 内のどこにあるかを気にする必要はありません。祖先をクリップして、DOM の深い階層にネストすることもできます。DevTools で、現在最上位のレイヤにある要素を確認することもできます。最上位レイヤについて詳しくは、こちらの記事をご覧ください。

DevTools の最上位レイヤのサポートのデモを示す GIF

  • 「簡易を閉じる」が表示されるすぐに使えるつまり、ポップオーバーの外側をクリックする、キーボードで別の要素に移動する、Esc キーを押すなど、閉じるシグナルでポップオーバーを閉じることができます。もう一度開いて、試してみましょう。

ポップオーバーには、他にどのようなメリットがありますか?さらに詳しく見てみましょう。ページ上のコンテンツを使用したこのデモを考えてみましょう。

フローティング アクション ボタンの位置は固定されており、z-index が高く設定されています。

.fab {
  position: fixed;
  z-index: 99999;
}

ポップオーバーのコンテンツは DOM 内にネストされていますが、ポップオーバーを開くと、そのポップオーバーが固定位置の要素より上に表示されます。スタイルを設定する必要はありません。

また、ポップオーバーに ::backdrop 疑似要素が追加されました。最上位レイヤのすべての要素には、スタイル設定可能な ::backdrop 疑似要素が割り当てられます。この例では、低めのアルファ背景色と背景フィルタで ::backdrop のスタイルを設定しています。これにより、基になるコンテンツがぼかされます。

ポップオーバーのスタイルを設定する

ポップオーバーのスタイル設定に目を向けましょう。デフォルトでは、ポップオーバーの位置は固定され、パディングが適用されます。また、display: none もあります。これをオーバーライドしてポップオーバーを表示できます。最上位レイヤにプロモートされません。

[popover] { display: block; }

ポップオーバーをプロモートする方法にかかわらず、最上位レイヤにポップオーバーをプロモートしたら、レイアウトや配置が必要になることがあります。最上位レイヤをターゲットにして、

:open {
  display: grid;
  place-items: center;
}

デフォルトでは、ポップオーバーは margin: auto を使用してビューポートの中央に配置されます。ただし、場合によっては、ポジショニングを明示したい場合もあります。例:

[popover] {
  top: 50%;
  left: 50%;
  translate: -50%;
}

CSS グリッドやフレックスボックスを使用してポップオーバー内にコンテンツを配置したい場合は、要素でラップすることをおすすめします。それ以外の場合は、ポップオーバーが最上位レイヤに配置されたら display を変更する別のルールを宣言する必要があります。デフォルトに設定すると、display: none をオーバーライドしてデフォルトで表示されます。

[popover]:open {
 display: flex;
}

このデモを試してみると、ポップオーバーが入出力に切り替わることがわかります。ポップオーバーを切り替えるには、:open 疑似セレクタを使用します。:open 疑似セレクタは、表示されている(つまり最上位レイヤにある)ポップオーバーに一致します。

この例では、カスタム プロパティを使用して移行を促進しています。また、ポップオーバーの ::backdrop にも遷移を適用できます。

[popover] {
  --hide: 1;
  transition: transform 0.2s;
  transform: translateY(calc(var(--hide) * -100vh))
            scale(calc(1 - var(--hide)));
}

[popover]::backdrop {
  transition: opacity 0.2s;
  opacity: calc(1 - var(--hide, 1));
}


[popover]:open::backdrop  {
  --hide: 0;
}

ここでのヒントは、動きのメディアクエリで切り替えとアニメーションをグループ化することです。これはタイミングの維持にも役立ちます。これは、カスタム プロパティを介して popover::backdrop の間で値を共有できないためです。

@media(prefers-reduced-motion: no-preference) {
  [popover] { transition: transform 0.2s; }
  [popover]::backdrop { transition: opacity 0.2s; }
}

これまでに、popovertoggletarget を使用してポップオーバーを表示してきました。閉じるには [簡易表示] を使用します。ただし、使用可能な popovershowtarget 属性と popoverhidetarget 属性も取得できます。それを非表示にするボタンをポップオーバーに追加し、popovershowtarget を使用するように切り替えボタンを変更しましょう。

<div id="code-popover" popover>
  <button popoverhidetarget="code-popover">Hide Code</button>
</div>
<button popovershowtarget="code-popover">Reveal Code</button>

前述のとおり、Popover API は従来のポップアップという概念にとどまらず、通知、メニュー、ツールチップなど、あらゆる種類のシナリオに対応できます。

これらのシナリオによっては、異なるインタラクション パターンが必要になります。マウスオーバーなどの操作。popoverhovertarget 属性の使用はテストされましたが、現在は実装されていません。

<div popoverhovertarget="hover-popover">Hover for Code</div>

要素にカーソルを合わせてターゲットを表示するという考え方です。この動作は CSS プロパティで設定できます。これらの CSS プロパティでは、ポップオーバーが反応する要素のオン / オフを切り替える時間を定義します。テスト対象のデフォルトの動作では、:hover の明示的な 0.5s の後にポップオーバーが表示されていました。その場合は、軽く閉じるか、別のポップオーバーを開く必要があります(詳しくは後述)。これは、ポップオーバーの非表示の時間を Infinity に設定したことが原因でした。

それまでの間は、JavaScript を使用してこの機能をポリフィルできます。

let hoverTimer;
const HOVER_TRIGGERS = document.querySelectorAll("[popoverhovertarget]");
const tearDown = () => {
  if (hoverTimer) clearTimeout(hoverTimer);
};
HOVER_TRIGGERS.forEach((trigger) => {
  const popover = document.querySelector(
    `#${trigger.getAttribute("popoverhovertarget")}`
  );
  trigger.addEventListener("pointerenter", () => {
    hoverTimer = setTimeout(() => {
      if (!popover.matches(":open")) popover.showPopOver();
    }, 500);
    trigger.addEventListener("pointerleave", tearDown);
  });
});

明確なホバー ウィンドウを設定する利点は、ユーザーの操作が意図的なものになることです(たとえば、ターゲットの上にポインタを渡すなど)。ユーザーが意図していない限り、ポップアップを表示することはありません。

このデモをお試しください。ウィンドウを 0.5s に設定してターゲットにカーソルを合わせます。


一般的なユースケースと例を確認する前に、いくつか確認しましょう。


ポップオーバーの種類

JavaScript 以外のインタラクションの動作については、すでに説明しました。では、ポップオーバー動作全体についてはどうでしょうか。「ライトを閉じる」を表示したくない場合はどうすればよいですか?ポップオーバーにシングルトン パターンを適用したり、

Popover API では、動作が異なる 3 種類のポップオーバーを指定できます。

[popover=auto]/[popover]:

  • ネストのサポート。これは、単に DOM にネストされていることを意味するわけではありません。祖先ポップオーバーの定義は次のとおりです。 <ph type="x-smartling-placeholder">
      </ph>
    • DOM の位置(子)ごとに関連付けられています
    • popovertoggletargetpopovershowtarget などの子要素の属性をトリガーして関連付けます。
    • anchor 属性による関連(開発中の CSS Anchoring API)。
  • ライトを閉じる。
  • 開くと、祖先ポップオーバー以外の他のポップオーバーは閉じられます。以下のデモで、祖先ポップオーバーのネストの仕組みを確認してください。popoverhidetarget/popovershowtarget インスタンスの一部を popovertoggletarget に変更すると、どのように変わるかをご覧ください。
  • 1 つを軽く閉じるとすべて閉じますが、スタック内の 1 つを閉じると、スタック内でそれより上位のもののみが非表示になります。

[popover=manual]:

  • 他のポップオーバーは閉じません。
  • 消灯します。
  • トリガー要素または JavaScript により明示的に却下する必要があります。

JavaScript API

ポップオーバーをより細かく制御する必要がある場合は、JavaScript を使用できます。showPopover メソッドと hidePopover メソッドの両方が返されます。また、リッスンする popovershow イベントと popoverhide イベントもあります。

ポップオーバーを表示する js popoverElement.showPopover() ポップオーバーを非表示にするには:

popoverElement.hidePopover()

表示されるポップオーバーをリッスンします。

popoverElement.addEventListener('popovershow', doSomethingWhenPopoverShows)

ポップオーバーの表示をリッスンし、表示をキャンセルします。

popoverElement.addEventListener('popovershow',event => {
  event.preventDefault();
  console.warn(We blocked a popover from being shown);
})

ポップオーバーが非表示になるのを待機します。

popoverElement.addEventListener('popoverhide', doSomethingWhenPopoverHides)

非表示にするポップオーバーはキャンセルできません。

popoverElement.addEventListener('popoverhide',event => {
  event.preventDefault();
  console.warn("You aren't allowed to cancel the hiding of a popover");
})

ポップオーバーが最上位レイヤにあるかどうかを確認します。

popoverElement.matches(':open')

これにより、あまり一般的でないシナリオでも十分なパワーが提供されます。たとえば、操作しない状態が一定時間続くとポップオーバーを表示します。

このデモにはポップオーバーの音声があるため、音声を再生するには JavaScript が必要です。クリックすると、ポップオーバーが非表示になり、音声が再生された後、再び表示されます。

ユーザー補助

Popover API では、ユーザー補助が最先端技術として考えられています。ユーザー補助のマッピングでは、必要に応じてポップオーバーをトリガー要素に関連付けます。つまり、popovertoggletarget などのトリガー属性のいずれかを使用する場合、aria-haspopup などの aria-* 属性を宣言する必要はありません。

フォーカス管理では、オートフォーカス属性を使用して、ポップオーバー内の要素にフォーカスを移動できます。これはダイアログの場合と同じですが、違いはフォーカスを返すときで、これは「Light Close」が原因です。ほとんどの場合、ポップオーバーを閉じると、以前にフォーカスされていた要素にフォーカスが戻ります。ただし、ライトを閉じると、クリックされた要素にフォーカスが移動します(フォーカスを取得できる場合)。解説内のフォーカス管理に関するセクションをご確認ください。

全画面表示を開く必要があります動作を確認することをおすすめします

このデモでは、フォーカスされている要素に緑色の枠線が表示されます。キーボードでインターフェースをタブで移動してみます。ポップオーバーが閉じられたときにフォーカスが返される場所を確認してください。Tab キーを押すと、ポップオーバーが閉じていることに気付くかもしれません。これは仕様です。ポップオーバーにはフォーカス管理機能がありますが、フォーカスはトラップされません。また、キーボード ナビゲーションは、フォーカスがポップオーバーから外れるとクローズシグナルを認識します。

アンカリング(開発中)

ポップオーバーに関しては、要素をトリガーに固定するという厄介なパターンがあります。たとえば、ツールチップがトリガーの上に表示されるように設定されている場合に、ドキュメントがスクロールされた場合などです。このツールチップはビューポートによって切り取られることがあります。現時点では、「フローティング UI」などの JavaScript サービスを使用してこの問題に対処できます。その場合、ツールチップの位置が変わり、希望する掲載順位の順番に基づいてこの状態が停止するようになります。

ただし、これをスタイルで定義できるようにする必要があります。これに対処するため、Popover API と並行して開発中のコンパニオン API も開発中です。「CSS アンカーの配置」API を使用すると、要素を他の要素にテザリングできます。これは、要素がビューポートによって切り取られないように再配置する方法で行われます。

このデモでは、現在の状態で Anchoring API を使用します。ボートの位置は、ビューポート内のアンカーの位置に対応します。

このデモを機能させる CSS のスニペットを以下に示します。JavaScript は必要ありません。

.anchor {
  --anchor-name: --anchor;
}
.anchored {
  position: absolute;
  position-fallback: --compass;
}
@position-fallback --compass {
  @try {
    bottom: anchor(--anchor top);
    left: anchor(--anchor right);
  }
  @try {
    top: anchor(--anchor bottom);
    left: anchor(--anchor right);
  }
}

仕様をご確認ください。この API 用のポリフィルもあります。

ポップオーバーの機能と仕組みを理解したところで、いくつか例を見ていきましょう。

通知

このデモでは、「クリップボードにコピー」する方法を紹介します。通知を受け取ります。

  • [popover=manual] を使用する。
  • アクション時に showPopover のポップオーバーを表示します。
  • 2000ms のタイムアウト後に、hidePopover で非表示にします。

トースト

このデモでは、最上位レイヤを使用してトースト スタイルの通知を表示します。

  • manual タイプの 1 つのポップオーバーはコンテナとして機能します。
  • 新しい通知がポップオーバーに追加され、ポップオーバーが表示されます。
  • クリック時に Web Animations API で削除され、DOM から削除されます。
  • 表示するトーストがない場合、ポップオーバーは非表示になります。

ネストされたメニュー

このデモでは、ネストされたナビゲーション メニューがどのように機能するかを示します。

  • ネストされたポップオーバーを許可するため、[popover=auto] を使用します。
  • キーボードで移動するには、各プルダウンの最初のリンクで autofocus を使用します。
  • これは、CSS Anchoring API に最適です。ただし、このデモでは、少量の JavaScript を使用し、カスタム プロパティを使用して位置を更新できます。
const ANCHOR = (anchor, anchored) => () => {
  const { top, bottom, left, right } = anchor.getBoundingClientRect();
  anchored.style.setProperty("--top", top);
  anchored.style.setProperty("--right", right);
  anchored.style.setProperty("--bottom", bottom);
  anchored.style.setProperty("--left", left);
};

PRODUCTS_MENU.addEventListener("popovershow", ANCHOR(PRODUCT_TARGET, PRODUCTS_MENU));

このデモでは autofocus を使用するため、「全画面ビュー」で開く必要があります。使用します。

メディア ポップオーバー

このデモでは、メディアをポップアップする方法を説明します。

  • 軽量の閉じるには [popover=auto] を使用します。
  • JavaScript が動画の play イベントをリッスンし、動画をポップアップします。
  • ポップオーバーの popoverhide イベントによって動画が一時停止します。

Wiki スタイルのポップオーバー

このデモでは、メディアを含むインライン コンテンツのツールチップを作成する方法を示します。

  • [popover=auto] を使用します。1 つを表示すると、他の 1 つは祖先ではないため、もう 1 つは非表示になります。
  • JavaScript を使用して pointerenter に表示されます。
  • もう一つの最適な候補は、CSS Anchoring API です。

このデモでは、ポップオーバーを使用してナビゲーション ドロワーを作成します。

  • 軽量の閉じるには [popover=auto] を使用します。
  • autofocus を使用して、最初のナビゲーション アイテムをフォーカスします。

背景の管理

このデモでは、1 つの ::backdrop のみを表示する複数のポップオーバーの背景を管理する方法を示します。

  • JavaScript を使用して、表示されるポップオーバーのリストを管理します。
  • 最上位レイヤの最下層のポップオーバーにクラス名を適用します。

カスタム カーソルのポップオーバー

このデモでは、popover を使用して canvas を最上位レイヤにプロモートし、それを使用してカスタム カーソルを表示する方法を示します。

  • canvasshowPopover[popover=manual] を使用して最上位レイヤにプロモートします。
  • 他のポップオーバーを開いた場合は、canvas ポップオーバーを表示または非表示にして、上部に表示されるようにします。

アクションシートのポップオーバー

このデモでは、ポップオーバーをアクション シートとして使用する方法を示します。

  • display をオーバーライドしてデフォルトでポップオーバーを表示します。
  • アクション シートがポップオーバー トリガーで開かれます。
  • ポップオーバーが表示されると、一番上のレイヤにプロモートされ、ビューに変換されます。
  • ライトを閉じると元に戻すことができます。

キーボード起動時のポップオーバー

このデモでは、コマンド パレット スタイルの UI でポップオーバーを使用する方法を示します。

  • cmd + j キーでポップオーバーを表示します。
  • inputautofocus でフォーカスされています。
  • コンボボックスは、メイン入力の下にある 2 つ目の popover です。
  • プルダウンが表示されていない場合、ライトを閉じるとパレットが閉じます。
  • Anchoring API のもう一つの候補は

時間指定ポップオーバー

このデモでは、4 秒後に非アクティブなポップオーバーを表示します。ログアウト モーダルを表示するためにユーザーに関する安全な情報を保持するアプリでよく使用される UI パターン。

  • JavaScript を使用して、非アクティブな状態が一定時間続くとポップオーバーを表示します。
  • ポップオーバー表示でタイマーをリセットします。

スクリーンセーバー

前のデモと同様に、サイトにちょっとした工夫を加え、スクリーンセーバーを追加できます。

  • JavaScript を使用して、非アクティブな状態が一定時間続くとポップオーバーを表示します。
  • ライトの非表示: タイマーを非表示にしたり、リセットしたりできます。

キャレットフォロー

このデモでは、入力カーソルの後にポップオーバーを表示する方法を説明します。

  • 選択、キーイベント、または特殊文字の入力に基づいてポップオーバーを表示します。
  • JavaScript を使用して、スコープのカスタム プロパティでポップオーバーの位置を更新します。
  • このパターンでは、表示されるコンテンツとアクセシビリティについて熟慮する必要があります。
  • テキスト編集 UI やタグ付けできるアプリでよく見られます。

フローティング アクション ボタン メニュー

このデモでは、ポップオーバーを使用して、JavaScript を使用せずにフローティング アクション ボタン メニューを実装する方法を示します。

  • showPopover メソッドで manual タイプのポップオーバーをプロモートします。これがメインボタンです。
  • メニューも、メインボタンのターゲットとなっているポップオーバーです。
  • popovertoggletarget でメニューが開きます。
  • autofocus を使用して、表示されている最初のメニュー項目にフォーカスします。
  • ライトを閉じるとメニューが閉じます。
  • アイコンのツイストには :has() を使用します。:has() について詳しくは、こちらの記事をご覧ください。

これで作業は完了です。

以上が、Open UI の取り組みの一環として今後リリースされるポップオーバーの概要です。賢く使うと、このウェブ プラットフォームへの相乗効果が生まれるでしょう。

Open UI をご確認ください。ポップオーバーの解説は、API の進化に合わせて最新の状態に保たれています。こちらは全デモのコレクションです。

来てくれてありがとう!


写真撮影: Madison Oren(出典: Unsplash