Действия для настраиваемых кнопок

Веб-функции: Команды Invoker

Invoker Commands API позволяет кнопкам запускать действия с целевыми элементами декларативно с помощью HTML-атрибутов. Такой подход снижает потребность в ручных прослушивателях событий и отделяет пользовательский интерфейс от деталей реализации. Для специальных действий, связанных с конкретным приложением, можно задать собственные названия команд. Специальные команды должны начинаться с двойного дефиса (--), чтобы избежать конфликтов со встроенными командами браузера, которые могут появиться в будущем.

Реализация

  1. Определите целевой элемент. Укажите элемент, который будет реагировать на действие. Если у товара нет уникального значения атрибута id, добавьте его.
  2. Настройте кнопку вызова. Используйте атрибут commandfor, чтобы указать на id цели, и атрибут command, чтобы указать название специальной команды (с префиксом --).
  3. Обработка события команды. Прикрепите прослушиватель событий command непосредственно к целевому элементу. Объект события содержит свойство command и свойство target (ссылающееся на элемент, идентифицированный commandfor).
  4. Обработка состояний ARIA. Специальные команды не имеют встроенной семантики, поэтому вам нужно обрабатывать такие состояния, как aria-pressed или aria-expanded.

Пример: пользовательские элементы управления анимацией

<!-- The target element that will respond to custom commands -->
<div id="action-target" class="target">
  Action Target
</div>

<!-- Buttons declaratively linked to the target element -->
<!-- Each button sends a unique custom command starting with '--' -->
<button commandfor="action-target" command="--spin">
  Spin
</button>

<button commandfor="action-target" command="--grow">
  Grow
</button>

<button commandfor="action-target" command="--reset">
  Reset All
</button>

<script>
  // 1. **Optional:** Define a registry of requested actions for cleaner logic
const commandRegistry = {
  '--spin': (target, source) => {
    const isSpun = target.classList.toggle('is-spun');
    // Set ARIA states, as custom commands have no inherent semantics.
    source?.setAttribute('aria-pressed', isSpun);
  },
  '--grow': (target, source) => {
    const isGrown = target.classList.toggle('is-grown');
    source?.setAttribute('aria-pressed', isGrown);
  },
  '--reset': (target) => {
    target.classList.remove('is-spun', 'is-grown');
    // Reset all associated buttons' ARIA states
    document.querySelectorAll(`button[commandfor="${target.id}"]`).forEach(btn => {
      btn.setAttribute('aria-pressed', 'false');
    });
  },
};

  // 2. **Mandatory:** Listen for the 'command' event directly on the target element
  // (This is necessary because the native 'command' event does not bubble)
  document.getElementById('action-target').addEventListener('command', (event) => {
    const command = event.command;
    const target = event.target;
    const source = event.source; // event.source refers to the triggering button
    const action = commandRegistry[command];

    if (action) {
      action(target, source);
    }
  });
</script>

Основные ограничения

  • Префикс специальных команд. ОБЯЗАТЕЛЬНО: все названия специальных команд должны начинаться с -- (например, command="--my-action").
  • Таргетинг. Атрибут commandfor должен соответствовать атрибуту id элемента в том же дереве документа.
  • Без всплытия. Событие command не всплывает. Если возможных целей несколько, добавьте { capture: true } в обработчик событий и прослушивайте родительский элемент.
  • Корневые элементы Shadow DOM. Если целевой элемент может находиться в корневом элементе Shadow DOM, используйте event.composedPath()[0] вместо event.target.
  • Специальные возможности. Специальные команды не имеют встроенной семантики, и вы должны явно применять любые состояния.

Специальные возможности

  • Если кнопка показывает дополнительный контент, используйте атрибут aria-expanded, по умолчанию заданный как aria-expanded="false" и aria-expanded="true", когда показывается сопутствующий контент.
  • Если кнопка сама по себе выполняет действие (например, переключатель, который может воспроизводить и приостанавливать, кнопки регулировки громкости, которые могут отключать и включать звук), используйте атрибут aria-pressed, задав для него значение aria-pressed="false", когда он неактивен, и aria-pressed="true", когда он активен.
  • Если кнопка открывает элемент с ролью menu, listbox, tree, grid или dialog, то у элемента управления должен быть атрибут aria-haspopup, значение которого соответствует роли открываемого элемента.
  • Если кнопка отключается при активации, вместо disabled используйте aria-disabled="true", чтобы не потерять фокус.

Резервные стратегии

Если 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);
  }
 });