Библиотека ally.js предназначена для управления доступностью веб-компонентов, особенно интерактивных элементов, таких как меню и подменю. Она позволяет эффективно контролировать фокус, обработку клавиатуры и атрибуты ARIA, что критично для вложенных структур.
Для работы с вложенными подменю важно правильно структурировать HTML и подключить ally.js:
<ul role="menu" id="main-menu">
<li role="menuitem" tabindex="0" aria-haspopup="true" aria-expanded="false">
Файл
<ul role="menu" class="submenu">
<li role="menuitem" tabindex="-1">Создать</li>
<li role="menuitem" tabindex="-1">Открыть</li>
</ul>
</li>
<li role="menuitem" tabindex="0">Правка</li>
</ul>
role="menuitem" и role="menu" определяют
семантику элементов для экранных читалок.aria-haspopup="true" указывает на наличие подменю.aria-expanded управляет состоянием раскрытия
подменю.tabindex контролирует порядок навигации клавишей
Tab.Вложенные подменю требуют динамического управления фокусом. Ally.js предоставляет функцию focusable для получения всех элементов, доступных для фокуса:
const menuItems = ally.query.focusable({ context: document.getElementById('main-menu') });
menuItems.forEach(item => {
item.addEventListener('keydown', event => {
if (event.key === 'ArrowRight') {
const submenu = item.querySelector('.submenu');
if (submenu) {
item.setAttribute('aria-expanded', 'true');
ally.focus.first({ context: submenu });
}
}
if (event.key === 'ArrowLeft') {
const parentMenu = item.closest('.submenu');
if (parentMenu) {
parentMenu.parentElement.setAttribute('aria-expanded', 'false');
parentMenu.parentElement.focus();
}
}
});
});
ally.query.focusable возвращает элементы, которые можно
сфокусировать, включая скрытые и динамически добавленные.keydown обрабатывают навигацию стрелками,
обеспечивая правильное раскрытие и скрытие подменю.Скрытие и показ подменю требует синхронизации с ARIA-атрибутами:
function toggleSubmenu(item, open) {
const submenu = item.querySelector('.submenu');
if (!submenu) return;
submenu.style.display = open ? 'block' : 'none';
item.setAttribute('aria-expanded', open ? 'true' : 'false');
}
document.querySelectorAll('[role="menuitem"][aria-haspopup="true"]').forEach(item => {
item.addEventListener('mouseover', () => toggleSubmenu(item, true));
item.addEventListener('mouseout', () => toggleSubmenu(item, false));
});
aria-expanded обновляется синхронно с видимостью.Вложенные подменю могут иметь несколько уровней. Ally.js упрощает рекурсивный обход элементов для фокуса:
function handleNestedMenu(item) {
const submenu = item.querySelector('.submenu');
if (!submenu) return;
ally.query.focusable({ context: submenu }).forEach(subItem => {
subItem.addEventListener('keydown', event => {
if (event.key === 'ArrowRight') handleNestedMenu(subItem);
if (event.key === 'ArrowLeft') {
const parent = subItem.closest('.submenu');
if (parent) {
parent.style.display = 'none';
parent.parentElement.focus();
}
}
});
});
}
document.querySelectorAll('[role="menuitem"][aria-haspopup="true"]').forEach(handleNestedMenu);
Для полного соответствия стандартам доступности важно обрабатывать клавиши:
ArrowDown — переход к следующему элементу меню.ArrowUp — переход к предыдущему элементу.ArrowRight — раскрытие подменю и фокус на первом
элементе.ArrowLeft — возврат к родительскому меню.Escape — закрытие текущего подменю и возврат фокуса на
родительский элемент.Пример обработки Escape:
document.addEventListener('keydown', event => {
if (event.key === 'Escape') {
const openSubmenus = document.querySelectorAll('[aria-expanded="true"]');
openSubmenus.forEach(menu => {
menu.setAttribute('aria-expanded', 'false');
const submenu = menu.querySelector('.submenu');
if (submenu) submenu.style.display = 'none';
menu.focus();
});
}
});
Ally.js позволяет работать с динамически добавляемыми подменю, используя mutation observer:
const observer = new MutationObserver(mutations => {
mutations.forEach(mutation => {
if (mutation.addedNodes.length) {
mutation.addedNodes.forEach(node => {
if (node.nodeType === 1 && node.matches('[role="menuitem"]')) {
handleNestedMenu(node);
}
});
}
});
});
observer.observe(document.getElementById('main-menu'), { childList: true, subtree: true });
menuitem с подменю должен иметь
aria-haspopup="true" и управлять
aria-expanded.display: none), открываться через ally.js при фокусе или
навигации клавишами.ally.query.focusable для обхода всех
интерактивных элементов, включая динамически созданные.Ally.js позволяет создавать вложенные подменю, полностью соответствующие стандартам доступности WAI-ARIA, с гибким управлением фокусом и клавиатурной навигацией, упрощая поддержку сложных интерфейсов.