Utilizza l'API Invoker Commands per attivare/disattivare la visibilità degli elementi <dialog> e [popover] direttamente dai pulsanti HTML, eliminando la necessità di listener di eventi JavaScript personalizzati.
Applicando gli attributi commandfor (ID target) e command (azione) a un <button>, il browser gestisce automaticamente le modifiche dello stato di apertura/chiusura, la gestione dello stato attivo e le associazioni di accessibilità (come aria-expanded). Questo approccio dichiarativo è consigliato perché rimuove il codice boilerplate fragile, garantisce che le interazioni siano funzionali immediatamente dopo l'analisi HTML e garantisce un'esperienza utente solida e accessibile in modo nativo.
Implementazione dei popover dichiarativi
I popover possono essere aperti e chiusi utilizzando un unico pulsante.
<!-- MANDATORY: The commandfor attribute links the invoker to the ID of the target element so the browser knows what to control. -->
<!-- MANDATORY: The command attribute specifies the action to perform. Use 'toggle-popover' to handle both open and close states automatically. -->
<button commandfor="my-popover" command="toggle-popover">
Toggle Popover
</button>
<!-- MANDATORY: The target element must have the popover attribute to be controlled as a popover. -->
<div id="my-popover" popover>
<p>Popover content goes here.</p>
</div>
Se devi controllare l'apertura e la chiusura con pulsanti separati, puoi utilizzare i comandi show-popover e hide-popover.
<!-- MANDATORY: Use 'show-popover' to explicitly open the popover. It will not close the popover if clicked again. -->
<button commandfor="my-explicit-popover" command="show-popover">
Show Popover
</button>
<div id="my-explicit-popover" popover="manual">
<p>This popover is explicitly opened and closed by separate buttons.</p>
<!-- MANDATORY: Use 'hide-popover' to explicitly close the targeted popover. -->
<button commandfor="my-explicit-popover" command="hide-popover">
Hide Popover
</button>
</div>
Implementazione di finestre di dialogo modali dichiarative
A differenza dei popover, le finestre di dialogo modali in genere utilizzano pulsanti separati per l'apertura e la chiusura. Utilizza il comando show-modal in particolare quando devi aprire una finestra di dialogo come modale.
<!-- MANDATORY: Use command="show-modal" to trigger the dialog as a modal, trapping focus and preventing interaction with the rest of the page. -->
<!-- MANDATORY: The commandfor attribute connects this button to the dialog ID. -->
<button commandfor="confirm-dialog" command="show-modal">
Open Confirmation
</button>
<dialog id="confirm-dialog">
<p>Are you sure you want to proceed?</p>
<!-- MANDATORY: Use command="close" to dismiss the dialog safely. -->
<button commandfor="confirm-dialog" command="close">
Cancel
</button>
</dialog>
Strategie di riserva
Poiché i comandi Invoker e i popover non sono ancora supportati universalmente, DEVI utilizzare i polyfill come fallback per i browser meno recenti.
Polyfill dei comandi di Invoker
OBBLIGATORIO: rileva il supporto della funzionalità controllando la proprietà commandForElement sul prototipo HTMLButtonElement. NON controllare l'oggetto window o document. DEVI importare dinamicamente il polyfill solo quando la funzionalità nativa non è presente. NON caricare il polyfill in modo incondizionato.
Obbligatorio: ascolta l'evento "command" direttamente sull'elemento di destinazione perché l'evento "command" nativo non si propaga.
Opzione 1: utilizzo di un bundler
Installa il polyfill tramite npm (npm install invokers-polyfill). Questo approccio è destinato ai progetti che utilizzano un bundler (come Vite o Webpack) o mappe di importazione. Per tutte le altre configurazioni, utilizza l'opzione CDN di seguito.
// MANDATORY: Feature detect 'commandForElement' on HTMLButtonElement.prototype.
// Conditionally load the invokers-polyfill only in browsers lacking native support.
if (!('commandForElement' in HTMLButtonElement.prototype)) {
import('invokers-polyfill');
}
Opzione 2: utilizza una CDN
Per i progetti senza un bundler, importa dinamicamente il polyfill direttamente da una CDN all'interno di un <script type="module">.
<script type="module">
// MANDATORY: Feature detect 'commandForElement' on HTMLButtonElement.prototype.
// Conditionally load the invokers-polyfill from a CDN only in browsers lacking native support.
if (!('commandForElement' in HTMLButtonElement.prototype)) {
import('https://esm.run/invokers-polyfill');
}
</script>
Limitazioni del polyfill degli invoker
OBBLIGATORIO: questo polyfill non gestisce gli stati ARIA (ad es. aria-expanded) del pulsante di comando come fanno i browser nativi. Ti consigliamo vivamente di gestire questi stati autonomamente per assicurarti che il tuo sito sia completamente accessibile.
Se l'API Invoker Commands non è supportata, l'evento command non verrà attivato. Per un supporto completo su tutti i browser moderni, ti consigliamo di utilizzare invokers-polyfill da https://github.com/keithamus/invokers-polyfill tramite npm install o CDN.
Questo polyfill supporta completamente le azioni personalizzate (a partire da --) e invia l'evento command esattamente come l'API nativa.
Importazione dinamica (ottimizzazione delle prestazioni)
Per un rendimento ottimale, devi caricare il polyfill solo se il browser non supporta l'API in modo nativo. In questo modo, si risparmia larghezza di banda e si riduce il tempo di esecuzione degli script per gli utenti che utilizzano browser moderni.
NOTA: questo polyfill non gestisce gli stati ARIA (come aria-pressed o aria-expanded) per i comandi personalizzati. Devi sincronizzare manualmente questi stati nel listener di eventi per assicurarti che il tuo sito sia accessibile.
// 1. Conditionally load the polyfill
const hasNativeSupport = 'commandForElement' in HTMLButtonElement.prototype;
if (!hasNativeSupport) {
// Wrap in an async IIFE to avoid top-level await issues in older browsers
(async () => {
try {
await import('https://esm.run/invokers-polyfill');
} catch (err) {
console.error('Error loading fallback:', err);
}
})();
}
// 2. Manually manage ARIA states in your listener
document.getElementById('action-target').addEventListener('command', (event) => {
const command = event.command;
const target = event.target;
const source = event.source; // The button that triggered the command
if (command === '--spin') {
const isSpun = target.classList.toggle('is-spun');
// Polyfill tip: Manually update ARIA to match the new state
source?.setAttribute('aria-pressed', isSpun);
}
});
Fallback manuale (pattern tradizionale)
Se preferisci non utilizzare un polyfill, puoi utilizzare una combinazione di delega degli eventi per inviare gli eventi e un registro dei comandi per gestire le azioni. Questo è un pattern architetturale comune nello sviluppo JavaScript tradizionale che rimane altamente efficiente e scalabile.
// 1. **Optional:** Define a registry of requested actions for cleaner logic
const commandRegistry = {
'--spin': (target) => target.classList.toggle('is-spun'),
'--grow': (target) => target.classList.toggle('is-grown'),
'--reset': (target) => target.classList.remove('is-spun', 'is-grown'),
};
// 2. If CommandEvent doesn't exist, we assume no native support and provide the fallback
if (!globalThis.CommandEvent) {
globalThis.CommandEvent = class CommandEvent extends Event {
constructor(type, { source, command, ...options } = {}) {
super(type, options);
this.source = source;
this.command = command;
}
}
}
// 3. The fallback: Dispatch events manually if native support is missing
document.addEventListener('click', (event) => {
const button = event.composedPath().find((el) => el.matches?.("button[commandfor]"));
if (!button) return;
const target = document.getElementById(button.getAttribute('commandfor'));
const command = button.getAttribute('command');
if (target && command) {
target.dispatchEvent(new CommandEvent('command', {
command,
source: button,
}));
}
});
// 4. **Mandatory:** Register the unified listener directly on the target element
document.getElementById('action-target').addEventListener('command', (event) => {
const command = event.command;
const target = event.target;
const action = commandRegistry[command];
if (action) {
action(target);
}
});
Alternative e supporto del browser per Popover
L'API Popover è per lo più un potenziamento progressivo, ma i suoi comportamenti distintivi (promozione al livello superiore, chiusura con un clic e attivazione di popovertarget) non hanno un equivalente solo CSS. I browser meno recenti richiedono un polyfill o un fallback manuale se preferisci non fornirne uno.
Polyfill: per supportare l'attributo popover nei browser meno recenti, carica @oddbird/popover-polyfill in modo condizionale. OBBLIGATORIO: esegui il rilevamento delle funzionalità controllando la proprietà popover su HTMLElement.prototype e carica il polyfill solo quando manca il supporto nativo; NON caricarlo incondizionatamente.
Con un bundler o una mappa di importazione:
// MANDATORY: Feature detect 'popover' on HTMLElement.prototype.
if (!("popover" in HTMLElement.prototype)) {
import("@oddbird/popover-polyfill");
}
Senza un bundler, importa da una CDN all'interno di un <script type="module">:
<script type="module">
if (!("popover" in HTMLElement.prototype)) {
import("https://unpkg.com/@oddbird/popover-polyfill@latest/dist/popover.min.js");
}
</script>
Avvertenza sullo stile: il polyfill non può definire la pseudo-classe :popover-open reale, quindi applica una classe .\:popover-open. OBBLIGATORIO: combina i due elementi con :is() o :where(), altrimenti i browser che non dispongono di :popover-open ignorano l'intera regola:
[popover]:is(:popover-open, .\:popover-open) {
display: block;
}
In alternativa, per un fallback legacy senza un polyfill, utilizza position: fixed e calcola manualmente le coordinate tramite getBoundingClientRect() o affidati al posizionamento predefinito con inset: auto se è accettabile per il caso d'uso.