Управление прокруткой страницы

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

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

  • Фокус всегда циклически возвращается к первому или последнему элементу внутри контейнера.
  • При активации trap остальные элементы страницы остаются доступными только визуально, но не для клавиатурного фокуса.
  • Focus-trap обеспечивает совместимость с aria-атрибутами, улучшая доступность.

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

Создание focus-trap осуществляется с помощью функции createFocusTrap. Простейший пример:

import { createFocusTrap } from 'focus-trap';

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

// Активация trap
trap.activate();

// Деактивация trap
trap.deactivate();

Пояснения параметров:

  • onActivate и onDeactivate — функции обратного вызова, вызываемые при активации и деактивации.
  • clickOutsideDeactivates — разрешает закрытие trap при клике вне контейнера.
  • returnFocusOnDeactivate (по умолчанию true) — возвращает фокус к элементу, который его имел до активации.

Управление прокруткой страницы

Когда активен focus-trap, важно корректно управлять прокруткой страницы, чтобы пользователь не мог прокрутить фон, оставаясь внутри модального окна. Есть несколько подходов:

  1. Блокировка прокрутки через CSS:
body.modal-open {
  overflow: hidden;
}

При активации trap добавляется класс modal-open к <body>, при деактивации — удаляется. Этот метод предотвращает скролл всего документа, оставляя прокрутку возможной только внутри модального окна.

  1. Прокрутка внутри контейнера: Контейнер модального окна должен быть ограничен по высоте и иметь overflow-y: auto. Focus-trap не управляет скроллом контейнера автоматически, но корректно перемещает фокус по его элементам.
#modal {
  max-height: 80vh;
  overflow-y: auto;
}
  1. Дополнительные опции focus-trap для прокрутки: Focus-trap позволяет определить initialFocus и fallbackFocus для управления тем, какой элемент получает фокус при активации. Это важно, если первый интерактивный элемент находится за пределами видимой области контейнера. Например:
const trap = createFocusTrap(modal, {
  initialFocus: '#modal input:first-of-type',
  fallbackFocus: '#modal'
});

При активации trap браузер автоматически скроллит к элементу с фокусом.


Варианты активации и деактивации

Focus-trap поддерживает динамическое управление состоянием, что особенно важно для сложных интерфейсов с несколькими модальными окнами или всплывающими меню.

  • Программная активация: trap.activate()
  • Программная деактивация: trap.deactivate()
  • Автоматическая деактивация при событии: Например, при нажатии клавиши Escape:
const trap = createFocusTrap(modal, {
  escapeDeactivates: true
});
  • Деактивация при клике вне контейнера: clickOutsideDeactivates: true

Продвинутые настройки

Focus-trap предоставляет гибкие опции для управления поведением:

  • allowOutsideClick — разрешает клики по элементам вне trap без деактивации.
  • tabindex — можно временно установить tabindex="-1" на элементы вне trap, чтобы исключить их из цикла табуляции.
  • setReturnFocus — позволяет задать конкретный элемент, на который вернётся фокус после деактивации, вместо предыдущего активного.
const trap = createFocusTrap(modal, {
  setReturnFocus: document.querySelector('#openModalButton')
});

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

Если содержимое контейнера меняется после активации trap (например, добавляются новые поля формы), рекомендуется вызвать trap.updateTabbableNodes(). Это обновит список фокусируемых элементов и сохранит корректную навигацию.

trap.updateTabbableNodes();

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


Рекомендации по доступности

  • Всегда использовать aria-hidden="true" на фоновых элементах при активном trap, чтобы скринридеры не перемещались за пределы активного контейнера.
  • Задавать role="dialog" или role="menu" для контейнера, чтобы обозначить его семантически.
  • Следить за правильной последовательностью фокусируемых элементов и их логическим порядком табуляции.

Пример комплексной реализации

const modal = document.getElementById('modal');
const openBtn = document.getElementById('openModal');
const closeBtn = modal.querySelector('.close');

const trap = createFocusTrap(modal, {
  escapeDeactivates: true,
  clickOutsideDeactivates: true,
  initialFocus: modal.querySelector('input'),
  onActivate: () => document.body.classList.add('modal-open'),
  onDeactivate: () => document.body.classList.remove('modal-open'),
  setReturnFocus: openBtn
});

openBtn.addEventListener('click', () => trap.activate());
closeBtn.addEventListener('click', () => trap.deactivate());

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

  • Полный контроль фокуса внутри модального окна.
  • Автоматическую блокировку фонового скролла.
  • Поддержку клавиатурной навигации и доступность через aria.

Фокус в таком случае корректно удерживается внутри окна, а взаимодействие с остальной страницей становится невозможным до деактивации trap.