Dipublikasikan: 7 Maret 2025
Tombol sangat penting untuk membuat aplikasi web dinamis. Tombol membuka menu, mengaktifkan tindakan, dan mengirimkan formulir. API ini menyediakan fondasi interaktivitas di web. Membuat tombol sederhana dan mudah diakses dapat menyebabkan beberapa tantangan yang mengejutkan. Developer yang membuat frontend mikro atau sistem komponen dapat menemukan solusi yang menjadi lebih kompleks dari yang diperlukan. Meskipun framework membantu, web dapat melakukan lebih banyak hal di sini.
Chrome 135 memperkenalkan kemampuan baru untuk memberikan perilaku deklaratif dengan
atribut command
dan commandfor
baru, yang meningkatkan dan mengganti
atribut popovertargetaction
dan popovertarget
. Atribut baru ini dapat
ditambahkan ke tombol, sehingga browser dapat mengatasi beberapa masalah inti terkait
kesederhanaan dan aksesibilitas, serta memberikan fungsi umum bawaan.
Pola tradisional
Membuat perilaku tombol tanpa framework dapat menimbulkan beberapa tantangan menarik seiring berkembangnya kode produksi. Meskipun HTML menawarkan pengendali onclick
ke
tombol, hal ini sering kali tidak diizinkan di luar demo atau tutorial karena aturan
Kebijakan Keamanan Konten (CSP). Meskipun peristiwa ini dikirimkan pada elemen
tombol, tombol biasanya ditempatkan di halaman untuk mengontrol elemen lain
yang memerlukan kode untuk mengontrol dua elemen sekaligus. Anda juga perlu memastikan interaksi
ini dapat diakses oleh pengguna teknologi pendukung. Hal ini sering kali menyebabkan
kode terlihat seperti ini:
<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>
Pendekatan ini dapat sedikit rapuh, dan framework bertujuan untuk meningkatkan ergonomi. Pola umum dengan framework seperti React mungkin melibatkan pemetaan klik ke perubahan status:
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>
</>
);
}
Banyak framework lain juga bertujuan untuk memberikan ergonomi yang serupa, misalnya ini dapat ditulis di AlpineJS sebagai:
<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>
Saat menulis ini di Svelte, tampilannya mungkin seperti ini:
<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>
Beberapa sistem atau library desain mungkin melangkah lebih jauh, dengan menyediakan wrapper di sekitar elemen tombol yang mengenkapsulasi perubahan status. Hal ini memisahkan perubahan status di balik komponen pemicu, dengan mengorbankan sedikit fleksibilitas untuk meningkatkan ergonomi:
import {MenuTrigger, MenuContent} from 'my-design-system';
function MyMenu({children}) {
return (
<MenuTrigger>
<button>Open Menu</button>
</MenuTrigger>
<MenuContent>{children}</MenuContent>
);
}
Pola command dan commandfor
Dengan atribut command
dan commandfor
, tombol kini dapat melakukan tindakan
pada elemen lain secara deklaratif, sehingga memberikan ergonomi framework tanpa
menyedekatkan fleksibilitas. Tombol commandfor
menggunakan ID—mirip dengan
atribut for
—sedangkan command
menerima nilai bawaan, sehingga memungkinkan pendekatan yang lebih
portabel dan intuitif.
Contoh: Tombol menu terbuka dengan command dan commandfor
HTML berikut menyiapkan hubungan deklaratif antara tombol dan
menu yang memungkinkan browser menangani logika dan aksesibilitas untuk Anda. Anda tidak perlu mengelola aria-expanded
atau menambahkan JavaScript tambahan.
<button commandfor="my-menu" command="show-popover">
Open Menu
</button>
<div popover id="my-menu">
<!-- ... -->
</div>
Membandingkan command
dan commandfor
dengan popovertargetaction
dan popovertarget
Jika pernah menggunakan popover
sebelumnya, Anda mungkin sudah memahami atribut popovertarget
dan popovertargetaction
. Fungsi ini berfungsi mirip dengan commandfor
dan
command
—kecuali fungsi ini khusus untuk popover. Atribut command
dan
commandfor
sepenuhnya menggantikan atribut lama ini. Atribut
baru mendukung semua yang dilakukan atribut lama, serta menambahkan kemampuan
baru.
Perintah bawaan
Atribut command
memiliki serangkaian perilaku bawaan yang dipetakan ke berbagai
API untuk elemen interaktif:
show-popover
: Dipetakan keel.showPopover()
.hide-popover
: Dipetakan keel.hidePopover()
.toggle-popover
: Dipetakan keel.togglePopover()
.show-modal
: Dipetakan kedialogEl.showModal()
.close
: Dipetakan kedialogEl.close()
.
Perintah ini dipetakan ke pasangan JavaScript-nya, sekaligus menyederhanakan
aksesibilitas (seperti memberikan hubungan yang setara dengan aria-details
dan aria-expanded
), manajemen fokus, dan lainnya.
Contoh: Dialog konfirmasi dengan command
dan 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>
Mengklik tombol Delete Record akan membuka dialog sebagai modal, sedangkan
mengklik tombol Close, Cancel, atau Delete akan menutup dialog sekaligus
mengirim peristiwa "close"
pada dialog, yang memiliki properti returnValue
yang cocok dengan nilai tombol.
Hal ini mengurangi kebutuhan JavaScript di luar satu pemroses peristiwa
di dialog untuk menentukan tindakan selanjutnya:
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");
}
});
Perintah kustom
Selain perintah bawaan, Anda dapat menentukan perintah kustom menggunakan
awalan --
. Perintah kustom akan mengirim peristiwa "command"
pada elemen
target (seperti perintah bawaan), tetapi tidak akan pernah melakukan
logika tambahan seperti yang dilakukan nilai bawaan. Hal ini memberikan fleksibilitas untuk
mem-build komponen yang dapat bereaksi terhadap tombol dengan berbagai cara tanpa harus
menyediakan komponen wrapper, menjelajahi DOM untuk elemen target, atau memetakan
klik tombol ke perubahan status. Hal ini memungkinkan Anda menyediakan API dalam HTML untuk
komponen Anda:
<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>
Perintah di ShadowDOM
Mengingat atribut commandfor
menggunakan ID, ada batasan terkait
melintasi DOM bayangan. Dalam hal ini, Anda dapat menggunakan JavaScript API untuk menetapkan
properti .commandForElement
yang dapat menetapkan elemen apa pun, di seluruh root
bayangan:
<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>
Proposal mendatang dapat memberikan cara deklaratif untuk membagikan referensi di seluruh batas bayangan, seperti Proposal Target Referensi.
Apa langkah selanjutnya?
Kami akan terus mempelajari kemungkinan untuk perintah bawaan baru, guna mencakup fungsi umum yang digunakan situs. Ide yang diusulkan dibahas dalam Proposal UI Terbuka. Beberapa ide yang telah dieksplorasi:
- Membuka dan menutup elemen
<details>
. - Perintah
"show-picker"
untuk elemen<input>
dan<select>
, yang dipetakan keshowPicker()
. - Perintah pemutaran untuk elemen
<video>
dan<audio>
. - Menyalin konten teks dari elemen.
Kami menerima masukan komunitas. Jika Anda memiliki saran, jangan ragu untuk melaporkan masalah di Open UI Issue Tracker.
Pelajari lebih lanjut
Temukan informasi selengkapnya tentang command
dan commandfor
di
spesifikasi
dan di
MDN.