ARIA-роли для меню

Библиотека ally.js предназначена для обеспечения доступности веб-приложений. Одной из ключевых задач является управление элементами с ARIA-ролями, особенно для сложных компонентов интерфейса, таких как меню, выпадающие списки и панели навигации. Взаимодействие с меню требует точного понимания ролей menu, menuitem, menuitemcheckbox и menuitemradio.


Роли и их семантика

  • menu – контейнер для элементов меню. Используется для группировки элементов, которые представляют список доступных действий. Контейнер может быть горизонтальным или вертикальным, что отражается через атрибут aria-orientation.
  • menuitem – отдельный элемент меню, обычно выполняет действие при активации.
  • menuitemcheckbox – элемент меню с состоянием включено/выключено, аналог чекбокса.
  • menuitemradio – элемент меню с поведением радио-кнопки, где выбор одного элемента снимает выделение с других.

Все элементы меню должны быть фокусируемыми, чтобы поддерживать клавиатурную навигацию.


Инициализация и фокусировка

ally.js предоставляет методы для управления фокусом внутри меню. Основной подход — определить контейнер меню и элементы, которые можно фокусировать:

const menu = document.querySelector('[role="menu"]');

const items = ally.query.focusable({
  context: menu,
});

Здесь ally.query.focusable возвращает список всех фокусируемых элементов внутри меню, включая menuitem, menuitemcheckbox и menuitemradio.

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

if (items.length > 0) {
  ally.query.firstTabbable({ context: menu }).focus();
}

Использование методов firstTabbable и lastTabbable позволяет корректно обрабатывать циклическую навигацию по меню с клавиатуры.


Навигация с клавиатуры

Стандартная практика ARIA для меню предполагает следующие клавиши:

  • ArrowDown / ArrowUp – перемещение между элементами меню в вертикальном списке.
  • ArrowRight / ArrowLeft – перемещение в горизонтальном меню или вложенных подменю.
  • Home / End – переход к первому или последнему элементу меню.
  • Enter / Space – активация элемента меню.

С ally.js навигацию можно реализовать через прослушивание событий keydown и управление фокусом:

menu.addEventListener('keydown', (event) => {
  const current = document.activeElement;
  let index = items.indexOf(current);

  switch (event.key) {
    case 'ArrowDown':
      index = (index + 1) % items.length;
      items[index].focus();
      event.preventDefault();
      break;
    case 'ArrowUp':
      index = (index - 1 + items.length) % items.length;
      items[index].focus();
      event.preventDefault();
      break;
    case 'Home':
      items[0].focus();
      event.preventDefault();
      break;
    case 'End':
      items[items.length - 1].focus();
      event.preventDefault();
      break;
  }
});

Использование ally.js позволяет безопасно определять фокусируемые элементы, избегая попадания на скрытые или неактивные элементы, что критично для соблюдения стандартов доступности.


Работа с подменю

Подменю создаются как отдельные контейнеры menu, вложенные в menuitem с атрибутом aria-haspopup="true". ally.js облегчает управление фокусом при раскрытии подменю:

const parentItem = document.querySelector('[aria-haspopup="true"]');

parentItem.addEventListener('keydown', (event) => {
  if (event.key === 'ArrowRight') {
    const submenu = parentItem.querySelector('[role="menu"]');
    ally.query.firstTabbable({ context: submenu }).focus();
    event.preventDefault();
  }
});

С ally.js удобно реализовать циклическую навигацию внутри подменю и возвращение фокуса на родительский элемент при закрытии.


Управление состоянием menuitemcheckbox и menuitemradio

Для элементов с состоянием используется атрибут aria-checked. ally.js не изменяет эти атрибуты напрямую, но обеспечивает корректное управление фокусом при активации:

items.forEach((item) => {
  item.addEventListener('keydown', (event) => {
    if (event.key === 'Enter' || event.key === ' ') {
      if (item.getAttribute('role') === 'menuitemcheckbox') {
        const checked = item.getAttribute('aria-checked') === 'true';
        item.setAttribute('aria-checked', !checked);
      }
      if (item.getAttribute('role') === 'menuitemradio') {
        const siblings = item.parentElement.querySelectorAll('[role="menuitemradio"]');
        siblings.forEach(sib => sib.setAttribute('aria-checked', 'false'));
        item.setAttribute('aria-checked', 'true');
      }
      event.preventDefault();
    }
  });
});

Это гарантирует, что состояние элементов меню синхронизировано с ARIA и поддерживает корректную доступность для скринридеров.


Интеграция с динамическим контентом

ally.js поддерживает обновление фокусируемых элементов при изменении DOM. Для меню, которые динамически добавляют или удаляют пункты, используется наблюдение за изменениями с помощью MutationObserver:

const observer = new MutationObserver(() => {
  items = ally.query.focusable({ context: menu });
});

observer.observe(menu, { childList: true, subtree: true });

Это позволяет навигации оставаться корректной даже при динамическом изменении структуры меню.


Практические рекомендации

  • Все элементы с ролью меню должны быть фокусируемыми.
  • Использовать aria-orientation для отражения направления меню.
  • Для вложенных меню корректно обрабатывать aria-haspopup и возвращение фокуса на родителя.
  • Поддерживать aria-checked для элементов с состоянием.
  • Обновлять список фокусируемых элементов при динамических изменениях DOM.

Ally.js облегчает реализацию этих правил, гарантируя соблюдение стандартов ARIA и обеспечение полноценной клавиатурной навигации.