Вложенные модальные окна

Библиотека Focus-trap предназначена для управления фокусом внутри модальных окон и других интерактивных контейнеров. Основная задача — предотвратить уход фокуса за пределы активного интерфейса, особенно при использовании клавиатуры. Это критично для доступности (accessibility), поскольку пользователи с клавиатурой или экранными читалками должны оставаться внутри контекста активного элемента.

Focus-trap работает через перехват событий Tab и Shift+Tab, циклически перемещая фокус между фокусируемыми элементами внутри контейнера. Поддерживаются кнопки, ссылки, поля ввода, textarea и любые элементы с атрибутом tabindex="0" или большим положительным значением.


Инициализация и базовое использование

Для создания фокустрапа требуется указать контейнер и, при необходимости, конфигурацию. Пример:

import { createFocusTrap } from 'focus-trap';

const modal = document.getElementById('modal');
const focusTrap = createFocusTrap(modal, {
  escapeDeactivates: true,
  clickOutsideDeactivates: true,
  allowOutsideClick: true,
});

focusTrap.activate();

Ключевые параметры:

  • escapeDeactivates — при нажатии клавиши Esc фокус-трап деактивируется.
  • clickOutsideDeactivates — клик вне контейнера снимает фокус-трап.
  • allowOutsideClick — позволяет обработчикам внешнего клика выполняться без нарушения фокус-трапа.

Методы:

  • activate() — включает фокус-трап.
  • deactivate() — выключает фокус-трап.
  • pause() / unpause() — временно приостанавливают управление фокусом, не снимая его полностью.

Вложенные модальные окна

Сложность возникает при работе с несколькими модальными окнами, открывающимися одно поверх другого. Простая активация нового фокус-трапа поверх старого может нарушить логику циклического фокуса, если старый трап продолжает слушать события.

Основная стратегия

  1. Пауза верхнего трапа при открытии вложенного модального окна.
  2. Активация нового трапа для внутреннего модального окна.
  3. Возврат к предыдущему трапу после закрытия внутреннего окна через unpause() или повторную активацию.

Пример с двумя модальными окнами:

const outerModal = document.getElementById('outerModal');
const innerModal = document.getElementById('innerModal');

const outerTrap = createFocusTrap(outerModal, { escapeDeactivates: false });
const innerTrap = createFocusTrap(innerModal, { escapeDeactivates: true });

// Открытие внешнего модального окна
outerTrap.activate();

// Открытие внутреннего модального окна
document.getElementById('openInner').addEventListener('click', () => {
  outerTrap.pause(); // временно приостанавливаем старый трап
  innerTrap.activate(); // активируем вложенный трап
});

// Закрытие внутреннего модального окна
document.getElementById('closeInner').addEventListener('click', () => {
  innerTrap.deactivate();
  outerTrap.unpause(); // возвращаем управление внешнему трапу
});

Обработка клавиши Esc и других сочетаний

Для вложенных модальных окон важно, чтобы нажатие Esc закрывало только верхнее окно, не прерывая фокус-трап нижнего уровня:

const innerTrap = createFocusTrap(innerModal, {
  escapeDeactivates: true,
  onDeactivate: () => {
    outerTrap.unpause();
  }
});

Событие onDeactivate позволяет автоматически возвращать фокус к предыдущему контейнеру, сохраняя корректный цикл.


Особенности работы с динамическим контентом

Если модальное окно создаётся динамически, фокус-трап необходимо создавать после вставки элемента в DOM:

const dynamicModal = document.createElement('div');
dynamicModal.id = 'dynamicModal';
document.body.appendChild(dynamicModal);

const dynamicTrap = createFocusTrap(dynamicModal);
dynamicTrap.activate();

Для вложенных динамических модалей следует использовать аналогичную стратегию паузы и активации.


Настройка элементов фокусировки

Focus-trap по умолчанию учитывает все стандартные фокусируемые элементы. Можно вручную задать элементы, через которые будет циклировать фокус:

const customTrap = createFocusTrap(modal, {
  tabbableOptions: {
    displayCheck: 'full', // проверка видимости элементов
    includeContainer: true // включить сам контейнер в цикл
  }
});

Опции tabbableOptions позволяют фильтровать элементы по критериям видимости и включать кастомные селекторы для управления фокусом.


Поддержка анимаций и переходов

Если модальное окно закрывается с анимацией, деактивацию фокус-трапа лучше выполнять после завершения анимации. Это предотвращает мгновенное снятие фокуса до завершения визуальных переходов.

modal.addEventListener('transitionend', () => {
  trap.deactivate();
});

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

  • Для каждого модального уровня использовать отдельный фокус-трап.
  • При открытии нового окна всегда паузить предыдущий трап.
  • Использовать onDeactivate для автоматического возврата управления.
  • Следить за динамическими элементами и корректной инициализацией фокус-трапов после их добавления в DOM.
  • Настраивать tabbableOptions для исключения невидимых или неактивных элементов из цикла фокуса.

Эти подходы позволяют создавать сложные интерфейсы с несколькими уровнями модальных окон, сохраняя корректное управление клавиатурным фокусом и соответствие стандартам доступности.