Библиотека Tabbable предназначена для определения
элементов на странице, которые могут получить фокус при навигации с
помощью клавиши Tab. Она анализирует DOM-дерево и
возвращает список всех элементов, которые соответствуют критериям
интерактивности: ссылки, кнопки, поля ввода, элементы с атрибутом
tabindex ≥ 0 и т. п.
Функция tabbable(root) принимает корневой элемент или
весь документ и возвращает массив элементов, упорядоченных в том
порядке, в котором они будут получать фокус при нажатии
Tab. Ключевым аспектом является правильная обработка
tabindex и скрытых элементов, так как Tabbable игнорирует
элементы с display: none, visibility: hidden
или disabled.
Фокусная ловушка (focus trap) — это механизм, ограничивающий перемещение фокуса клавишей Tab внутри определённой области DOM. Обычно используется в модальных окнах, сайдбарах, выпадающих меню и других интерактивных компонентах, где важно удерживать фокус внутри контейнера.
Композиция нескольких ловушек фокуса возникает, когда на странице одновременно присутствует несколько таких областей: например, открыто модальное окно внутри сайдбара. Основная цель — сохранить естественный порядок навигации, не блокируя фокус полностью и не создавая конфликтов между ловушками.
Для реализации базовой ловушки используется следующий подход:
import { tabbable } from 'tabbable';
function trapFocus(container) {
const elements = tabbable(container);
const first = elements[0];
const last = elements[elements.length - 1];
function handleKeydown(event) {
if (event.key !== 'Tab') return;
if (event.shiftKey) {
if (document.activeElement === first) {
event.preventDefault();
last.focus();
}
} else {
if (document.activeElement === last) {
event.preventDefault();
first.focus();
}
}
}
container.addEventListener('keydown', handleKeydown);
return () => container.removeEventListener('keydown', handleKeydown);
}
Ключевые моменты:
При наличии нескольких модальных областей важно определить, какая из них активна. Для этого обычно создаётся стек ловушек:
const activeTraps = [];
function activateTrap(container) {
const deactivate = trapFocus(container);
activeTraps.push({ container, deactivate });
}
function deactivateTrap(container) {
const index = activeTraps.findIndex(t => t.container === container);
if (index >= 0) {
activeTraps[index].deactivate();
activeTraps.splice(index, 1);
}
}
Особенности композиции:
Tabbable учитывает текущее состояние DOM, поэтому при динамическом добавлении или удалении элементов список tabbable-элементов необходимо обновлять. Часто используется пересоздание ловушки при изменении контента:
function refreshTrap(container) {
deactivateTrap(container);
activateTrap(container);
}
Это особенно важно для компонентов с ленивой загрузкой или динамическими формами, чтобы новые элементы также участвовали в навигации.
tabindex: элементы с
положительным tabindex должны получать фокус в порядке
возрастания, элементы с tabindex="0" — в стандартном
DOM-порядке.Tabbable обеспечивает надёжное определение фокусируемых элементов, а правильная организация ловушек фокуса гарантирует предсказуемое поведение интерфейса даже в сложных многокомпонентных структурах.