建立下拉式選單
本指南涵蓋所有彈出式選單:下拉式選單、內容選單、溢位選單,以及開啟這些選單的選單列。這個範例是應用程式指令選單,其中的項目會變更介面 (建立、儲存、偏好設定)。
指令、連結和選單組成要素
兩個獨立選項。這裡的其他內容則適用於這兩者。
- 每個項目的元素。指令是
<button type="button">;目的地是<a href>,仍會在新分頁中開啟、複製為網址,且不需指令碼即可運作。下拉式選單可以自由混合這兩者:兩個連結和六個指令,或一個指令和六個連結,不會改變下方的任何內容。 - 互動模式 (每個下拉式選單)。方向鍵、
Home/End和Esc可將其設為選單:新增 focusgroup,讓它推斷每個項目 (無論是按鈕還是連結) 的menuitem,並使其看起來像一個選單 (連續列或帶有下拉式指標的觸發器;請參閱下方的「選單列或獨立選單按鈕」)。使用者Tab瀏覽的簡短連結清單是導覽列:沒有焦點群組、role="menu"、role="menuitem"或aria-haspopup="menu";保留彈出式視窗、Esc和焦點還原,並參閱導覽列指南。
內容絕不會決定角色。「產品」下拉式連結清單可以做為選單,「帳戶」下拉式指令清單則不能做為導覽。
只有在複合小工具具有一個定位點,且可透過方向鍵瀏覽時,才使用選單模式。請使用 <div focusgroup="menu nomemory">,而非 HTML <menu> 元素。請勿在共用標記中加入 role="menu" 或 role="menuitem":原生焦點群組會推論這些角色,而明確角色會阻擋該推論。請只在下方不支援瀏覽器的備用廣告中加入這些標記。nomemory 會從第一個項目開始重新開啟。Focusgroup 會移動焦點;指令碼會處理預覽、啟用和焦點還原。
開啟並定位選單
使用 commandfor 和 command="toggle-popover",將每個觸發條件設為指令叫用器。瀏覽器會在使用者點選、按下 Enter 鍵和空格鍵時開啟和關閉彈出式視窗,並將觸發條件保留在彈出式視窗的輕觸關閉群組中,因此點選開啟觸發條件會關閉該視窗。請勿使用 popovertarget 或 popovertargetaction,這個模式會改用 commandfor。
每個彈出式視窗都應緊接在觸發條件之後。只有在彈出式視窗不是下一個同層元素時,瀏覽器才會新增隱含的 aria-details 關係,因此這種位置可避免這種情況,不需額外屬性。
<!-- Command invoker: the browser toggles the popover on click, Enter, and Space -->
<button id="file-trigger" class="trigger" type="button" aria-haspopup="menu" aria-expanded="false"
commandfor="file-menu" command="toggle-popover">
File
</button>
<!-- Immediately after its trigger, so browsers add no implicit aria-details -->
<div id="file-menu" popover="auto">
<!-- Nested, not on the popover: Chromium otherwise drops focusgroup keyboard behavior -->
<div focusgroup="menu nomemory" aria-labelledby="file-trigger">
<button type="button">New file</button>
<button type="button">Save</button>
<button id="export-trigger" type="button" aria-haspopup="menu" aria-expanded="false"
commandfor="export-menu" command="toggle-popover">
Export <span aria-hidden="true">›</span>
</button>
<!-- Submenu: popover="auto" directly after its trigger, inside the parent focusgroup -->
<div id="export-menu" popover="auto">
<div focusgroup="menu nomemory" aria-labelledby="export-trigger">
<button type="button">PDF</button>
<button type="button">Plain Text</button>
</div>
</div>
</div>
</div>
為每個選單命名,並以 aria-labelledby 指向觸發條件。請將裝飾性子選單指標保留在 CSS 中,或新增 aria-hidden="true"。為含有 aria-expanded="true" 的觸發條件設定明顯的啟用樣式,讓使用者知道哪個選單已開啟或預覽。
選單列或獨立選單按鈕
為頂層觸發條件選擇一種形式。角色、鍵盤模型和樣式必須相符:視覺上分隔的按鈕代表每個按鈕都有一個 Tab 停靠點。
- 選單列。容器上的
role="menubar"(絕不位於彈出式視窗),樣式為一條連續長條,觸發條件之間沒有間隔。一個Tab停靠站;ArrowLeft/ArrowRight在觸發條件之間移動;懸停切換開啟選單。用於應用程式指令列。 - 獨立選單按鈕。每個觸發條件都是獨立的
Tab停靠點,看起來像獨立按鈕。沒有role="menubar"、觸發條件之間沒有方向鍵、焦點沒有預覽、懸停時不會切換。如果可導覽,請用<nav>包裝,否則不需角色。適用於工具列和網頁標頭。
本指南其餘部分將說明選單列形式。如果是獨立按鈕,請將所有內容保留在各個選單中,並捨棄選單列層級的行為。
使用 CSS 錨點定位功能設定選單位置
使用 CSS 錨定位置,而非 JavaScript 座標。command="toggle-popover" 和 showPopover({ source: trigger }) 都會設定隱含錨定,因此請勿新增個別選單的 anchor-name 或 position-anchor 規則。選單行為不得依賴錨定位置。
/* Top-level menus sit below their trigger. The implicit anchor comes from the invoker or source. */
[popover] {
position-area: block-end span-inline-end;
inset: auto;
}
/* A popover nested in another popover is a submenu: beside its parent item, flipping when it would overflow. */
[popover] [popover] {
position-area: inline-end span-block-end;
position-try-fallbacks: flip-inline, flip-block, --below;
}
/* Last resort for narrow viewports: directly below the trigger, indented, so the submenu never
covers its trigger. */
@position-try --below {
position-area: block-end span-inline-end;
margin-inline-start: 1rem;
/* Fit the area: the submenu's minimum width must not stop this fallback from fitting. */
min-inline-size: 0;
max-inline-size: calc(100% - 1rem);
}
請將 --below 放在最後,這樣只有在兩邊都不符合時,系統才會套用該值。它會在觸發條件的正下方開啟,因此在窄螢幕上,它會覆蓋觸發條件下方的同層級項目;這是為了保持在觸發條件旁邊而接受的折衷方案。此外,也讓子選單縮小至該區域 (min-inline-size: 0、max-inline-size 減去縮排)。系統會略過不合適的備用位置,如果沒有合適的位置,瀏覽器會保留基本位置,因此寬度過大的子選單會默默覆蓋觸發元素。
如要瞭解翻轉備用項在一般彈出式視窗上的運作方式,請參閱「可感知可視區域的下拉式選單」。該指南定位的是按鈕群組,而非選單;如果是選單,則優先採用子選單位置和上述 --below 備用方案。
開啟和關閉動畫
(選用步驟) Under 150 ms, opacity and a small translate only, same for submenus, inside @media (prefers-reduced-motion: no-preference). 焦點會移至 toggle,因此轉換不會延遲鍵盤使用者。
@media (prefers-reduced-motion: no-preference) {
/* Closing: this rule is in effect once :popover-open is removed. */
[popover] {
opacity: 0;
translate: 0 -.25rem;
transition: opacity .15s, translate .15s, display .15s allow-discrete, overlay .15s allow-discrete;
}
/* Opening: faster. */
[popover]:popover-open {
opacity: 1;
translate: 0 0;
transition-duration: .1s;
@starting-style { opacity: 0; translate: 0 -.25rem; }
}
/* Switching between open menubar menus is instant, as in native menubars. */
[role="menubar"]:has(> [popover]:popover-open) > [popover] {
transition: none;
}
}
@starting-style 不是 [popover] 的重複:關閉的彈出式視窗是 display: none,且轉換不會從未算繪狀態執行,因此會提供開啟前的值。如果沒有這個屬性,開啟時會直接跳到開啟狀態,只有關閉時會顯示動畫。display 和 overlay 搭配 allow-discrete,可讓彈出式視窗保持顯示在頂層,直到關閉轉場效果結束。
鍵盤和焦點
請勿在焦點群組中新增第二個 roving-tabindex 系統。指令碼只涵蓋叫用器和焦點群組未執行的動作:
- 選單列有一個定位點。
Tab會在第一個頂層觸發條件進入。ArrowLeft和ArrowRight可在頂層觸發條件之間移動,並在任一端換行。在觸發條件上,Home和End會進入選單的第一個或最後一個項目。- 注意:
Tab會傳回哪個觸發條件。APG 允許使用第一個或最後一個觸發條件。指南指定第一個,但示範使用ArrowLeft和ArrowRight移動定位停駐點,因此在鍵盤導覽後,Shift+Tab會返回最後到達的觸發條件。使用指標不會移動指標,因此相同的序列可能會落在不同的觸發條件上。兩種行為皆可接受,請擇一並保持一致。
- 注意:
- 預覽。聚焦頂層觸發條件會顯示其選單,並設定
aria-expanded="true"。焦點會停留在觸發條件上,不會聚焦在任何項目。模糊處理觸發條件會關閉僅限預覽的選單。 - 在預覽畫面上按下 Enter 鍵和空格鍵。關閉預覽畫面,並將焦點移回觸發條件。原生
toggle-popover動作會處理這項操作,因此指令碼不得攔截這些按鍵。 - 在預覽畫面上按下向下鍵。轉換為開啟的選單,並聚焦於第一個項目。
- 用滑鼠點選開啟或預覽的觸發條件。直接關閉,不會進入選單。
- 子選單觸發條件的已知差異。用指標點選子選單觸發條件,也會原生切換,因此可以關閉懸停時已預覽的子選單,或開啟尚未顯示的子選單。原生選單只會開啟,不會關閉。這項要求無害,因此導遊接受了。
- 游標點選關閉的觸發條件。原生開啟。焦點會停留在觸發條件上,不會聚焦在任何項目。只有鍵盤開啟時,才能進入選單。
- 將滑鼠游標懸停在項目上。聚焦時不顯示圓環,因此懸停和聚焦是同一個項目。
- 選單開啟時,將指標移到另一個頂層觸發條件上。關閉視窗並預覽滑鼠游標懸停的視窗,聚焦在觸發條件上,不顯示圓環。未開啟任何項目:懸停不會觸發任何動作。
- 將滑鼠游標懸停在子選單觸發條件上。短暫延遲後預覽子選單 (指標焦點會執行相同的預覽焦點)。同層項目會關閉該項目。
- 在選單中。
ArrowUp和ArrowDown可在已啟用的項目之間移動。Home和End會移至第一個和最後一個啟用的項目,由指令碼處理。 - 在開啟的頂層選單中,按向左鍵或向右鍵。關閉選單並預覽相鄰選單,焦點會放在觸發條件上,並在任一端換行。
ArrowDown接著進入該房間。向左和向右鍵不會進入選單。 - 在子選單觸發條件上按下向右鍵。開啟子選單,並將焦點移至第一個項目。
- 在沒有子選單的子選單項目上按下向右鍵。關閉階層並預覽下一個頂層選單,同時焦點仍停留在觸發條件上。
ArrowDown接著進入該空間。 - 在子選單中按下向左鍵或 Escape 鍵。只關閉該子選單,並將焦點還原至觸發條件。家長專用選單會保持開啟。
- 在指令上按下 Enter 鍵或空格鍵。啟用該項目並關閉所有開啟的選單。
- Tab 鍵。離開選單系統。請勿鎖定焦點或逐步執行每個指令。關閉時,焦點不得留在隱藏的內容中。
command 事件會在 toggle-popover 開啟選單前觸發,因此焦點無法移至選單。請改為在 toggle 事件中移動焦點。HTML 只會還原堆疊中第一個 popover="auto" 的焦點,因此巢狀層級必須明確將焦點還原至觸發條件。
導入互動
請為自己的標記實作上述行為。箭頭、Home/End、環繞和 Tab 結束處理是普通的 keydown 程式碼。以下依輸入類型分組,列出幾個容易出錯的部分。
鍵盤和焦點處理
- 一條開放路徑。透過鍵盤開啟會呼叫
trigger.click(),因此叫用器的toggle-popover是開啟選單的唯一方式。標示選單,讓toggle處理常式進入選單,只開啟鍵盤。指標開啟時,toggle會聚焦在沒有圓環的觸發條件上,因為pointerdown阻止了焦點,且方向鍵仍須運作。 - 預覽對焦。聚焦觸發條件會呼叫
showPopover({ source: trigger }),因此選單會錨定在該觸發條件。忽略從觸發條件本身選單內或另一個視窗傳回的焦點 (視窗blur上的標記、視窗focus後清除勾號,或切換應用程式重新開啟最後一個選單)。除非焦點移至blur,否則隱藏預覽選單。 - 將焦點移至
toggle,而非command。command會在選單開啟前觸發。 - 自行將焦點還原至觸發條件。在
hidePopover()之前聚焦觸發條件。指標啟用後,還原焦點時不顯示圓環 (focus({ focusVisible: false })),或在滑鼠點擊後,觸發條件顯示鍵盤焦點外框;鍵盤啟用時則保留圓環。event.detail === 0的點擊來自鍵盤。 - 一次關閉一個層級。在子選單中呼叫
Escape的stopPropagation()和ArrowLeft,或由父項的事件監聽器關閉。
指標處理
- 指標點擊切換。在
pointerdown上呼叫preventDefault(),這樣點擊就不會移動焦點,也不會在點擊中途開始預覽。記住按下的觸發條件。點選關閉或預覽的觸發條件,會直接開啟或關閉該條件,不會進入選單;只有透過鍵盤開啟才會進入選單。請勿攔截點擊。 - 懸停效果會隨著焦點移動。分別設定
:hover和:focus-visible的樣式。在項目上pointermove時,呼叫focus({ focusVisible: false });忽略該選項的瀏覽器不會為指標焦點繪製圓環。 - 滑鼠懸停切換開啟選單。在另一個頂層選單開啟時,
pointerenter頂層觸發條件:隱藏該選單,將焦點移至懸停的觸發條件 (不顯示圓環),並在焦點預覽 (見上文) 預覽該選單。沒有開啟的項目:不執行任何動作。 - 安全三角。固定的 CSS 橋接太窄:只有在指標從父項邊緣附近開始時,才能運作。讓 CSS 繪製橋接設定,並僅使用指令碼回報指標:
- 只有在
matchMedia('(hover: hover)').matches時才執行這項操作,與觸控和鍵盤無關。 - 當游標離開已開啟子選單的子選單觸發條件時,開始計時。略過沒有側邊的
--below子選單。 - 設定
--bridge-x和--bridge-y(指標,以像素為單位,相對於子選單)、--bridge-edge(子選單的近邊緣 x:指標位於左側時為0,翻轉時為其寬度) 和子選單上的--bridge-height,以及data-bridge以開啟橋接。 - 在大約 300 毫秒後、指標到達子選單中的項目時,或子選單關閉時停止。移除事件監聽器和屬性。之後不會執行任何作業。
- CSS 會為子選單提供
::before,並從指標到近邊緣角落裁剪成三角形。它會位於指標路徑上的同層項目上方,因此這些項目永遠不會收到關閉子選單的懸停事件。clip-path也會限制點擊測試,因此家長專用選單的其他部分仍可互動。 - 子選單需要
overflow: visible(彈出式視窗預設會裁剪),而虛擬元素會延伸到子選單之外,因此請依該範圍偏移形狀的點。 - 後續:曲線路徑 (
shape()曲線指令) 比直三角形更符合實際指標弧線。
- 只有在
[popover] [popover] {
overflow: visible;
&[data-bridge]::before {
content: "";
position: absolute;
inset: -100vh -100vw; /* Room for the triangle, which can extend far outside the submenu. */
clip-path: shape(
from calc(100vw + var(--bridge-x)) calc(100vh + var(--bridge-y)),
line to calc(100vw + var(--bridge-edge)) 100vh,
line to calc(100vw + var(--bridge-edge)) calc(100vh + var(--bridge-height)),
close
);
}
}
const canHover = matchMedia('(hover: hover)');
// Call on pointerleave of a submenu trigger, with that trigger's submenu.
function startBridge(submenu, event) {
if (!canHover.matches || !submenu.matches(':popover-open')) return;
const rect = submenu.getBoundingClientRect();
if (event.clientX >= rect.left && event.clientX <= rect.right) return; // --below: no side edge.
const style = submenu.style;
style.setProperty('--bridge-edge', `${event.clientX < rect.left ? 0 : rect.width}px`);
style.setProperty('--bridge-height', `${rect.height}px`);
const update = ({ clientX, clientY, target }) => {
if (target !== submenu && submenu.contains(target)) return stop(); // Reached an item.
style.setProperty('--bridge-x', `${clientX - rect.left}px`);
style.setProperty('--bridge-y', `${clientY - rect.top}px`);
};
const timer = setTimeout(() => stop(), 300);
const stop = () => {
clearTimeout(timer);
document.removeEventListener('pointermove', update);
submenu.removeEventListener('toggle', stop);
submenu.removeAttribute('data-bridge');
};
update(event);
submenu.dataset.bridge = '';
document.addEventListener('pointermove', update);
submenu.addEventListener('toggle', stop, { once: true });
}
「Hover follows focus」(項目 2) 仍是單純的 pointermove 處理常式,不需要三角形邏輯。
另外,請查看以下須知:
- 失焦。當焦點落在選單系統外時,關閉所有開啟的選單 (
documentfocusin監聽器)。 - 指令啟用。播報後,隱藏根選單並將焦點還原至觸發條件,如果啟用來自指標,則不顯示圓環。如果是可勾選的項目 (
menuitemcheckbox、menuitemradio),請等待約 300 毫秒再關閉,以便顯示變更後的勾號。將關閉觸發條件設為僅在:focus-visible上啟用,否則還原 (無環) 焦點會在指標選取後保持醒目顯示。 aria-expanded。請只在toggle事件監聽器中設定。這包括預覽和開啟狀態,且觸發條件的aria-expanded在預覽或開啟選單時會保持"true"。- 輔助工具:下列備用方案假設您為標記定義了輔助函式:
triggers(所有[commandfor]按鈕)、menuFor(trigger)、isOpen(menu)、closeAndRestore(menu, { fromPointer })(聚焦於觸發條件,fromPointer時沒有圓環,然後menu.hidePopover())、enterOnOpen(開啟後要進入的選單WeakSet) 和pointerTrigger(指標最後按下的觸發條件)。
子選單
- 使用
popover="auto"建立子選單,並放在父項focusgroup內的子選單觸發條件之後。popover="manual"。 - 巢狀結構可讓瀏覽器保持父項開啟、關閉同層子選單、在根選單關閉時關閉子項子選單,以及處理外部點擊關閉。
- 子選單觸發條件會保留
aria-haspopup="menu"和aria-expanded;toggle監聽器會讓aria-expanded保持同步。 - 鍵盤指令碼會按照上述方式處理巢狀
Escape和ArrowLeft。 - 子選單觸發條件絕不會同時執行指令:一個控制項無法同時啟用和開啟,
Enter也無法同時代表這兩者。將預設動作設為第一個子選單項目,或在觸發條件旁新增獨立的指令項目。如果必須使用合併控制項,請將其記錄為無障礙功能的折衷方案。
分隔符和可勾選項目
- 使用 focusgroup 內的
<hr>分隔項目群組:原生separator,不可聚焦,由 focusgroup 和備用項目略過。絕不使用樣式化的<div>。 - 布林值選項是具有
aria-checked和明確role="menuitemcheckbox"的<button type="button">。Focusgroup 不會推論這些角色,因此這是共用標記設定項目角色的唯一位置。 - 其中一個組合是
<button type="button">,內含role="menuitemradio"和aria-checked,位於role="group"內,內含aria-label,位於同一個焦點群組中。 - 指令碼切換按鈕
aria-checked。 - 在核取方塊上按
Enter鍵會切換狀態並關閉選單;按Space鍵會切換狀態並保持開啟 (APG)。按下任一按鍵即可關閉收音機。 - 從
[aria-checked="true"]::before算繪勾號,而非獨立的圖示元素 (請參閱下方的 CSS)。
不在本文討論範圍內
- 啟用意見回饋。原生選單會在關閉前閃爍所選項目。如有需要,可在
hidePopover()前顯示約 100 毫秒的視覺確認,但請勿延遲焦點還原或朗讀。 - 預測輸入。Focusgroup 則沒有。如有需要,請在可列印的
keydown上比對目前層級啟用的項目標籤,並在目前項目後聚焦下一個相符項目,然後換行。請勿攔截Space。
完成前請先驗證
指令無法保證行為,因此請測試結果。開啟網頁,並使用實體鍵盤和滑鼠檢查下列各項;修正並重新測試任何失敗的項目:
- 鍵盤。
Tab會停在第一個觸發條件上一次;ArrowDown會進入;Escape會關閉一個層級,並將焦點返回其觸發條件;Tab會關閉所有選單。 - 指標開啟。點選觸發條件會開啟選單,焦點會放在觸發條件上,且不會顯示圓環;再次點選則會關閉選單。
- 指標指令。點選指令會關閉所有選單,且觸發條件上不會顯示焦點環。
- 子選單。從子選單觸發條件的中間斜向移動到子選單項目,可讓子選單保持開啟;移動到不相關的同層級項目,則會關閉子選單。
- 窄螢幕:在 375 像素時,沒有任何子選單會覆蓋自己的觸發條件。
- 勾號。已勾選的項目會顯示勾號。
- 不留任何痕跡。沒有控制台錯誤,且觸發條件上沒有
popovertarget、popovertargetaction或aria-details。
對齊選單項目內容
如果資料列包含圖示、標籤和快速鍵或子選單指標,請為圖示或勾號保留固定大小的前置版位,並將結尾內容推到內嵌結尾。圖示的大小固定,因此無論項目是否有圖示,固定版位都能讓標籤對齊。請勿在資料列使用 subgrid:任何額外的內嵌子項 (例如徽章) 都會成為獨立的格線項目,導致版面配置不正確。如果快速鍵文字只是重複說明已可操作項目的按鍵,請對輔助技術隱藏該文字。
[focusgroup~="menu"] {
--menu-icon-size: 1.25rem;
}
/* Target items only. A closed submenu popover inside the focusgroup must keep display: none. */
[focusgroup~="menu"] > button,
[focusgroup~="menu"] > [role="group"] > button {
display: flex;
align-items: center;
gap: .5rem;
}
/* The leading slot holds the icon or check mark. */
[focusgroup~="menu"] button::before {
content: "";
flex: none;
inline-size: var(--menu-icon-size);
}
/* Check mark for checked items. This selector must be at least as specific as the base
::before rule above, or the empty content wins and the check mark never shows. */
[focusgroup~="menu"] button[aria-checked="true"]::before {
content: "✓";
}
/* Shortcut or submenu indicator at the inline end. */
[focusgroup~="menu"] .hint {
margin-inline-start: auto;
}
在按鈕內的 <span class="hint" aria-hidden="true"> 中放置結尾指標或快速鍵;子選單觸發條件已宣告其彈出式視窗。
漸進增強和備用方案
focusgroup 的備用方案和瀏覽器支援
在標記中保留 focusgroup="menu nomemory"。如果 'focusGroup' in HTMLElement.prototype 為 false,請安裝 Up、Down、Home 和 End,在每個選單容器上設定 role="menu",並在沒有角色的按鈕上設定 role="menuitem";請勿變更可勾選項目、<hr> 和 role="group"。如果已有原生 focusgroup,請勿安裝。無論如何,左、右、預覽、開啟和關閉都是作者指令碼。
if (!('focusGroup' in HTMLElement.prototype)) {
document.querySelectorAll('[focusgroup~="menu"]').forEach(group => {
group.setAttribute('role', 'menu');
group.querySelectorAll('button:not([role])').forEach(item => {
item.setAttribute('role', 'menuitem');
});
group.addEventListener('keydown', event => {
if (!['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(event.key)) return;
// Own items only: a nested submenu has its own focusgroup, a role="group" wrapper does not.
const items = [...group.querySelectorAll('button')]
.filter(item => item.closest('[focusgroup]') === group)
.filter(item => !item.disabled && item.getAttribute('aria-disabled') !== 'true');
const current = items.indexOf(document.activeElement);
if (current < 0) return;
const next = event.key === 'Home' ? 0 : event.key === 'End' ? items.length - 1 :
(current + (event.key === 'ArrowUp' ? -1 : 1) + items.length) % items.length;
event.preventDefault();
event.stopImmediatePropagation();
items[next].focus({ preventScroll: true });
}, true);
});
}
Popover 的備用方案和瀏覽器支援
Popover API 主要是漸進增強,但其定義行為 (頂層升級、輕觸關閉和 popovertarget 叫用) 沒有僅限 CSS 的對等項目。舊版瀏覽器需要 polyfill,如果不想提供,則需要手動備用方案。
Polyfill:如要在舊版瀏覽器中支援 popover 屬性,請視情況載入 @oddbird/popover-polyfill。必要:檢查 HTMLElement.prototype 上的 popover 屬性,偵測功能,並僅在缺少原生支援時載入 polyfill,請勿無條件載入。
使用套件組合器或匯入對應:
// MANDATORY: Feature detect 'popover' on HTMLElement.prototype.
if (!("popover" in HTMLElement.prototype)) {
import("@oddbird/popover-polyfill");
}
如果沒有繫結器,請從 <script type="module"> 內的 CDN 匯入:
<script type="module">
if (!("popover" in HTMLElement.prototype)) {
import("https://unpkg.com/@oddbird/popover-polyfill@latest/dist/popover.min.js");
}
</script>
樣式注意事項:Polyfill 無法定義實際的 :popover-open 虛擬類別,因此會改為套用 .\:popover-open 類別。必要:請使用 :is() 或 :where() 結合兩者,否則不支援 :popover-open 的瀏覽器會捨棄整個規則:
[popover]:is(:popover-open, .\:popover-open) {
display: block;
}
或者,如要使用不含 polyfill 的舊版備用方案,請使用 position: fixed,並透過 getBoundingClientRect() 手動計算座標,或在用途允許的情況下,使用 inset: auto 預設定位。
錨定定位的備用方案和瀏覽器支援
如果沒有 CSS 錨定定位,彈出式視窗會改用使用者代理程式的置中可視區域位置。請勿將這種設計用於選單,因為這會導致選單與觸發條件分離,且置中的子選單會遮蓋父項。在 @supports (position-area: block-end) 後方設定錨定規則,並在失敗時透過指令碼設定選單位置:
- 將選單放在觸發條件下方 (頂層) 或旁邊 (子選單),並在溢位時翻轉至另一側,並固定在可視區域。
- 在選單開啟時,於
toggle處理常式中執行,並將結果套用為選單上的left和top。搭配使用position: fixed、inset: auto和margin: 0,讓座標與可視區域相對。 - 請將這些覆寫值保留在
@supports not (position-area: block-end)內,這樣支援錨定定位的瀏覽器就不會受到影響。
如果指定 showPopover({ source }) 未建立隱含錨點 (ToggleEvent.prototype 上沒有 source) 的早期實作,請透過指令碼為每個觸發條件提供不重複的 anchor-name,並為每個選單提供相符的 position-anchor,而不是為每個選單編寫 CSS 規則。
如果瀏覽器完全不支援錨點定位,請參閱可視區域感知下拉式選單,瞭解 polyfill 做法。
Invoker 指令的備用方案和瀏覽器支援
如果沒有叫用器,command="toggle-popover" 就不會執行任何動作,因此指令碼必須切換選單。
- 偵測介面功能,而非例項:
'commandForElement' in HTMLButtonElement.prototype。 - 在每個觸發條件
click上,呼叫preventDefault(),讓具有叫用器的瀏覽器無法切換兩次。 - 如果選單已開啟,請按
closeAndRestore(menu)關閉。否則請呼叫menu.showPopover({ source: trigger })。 - 除非點擊來自按下的觸發條件 (
pointerTrigger),否則請將選單標示為enterOnOpen,這樣只有鍵盤開啟時才會進入選單。 Enter和Space仍可運作,因為這兩個指令會產生按鈕點擊。