Работа с Shadow DOM

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

При работе с Shadow DOM необходимо учитывать, что внутренние элементы инкапсулированы, а обычные методы поиска фокуса (например, querySelectorAll) по дереву документа не видят элементы внутри shadow-root. Focus-trap предоставляет возможности для интеграции с shadow-root, но требует явного указания контейнера.


Создание и инициализация Focus-trap

Для инициализации используется функция createFocusTrap, которая принимает контейнер и объект с опциями. В случае Shadow DOM контейнером должен быть корень shadow-root или элемент внутри него.

import { createFocusTrap } from 'focus-trap';

const shadowHost = document.querySelector('#shadow-host');
const shadowRoot = shadowHost.shadowRoot;

const trap = createFocusTrap(shadowRoot, {
  escapeDeactivates: true,
  clickOutsideDeactivates: false,
  initialFocus: shadowRoot.querySelector('button:first-of-type'),
});

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

  • escapeDeactivates: управление поведением при нажатии клавиши Escape. В Shadow DOM важно убедиться, что событие корректно всплывает до слушателя.
  • clickOutsideDeactivates: отключение ловушки при клике вне контейнера. Внутри shadow-root клики распознаются относительно host-элемента.
  • initialFocus: установка начального фокуса. Нужно указывать элемент, доступный внутри shadow-root.

Особенности работы с элементами внутри Shadow DOM

Focus-trap использует метод tabbable для определения фокусируемых элементов. В Shadow DOM стандартный поиск не работает, поэтому требуется передавать опцию tabbableOptions с кастомной функцией поиска элементов:

import { tabbable } from 'tabbable';

const trap = createFocusTrap(shadowRoot, {
  tabbableOptions: {
    getShadowRoot: () => shadowRoot
  }
});

Важные детали:

  • Любой slot внутри shadow-root автоматически учитывается библиотекой, если элементы в нем tabbable.
  • Если используются динамически добавляемые элементы, необходимо заново вызывать методы обновления фокусируемых элементов или пересоздавать trap.

Управление активацией и деактивацией

Focus-trap предоставляет методы activate() и deactivate(), которые контролируют жизненный цикл ловушки фокуса.

trap.activate();   // активирует ловушку, фокус будет ограничен shadow-root
trap.deactivate(); // снимает ограничение

Особенности для Shadow DOM:

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

Обработка событий и всплытия

Shadow DOM инкапсулирует события, что может влиять на работу focus-trap:

  • Keyboard events (Tab, Shift+Tab, Escape) должны быть обработаны на уровне shadow-root или на host-элементе.
  • Опция document в Focus-trap по умолчанию использует глобальный document, но внутри Shadow DOM корректнее передавать кастомный объект с addEventListener и removeEventListener, чтобы события правильно отслеживались.

Пример:

const trap = createFocusTrap(shadowRoot, {
  document: shadowRoot,
  escapeDeactivates: true
});

Динамическое обновление фокуса

Если внутри shadow-root появляются новые интерактивные элементы после активации trap, их необходимо добавить в список tabbable:

trap.updateTabbableNodes(); // пересчитывает доступные элементы

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


Совместимость с кастомными элементами

Shadow DOM часто используется в веб-компонентах. Для корректной работы focus-trap:

  • Указывать начальный фокус на интерактивный элемент внутри компонента.
  • При использовании слотов убедиться, что элементы в slot имеют tabindex ≥ 0 или являются стандартными tabbable-элементами.
  • Вложенные веб-компоненты с собственным shadow-root требуют либо отдельной ловушки, либо проксирования tabbable-элементов через host.

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

  • Для каждого модального или всплывающего интерфейса создавать отдельный focus-trap.
  • Всегда явно указывать initialFocus, чтобы избежать проблем с фокусом в Shadow DOM.
  • Пересоздавать или обновлять trap при динамическом изменении DOM внутри shadow-root.
  • Обрабатывать события клавиатуры на уровне shadow-root для предотвращения потери фокуса.

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