Связь input и listbox через ARIA

Для реализации доступного компонента выбора из списка важно правильно связать элементы 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 для управления фокусом

Библиотека 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:

  • ArrowDown / ArrowUp — перемещение по элементам списка; обновление aria-activedescendant.
  • Enter — выбор элемента; закрытие списка и обновление значения input.
  • Escape — закрытие списка без изменения значения.

Пример обработки стрелок:

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

При открытии и закрытии списка необходимо синхронизировать 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

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-связей и управление фокусом.