React и хуки

Библиотека Tabbable предназначена для определения элементов веб-страницы, на которые можно установить фокус с помощью клавиши Tab. Она упрощает управление навигацией по интерактивным элементам, особенно в сложных пользовательских интерфейсах. Основная задача — выявить все элементы, доступные для табуляции, учитывая их видимость, состояние и особенности HTML.

Ключевые функции библиотеки:

  • tabbable(container) — возвращает массив всех элементов внутри container, которые могут получать фокус через Tab.
  • isTabbable(element) — проверяет, доступен ли конкретный элемент для табуляции.
  • focusable(container) — возвращает элементы, которые могут получать фокус как с помощью клавиатуры, так и программно.

Эти методы учитывают атрибуты tabindex, disabled, CSS-свойства display: none и visibility: hidden, а также элементы, скрытые с помощью ARIA.


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

Tabbable классифицирует элементы по их возможности получения фокуса:

  1. Нативно фокусируемые элементы<button>, <input>, <select>, <textarea>, <a> с атрибутом href.
  2. Элементы с tabindex — любое DOM-узло с положительным или нулевым tabindex. Значение tabindex="0" добавляет элемент в порядок табуляции после нативно фокусируемых элементов, отрицательное значение (tabindex="-1") исключает элемент из последовательности табуляции, но оставляет доступным для программного фокуса.

Пример получения всех tabbable-элементов в контейнере:

import tabbable from 'tabbable';

const container = document.getElementById('form-container');
const tabbableElements = tabbable(container);

tabbableElements.forEach(el => {
  console.log(el.tagName, el.tabIndex);
});

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


Интеграция с React и хуками

В React часто требуется динамически управлять фокусом при рендере компонентов. Библиотека Tabbable идеально подходит для использования внутри хуков, таких как useEffect или useLayoutEffect.

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

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

function FormSection() {
  const containerRef = useRef(null);

  useEffect(() => {
    if (containerRef.current) {
      const elements = tabbable(containerRef.current);
      if (elements.length > 0) {
        elements[0].focus(); // Устанавливаем фокус на первый tabbable элемент
      }
    }
  }, []);

  return (
    <div ref={containerRef}>
      <input type="text" placeholder="Имя" />
      <button>Отправить</button>
    </div>
  );
}

В этом примере первый tabbable-элемент получает фокус сразу после монтирования компонента. Использование useRef обеспечивает прямой доступ к DOM-контейнеру без повторного рендера.

Динамическое управление списком tabbable-элементов

Для компонентов, где элементы появляются или исчезают в зависимости от состояния, можно комбинировать Tabbable с useEffect, реагируя на изменения состояния:

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

function DynamicForm() {
  const [fields, setFields] = useState([{ id: 1 }]);
  const containerRef = useRef(null);

  useEffect(() => {
    if (containerRef.current) {
      const elements = tabbable(containerRef.current);
      if (elements.length > 0) {
        elements[elements.length - 1].focus(); // Фокус на последнем добавленном элементе
      }
    }
  }, [fields]);

  const addField = () => setFields([...fields, { id: fields.length + 1 }]);

  return (
    <div ref={containerRef}>
      {fields.map(field => (
        <input key={field.id} placeholder={`Поле ${field.id}`} />
      ))}
      <button onCl ick={addField}>Добавить поле</button>
    </div>
  );
}

Такой подход гарантирует, что новые элементы автоматически становятся доступными для клавиатурной навигации.


Обработка нестандартных элементов и кастомных компонентов

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

  1. Добавить tabIndex="0" для включения в последовательность табуляции.
  2. Проверить видимость через CSS (display, visibility).
  3. Обработать события onKeyDown для имитации стандартного поведения клавиш.

Пример кастомного компонента кнопки:

function CustomButton({ onClick, children }) {
  return (
    <div
      tabIndex={0}
      role="button"
      onCl ick={onClick}
      onKeyD own={(e) => {
        if (e.key === 'Enter' || e.key === ' ') {
          onClick();
        }
      }}
      style={{ padding: '8px', border: '1px solid #ccc', display: 'inline-block', cursor: 'pointer' }}
    >
      {children}
    </div>
  );
}

Теперь Tabbable распознает этот div как tabbable элемент, и клавиатурная навигация будет работать корректно.


Важные нюансы и лучшие практики

  • Элементы с disabled или aria-hidden="true" автоматически исключаются из списка tabbable.
  • Последовательность табуляции определяется нативным порядком документа, затем элементами с положительным tabindex.
  • Для модальных окон рекомендуется использовать tabbable для циклической навигации внутри модального контента, чтобы пользователь не уходил за пределы модального окна.

Пример циклической навигации:

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

function Modal({ onClose }) {
  const modalRef = useRef(null);

  useEffect(() => {
    const elements = tabbable(modalRef.current);
    const first = elements[0];
    const last = elements[elements.length - 1];

    const handleKey = (e) => {
      if (e.key === 'Tab') {
        if (e.shiftKey && document.activeElement === first) {
          e.preventDefault();
          last.focus();
        } else if (!e.shiftKey && document.activeElement === last) {
          e.preventDefault();
          first.focus();
        }
      }
    };

    modalRef.current.addEventListener('keydown', handleKey);
    return () => modalRef.current.removeEventListener('keydown', handleKey);
  }, []);

  return (
    <div ref={modalRef} role="dialog" aria-modal="true">
      <input placeholder="Имя" />
      <button onCl ick={onClose}>Закрыть</button>
    </div>
  );
}

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


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