彈出式視窗:他們和 #39 的爆發,正要復甦!

開放式 UI 計畫的目標,是讓開發人員更輕鬆地打造優質使用者體驗。為此,我們試圖處理開發人員遇到的更多問題模式。我們會提供更優質的平台內建 API 和元件,以實現這項目標。

彈出式視窗之一就是彈出式視窗,在「開啟使用者介面」中描述為「彈出式視窗」。

彈出式視窗長期以來都相當惡名昭彰。這部分是因為這些應用程式是透過建構和部署方式進行。這類模式不容易建立,但可以引導使用者前往特定內容,或讓使用者瞭解網站上的內容,因此具有很大的價值,尤其是在適當情況下使用時。

建構彈出式視窗時,通常會遇到兩個主要問題:

  • 如何確保圖片顯示在其他內容上方,且位置合適。
  • 如何提升易用性 (鍵盤好上手、可聚焦等)。

內建的 Popover API 提供了各種目標,且全部的目標都一樣,是讓開發人員能輕鬆建構這種模式。其中值得注意的目標包括:

  • 讓元素及其子項可輕鬆顯示在文件的其餘部分上方。
  • 提供無障礙的服務。
  • 大部分常見行為 (輕型關閉、單例、堆疊等) 不需要 JavaScript。

如要查看彈出式視窗的完整規格,請前往 OpenUI 網站

瀏覽器相容性

您現在可以在哪些地方使用內建的 Popover API?在撰寫本文時,Chrome Canary 已支援這項功能,但必須啟用「Experimental web platform features」標記。

如要啟用該標記,請開啟 Chrome Canary 並前往 chrome://flags。然後啟用「實驗性網站平台功能」旗標。

開發人員如果想在正式環境中測試這項功能,可以選擇來源試用

最後,我們正在為 API 開發 polyfill。請務必前往 github.com/oddbird/popup-polyfill 查看存放區。

您可以透過以下方式查看彈出式視窗支援功能:

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

目前的解決方案

你目前可以採取哪些行動,讓你的內容獲得更多曝光?如果瀏覽器支援,您可以使用 HTML 對話方塊元素。您需要在「Modal」表單中使用這項功能。這項功能需要使用 JavaScript。

Dialog.showModal();

但也有一些無障礙設計考量。舉例來說,如果要服務使用 Safari 15.4 以下版本的使用者,建議使用 a11y-dialog

您也可以使用許多 popover、警示或工具提示程式庫中的其中一個。其中許多功能的運作方式都很相似。

  • 將一些容器附加至主體,以便顯示彈出式視窗。
  • 並將其樣式設為位於其他所有元素之上。
  • 建立元素並附加至容器,以顯示彈出式視窗。
  • 從 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 的巢狀結構下,加上裁切上階。您也可以透過開發人員工具查看目前位於頂層的元素。如要進一步瞭解頂層,請參閱這篇文章

開發人員工具頂層支援功能的 GIF 示範

  • 您可以直接使用「Light Dismiss」。也就是說,您可以透過關閉信號關閉彈出式視窗,例如點選彈出式視窗外部、透過鍵盤前往其他元素,或按下 Esc 鍵。再次開啟應用程式,試試看吧!

彈出式視窗還有什麼好處?讓我們進一步舉例說明。請參考這個示範,其中包含網頁上的部分內容。

該懸浮動作按鈕具有固定位置,且 z-index 偏高。

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

彈出式視窗內容會在 DOM 中巢狀,但當您開啟彈出式視窗時,系統會將其置於固定位置元素之上。您不需要設定任何樣式。

您可能也會發現,彈出式視窗現在有 ::backdrop 虛擬元素。頂層中的所有元素都會取得可樣式的 ::backdrop 擬造元素。本範例會使用較低的 alpha 值背景顏色和背景過濾器,為 ::backdrop 設定樣式,並模糊處理底層內容。

設定彈出式視窗樣式

接下來,我們將著手設定彈出式視窗的樣式。根據預設,彈出式視窗會顯示固定位置和一些套用邊框間距。也包含 display: none。您可以覆寫這項設定來顯示彈出式視窗。但不會將其升級至頂層。

[popover] { display: block; }

無論您以何種方式宣傳彈出式視窗,一旦將彈出式視窗推送到上層,您可能需要將其放置在上層或放置其位置。但無法指定頂層圖層

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

根據預設,彈出式視窗會使用 margin: auto 在可視區域的中央顯示。但在某些情況下,您可能會想更明確的定位。例如:

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

如果您想使用 CSS 格線或 Flexbox 在彈出式視窗中排版內容,建議您將內容包裝在元素中。否則,您需要宣告另外一項規則,當彈出式視窗出現在頂層圖層時,變更 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 顯示彈出式視窗。我們使用「Light dismiss」關閉通知。不過,您也可以使用 popovershowtargetpopoverhidetarget 屬性。我們來在彈出式視窗中新增按鈕,讓彈出式視窗隱藏起來,並變更切換鈕,使用 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 屬性會定義 popover 對元素反應時,游標懸停在元素上和離開元素的時間間隔。實驗的預設行為是在明確的 :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 互動行為。但如果是整體彈出式視窗行為呢?如果不想使用「Light dismiss」功能,或者,您想在彈出式視窗中套用單例模式嗎?

您可以使用 Popover API 指定三種行為不同的彈出式視窗。

[popover=auto]/[popover]

  • 支援巢狀結構。這並不表示必須在 DOM 中巢狀排列。祖系彈出式視窗的定義如下:
    • 以 DOM 位置 (子項) 相關。
    • 透過觸發子元素 (例如 popovertoggletargetpopovershowtarget 等) 的屬性而彼此相關。
    • 並透過 anchor 屬性建立關聯 (CSS Anchoring API 仍在開發中)。
  • 光線關閉。
  • 開啟會關閉其他非祖系彈出式視窗。請試用下方示範內容,瞭解如何使用祖系彈出式視窗巢狀結構。瞭解將部分 popoverhidetarget/popovershowtarget 例項變更為 popovertoggletarget 後的變化情形。
  • 輕觸式按鈕會一併關閉所有燈,但在堆疊中關閉一個燈時,只會關閉堆疊中上方的燈。

[popover=manual]

  • 不會關閉其他彈出式視窗。
  • 未亮燈。
  • 必須透過觸發元素或 JavaScript 明確關閉。

JavaScript API

如要進一步控管彈出式視窗,您可以使用 JavaScript。您會同時取得 showPopoverhidePopover 方法。您也可以監聽 popovershowpopoverhide 事件:

顯示彈出式視窗 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-* 屬性,例如 aria-haspopup

如要管理焦點,您可以使用 autofocus 屬性,將焦點移至彈出式視窗中的元素。這與對話方塊相同,但差異在於返回焦點時,因為輕型關閉。在大多數情況下,關閉彈出式視窗後,焦點會移回先前已聚焦的元素。不過,如果點選的元素可取得焦點,焦點會移至該元素。請參閱說明文件中關於專注模式管理的部分

您必須開啟此示範的「全螢幕版本」,才能查看實際運作情形。

在本示範中,聚焦的元素會顯示綠色外框。試試看用鍵盤在介面各處分頁。請注意,當彈出式視窗關閉時,焦點會傳回至何處。您可能也會注意到,如果您按下分頁鍵,彈出式視窗就會關閉。這是正常的。雖然彈出式視窗有聚焦管理功能,但不會捕捉焦點。當焦點移出彈出式視窗時,鍵盤導覽會識別關閉信號。

錨定 (開發中)

就彈出視窗而言,有個難以因應的模式,就是將元素固定在觸發條件。舉例來說,假設將工具提示設為顯示在觸發程序上方,但文件遭捲動。該工具提示可能會遭到檢視區截斷。目前有「浮動式 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 也會有 polyfill。

範例

您現在已熟悉彈出式視窗的功能和使用方式,接下來我們來看看一些範例。

通知

這個示範會顯示「複製到剪貼簿」通知。

  • 使用 [popover=manual]
  • 在動作上顯示彈出式視窗,並使用 showPopover
  • 2000ms 逾時後,請使用 hidePopover 隱藏。

浮動式訊息

此示範使用頂層圖層顯示浮動式訊息樣式通知。

  • 一個類型為 manual 的彈出式視窗可做為容器。
  • 系統會將新通知附加至彈出式視窗,並顯示彈出式視窗。
  • 系統會在點擊時使用 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]。顯示其中一個會隱藏其他非祖先的類別,
  • 使用 JavaScript 在 pointerenter 上顯示。
  • 另一個最適合使用 CSS Anchoring API 的方法。

這項示範會使用彈出式視窗建立導覽匣。

  • 使用 [popover=auto] 關閉燈光。
  • 使用 autofocus 將焦點放在第一個導覽項目。

管理背景

這個示範會說明如何管理多個彈出式視窗的背景,並只讓其中一個 ::backdrop 顯示。

  • 使用 JavaScript 維護可見的彈出式視窗清單。
  • 將類別名稱套用至頂層中位於最底層的彈出式視窗。

自訂游標彈出式視窗

本示範說明如何使用 popovercanvas 升級為頂層圖層,並用來顯示自訂遊標。

  • 使用 showPopover[popover=manual],將 canvas 升級至頂層。
  • 開啟其他彈出式視窗時,請隱藏並顯示 canvas 彈出式視窗,確保彈出式視窗位於頂端。

動作表彈出式視窗

這個示範會說明如何使用彈出式視窗做為動作列。

  • 讓彈出式視窗在預設情況下覆寫 display
  • 使用彈出式視窗觸發條件開啟 Actionsheet。
  • 彈出式視窗顯示時,會提升至頂層並轉換為檢視畫面。
  • 可使用 LightDismiss 返回。

鍵盤啟用的彈出式視窗

這個示範影片說明如何使用彈出式視窗,為指令區塊樣式的 UI 提供支援。

  • 使用 cmd + j 鍵可顯示彈出式視窗。
  • inputautofocus 重疊。
  • 組合方塊是位於主要輸入項下方的第二個 popover
  • 如果沒有下拉式選單,輕觸關閉動作會關閉色盤。
  • Anchoring API 的另一個候選項

計時式彈出式視窗

這個示範會在四秒後顯示閒置彈出式視窗。在應用程式中經常用於顯示登出模式,以便保留使用者機密資訊的 UI 模式。

  • 使用 JavaScript 在一段時間未使用時顯示彈出式視窗。
  • 在彈出式視窗顯示時,重設計時器。

螢幕保護程式

與前一個示範相同,您可以為網站增添一點奇幻感,並新增螢幕保護程式。

  • 使用 JavaScript 在一段時間未使用時顯示彈出式視窗。
  • 輕觸燈具即可關閉並重設計時器。

文字插入點醒目顯示

這個示範會說明如何讓彈出式視窗追隨輸入代碼。

  • 根據選取項目、按鍵事件或特殊字元輸入內容,顯示彈出式視窗。
  • 使用 JavaScript 以限定範圍的自訂屬性更新彈出位置。
  • 您必須考量顯示的內容和無障礙設計。
  • 這類元素經常出現在可標記的文字編輯 UI 和應用程式中。

懸浮動作按鈕選單

這個示範會說明如何使用彈出式視窗,在不使用 JavaScript 的情況下實作懸浮動作按鈕選單。

  • 使用 showPopover 方法宣傳 manual 類型的彈出式視窗。這是主要按鈕。
  • 選單是另一個彈出式視窗,也是主要按鈕的目標。
  • 使用 popovertoggletarget 開啟選單。
  • 使用 autofocus 將焦點移至節目中的第一個選單項目。
  • 光線關閉會關閉選單。
  • 圖示扭曲效果使用 :has()。如要進一步瞭解 :has(),請參閱這篇文章

這樣就大功告成了!

以上就是彈出式視窗的簡介,這項功能將在日後的開放式 UI 計畫中推出。只要妥善運用,就能為網頁平台帶來更多優異的體驗。

請務必查看「Open UI」。隨著 API 的演進,彈出式說明會隨之更新。以下是所有示範的集合

感謝你「突然」造訪!


相片來源:Madison Oren,發表於 Unsplash 網站上