Usa la API de Invoker Commands para activar o desactivar la visibilidad de los elementos <dialog> y [popover] directamente desde los botones HTML, lo que elimina la necesidad de usar objetos de escucha de eventos de JavaScript personalizados.
Cuando se aplican los atributos commandfor (ID de destino) y command (acción) a un elemento <button>, el navegador controla automáticamente los cambios de estado de apertura y cierre, la administración del enfoque y las vinculaciones de accesibilidad (como aria-expanded). Se recomienda este enfoque declarativo porque quita el código estándar frágil, garantiza que las interacciones sean funcionales de inmediato después del análisis de HTML y garantiza una experiencia del usuario sólida y accesible de forma nativa.
Implementación de ventanas emergentes declarativas
Los popovers se pueden abrir y cerrar con un solo botón.
<!-- 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 necesitas controlar la apertura y el cierre con botones independientes, puedes usar los comandos show-popover y 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>
Implementa diálogos modales declarativos
A diferencia de los cuadros emergentes, los diálogos modales suelen usar botones independientes para abrir y cerrar. Usa el comando show-modal específicamente cuando necesites abrir un diálogo como modal.
<!-- 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>
Estrategias de resguardo
Debido a que los comandos de invocador y los popovers aún no son compatibles de forma universal, DEBES usar polyfills como resguardos para los navegadores más antiguos.
Comandos de invocador de polyfill
OBLIGATORIO: Verifica la compatibilidad de la función comprobando la propiedad commandForElement en el prototipo HTMLButtonElement. NO verifiques el objeto window ni document. DEBES importar de forma dinámica el polyfill solo cuando falte la función nativa. NO cargues el polyfill de forma incondicional.
Obligatorio: Escucha el evento "command" directamente en el elemento de destino, ya que el evento "command" nativo no se propaga.
Opción 1: Usar un agrupador
Instala el polyfill a través de npm (npm install invokers-polyfill). Este enfoque es para proyectos que usan un agrupador (como Vite o Webpack) o mapas de importación. Para todas las demás configuraciones, usa la opción de CDN que se indica a continuación.
// 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');
}
Opción 2: Usar una CDN
En el caso de los proyectos sin un empaquetador, importa de forma dinámica el polyfill directamente desde una CDN dentro de 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>
Limitaciones del polyfill de invocadores
OBLIGATORIO: Este polyfill no controla los estados de ARIA (p.ej., aria-expanded) del botón de comando de la misma manera que los navegadores nativos. Te recomendamos que controles estos estados por tu cuenta para asegurarte de que tu sitio sea completamente accesible.
Si no se admite la API de Invoker Commands, no se activará el evento command. Para obtener compatibilidad total con todos los navegadores actualizados, se recomienda usar el polyfill de invocadores de https://github.com/keithamus/invokers-polyfill a través de npm install o CDN.
Este polyfill admite por completo las acciones personalizadas (que comienzan con --) y despacha el evento command exactamente como la API nativa.
Importación dinámica (optimización del rendimiento)
Para obtener el mejor rendimiento, solo debes cargar el polyfill si el navegador no admite la API de forma nativa. Esto ahorra ancho de banda y reduce el tiempo de ejecución de la secuencia de comandos para los usuarios de navegadores modernos.
NOTA: Este polyfill no administra los estados de ARIA (como aria-pressed o aria-expanded) para los comandos personalizados. Debes sincronizar manualmente estos estados en tu objeto de escucha de eventos para garantizar que se pueda acceder a tu sitio.
// 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);
}
});
Resguardo manual (patrón tradicional)
Si prefieres no usar un polyfill, puedes usar una combinación de delegación de eventos para enviar eventos y un registro de comandos para controlar las acciones. Este es un patrón de arquitectura común en el desarrollo tradicional de JavaScript que sigue siendo altamente eficiente y escalable.
// 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);
}
});
Resguardos y compatibilidad de navegadores para Popover
La API de Popover es, en su mayor parte, una mejora progresiva, pero sus comportamientos definitorios (promoción de la capa superior, cierre rápido y la invocación de popovertarget) no tienen un equivalente solo de CSS. Los navegadores más antiguos necesitan un polyfill o un resguardo manual si prefieres no enviar uno.
Polyfill: Para admitir el atributo popover en navegadores más antiguos, carga @oddbird/popover-polyfill de forma condicional. OBLIGATORIO: Detecta la función verificando la propiedad popover en HTMLElement.prototype y carga el polyfill solo cuando no haya compatibilidad nativa. NO lo cargues de forma incondicional.
Con un empaquetador o un mapa de importación:
// MANDATORY: Feature detect 'popover' on HTMLElement.prototype.
if (!("popover" in HTMLElement.prototype)) {
import("@oddbird/popover-polyfill");
}
Sin un empaquetador, importa desde una CDN dentro de 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>
Advertencia de diseño: El polyfill no puede definir la pseudoclase :popover-open real, por lo que aplica una clase .\:popover-open en su lugar. OBLIGATORIO: Combina las dos con :is() o :where(); de lo contrario, los navegadores que no tengan :popover-open descartarán toda la regla:
[popover]:is(:popover-open, .\:popover-open) {
display: block;
}
Como alternativa, para un resguardo heredado sin un polyfill, usa position: fixed y calcula manualmente las coordenadas a través de getBoundingClientRect() o confía en el posicionamiento predeterminado con inset: auto si eso es aceptable para el caso de uso.