Композиция нескольких ловушек фокуса

Библиотека 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);
}

Ключевые моменты:

  • Составляется список всех tabbable-элементов внутри контейнера.
  • Отслеживается нажатие клавиши Tab и комбинации с Shift.
  • Перемещение фокуса циклично: при достижении последнего элемента фокус возвращается к первому и наоборот.
  • Возврат функции позволяет корректно удалять ловушку.

Комбинирование нескольких ловушек

При наличии нескольких модальных областей важно определить, какая из них активна. Для этого обычно создаётся стек ловушек:

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);
  }
}

Особенности композиции:

  • Ловушки работают по принципу «последняя активная — первая обрабатывается».
  • При открытии вложенного модального окна фокус удерживается внутри него, не перескакивая на родительскую область.
  • После закрытия внутреннего окна фокус возвращается к последней активной ловушке на стеке.

Управление динамическим DOM

Tabbable учитывает текущее состояние DOM, поэтому при динамическом добавлении или удалении элементов список tabbable-элементов необходимо обновлять. Часто используется пересоздание ловушки при изменении контента:

function refreshTrap(container) {
  deactivateTrap(container);
  activateTrap(container);
}

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


Расширенные сценарии

  1. Фокус между окнами: при одновременном открытии нескольких модальных окон необходимо разрешить только верхнему окну получать фокус. Остальные окна становятся inert или временно недоступны.
  2. Поддержка tabindex: элементы с положительным tabindex должны получать фокус в порядке возрастания, элементы с tabindex="0" — в стандартном DOM-порядке.
  3. Вложенные ловушки: при вложении модальных компонентов важно проверять, что родительская ловушка не препятствует навигации внутри дочерней. Это реализуется через условную активацию ловушки только для верхнего уровня.
  4. Адаптация для accessibility: использование Tabbable упрощает поддержку стандартов WCAG, так как правильно исключает невидимые и неинтерактивные элементы из фокусной навигации.

Практические рекомендации

  • Всегда хранить стек активных ловушек для контроля порядка навигации.
  • Избегать ручного управления фокусом на отдельных элементах без пересмотра всех tabbable-элементов.
  • При удалении элементов из DOM обновлять список tabbable-элементов, иначе возможны «потерянные» позиции фокуса.
  • Тестировать работу клавиатурной навигации с комбинацией Tab + Shift и при открытии нескольких окон.

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