Библиотека 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 для меню предполагает следующие клавиши:
С 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 для элементов с
состоянием.Ally.js облегчает реализацию этих правил, гарантируя соблюдение стандартов ARIA и обеспечение полноценной клавиатурной навигации.