Используйте Invoker Commands API, чтобы переключать видимость элементов <dialog> и [popover] прямо с помощью HTML-кнопок, не создавая собственные прослушиватели событий JavaScript.
Применяя атрибуты commandfor (идентификатор цели) и command (действие) к элементу <button>, браузер автоматически обрабатывает изменения состояния открытия/закрытия, управление фокусом и привязки специальных возможностей (например, aria-expanded). Этот декларативный подход рекомендуется, поскольку он удаляет хрупкий шаблонный код, обеспечивает немедленную функциональность взаимодействий после синтаксического анализа HTML и гарантирует надежный, изначально доступный пользовательский интерфейс.
Как реализовать декларативные всплывающие окна
Всплывающие окна можно открывать и закрывать с помощью одной кнопки.
<!-- 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>
Если вам нужно управлять открытием и закрытием с помощью отдельных кнопок, вы можете использовать команды show-popover и 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>
Как реализовать декларативные модальные диалоговые окна
В отличие от всплывающих окон, в модальных диалоговых окнах обычно используются отдельные кнопки для открытия и закрытия. Используйте команду show-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>
Резервные стратегии
Поскольку команды вызова и всплывающие окна пока поддерживаются не везде, вы ДОЛЖНЫ использовать полифилы в качестве резервных вариантов для старых браузеров.
Полифиллинг команд Invoker
ОБЯЗАТЕЛЬНО. Проверьте поддержку функции, убедившись в наличии свойства commandForElement в прототипе HTMLButtonElement. НЕ проверяйте объект window или document. Вы ДОЛЖНЫ динамически импортировать полифил, только если встроенная функция отсутствует. НЕ загружайте полифил без условий.
Обязательно. Отслеживайте событие command непосредственно в целевом элементе, поскольку нативное событие command не всплывает.
Вариант 1. Использование сборщика
Установите полифил через npm (npm install invokers-polyfill). Этот подход предназначен для проектов, в которых используется сборщик (например, Vite или Webpack) или карты импорта. Для всех остальных конфигураций используйте вариант CDN ниже.
// 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');
}
Вариант 2. Использование CDN
Если в проекте нет сборщика, динамически импортируйте полифил напрямую из CDN в <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>
Ограничения полифила Invokers
ОБЯЗАТЕЛЬНО. Этот полифил не обрабатывает состояния ARIA (например, aria-expanded) кнопки команды так, как это делают нативные браузеры. Настоятельно рекомендуем обрабатывать эти состояния самостоятельно, чтобы обеспечить полную доступность сайта.
Если Invoker Commands API не поддерживается, событие command не будет активировано. Чтобы обеспечить полную поддержку во всех современных браузерах, рекомендуется использовать invokers-polyfill с сайта https://github.com/keithamus/invokers-polyfill через npm install или CDN.
Этот полифил полностью поддерживает специальные действия (начиная с --) и отправляет событие command точно так же, как нативный API.
Динамический импорт (оптимизация производительности)
Для оптимальной производительности загружайте полифил, только если браузер не поддерживает API. Это позволяет экономить трафик и сокращает время выполнения скриптов в современных браузерах.
Примечание. Этот полифил не управляет состояниями ARIA (такими как aria-pressed или aria-expanded) для пользовательских команд. Чтобы обеспечить доступность сайта, необходимо вручную синхронизировать эти состояния в прослушивателе событий.
// 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);
}
});
Резервное объявление, заданное вручную (традиционный шаблон)
Если вы не хотите использовать полифил, то можете отправлять события с помощью делегирования событий и обрабатывать действия с помощью реестра команд. Это распространенный архитектурный шаблон в традиционной разработке на JavaScript, который остается высокоэффективным и масштабируемым.
// 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);
}
});
Резервные варианты и поддержка Popover в браузерах
Popover API в основном представляет собой прогрессивное улучшение, но его определяющие характеристики – продвижение верхнего слоя, легкое отклонение и вызов popovertarget – не имеют эквивалента только для CSS. Для старых браузеров нужен полифил или резервный вариант, если вы не хотите его использовать.
Полифил. Чтобы обеспечить поддержку атрибута popover в старых браузерах, условно загрузите @oddbird/popover-polyfill. ОБЯЗАТЕЛЬНО. Проверьте наличие свойства popover в HTMLElement.prototype и загрузите полифил, только если встроенная поддержка отсутствует. Не загружайте его без необходимости.
С помощью сборщика или карты импорта:
// MANDATORY: Feature detect 'popover' on HTMLElement.prototype.
if (!("popover" in HTMLElement.prototype)) {
import("@oddbird/popover-polyfill");
}
Если вы не используете сборщик, импортируйте из CDN в <script type="module">:
<script type="module">
if (!("popover" in HTMLElement.prototype)) {
import("https://unpkg.com/@oddbird/popover-polyfill@latest/dist/popover.min.js");
}
</script>
Ограничение, связанное со стилями. Полифил не может определить настоящий псевдокласс :popover-open, поэтому вместо него применяется класс .\:popover-open. ОБЯЗАТЕЛЬНО. Объедините эти два правила с помощью :is() или :where(), иначе браузеры, не поддерживающие :popover-open, будут игнорировать правило целиком.
[popover]:is(:popover-open, .\:popover-open) {
display: block;
}
В качестве альтернативы для устаревшего резервного варианта без полифила используйте position: fixed и вручную рассчитайте координаты с помощью getBoundingClientRect() или используйте позиционирование по умолчанию с помощью inset: auto, если это допустимо для вашего случая.