HTML 中引入 command 和 commandfor 新特性
HTML 中引入 command 和 commandfor 新特性
使用集合保持井井有条,根据您的偏好保存和分类内容。
Keith Cirkel
GitHub Mastodon Bluesky
发布时间:2025 年 3 月 7 日
按钮是构建动态 Web 应用程序的基础。按钮打开菜单、切换操作和提交表单,它们提供了 Web 上交互性的基础。使按钮简单且易于访问可能会带来一些令人惊讶的挑战。构建微前端或组件系统的开发人员可能会遇到比必要更复杂的解决方案。虽然框架有所帮助,但 Web 在这里可以做得更多。
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>
);
}
command
和 commandfor
模式
使用 command
和 commandfor
属性,按钮现在可以以声明方式对其他元素执行操作,从而带来框架的人体工程学,而不会牺牲灵活性。 commandfor
按钮采用 ID(类似于 for
属性),而 command
接受内置值,从而实现更便携和直观的方法。
示例:带有 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
,只是它们特定于 popover。 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
属性。 这减少了对 JavaScript 的需求,除了对话框上的单个事件侦听器来确定接下来该做什么:
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,因此在跨越 shadow DOM 方面存在限制。 在这些情况下,您可以使用 JavaScript API 设置 .commandForElement
属性,该属性可以设置任何元素,跨 shadow root:
<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>
未来的提案可能会提供一种声明性方法来共享跨 shadow 边界的引用,例如 Reference Target Proposal。
接下来是什么?
我们将继续探索新内置命令的可能性,以涵盖网站使用的常用功能。 拟议的想法包含在 Open UI Proposal 中。 已经探索过的一些想法:
- 打开和关闭
<details>
元素。 <input>
和<select>
元素的"show-picker"
命令,映射到showPicker()
。<video>
和<audio>
元素的播放命令。- 从元素复制文本内容。
我们欢迎社区的意见 - 如果您有任何建议,请随时在 Open UI Issue Tracker 上提交问题。
了解更多
在 规范 和 MDN 上查找有关 command
和 commandfor
的更多信息。
除非另有说明,否则本页面的内容根据 Creative Commons Attribution 4.0 License 获得许可,代码示例根据 Apache 2.0 License 获得许可。 有关详细信息,请参阅 Google Developers Site Policies。 Java 是 Oracle 和/或其附属公司的注册商标。
上次更新时间 2025-03-07 UTC。