Для реализации доступного компонента выбора из списка важно правильно
связать элементы input и listbox через
ARIA-атрибуты. Основной принцип заключается в использовании
aria-controls, aria-expanded и
aria-activedescendant на поле ввода, а также в правильной
разметке самого listbox и его элементов
(option).
input[aria-controls] — указывает на
id элемента listbox, которым управляет поле
ввода.input[aria-expanded] — информирует о
текущем состоянии списка (true — список открыт,
false — закрыт).input[aria-activedescendant] —
содержит id активного элемента внутри listbox,
позволяя скринридерам отслеживать текущий выбор.Эти атрибуты должны динамически обновляться при взаимодействии пользователя: при открытии списка, навигации стрелками и выборе элемента.
Библиотека ally.js предоставляет инструменты для
контроля фокуса и проверки доступности интерактивных элементов. Для
списка выбора важно корректно управлять фокусом на элементах
option без нарушения логики ARIA.
import * as ally from 'ally.js';
// Пример инициализации focus trap на listbox
const listbox = document.getElementById('myListbox');
ally.focus.trap({
context: listbox,
ignoreScroll: true,
allowOutsideClick: true
});
Использование focus.trap гарантирует, что при открытом
списке фокус останется внутри listbox, а клавиши навигации
будут правильно обрабатываться.
Для корректной работы с клавиатурой необходимо отслеживать события
keydown на input:
aria-activedescendant.input.Пример обработки стрелок:
input.addEventListener('keydown', (event) => {
const options = listbox.querySelectorAll('[role="option"]');
let index = Array.from(options).findIndex(opt => opt.id === input.getAttribute('aria-activedescendant'));
if (event.key === 'ArrowDown') {
index = (index + 1) % options.length;
input.setAttribute('aria-activedescendant', options[index].id);
options[index].scrollIntoView({ block: 'nearest' });
event.preventDefault();
}
if (event.key === 'ArrowUp') {
index = (index - 1 + options.length) % options.length;
input.setAttribute('aria-activedescendant', options[index].id);
options[index].scrollIntoView({ block: 'nearest' });
event.preventDefault();
}
});
При открытии и закрытии списка необходимо синхронизировать
aria-expanded:
function openListbox() {
listbox.style.display = 'block';
input.setAttribute('aria-expanded', 'true');
}
function closeListbox() {
listbox.style.display = 'none';
input.setAttribute('aria-expanded', 'false');
input.removeAttribute('aria-activedescendant');
}
Каждое изменение фокуса и выбор элемента должны отражаться в ARIA-атрибутах, чтобы скринридеры точно информировали пользователя о текущем состоянии компонента.
ally.js позволяет проверять доступность элементов и корректность ARIA-связей:
// Проверка доступности input
const isInputFocusable = ally.is.focusable(input);
console.log(isInputFocusable); // true, если input доступен для фокуса
// Проверка видимости и доступности listbox
const visibleOptions = ally.query.firstTabbable(listbox);
Регулярная проверка гарантирует, что элементы списка и поле ввода остаются интерактивными и корректно воспринимаются вспомогательными технологиями.
После того как пользователь выбирает элемент:
input.value обновляется.aria-expanded="false").aria-activedescendant удаляется.input.listbox.addEventListener('click', (event) => {
const option = event.target.closest('[role="option"]');
if (option) {
input.value = option.textContent;
closeListbox();
input.focus();
}
});
Если список опций динамический (например, фильтруется по вводу
пользователя), aria-activedescendant и
aria-controls должны корректно обновляться при каждом
изменении DOM. ally.js помогает отслеживать новые интерактивные элементы
и проверять их фокусируемость:
ally.query.tabbable(listbox).forEach(option => {
// Добавление обработчиков для динамически созданных элементов
option.addEventListener('mouseenter', () => {
input.setAttribute('aria-activedescendant', option.id);
});
});
Эта практика обеспечивает доступность даже при изменении набора элементов в реальном времени, сохраняя консистентность ARIA-связей и управление фокусом.