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

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

Фокусировка обеспечивается с помощью методов activate() и deactivate(). При активации ловушки фокус автоматически переводится на первый доступный элемент, а при деактивации возвращается к элементу, который имел фокус до активации.

import { createFocusTrap } from 'focus-trap';

const modal = document.getElementById('modal');
const trap = createFocusTrap(modal);

trap.activate();
// Для выхода из ловушки
trap.deactivate();

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

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

Вложенные ловушки

Вложенная ловушка — это когда внутри активной ловушки создаётся новая. Основные правила:

  1. Деактивация внешней ловушки при активации внутренней. Это предотвращает потерю контроля над фокусом: только внутренняя ловушка принимает события клавиатуры.

  2. Возврат к внешней ловушке после закрытия внутренней. При деактивации внутренней ловушки необходимо повторно активировать внешнюю.

Пример:

const outerTrap = createFocusTrap(document.getElementById('modalOuter'), {
  onDeactivate: () => console.log('Внешняя ловушка деактивирована')
});

const innerTrap = createFocusTrap(document.getElementById('modalInner'), {
  onActivate: () => {
    outerTrap.deactivate();
  },
  onDeactivate: () => {
    outerTrap.activate();
  }
});

// Активация внешней ловушки
outerTrap.activate();

// Активация внутренней
innerTrap.activate();

Ключевой момент: порядок активации и деактивации должен строго соблюдаться, иначе фокус может потеряться.


Последовательная активация и переключение

Для интерфейсов с несколькими независимыми модальными окнами требуется управление стеком активных ловушек. Один из подходов — хранение массива активных ловушек и переключение между ними:

const trapsStack = [];

function activateTrap(trap) {
  if (trapsStack.length > 0) {
    trapsStack[trapsStack.length - 1].deactivate();
  }
  trap.activate();
  trapsStack.push(trap);
}

function deactivateTrap() {
  const trap = trapsStack.pop();
  trap.deactivate();
  if (trapsStack.length > 0) {
    trapsStack[trapsStack.length - 1].activate();
  }
}

Этот подход обеспечивает:

  • Корректное возвращение фокуса к предыдущей активной ловушке.
  • Отсутствие конфликтов между параллельными модальными окнами.
  • Возможность создавать многоуровневые интерфейсы, где каждый уровень управляет своим фокусом.

Настройка поведения при кликах вне контейнера

Focus-trap поддерживает опцию clickOutsideDeactivates. В сложных композициях с несколькими ловушками важно настроить её правильно, чтобы клики на фоне не деактивировали внешние ловушки преждевременно.

const trap = createFocusTrap(modal, {
  clickOutsideDeactivates: true,
  escapeDeactivates: true
});

Рекомендация: Для вложенных ловушек внешняя ловушка должна не реагировать на клики вне контейнера, пока активна внутренняя. Это предотвращает случайное закрытие внешнего модального окна.


Управление фокусом вручную

Для более сложных сценариев можно использовать методы pause() и unpause(). Это особенно актуально при динамическом создании элементов внутри активной ловушки:

trap.pause();   // временно приостанавливает ловушку
// манипуляции с DOM
trap.unpause(); // возобновляет управление фокусом

Такой подход позволяет:

  • Временное отключение ловушки без потери состояния.
  • Управление фокусом при переходе между внутренними и внешними модальными окнами.

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

  • Всегда хранить стек активных ловушек, чтобы корректно возвращать фокус.
  • Внутренние ловушки должны деактивировать внешние при активации.
  • Использовать pause() для динамических изменений DOM внутри активной ловушки.
  • Настраивать clickOutsideDeactivates и escapeDeactivates индивидуально для каждого уровня, чтобы избежать конфликтов.
  • Проверять, что первый и последний элементы фокусируемы, иначе ловушка будет “залипать”.

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