Работа с кастомными контролами

Библиотека ally.js предоставляет мощный набор инструментов для управления доступностью (a11y) в веб-приложениях. Одной из ключевых задач является работа с кастомными контролами, которые не являются стандартными элементами формы (<input>, <button>, <select>), но требуют полной доступности для пользователей с ассистивными технологиями.

Кастомные контролы могут включать: переключатели (switch), слайдеры, вкладки (tabs), модальные окна с кнопками управления и другие интерактивные элементы, созданные с использованием div, span или других блочных элементов. В таких случаях необходимо самостоятельно управлять фокусом, состоянием и взаимодействием с клавиатурой.


Фокусировка и управление вкладками

ally.js предоставляет функции для управления фокусом и навигацией:

  • ally.query.focusable({ context }) — возвращает список всех элементов, которые могут получить фокус внутри указанного контейнера.
  • ally.maintain.focus({ context }) — позволяет “заморозить” фокус внутри контейнера, что особенно важно для модальных окон и диалогов.
  • ally.focus.trap({ context }) — более высокоуровневая функция для реализации ловушек фокуса (focus trap), которая обеспечивает циклическую навигацию между фокусируемыми элементами.

Пример реализации ловушки фокуса для кастомного модального окна:

const modal = document.getElementById('customModal');

ally.focus.trap({
  context: modal,
  escapeDeactivates: true, // выход при нажатии Esc
  clickOutsideDeactivates: true, // клик вне модального окна снимает фокус
});

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

Для кастомных переключателей или чекбоксов необходимо поддерживать правильные атрибуты ARIA:

  • role="switch" или role="checkbox" — обозначает семантику элемента.
  • aria-checked="true|false" — отображает текущее состояние.
  • tabindex="0" — делает элемент фокусируемым через клавиатуру.

Изменение состояния с обновлением ARIA:

const toggle = document.getElementById('customToggle');

toggle.addEventListener('click', () => {
  const isChecked = toggle.getAttribute('aria-checked') === 'true';
  toggle.setAttribute('aria-checked', !isChecked);
});

toggle.addEventListener('keydown', (event) => {
  if (event.key === 'Enter' || event.key === ' ') {
    event.preventDefault();
    toggle.click();
  }
});

Клавиатурная навигация

ally.js облегчает управление клавиатурными событиями для кастомных контролов:

  • ally.query.tabster({ context }) — позволяет получать последовательность табуляции элементов.
  • Для сложных компонентов, таких как слайдеры или вкладки, важно реализовать обработку стрелок:
document.addEventListener('keydown', (event) => {
  if (event.target.classList.contains('custom-slider')) {
    if (event.key === 'ArrowRight') {
      moveSliderNext(event.target);
    } else if (event.key === 'ArrowLeft') {
      moveSliderPrev(event.target);
    }
  }
});

Доступность динамических изменений

Для динамически создаваемых элементов ally.js предоставляет инструменты для отслеживания изменений и их доступности:

  • ally.query.focusable({ context, includeContext: true }) — позволяет проверять новые элементы после их добавления в DOM.
  • ally.focus.visible({ context }) — помогает определить, какой элемент имеет видимый фокус для корректного взаимодействия с клавиатурой и экранными читалками.

Пример для динамически добавленных кнопок:

const container = document.getElementById('dynamicControls');

function addButton(label) {
  const button = document.createElement('button');
  button.textContent = label;
  container.appendChild(button);
  const focusable = ally.query.focusable({ context: container });
  console.log('Focusable elements:', focusable);
}

Совмещение кастомных контролов с формами

При интеграции кастомных контролов с формами важно:

  1. Связывать визуальный контрол с реальным <input> с помощью aria-hidden="true" для элемента input, если нужно сохранить стандартное поведение формы.
  2. Обновлять значение скрытого input при изменении состояния кастомного элемента:
const hiddenInput = document.querySelector('#realCheckbox');

toggle.addEventListener('click', () => {
  const isChecked = toggle.getAttribute('aria-checked') === 'true';
  hiddenInput.checked = isChecked;
});
  1. Использовать ally.focus.trap и ally.query.focusable для управления фокусом внутри формы, если есть модальные окна или всплывающие панели.

Работа с группой контролов

Для сложных интерфейсов, таких как вкладки или набор радиокнопок, нужно управлять фокусом внутри группы:

  • role="tablist" и role="tab" — для вкладок.
  • aria-selected="true|false" — текущая выбранная вкладка.
  • Клавиши стрелок обеспечивают навигацию между элементами:
const tabs = document.querySelectorAll('[role="tab"]');

tabs.forEach(tab => {
  tab.addEventListener('keydown', (event) => {
    let index = Array.prototype.indexOf.call(tabs, event.target);
    if (event.key === 'ArrowRight') {
      tabs[(index + 1) % tabs.length].focus();
    } else if (event.key === 'ArrowLeft') {
      tabs[(index - 1 + tabs.length) % tabs.length].focus();
    }
  });
});

Использование ally.js позволяет при этом проверять доступность всех элементов группы и корректно управлять фокусом после динамических изменений.


Итоги по работе с кастомными контролами

  • Все интерактивные элементы должны быть фокусируемыми через клавиатуру (tabindex или стандартные элементы).
  • ARIA-атрибуты (role, aria-checked, aria-selected) обеспечивают совместимость с экранными читалками.
  • ally.js облегчает управление фокусом, ловушками фокуса, проверку доступных элементов и динамическое обновление.
  • Кастомные контролы должны корректно реагировать на клавиатурные события и синхронизироваться с внутренними данными форм.
  • Группы элементов требуют особого внимания к навигации стрелками и правильному обновлению состояния.

Эти принципы позволяют создавать полностью доступные кастомные элементы, которые интегрируются с существующими формами и компонентами интерфейса, обеспечивая удобство использования для всех пользователей.