ポップアップ: 復活!

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

そうした問題領域の 1 つにポップアップがあります。ポップアップは、Open UI では「ポップオーバー」と呼ばれます。

ポップオーバーは、長い間、かなり二極化した評判を得てきました。その理由の 1 つは、構築とデプロイの方法によるものです。簡単にうまく作成できるパターンではありませんが、ユーザーを特定のものに導いたり、サイトのコンテンツを認識させたりすることで、大きな価値を生み出すことができます。特に、センスよく使うと効果的です。

ポップオーバーを作成する場合、主に 2 つの懸念点があります。

  • 他のコンテンツより上に適した適切な場所に配置する方法
  • アクセスしやすくする方法(キーボード操作に対応、フォーカス可能など)。

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

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

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

ブラウザの互換性

組み込みの Popover API をどこで使用できるでしょうか。このドキュメントの作成時点では、Chrome Canary では「試験運用版ウェブ プラットフォーム機能」のフラグによってサポートされます。

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

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

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

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

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

現在のソリューション

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

Dialog.showModal();

アクセシビリティに関して考慮すべき点がいくつかあります。たとえば、バージョン 15.4 より前の Safari のユーザーを対象とする場合は、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

  • 「Light Close」をすぐに使用できます。つまり、ポップオーバーの外側をクリックする、キーボードで別の要素に移動する、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 内にネストされているという意味ではありません。祖先ポップオーバーの定義は次のとおりです。
    • 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-* 属性を宣言する必要はありません。

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

このデモが動作するか確認するには、「全画面バージョン」を開く必要があります。

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

アンカー(開発中)

ポップオーバーに関しては、要素をトリガーに固定するという難しいパターンもあります。たとえば、ツールチップがトリガーの上に表示されるように設定されていても、ドキュメントがスクロールされた場合です。ツールチップがビューポートによって切り捨てられることがあります。現在、フローティング UI などの JavaScript でこの問題に対応できます。このような事態を避けるために、ツールチップは適切な位置に並び替えられます。

ただし、これを独自のスタイルで定義できるようにしたいと考えています。これに対処するために、Popover API と並行して開発中のコンパニオン API があります。「CSS Anchor Positioning」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 つのポップオーバーがコンテナとして機能します。
  • 新しい通知がポップオーバーに追加され、ポップオーバーが表示されます。
  • クリック時にウェブ アニメーション 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 つを表示すると、他のメンバーは祖先ではないため非表示になります。
  • JavaScript を使用して pointerenter で表示されます。
  • また、CSS Anchoring API も最適な候補です。

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

  • ライトを閉じるために [popover=auto] を使用します。
  • autofocus を使用して、最初のナビゲーション アイテムをフォーカスします。
をご覧ください。

背景の管理

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

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

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

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

  • canvasshowPopover[popover=manual] で最上位レイヤにプロモートします。
  • 他のポップオーバーが開いている場合は、canvas ポップオーバーの表示と非表示を切り替えて、一番上になっていることを確認します。

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

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

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

キーボードでポップオーバーを有効にしました

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

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

タイミング指定ポップオーバー

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

  • JavaScript を使用して、一定時間操作が行われなかった場合にポップオーバーを表示します。
  • ポップオーバー表示でタイマーをリセットします。

スクリーンセーバー

前のデモと同様に、サイトにちょっとした幻想的な要素を追加して、スクリーンセーバーを追加できます。

  • JavaScript を使用して、一定時間操作が行われなかった場合にポップオーバーを表示します。
  • タイマーの非表示とリセットを行うには、ライトの消灯ボタンを押します。

カーソルの後に続く

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

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

フローティング操作ボタン メニュー

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

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

これで作業は完了です。

以上がポップオーバーの概要です。これから、Open UI イニシアチブの一環として導入される予定です。よく考えて使用すれば、ウェブ プラットフォームに素晴らしいものになるでしょう。

UI を開くを必ず確認してください。ポップオーバーの説明は、API の進化に合わせて常に最新の状態に保たれています。すべてのデモのコレクションは、次のとおりです。

来てくれてありがとう!


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