Utilisez l'API Invoker Commands pour activer ou désactiver la visibilité des éléments <dialog> et [popover] directement à partir de boutons HTML, ce qui évite d'avoir à utiliser des écouteurs d'événements JavaScript personnalisés.
En appliquant les attributs commandfor (ID cible) et command (action) à un <button>, le navigateur gère automatiquement les changements d'état d'ouverture/de fermeture, la gestion du focus et les liaisons d'accessibilité (comme aria-expanded). Cette approche déclarative est recommandée, car elle supprime le code récurrent fragile, garantit que les interactions sont fonctionnelles dès l'analyse HTML et garantit une expérience utilisateur robuste et accessible de manière native.
Implémenter des popovers déclaratifs
Les popovers peuvent être ouverts et fermés à l'aide d'un seul bouton.
<!-- 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>
Si vous devez contrôler l'ouverture et la fermeture avec des boutons distincts, vous pouvez utiliser les commandes show-popover et 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>
Implémenter des boîtes de dialogue modales déclaratives
Contrairement aux popovers, les boîtes de dialogue modales utilisent généralement des boutons distincts pour l'ouverture et la fermeture. Utilisez la commande show-modal spécifiquement lorsque vous devez ouvrir une boîte de dialogue en tant que fenêtre 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>
Stratégies de remplacement
Comme les commandes d'appel et les popovers ne sont pas encore universellement compatibles, vous DEVEZ utiliser des polyfills comme solutions de secours pour les anciens navigateurs.
Polyfilling des commandes d'appelant
OBLIGATOIRE : Détectez la compatibilité de la fonctionnalité en vérifiant la propriété commandForElement sur le prototype HTMLButtonElement. Ne vérifiez PAS l'objet window ou document. Vous DEVEZ importer dynamiquement le polyfill uniquement lorsque la fonctionnalité native est manquante. NE CHARGEZ PAS le polyfill de manière inconditionnelle.
Obligatoire : écoutez l'événement "command" directement sur l'élément cible, car l'événement "command" natif ne se propage pas.
Option 1 : Utiliser un bundler
Installez le polyfill via npm (npm install invokers-polyfill). Cette approche est destinée aux projets utilisant un bundler (comme Vite ou Webpack) ou des mappages d'importation. Pour toutes les autres configurations, utilisez l'option CDN ci-dessous.
// 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');
}
Option 2 : Utiliser un CDN
Pour les projets sans bundler, importez dynamiquement le polyfill directement depuis un CDN dans 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>
Limites du polyfill des invocateurs
OBLIGATOIRE : Ce polyfill ne gère pas les états ARIA (par exemple, aria-expanded) du bouton de commande comme le font les navigateurs natifs. Nous vous encourageons vivement à gérer vous-même ces états pour vous assurer que votre site est entièrement accessible.
Si l'API Invoker Commands n'est pas compatible, l'événement command ne se déclenchera pas. Pour une compatibilité totale avec tous les navigateurs récents, nous vous recommandons d'utiliser le polyfill invokers-polyfill de https://github.com/keithamus/invokers-polyfill via npm install ou un CDN.
Ce polyfill est entièrement compatible avec les actions personnalisées (à partir de --) et distribue l'événement command exactement comme l'API native.
Importation dynamique (optimisation des performances)
Pour des performances optimales, ne chargez le polyfill que si le navigateur n'est pas compatible avec l'API de manière native. Cela permet d'économiser de la bande passante et de réduire le temps d'exécution des scripts pour les utilisateurs de navigateurs récents.
REMARQUE : Ce polyfill ne gère pas les états ARIA (comme aria-pressed ou aria-expanded) pour les commandes personnalisées. Vous devez synchroniser manuellement ces états dans votre écouteur d'événements pour vous assurer que votre site est accessible.
// 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);
}
});
Création de remplacement manuelle (modèle traditionnel)
Si vous préférez ne pas utiliser de polyfill, vous pouvez combiner la délégation d'événements pour distribuer les événements et un registre de commandes pour gérer les actions. Il s'agit d'un schéma architectural communément utilisé dans le développement JavaScript traditionnel, qui reste très efficace et évolutif.
// 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);
}
});
Solutions de remplacement et compatibilité des navigateurs pour Popover
L'API Popover est principalement une amélioration progressive, mais ses comportements déterminants (promotion de la couche supérieure, fermeture par clic en dehors de l'élément et appel de popovertarget) n'ont pas d'équivalent CSS uniquement. Les anciens navigateurs ont besoin d'un polyfill ou d'un remplacement manuel si vous préférez ne pas en fournir.
Polyfill : pour que l'attribut popover soit compatible avec les anciens navigateurs, chargez @oddbird/popover-polyfill de manière conditionnelle. OBLIGATOIRE : Détectez la fonctionnalité en vérifiant la propriété popover sur HTMLElement.prototype, et chargez le polyfill uniquement lorsque la compatibilité native est manquante. Ne le chargez PAS de manière inconditionnelle.
Avec un bundler ou une carte d'importation :
// MANDATORY: Feature detect 'popover' on HTMLElement.prototype.
if (!("popover" in HTMLElement.prototype)) {
import("@oddbird/popover-polyfill");
}
Sans bundler, importez depuis un CDN dans 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>
Mise en garde concernant l'application de styles : le polyfill ne peut pas définir la véritable pseudo-classe :popover-open. Il applique donc une classe .\:popover-open à la place. OBLIGATOIRE : Combinez les deux avec :is() ou :where(), sinon les navigateurs qui ne disposent pas de :popover-open ignorent l'intégralité de la règle :
[popover]:is(:popover-open, .\:popover-open) {
display: block;
}
Vous pouvez également utiliser position: fixed pour une solution de remplacement sans polyfill et calculer manuellement les coordonnées via getBoundingClientRect(), ou vous appuyer sur le positionnement par défaut avec inset: auto si cela convient à votre cas d'utilisation.