focus-lock

Focus-trap — это специализированная библиотека JavaScript, предназначенная для управления фокусом внутри определённых областей DOM. Она гарантирует, что клавиатурная навигация (Tab, Shift+Tab) остаётся ограниченной заданной зоной, что особенно важно для модальных окон, всплывающих панелей и интерактивных виджетов.

Библиотека реализует т.н. “ловушку фокуса”, предотвращая уход фокуса за пределы контейнера, пока он активен. Основной принцип: любые элементы, находящиеся вне зоны ловушки, становятся недоступными для клавиатуры до деактивации фокуса.

import { createFocusTrap } from 'focus-trap';

const modal = document.getElementById('modal');
const focusTrap = createFocusTrap(modal, {
  onActivate: () => modal.classList.add('is-active'),
  onDeactivate: () => modal.classList.remove('is-active'),
});

document.getElementById('openModal').addEventListener('click', () => {
  focusTrap.activate();
});

document.getElementById('closeModal').addEventListener('click', () => {
  focusTrap.deactivate();
});

Основные методы и свойства

  1. activate(options) — активирует ловушку фокуса. Опции могут включать:

    • returnFocus — возвращает фокус на исходный элемент после деактивации.
    • onActivate — коллбек при активации.
    • onDeactivate — коллбек при деактивации.
  2. deactivate(options) — деактивирует ловушку. Опции аналогичны activate, позволяют контролировать возвращение фокуса.

  3. pause() / unpause() — временно приостанавливает ловушку, позволяя пользователю покинуть контейнер без деактивации.

Конфигурационные опции

  • initialFocus — задаёт элемент, который получает фокус при активации. Может быть селектором или функцией, возвращающей элемент.
  • fallbackFocus — элемент, на который будет установлен фокус, если initialFocus недоступен.
  • clickOutsideDeactivates — позволяет деактивировать ловушку при клике за пределами контейнера.
  • escapeDeactivates — деактивирует ловушку при нажатии клавиши Escape.
  • allowOutsideClick — разрешает клики вне контейнера, не отключая ловушку полностью.
  • tabbableOptions — объект с настройками для поиска tabbable-элементов внутри контейнера.
const focusTrap = createFocusTrap('#drawer', {
  initialFocus: '#firstInput',
  fallbackFocus: '#fallbackButton',
  escapeDeactivates: true,
  clickOutsideDeactivates: true
});

Поведение при вложенных и динамических элементах

Focus-trap корректно работает с динамически изменяемым контентом. Если внутри контейнера появляются новые tabbable-элементы, они автоматически учитываются при навигации по Tab/Shift+Tab. Для вложенных модальных окон можно создать отдельную ловушку для каждого слоя, используя pause() на родительской ловушке при активации дочерней.

const parentTrap = createFocusTrap('#parentModal');
const childTrap = createFocusTrap('#childModal', {
  onActivate: () => parentTrap.pause(),
  onDeactivate: () => parentTrap.unpause()
});

Интеграция с ARIA и доступностью

Focus-trap значительно повышает доступность интерфейсов:

  • Устанавливает корректный порядок навигации по клавиатуре.
  • Поддерживает ARIA-ролей для модальных окон (aria-modal="true").
  • Позволяет возвращать фокус на элемент, открывающий модальное окно, что улучшает опыт для пользователей с клавиатурой или экранными читалками.
<div id="modal" role="dialog" aria-modal="true">
  <button id="closeModal">Закрыть</button>
  <input type="text" placeholder="Имя">
</div>

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

  1. Всегда задавать initialFocus или fallbackFocus, чтобы предотвратить потерю фокуса.
  2. Использовать returnFocus: true при деактивации модальных окон, чтобы пользователи возвращались к исходной точке взаимодействия.
  3. Комбинировать clickOutsideDeactivates и escapeDeactivates для удобства закрытия всплывающих элементов.
  4. Не блокировать навигацию полностью: при необходимости временно использовать pause() для вложенных интерфейсов.

Сравнение с нативными подходами

Без Focus-trap часто приходится вручную управлять фокусом через обработчики событий keydown и методы element.focus(). Это увеличивает вероятность ошибок, особенно при динамическом контенте. Focus-trap абстрагирует эти задачи и обеспечивает консистентное поведение во всех браузерах, включая управление tabindex и обработку неактивных элементов.

Работа с React и другими фреймворками

Focus-trap легко интегрируется с React, Vue или Angular. Для React есть отдельная обёртка focus-trap-react, позволяющая управлять ловушкой через JSX и хуки жизненного цикла компонента.

import FocusTrap from 'focus-trap-react';

function Modal({ isOpen, onClose }) {
  return (
    <FocusTrap active={isOpen} focusTrapOptions={{ onDeactivate: onClose }}>
      <div className="modal">
        <button onCl ick={onClose}>Закрыть</button>
        <input type="text" />
      </div>
    </FocusTrap>
  );
}

Использование таких обёрток упрощает управление состоянием и обеспечивает совместимость с виртуальным DOM, автоматически синхронизируя фокус с компонентами.