發布日期:2025 年 3 月 7 日
按鈕是製作動態網頁應用程式不可或缺的元素。按鈕可開啟選單、切換動作,以及提交表單。這些元素是網路互動功能的基礎。讓按鈕簡單易用可能會帶來一些意想不到的挑戰。開發人員在建構微前端或元件系統時,可能會遇到解決方案變得過於複雜的情況。雖然架構有助於解決問題,但網路可以提供更多協助。
Chrome 135 推出了新功能,可透過新的 command
和 commandfor
屬性提供宣告行為,並強化及取代 popovertargetaction
和 popovertarget
屬性。這些新屬性可新增至按鈕,讓瀏覽器解決簡易性和無障礙設計方面的部分核心問題,並提供內建的常用功能。
傳統模式
在沒有架構的情況下建構按鈕行為,可能會在實際程式碼演進過程中帶來一些有趣的挑戰。雖然 HTML 會為按鈕提供 onclick
處理常式,但由於內容安全性政策 (CSP) 規則,這些常式通常無法在示範或教學課程以外使用。雖然這些事件會在按鈕元素上分派,但按鈕通常會放在網頁上,用於控制需要程式碼一次控制兩個元素的其他元素。您也必須確保輔助技術使用者能存取這項互動。這通常會導致程式碼看起來像這樣:
<div class="menu-wrapper">
<button class="menu-opener" aria-expanded="false">
Open Menu
</button>
<div popover class="menu-content">
<!-- ... -->
</div>
</div>
<script type="module">
document.addEventListener('click', e => {
const button = e.target;
if (button.matches('.menu-opener')) {
const menu = button
.closest('.menu-wrapper')
.querySelector('.menu-content');
if (menu) {
button.setAttribute('aria-expanded', 'true');
menu.showPopover();
menu.addEventListener('toggle', e => {
// reset back to aria-expanded=false on close
if (e.newState == 'closed') {
button.setAttribute('aria-expanded', 'false');
}
}, {once: true})
}
}
});
</script>
這種做法可能會比較不穩定,而架構旨在改善人體工學。使用 React 等架構時,常見的模式可能會將點擊事件對應至狀態變更:
function MyMenu({ children }) {
const [isOpen, setIsOpen] = useState(false);
const open = useCallback(() => setIsOpen(true), []);
const handleToggle = useCallback((e) => {
// popovers have light dismiss which influences our state
setIsOpen(e.newState === 'open')
}, []);
const popoverRef = useRef(null);
useEffect(() => {
if (popoverRef.current) {
if (isOpen) {
popoverRef.current.showPopover();
} else {
popoverRef.current.hidePopover();
}
}
}, [popoverRef, isOpen]);
return (
<>
<button onClick={open} aria-expanded={isOpen}>
Open Menu
</button>
<div popover onToggle={handleToggle} ref={popoverRef}>
{children}
</div>
</>
);
}
許多其他架構也旨在提供類似的人體工學設計,例如,這段程式碼可能會以 AlpineJS 編寫成以下形式:
<div x-data="{open: false}">
<button @click="open = !open; $refs.popover.showPopover()" :aria-expanded="open">
Open Menu
</button>
<div popover x-ref="popover" @toggle="open = $event.newState === 'open'">
<!-- ... -->
</div>
</div>
在 Svelte 中編寫這段程式碼時,可能會像這樣:
<script>
let popover;
let open = false;
function togglePopover() {
open ? popover.hidePopover() : popover.showPopover();
open = !open;
}
</script>
<button on:click={togglePopover} aria-expanded={open}>
Open Menu
</button>
<div bind:this={popover} popover>
<!-- ... -->
</div>
某些設計系統或程式庫可能會更進一步,在按鈕元素周圍提供包裝函式,以便封裝狀態變更。這會將狀態變更抽象化,並在觸發事件元件後方進行,以便在犧牲一點彈性,換取更佳的人因設計:
import {MenuTrigger, MenuContent} from 'my-design-system';
function MyMenu({children}) {
return (
<MenuTrigger>
<button>Open Menu</button>
</MenuTrigger>
<MenuContent>{children}</MenuContent>
);
}
指令和 commandfor 模式
有了 command
和 commandfor
屬性,按鈕現在可以以宣告方式對其他元素執行動作,在不犧牲彈性的情況下,提供架構的人體工學。commandfor
按鈕會採用 ID (類似 for
屬性),而 command
則會接受內建值,提供更具可攜性和直覺性的做法。
範例:含有指令和 commandfor 的開啟選單按鈕
以下 HTML 會在按鈕和選單之間建立宣告式關係,讓瀏覽器處理邏輯和無障礙功能。您不需要管理 aria-expanded
或新增任何額外的 JavaScript。
<button commandfor="my-menu" command="show-popover">
Open Menu
</button>
<div popover id="my-menu">
<!-- ... -->
</div>
比較 command
和 commandfor
與 popovertargetaction
和 popovertarget
如果您曾使用 popover
,可能就熟悉 popovertarget
和 popovertargetaction
屬性。這些方法的運作方式與 commandfor
和 command
類似,只是分別適用於彈出式視窗。command
和 commandfor
屬性完全取代這些舊屬性。新屬性支援舊屬性支援的所有功能,並新增其他功能。
內建指令
command
屬性具有一組內建行為,可對應至互動式元素的各種 API:
show-popover
:對應至el.showPopover()
。hide-popover
:對應至el.hidePopover()
。toggle-popover
:對應至el.togglePopover()
。show-modal
:對應至dialogEl.showModal()
。close
:對應至dialogEl.close()
。
這些指令會對應至對應的 JavaScript 指令,同時簡化無障礙功能 (例如提供 aria-details
和 aria-expanded
等同關係)、焦點管理等。
範例:含有 command
和 commandfor
的確認對話方塊
<button commandfor="confirm-dialog" command="show-modal">
Delete Record
</button>
<dialog id="confirm-dialog">
<header>
<h1>Delete Record?</h1>
<button commandfor="confirm-dialog" command="close" aria-label="Close" value="close">
<img role="none" src="/close-icon.svg">
</button>
</header>
<p>Are you sure? This action cannot be undone</p>
<footer>
<button commandfor="confirm-dialog" command="close" value="cancel">
Cancel
</button>
<button commandfor="confirm-dialog" command="close" value="delete">
Delete
</button>
</footer>
</dialog>
按一下「Delete Record」按鈕,系統會以模態方式開啟對話方塊,而按一下「Close」、「Cancel」或「Delete」按鈕則會關閉對話方塊,並在對話方塊上調度 "close"
事件,該事件具有與按鈕值相符的 returnValue
屬性。這樣一來,您就不需要在對話方塊中使用單一事件事件監聽器,以便判斷下一步要採取哪些行動:
dialog.addEventListener("close", (event) => {
if (event.target.returnValue == "cancel") {
console.log("cancel was clicked");
} else if (event.target.returnValue == "close") {
console.log("close was clicked");
} else if (event.target.returnValue == "delete") {
console.log("delete was clicked");
}
});
自訂指令
除了內建指令外,您也可以使用 --
前置字元定義自訂指令。自訂指令會在目標元素上調度 "command"
事件 (就像內建指令一樣),但不會執行任何內建值的額外邏輯。這可讓您彈性地建構可以以各種方式回應按鈕的元件,而無需提供包裝函式元件、遍歷目標元素的 DOM,或將按鈕點擊事件對應至狀態變更。這樣一來,您就可以為元件提供 HTML 內部的 API:
<button commandfor="the-image" command="--rotate-landscape">
Landscape
</button>
<button commandfor="the-image" command="--rotate-portrait">
Portrait
</button>
<img id="the-image" src="photo.jpg">
<script type="module">
const image = document.getElementById("the-image");
image.addEventListener("command", (event) => {
if ( event.command == "--rotate-landscape" ) {
image.style.rotate = "-90deg"
} else if ( event.command == "--rotate-portrait" ) {
image.style.rotate = "0deg"
}
});
</script>
ShadowDOM 中的指令
由於 commandfor
屬性會使用 ID,因此跨越影子 DOM 時會受到限制。在這種情況下,您可以使用 JavaScript API 設定 .commandForElement
屬性,該屬性可在陰影根目錄中設定任何元素:
<my-element>
<template shadowrootmode=open>
<button command="show-popover">Show popover</button>
<slot></slot>
</template>
<div popover><!-- ... --></div>
</my-element>
<script>
customElements.define("my-element", class extends HTMLElement {
connectedCallback() {
const popover = this.querySelector('[popover]');
// The commandForElement can set cross-shadow root elements.
this.shadowRoot.querySelector('button').commandForElement = popover;
}
});
</script>
日後的提案可能會提供宣告式方法,讓您在陰影邊界之間共用參照,例如參照目標提案。
後續步驟
我們會持續探索新的內建指令的可能性,以涵蓋網站使用的常見功能。開放式 UI 提案中會說明建議的想法。我們已探索過的部分想法如下:
- 開啟和關閉
<details>
元素。 <input>
和<select>
元素的"show-picker"
指令,對應至showPicker()
。<video>
和<audio>
元素的播放指令。- 複製元素中的文字內容。
歡迎社群提供意見。如有建議,歡迎在 Open UI Issue Tracker 中提出問題。