ARIA атрибуты

В библиотеке Tom Select доступность строится вокруг корректного использования ARIA-атрибутов, которые обеспечивают взаимодействие с экранными считывателями и вспомогательными технологиями. Основная цель ARIA в данном контексте — превратить кастомный UI-компонент в семантически эквивалентный нативному <select> элементу.

Ключевая идея заключается в том, что визуально компонент может быть полностью переопределён, но логическая модель должна оставаться прозрачной для assistive technologies. Для этого Tom Select динамически управляет набором ARIA-атрибутов на основе текущего состояния.


Базовая ARIA-структура компонента

После инициализации Tom Select формирует DOM-структуру, где основной контейнер получает роль интерактивного комбинированного элемента:

  • role="combobox" — обозначает поле ввода с выпадающим списком
  • aria-expanded — отражает состояние раскрытия списка
  • aria-haspopup="listbox" — указывает наличие связанного списка опций
  • aria-controls — связывает input с элементом списка
  • aria-activedescendant — управляет текущим выделенным элементом

Пример упрощённой структуры:

<div class="ts-wrapper">
  <input
    type="text"
    role="combobox"
    aria-autocomplete="list"
    aria-expanded="false"
    aria-controls="ts-dropdown"
    aria-activedescendant=""
  />
  <div role="listbox" id="ts-dropdown"></div>
</div>

Управление состоянием раскрытия списка

Состояние раскрытия является ключевым для ARIA-логики. При открытии dropdown:

  • aria-expanded устанавливается в true
  • список получает визуальную и логическую активность
  • обновляется связь aria-controls

При закрытии:

  • aria-expanded становится false
  • aria-activedescendant очищается или сбрасывается

Фрагмент логики обновления:

function setDropdownState(isOpen) {
  const input = this.control_input;

  input.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
  if (!isOpen) {
    input.setAttribute('aria-activedescendant', '');
  }
}

ARIA для списка опций (listbox)

Элементы списка в Tom Select получают роль option внутри контейнера listbox.

Каждый элемент:

  • role="option"
  • aria-selected="true|false"
  • уникальный id для связывания с aria-activedescendant

Пример:

<div role="listbox" id="ts-dropdown">
  <div role="option" id="opt-1" aria-selected="false">Option 1</div>
  <div role="option" id="opt-2" aria-selected="true">Option 2</div>
</div>

При навигации клавиатурой Tom Select изменяет aria-activedescendant, не перемещая фокус внутрь списка. Это важная техника, позволяющая сохранить контроль над input.


aria-selected и многовыбор

В режиме множественного выбора (maxItems > 1) ARIA-логика усложняется, так как несколько элементов могут быть выбраны одновременно.

Каждый выбранный элемент:

  • получает aria-selected="true"
  • синхронизируется с внутренним состоянием items

Удаление элемента автоматически обновляет атрибут:

function updateOptionState(option, selected) {
  option.setAttribute('aria-selected', selected ? 'true' : 'false');
}

Важно, что ARIA-состояние должно всегда строго соответствовать внутреннему массиву выбранных значений.


aria-activedescendant и клавиатурная навигация

Одним из ключевых механизмов доступности является aria-activedescendant. Он позволяет управлять фокусом внутри списка без фактического перемещения фокуса DOM.

При нажатии стрелок:

  • вычисляется следующий активный элемент
  • его id записывается в aria-activedescendant

Пример логики:

function setActiveOption(option) {
  const input = this.control_input;

  if (!option) return;

  input.setAttribute(
    'aria-activedescendant',
    option.getAttribute('id')
  );
}

Такой подход обеспечивает:

  • стабильный фокус на input
  • корректное озвучивание изменений screen reader’ами
  • отсутствие потери контекста ввода

aria-autocomplete и поведение ввода

Tom Select использует:

  • aria-autocomplete="list"

Это означает, что ввод пользователя сопровождается отображением списка возможных значений. В отличие от inline или both, режим list предполагает, что автодополнение происходит через dropdown.

При фильтрации списка значение ARIA-атрибутов не меняется, но обновляется содержимое listbox.


Синхронизация ARIA и фильтрации

При каждом вводе текста происходит:

  1. фильтрация массива options
  2. пересоздание DOM-элементов списка
  3. повторное назначение ARIA-атрибутов

Критически важно, чтобы:

  • каждый новый option имел уникальный id
  • aria-selected соответствовал состоянию данных
  • aria-activedescendant не ссылался на удалённый элемент

Пример генерации:

function renderOption(item) {
  const el = document.createElement('div');

  el.setAttribute('role', 'option');
  el.setAttribute('id', `opt-${item.value}`);
  el.setAttribute('aria-selected', 'false');

  el.textContent = item.text;

  return el;
}

ARIA в режиме тегов (tagging)

В режиме тегов каждый созданный элемент становится интерактивной сущностью.

Для доступности используются:

  • aria-label для удаления
  • role="button" или встроенные controls
  • aria-live для уведомления об изменениях

Пример тега:

<div class="item" aria-label="Remove tag JavaScript" role="button">
  JavaScript
</div>

Удаление тега должно сопровождаться обновлением списка доступных опций и синхронизацией ARIA-атрибутов.


aria-live области для уведомлений

Для динамических изменений (например, добавление/удаление элементов) используется aria-live:

  • aria-live="polite" — ненавязчивые уведомления
  • aria-live="assertive" — срочные изменения

Пример:

<div aria-live="polite" class="ts-screenreader-status"></div>

Обновление статуса:

function announce(message) {
  const region = document.querySelector('.ts-screenreader-status');
  region.textContent = message;
}

Используется для сообщений:

  • «3 элемента выбрано»
  • «Элемент удалён»
  • «Список обновлён»

ARIA и disabled-состояния

Отключённые элементы должны корректно отражаться в доступности:

  • aria-disabled="true"
  • визуальное приглушение
  • исключение из навигации
function setDisabled(option, isDisabled) {
  option.setAttribute('aria-disabled', isDisabled ? 'true' : 'false');
}

Важно, что disabled в ARIA не всегда равен HTML-disabled: логика навигации должна учитывать оба состояния.


Группы опций и accessibility

При использовании групп (optgroups) структура дополняется:

  • role="group"
  • aria-label или aria-labelledby

Пример:

<div role="group" aria-label="Frontend">
  <div role="option">React</div>
  <div role="option">Vue</div>
</div>

Это позволяет screen reader’ам логически разделять наборы данных.


Консистентность ARIA-состояния

Одной из сложностей реализации является постоянная синхронизация:

  • DOM состояния
  • внутреннего массива items
  • визуального UI
  • ARIA-атрибутов

Любое рассогласование приводит к некорректному поведению вспомогательных технологий.

Типичная проблема — удалённый option остаётся в aria-activedescendant. Решение заключается в централизованной функции синхронизации:

function syncAriaState() {
  updateExpandedState();
  updateSelectedOptions();
  updateActiveDescendant();
}

Роль keyboard model в ARIA-архитектуре

Клавиатурная модель тесно связана с ARIA:

  • ArrowDown / ArrowUp — изменение aria-activedescendant
  • Enter — активация aria-selected
  • Escape — закрытие списка и сброс состояния

Каждое действие сопровождается обновлением соответствующих атрибутов, что делает компонент предсказуемым для assistive technologies.


Ошибки реализации ARIA в кастомных интеграциях

При расширении Tom Select часто встречаются ошибки:

  • отсутствие уникальных id у options
  • несоответствие aria-selected и фактического состояния
  • удаление DOM без обновления ARIA-ссылок
  • конфликт нескольких aria-live областей
  • потеря синхронизации при асинхронной загрузке данных

Такие проблемы приводят к тому, что компонент становится недоступным, несмотря на внешне корректный интерфейс.


Итоговая модель ARIA-архитектуры

Внутренняя модель Tom Select строится как связка:

  • combobox (input)
  • listbox (dropdown)
  • option (элементы)
  • live region (уведомления)
  • state attributes (expanded, selected, activedescendant)

Эта структура обеспечивает поведение, эквивалентное нативному <select>, при сохранении полной кастомизации интерфейса и расширяемости логики.