Кастомные компоненты

Библиотека Tabbable в JavaScript предназначена для поиска и управления элементами интерфейса, доступными для навигации с клавиатуры через клавишу Tab. Она используется для обеспечения доступности (accessibility) и управления фокусом в кастомных компонентах.

Основной принцип работы библиотеки заключается в том, что она проверяет элементы на соответствие условиям фокусируемости: видимость, наличие атрибута tabindex, свойства disabled, hidden и CSS-свойств, влияющих на отображение.

Установка и подключение

Установка производится через npm:

npm install tabbable

Подключение в проекте:

import { tabbable, isTabbable } from 'tabbable';
  • tabbable(container) — возвращает массив всех элементов внутри container, доступных для табуляции.
  • isTabbable(element) — возвращает true, если элемент может быть сфокусирован через Tab.

Поиск фокусируемых элементов

Для кастомных компонентов важно правильно определить элементы, которые должны участвовать в табуляции. tabbable() выполняет комплексную проверку:

  1. Видимость элемента: элемент не должен быть скрыт через display: none или visibility: hidden.

  2. Атрибуты управления фокусом:

    • tabindex="-1" делает элемент доступным для программного фокуса, но исключает из таб-цикла.
    • tabindex="0" добавляет элемент в стандартный таб-цикл.
    • Положительные tabindex позволяют задавать порядок фокуса вручную.
  3. Отключенные элементы: disabled исключает элемент из табуляции.

Пример использования:

const container = document.getElementById('modal');
const focusableElements = tabbable(container);
console.log(focusableElements); // массив всех tabbable элементов внутри модалки

Работа с кастомными компонентами

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

Пример: кастомный селект

Кастомный селект может быть реализован с элементами div и span вместо <select> и <option>. Для корректной табуляции:

  1. Каждому интерактивному элементу назначается tabindex="0".

  2. Для закрытого состояния меню фокус может быть ограничен:

    • Только кнопка открытия селекта доступна для табуляции.
    • Список опций исключен из таб-цикла до открытия.
  3. При открытии списка вызывается tabbable() для поиска всех опций и управление фокусом через клавиши ArrowUp/ArrowDown.

const optionsContainer = document.querySelector('.custom-select-options');

function getFocusableOptions() {
  return tabbable(optionsContainer);
}

document.addEventListener('keydown', (event) => {
  if (event.key === 'ArrowDown') {
    const focusables = getFocusableOptions();
    focusables[0].focus();
  }
});

Управление циклом фокуса в модальном окне

Для модального окна важно захватывать фокус внутри контейнера. Это предотвращает уход фокуса на элементы под модалкой. Пример паттерна «фокус-ловушка»:

const modal = document.querySelector('#modal');
const focusableEls = tabbable(modal);
const firstEl = focusableEls[0];
const lastEl = focusableEls[focusableEls.length - 1];

modal.addEventListener('keydown', (e) => {
  if (e.key === 'Tab') {
    if (e.shiftKey && document.activeElement === firstEl) {
      e.preventDefault();
      lastEl.focus();
    } else if (!e.shiftKey && document.activeElement === lastEl) {
      e.preventDefault();
      firstEl.focus();
    }
  }
});

Работа с динамическими элементами

Библиотека автоматически проверяет текущее состояние DOM. Если элементы появляются или исчезают динамически, tabbable(container) всегда возвращает актуальный список. Это особенно важно для компонентов, где элементы добавляются через JavaScript после инициализации.

Интеграция с React и другими фреймворками

В React, Vue или Angular компоненты часто генерируют элементы динамически. Для корректной табуляции:

  1. Использовать ref для доступа к DOM-узлам контейнера.
  2. Вызывать tabbable(ref.current) после рендера компонента.
  3. Для списков с виртуализацией важно пересчитывать tabbable элементы при изменении видимых элементов.

Пример в React:

import { useEffect, useRef } from 'react';
import { tabbable } from 'tabbable';

function Modal({ isOpen }) {
  const modalRef = useRef();

  useEffect(() => {
    if (isOpen && modalRef.current) {
      const focusables = tabbable(modalRef.current);
      focusables[0]?.focus();
    }
  }, [isOpen]);

  return (
    <div ref={modalRef} className="modal">
      <button>Закрыть</button>
      <input type="text" />
      <button>Подтвердить</button>
    </div>
  );
}

Рекомендации по использованию

  • Всегда проверять, что элементы скрытые через CSS не попадают в таб-цикл.
  • Для сложных компонентов с кастомной навигацией использовать комбинацию tabindex и событий клавиатуры.
  • Программно управлять фокусом только в пределах контейнера, чтобы не ломать естественный порядок табуляции.
  • При работе с анимацией появления/исчезновения элементов пересчитывать список tabbable элементов после завершения анимации.

Итоговая структура работы с Tabbable

  1. Определение контейнера с интерактивными элементами.
  2. Использование tabbable() для поиска фокусируемых элементов.
  3. Настройка tabindex для кастомных компонентов.
  4. Управление циклом фокуса в модальных или динамических компонентах.
  5. Обновление списка элементов при изменении DOM.

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