Библиотека Tabbable предназначена для определения элементов веб-страницы, на которые можно установить фокус с помощью клавиши Tab. Она упрощает управление навигацией по интерактивным элементам, особенно в сложных пользовательских интерфейсах. Основная задача — выявить все элементы, доступные для табуляции, учитывая их видимость, состояние и особенности HTML.
Ключевые функции библиотеки:
tabbable(container) — возвращает массив всех элементов
внутри container, которые могут получать фокус через
Tab.isTabbable(element) — проверяет, доступен ли конкретный
элемент для табуляции.focusable(container) — возвращает элементы, которые
могут получать фокус как с помощью клавиатуры, так и программно.Эти методы учитывают атрибуты tabindex,
disabled, CSS-свойства display: none и
visibility: hidden, а также элементы, скрытые с помощью
ARIA.
Tabbable классифицирует элементы по их возможности получения фокуса:
<button>, <input>,
<select>, <textarea>,
<a> с атрибутом href.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 часто требуется динамически управлять фокусом при рендере
компонентов. Библиотека Tabbable идеально подходит для
использования внутри хуков, таких как
useEffect или useLayoutEffect.
useEffectimport 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 с
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 необходимо:
tabIndex="0" для включения в
последовательность табуляции.display,
visibility).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 и обеспечивать корректную навигацию по всем интерактивным элементам.