使用 Invoker Commands API,直接透過 HTML 按鈕切換 <dialog> 和 [popover] 元素的顯示設定,不必使用自訂 JavaScript 事件監聽器。
只要將 commandfor (目標 ID) 和 command (動作) 屬性套用至 <button>,瀏覽器就會自動處理開啟/關閉狀態變更、焦點管理和無障礙功能繫結 (例如 aria-expanded)。建議採用這種宣告式方法,因為這種方法可移除不穩定的樣板程式碼、確保互動在 HTML 剖析後立即生效,並保證提供穩固、原生無障礙的使用者體驗。
導入宣告式彈出式視窗
使用單一按鈕即可切換開啟和關閉彈出式視窗。
<!-- MANDATORY: The commandfor attribute links the invoker to the ID of the target element so the browser knows what to control. -->
<!-- MANDATORY: The command attribute specifies the action to perform. Use 'toggle-popover' to handle both open and close states automatically. -->
<button commandfor="my-popover" command="toggle-popover">
Toggle Popover
</button>
<!-- MANDATORY: The target element must have the popover attribute to be controlled as a popover. -->
<div id="my-popover" popover>
<p>Popover content goes here.</p>
</div>
如需使用不同按鈕控制開啟和關閉,可以使用 show-popover 和 hide-popover 指令。
<!-- MANDATORY: Use 'show-popover' to explicitly open the popover. It will not close the popover if clicked again. -->
<button commandfor="my-explicit-popover" command="show-popover">
Show Popover
</button>
<div id="my-explicit-popover" popover="manual">
<p>This popover is explicitly opened and closed by separate buttons.</p>
<!-- MANDATORY: Use 'hide-popover' to explicitly close the targeted popover. -->
<button commandfor="my-explicit-popover" command="hide-popover">
Hide Popover
</button>
</div>
導入宣告式互動視窗
與彈出式視窗不同,互動視窗通常會使用不同的按鈕來開啟和關閉。如需以互動視窗形式開啟對話方塊,請使用 show-modal 指令。
<!-- MANDATORY: Use command="show-modal" to trigger the dialog as a modal, trapping focus and preventing interaction with the rest of the page. -->
<!-- MANDATORY: The commandfor attribute connects this button to the dialog ID. -->
<button commandfor="confirm-dialog" command="show-modal">
Open Confirmation
</button>
<dialog id="confirm-dialog">
<p>Are you sure you want to proceed?</p>
<!-- MANDATORY: Use command="close" to dismiss the dialog safely. -->
<button commandfor="confirm-dialog" command="close">
Cancel
</button>
</dialog>
備用策略
由於叫用器指令和彈出式視窗尚未全面支援,因此您「必須」使用 polyfill 做為舊版瀏覽器的備用方案。
Polyfill 叫用者指令
必要:檢查 HTMLButtonElement 原型上的 commandForElement 屬性,偵測是否支援這項功能。請勿檢查視窗或文件物件。只有在缺少原生功能時,才「必須」動態匯入 polyfill。請勿無條件載入 polyfill。必要:直接在目標元素上監聽「command」事件,因為原生「command」事件不會向上傳遞。
方法 1:使用整合工具
透過 npm (npm install invokers-polyfill) 安裝 polyfill。這個方法適用於使用整合工具 (例如 Vite 或 Webpack) 或匯入對應的專案。如果是其他設定,請使用下方的 CDN 選項。
// MANDATORY: Feature detect 'commandForElement' on HTMLButtonElement.prototype.
// Conditionally load the invokers-polyfill only in browsers lacking native support.
if (!('commandForElement' in HTMLButtonElement.prototype)) {
import('invokers-polyfill');
}
方法 2:使用 CDN
如果專案沒有套件,請直接從 <script type="module"> 內的 CDN 動態匯入 polyfill。
<script type="module">
// MANDATORY: Feature detect 'commandForElement' on HTMLButtonElement.prototype.
// Conditionally load the invokers-polyfill from a CDN only in browsers lacking native support.
if (!('commandForElement' in HTMLButtonElement.prototype)) {
import('https://esm.run/invokers-polyfill');
}
</script>
Invokers Polyfill 限制
必要:這個 Polyfill 不會像原生瀏覽器一樣處理指令按鈕的 ARIA 狀態 (例如 aria-expanded)。強烈建議您自行處理這些狀態,確保網站完全無障礙。
如果瀏覽器不支援 Invoker Commands API,就不會觸發 command 事件。如要完整支援所有新式瀏覽器,建議透過 npm install 或 CDN,使用 https://github.com/keithamus/invokers-polyfill 的 invokers-polyfill。
這個 polyfill 完全支援自訂動作 (從 -- 開始),並會像原生 API 一樣調度 command 事件。
動態匯入 (效能最佳化)
為獲得最佳效能,只有在瀏覽器不支援原生 API 時,才應載入 Polyfill。這樣可以節省頻寬,並縮短新式瀏覽器使用者的指令碼執行時間。
注意:這個 polyfill 不會管理自訂指令的 ARIA 狀態 (例如 aria-pressed 或 aria-expanded)。您必須在事件監聽器中手動同步處理這些狀態,確保網站可供存取。
// 1. Conditionally load the polyfill
const hasNativeSupport = 'commandForElement' in HTMLButtonElement.prototype;
if (!hasNativeSupport) {
// Wrap in an async IIFE to avoid top-level await issues in older browsers
(async () => {
try {
await import('https://esm.run/invokers-polyfill');
} catch (err) {
console.error('Error loading fallback:', err);
}
})();
}
// 2. Manually manage ARIA states in your listener
document.getElementById('action-target').addEventListener('command', (event) => {
const command = event.command;
const target = event.target;
const source = event.source; // The button that triggered the command
if (command === '--spin') {
const isSpun = target.classList.toggle('is-spun');
// Polyfill tip: Manually update ARIA to match the new state
source?.setAttribute('aria-pressed', isSpun);
}
});
手動備用 (傳統模式)
如果不想使用 polyfill,可以結合事件委派來調度事件,並使用指令註冊表來處理動作。這是傳統 JavaScript 開發中常見的架構模式,仍具有高效率和可擴充性。
// 1. **Optional:** Define a registry of requested actions for cleaner logic
const commandRegistry = {
'--spin': (target) => target.classList.toggle('is-spun'),
'--grow': (target) => target.classList.toggle('is-grown'),
'--reset': (target) => target.classList.remove('is-spun', 'is-grown'),
};
// 2. If CommandEvent doesn't exist, we assume no native support and provide the fallback
if (!globalThis.CommandEvent) {
globalThis.CommandEvent = class CommandEvent extends Event {
constructor(type, { source, command, ...options } = {}) {
super(type, options);
this.source = source;
this.command = command;
}
}
}
// 3. The fallback: Dispatch events manually if native support is missing
document.addEventListener('click', (event) => {
const button = event.composedPath().find((el) => el.matches?.("button[commandfor]"));
if (!button) return;
const target = document.getElementById(button.getAttribute('commandfor'));
const command = button.getAttribute('command');
if (target && command) {
target.dispatchEvent(new CommandEvent('command', {
command,
source: button,
}));
}
});
// 4. **Mandatory:** Register the unified listener directly on the target element
document.getElementById('action-target').addEventListener('command', (event) => {
const command = event.command;
const target = event.target;
const action = commandRegistry[command];
if (action) {
action(target);
}
});
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 預設定位。