Ngày phát hành: 7 tháng 3 năm 2025
Nút là thành phần thiết yếu để tạo ứng dụng web động. Nút mở trình đơn, bật/tắt thao tác và gửi biểu mẫu. Các lớp này cung cấp nền tảng cho tính tương tác trên web. Việc tạo các nút đơn giản và dễ tiếp cận có thể dẫn đến một số thách thức đáng ngạc nhiên. Nhà phát triển xây dựng giao diện người dùng vi mô hoặc hệ thống thành phần có thể gặp phải các giải pháp phức tạp hơn mức cần thiết. Mặc dù các khung có thể giúp ích, nhưng web có thể làm được nhiều việc hơn ở đây.
Chrome 135 ra mắt các tính năng mới để cung cấp hành vi khai báo bằng các thuộc tính command
và commandfor
mới, nâng cao và thay thế các thuộc tính popovertargetaction
và popovertarget
. Bạn có thể thêm các thuộc tính mới này vào các nút, cho phép trình duyệt giải quyết một số vấn đề cốt lõi liên quan đến tính đơn giản và khả năng hỗ trợ tiếp cận, đồng thời cung cấp chức năng phổ biến tích hợp sẵn.
Mẫu truyền thống
Việc xây dựng hành vi của nút mà không có khung có thể gây ra một số thách thức thú vị khi mã phát hành phát triển. Mặc dù HTML cung cấp trình xử lý onclick
cho các nút, nhưng các trình xử lý này thường không được phép sử dụng bên ngoài các bản minh hoạ hoặc hướng dẫn do các quy tắc của Chính sách bảo mật nội dung (CSP). Mặc dù các sự kiện này được gửi trên các phần tử nút, nhưng các nút thường được đặt trên một trang để kiểm soát các phần tử khác yêu cầu mã để kiểm soát hai phần tử cùng một lúc. Bạn cũng cần đảm bảo người dùng công nghệ hỗ trợ có thể sử dụng tính năng tương tác này. Điều này thường dẫn đến mã có dạng như sau:
<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>
Phương pháp này có thể hơi cứng nhắc và các khung nhằm cải thiện tính công thái học. Một mẫu phổ biến với khung như React có thể liên quan đến việc liên kết lượt nhấp với một thay đổi trạng thái:
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>
</>
);
}
Nhiều khung khác cũng hướng đến việc cung cấp các tính năng tương tự về mặt công thái học, ví dụ: bạn có thể viết nội dung này trong AlpineJS như sau:
<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>
Khi viết mã này trong Svelte, bạn có thể thấy mã có dạng như sau:
<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>
Một số hệ thống thiết kế hoặc thư viện có thể tiến xa hơn bằng cách cung cấp trình bao bọc xung quanh các thành phần nút đóng gói các thay đổi về trạng thái. Tóm tắt này thay đổi trạng thái ở phía sau một thành phần kích hoạt, đánh đổi một chút tính linh hoạt để cải thiện tính công thái học:
import {MenuTrigger, MenuContent} from 'my-design-system';
function MyMenu({children}) {
return (
<MenuTrigger>
<button>Open Menu</button>
</MenuTrigger>
<MenuContent>{children}</MenuContent>
);
}
Mẫu command và commandfor
Với các thuộc tính command
và commandfor
, các nút hiện có thể thực hiện các thao tác
trên các phần tử khác theo cách khai báo, mang lại tính công thái học của khung mà không
hy sinh tính linh hoạt. Nút commandfor
nhận một mã nhận dạng (tương tự như thuộc tính for
), trong khi command
chấp nhận các giá trị tích hợp, cho phép một phương pháp linh hoạt và trực quan hơn.
Ví dụ: Nút mở trình đơn có lệnh và commandfor
HTML sau đây thiết lập mối quan hệ khai báo giữa nút và trình đơn, cho phép trình duyệt xử lý logic và khả năng hỗ trợ tiếp cận cho bạn. Bạn không cần quản lý aria-expanded
hoặc thêm bất kỳ JavaScript nào khác.
<button commandfor="my-menu" command="show-popover">
Open Menu
</button>
<div popover id="my-menu">
<!-- ... -->
</div>
So sánh command
và commandfor
với popovertargetaction
và popovertarget
Nếu đã từng sử dụng popover
, bạn có thể quen thuộc với các thuộc tính popovertarget
và popovertargetaction
. Các phương thức này hoạt động tương tự như commandfor
và command
, ngoại trừ việc chúng dành riêng cho cửa sổ bật lên. Các thuộc tính command
và commandfor
thay thế hoàn toàn các thuộc tính cũ này. Các thuộc tính mới hỗ trợ mọi chức năng mà các thuộc tính cũ đã hỗ trợ, cũng như thêm các chức năng mới.
Lệnh tích hợp
Thuộc tính command
có một tập hợp các hành vi tích hợp liên kết với nhiều API cho các thành phần tương tác:
show-popover
: Ánh xạ đếnel.showPopover()
.hide-popover
: Ánh xạ đếnel.hidePopover()
.toggle-popover
: Ánh xạ đếnel.togglePopover()
.show-modal
: Ánh xạ đếndialogEl.showModal()
.close
: Ánh xạ đếndialogEl.close()
.
Các lệnh này liên kết với các lệnh tương đương trong JavaScript, đồng thời đơn giản hoá khả năng hỗ trợ tiếp cận (chẳng hạn như cung cấp mối quan hệ tương đương aria-details
và aria-expanded
), quản lý tiêu điểm và nhiều tính năng khác.
Ví dụ: Hộp thoại xác nhận với command
và 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>
Khi nhấp vào nút Delete Record (Xoá bản ghi), hộp thoại sẽ mở dưới dạng một hộp thoại phương thức, trong khi nhấp vào nút Close (Đóng), Cancel (Huỷ) hoặc Delete (Xoá) sẽ đóng hộp thoại, đồng thời gửi một sự kiện "close"
trên hộp thoại có thuộc tính returnValue
khớp với giá trị của nút.
Điều này giúp giảm nhu cầu sử dụng JavaScript ngoài một trình nghe sự kiện duy nhất trên hộp thoại để xác định việc cần làm tiếp theo:
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");
}
});
Lệnh tuỳ chỉnh
Ngoài các lệnh tích hợp, bạn có thể xác định các lệnh tuỳ chỉnh bằng cách sử dụng tiền tố --
. Các lệnh tuỳ chỉnh sẽ gửi một sự kiện "command"
trên phần tử mục tiêu (giống như các lệnh tích hợp), nhưng sẽ không bao giờ thực hiện bất kỳ logic bổ sung nào như các giá trị tích hợp. Điều này mang lại sự linh hoạt cho việc tạo các thành phần có thể phản ứng với các nút theo nhiều cách mà không cần cung cấp các thành phần trình bao bọc, di chuyển qua DOM cho phần tử mục tiêu hoặc liên kết các lượt nhấp vào nút với các thay đổi trạng thái. Điều này cho phép bạn cung cấp API trong HTML cho các thành phần của mình:
<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>
Lệnh trong ShadowDOM
Do thuộc tính commandfor
nhận một mã nhận dạng, nên có các quy định hạn chế về việc vượt qua DOM bóng. Trong những trường hợp này, bạn có thể sử dụng API JavaScript để đặt thuộc tính .commandForElement
. Thuộc tính này có thể đặt bất kỳ phần tử nào trên các gốc bóng:
<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>
Các đề xuất trong tương lai có thể cung cấp một cách khai báo để chia sẻ tệp đối chiếu trên các ranh giới bóng, chẳng hạn như Đề xuất mục tiêu tham chiếu.
Tiếp theo là gì?
Chúng tôi sẽ tiếp tục khám phá các khả năng cho các lệnh tích hợp mới để bao gồm các chức năng phổ biến mà các trang web sử dụng. Đề xuất giao diện người dùng mở đề cập đến các ý tưởng được đề xuất. Một số ý tưởng đã được khám phá:
- Mở và đóng các phần tử
<details>
. - Lệnh
"show-picker"
cho các phần tử<input>
và<select>
, liên kết đếnshowPicker()
. - Lệnh phát cho các phần tử
<video>
và<audio>
. - Sao chép nội dung văn bản từ các phần tử.
Chúng tôi hoan nghênh ý kiến đóng góp của cộng đồng. Nếu bạn có đề xuất, đừng ngại gửi vấn đề trên Công cụ theo dõi lỗi giao diện người dùng mở.
Tìm hiểu thêm
Tìm thêm thông tin về command
và commandfor
trong
quy cách
và trên
MDN.