Gepubliceerd: 7 maart 2025
Knoppen zijn essentieel voor het maken van dynamische webapplicaties. Knoppen openen menu's, schakelen tussen acties en verzenden formulieren. Ze vormen de basis van interactiviteit op internet. Het eenvoudig en toegankelijk maken van knoppen kan tot verrassende uitdagingen leiden. Ontwikkelaars die micro-frontends of componentsystemen bouwen, kunnen oplossingen tegenkomen die complexer worden dan nodig. Hoewel frameworks helpen, kan het internet hier meer doen.
Chrome 135 introduceert nieuwe mogelijkheden voor het bieden van declaratief gedrag met de nieuwe command
en commandfor
attributen, waarbij de popovertargetaction
en popovertarget
-attributen worden verbeterd en vervangen. Deze nieuwe kenmerken kunnen aan knoppen worden toegevoegd, waardoor de browser enkele kernproblemen rond eenvoud en toegankelijkheid kan aanpakken en ingebouwde gemeenschappelijke functionaliteit kan bieden.
Traditionele patronen
Het bouwen van knopgedrag zonder raamwerk kan een aantal interessante uitdagingen opleveren naarmate de productiecode evolueert. Hoewel HTML onclick
handlers voor knoppen biedt, zijn deze buiten demo's of tutorials vaak niet toegestaan vanwege regels voor Content Security Policy (CSP). Hoewel deze gebeurtenissen op knopelementen worden verzonden, worden knoppen meestal op een pagina geplaatst om andere elementen te besturen, waarvoor code nodig is om twee elementen tegelijk te besturen. U moet er ook voor zorgen dat deze interactie toegankelijk is voor gebruikers van ondersteunende technologie. Dit leidt er vaak toe dat code er ongeveer zo uitziet:
<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>
Deze aanpak kan een beetje broos zijn en de raamwerken zijn erop gericht de ergonomie te verbeteren. Een veel voorkomend patroon bij een raamwerk als React kan het in kaart brengen van de klik op een statusverandering inhouden:
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>
</>
);
}
Veel andere raamwerken zijn ook bedoeld om vergelijkbare ergonomie te bieden, dit kan bijvoorbeeld in AlpineJS worden geschreven als:
<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>
Terwijl het schrijven van dit in Svelte er ongeveer zo uit zou kunnen zien:
<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>
Sommige ontwerpsystemen of bibliotheken kunnen nog een stap verder gaan door wrappers rond knopelementen aan te bieden die de statusveranderingen inkapselen. Dit vat statusveranderingen achter een triggercomponent samen, waarbij een beetje flexibiliteit wordt ingeruild voor verbeterde ergonomie:
import {MenuTrigger, MenuContent} from 'my-design-system';
function MyMenu({children}) {
return (
<MenuTrigger>
<button>Open Menu</button>
</MenuTrigger>
<MenuContent>{children}</MenuContent>
);
}
De opdracht en opdracht voor patroon
Met het command
en commandfor
attributen kunnen knoppen nu declaratief acties uitvoeren op andere elementen, waardoor de ergonomie van een raamwerk ontstaat zonder dat dit ten koste gaat van de flexibiliteit. De commandfor
knop neemt een ID aan (vergelijkbaar met het for
attribuut), terwijl command
ingebouwde waarden accepteert, waardoor een meer draagbare en intuïtieve aanpak mogelijk wordt.
Voorbeeld: een open menuknop met opdracht en opdrachtvoor
De volgende HTML zorgt voor declaratieve relaties tussen de knop en het menu, waardoor de browser de logica en toegankelijkheid voor u kan afhandelen. Het is niet nodig om aria-expanded
te beheren of extra JavaScript toe te voegen.
<button commandfor="my-menu" command="show-popover">
Open Menu
</button>
<div popover id="my-menu">
<!-- ... -->
</div>
command
en commandfor
vergelijken met popovertargetaction
en popovertarget
Als u al eerder popover
heeft gebruikt, bent u wellicht bekend met de kenmerken popovertarget
en popovertargetaction
. Deze werken op dezelfde manier als respectievelijk commandfor
en command
, behalve dat ze specifiek zijn voor popovers. De attributen command
en commandfor
vervangen deze oudere attributen volledig. De nieuwe attributen ondersteunen alles wat de oudere attributen deden, en voegden ook nieuwe mogelijkheden toe.
Ingebouwde opdrachten
Het command
heeft een reeks ingebouwde gedragingen die verwijzen naar verschillende API's voor interactieve elementen:
-
show-popover
: verwijst naarel.showPopover()
. -
hide-popover
: verwijst naarel.hidePopover()
. -
toggle-popover
: Wordt toegewezen aanel.togglePopover()
. -
show-modal
: verwijst naardialogEl.showModal()
. -
close
: verwijst naardialogEl.close()
.
Deze commando's verwijzen naar hun JavaScript-tegenhangers, terwijl ze ook de toegankelijkheid stroomlijnen (zoals het verstrekken van de aria-details
en aria-expanded
equivalente relaties), focusbeheer en meer.
Voorbeeld: een bevestigingsdialoogvenster met command
en 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>
Als u op de knop Record verwijderen klikt, wordt het dialoogvenster als modaal geopend, terwijl als u op de knoppen Sluiten , Annuleren of Verwijderen klikt, het dialoogvenster wordt gesloten, terwijl er ook een "close"
-gebeurtenis in het dialoogvenster wordt verzonden, die een returnValue
eigenschap heeft die overeenkomt met de waarde van de knop. Hierdoor is er minder JavaScript nodig dan een enkele gebeurtenislistener in het dialoogvenster om te bepalen wat er vervolgens moet gebeuren:
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");
}
});
Aangepaste opdrachten
Naast de ingebouwde opdrachten kunt u aangepaste opdrachten definiëren met behulp van het voorvoegsel --
. Aangepaste opdrachten verzenden een "command"
-gebeurtenis op het doelelement (net als de ingebouwde opdrachten), maar zullen verder nooit enige extra logica uitvoeren zoals de ingebouwde waarden dat doen. Dit biedt flexibiliteit voor het bouwen van componenten die op verschillende manieren op knoppen kunnen reageren zonder dat er wrappercomponenten hoeven te worden gebruikt, de DOM moet worden doorkruist voor het doelelement of knopklikken moeten worden toegewezen aan statuswijzigingen. Hiermee kunt u een API binnen HTML voor uw componenten leveren:
<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>
Commando's in de ShadowDOM
Aangezien het commandfor
attribuut een ID nodig heeft, zijn er beperkingen bij het overschrijden van de schaduw-DOM. In deze gevallen kunt u de JavaScript-API gebruiken om de eigenschap .commandForElement
in te stellen, waarmee elk element kan worden ingesteld, over schaduwwortels heen:
<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>
Toekomstige voorstellen kunnen een declaratieve manier bieden om referenties over schaduwgrenzen heen te delen, zoals het Reference Target Proposal .
Wat is het volgende?
We blijven mogelijkheden verkennen voor nieuwe ingebouwde opdrachten, om de algemene functionaliteit te dekken die websites gebruiken. Voorgestelde ideeën worden behandeld in het Open UI-voorstel . Enkele ideeën die al zijn onderzocht:
-
<details>
-elementen openen en sluiten. - Een
"show-picker"
-opdracht voor<input>
en<select>
-elementen, toegewezen aanshowPicker()
. - Afspeelopdrachten voor
<video>
en<audio>
-elementen. - Tekstinhoud uit elementen kopiëren.
We verwelkomen input van de gemeenschap. Als u suggesties heeft, aarzel dan niet om een probleem in te dienen via de Open UI Issue Tracker .
Meer informatie
Meer informatie over command
en commandfor
vindt u in de specificatie en op MDN .