Интеграция с Mutation Observer

Focus-trap — это библиотека для управления фокусом в веб-приложениях. Она позволяет создать ловушку фокуса, удерживая его внутри определённого элемента (например, модального окна или панели навигации), пока не будет выполнено явное действие выхода. Для динамических интерфейсов, где элементы DOM могут появляться или исчезать, интеграция с Mutation Observer обеспечивает автоматическое управление фокусом без ручного отслеживания изменений.


Основы Mutation Observer

MutationObserver — это встроенный объект JavaScript, который позволяет отслеживать изменения DOM:

  • Добавление или удаление узлов (childList)
  • Изменение атрибутов элементов (attributes)
  • Изменение текста (characterData)
  • Глубокий или поверхностный мониторинг (subtree)

Пример создания базового наблюдателя:

const observer = new MutationObserver((mutationsList) => {
  for (const mutation of mutationsList) {
    console.log(mutation);
  }
});

observer.observe(document.body, { childList: true, subtree: true });

Интеграция с Focus-trap

В статическом интерфейсе focus-trap активируется один раз на элементе. В динамическом интерфейсе, например, при подгрузке модального окна через AJAX, элемент может появиться в DOM позже. Здесь на помощь приходит Mutation Observer.

Инициализация ловушки фокуса на динамических элементах

import { createFocusTrap } from 'focus-trap';

const observer = new MutationObserver((mutationsList) => {
  for (const mutation of mutationsList) {
    mutation.addedNodes.forEach(node => {
      if (node.nodeType === Node.ELEMENT_NODE && node.matches('.modal')) {
        const trap = createFocusTrap(node, {
          onActivate: () => console.log('Focus trap activated'),
          onDeactivate: () => console.log('Focus trap deactivated'),
          escapeDeactivates: true,
          clickOutsideDeactivates: true,
        });
        trap.activate();
      }
    });
  }
});

observer.observe(document.body, { childList: true, subtree: true });

В этом примере наблюдатель отслеживает добавление элементов с классом .modal. Как только такой элемент появляется в DOM, создаётся и активируется ловушка фокуса.


Обновление существующих ловушек

Иногда модальные окна или панели меняют содержимое динамически. Для таких случаев важно обновлять tabbable elements внутри focus-trap. Метод updateContainerElements() позволяет пересканировать DOM внутри ловушки:

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

// после динамического изменения контента
trap.updateContainerElements();

Если изменения происходят через Mutation Observer, вызов updateContainerElements() можно интегрировать прямо в callback:

const observer = new MutationObserver((mutationsList) => {
  mutationsList.forEach(mutation => {
    if (mutation.target.matches('.modal')) {
      trap.updateContainerElements();
    }
  });
});

observer.observe(document.querySelector('.modal'), { childList: true, subtree: true });

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

В интерфейсах с несколькими модальными окнами или панелями важно правильно включать и отключать фокус:

  • onActivate — запускать при открытии модального окна
  • onDeactivate — запускать при закрытии или удалении элемента из DOM

Для автоматизации деактивации через Mutation Observer можно проверять удаление узлов:

const observer = new MutationObserver((mutationsList) => {
  for (const mutation of mutationsList) {
    mutation.removedNodes.forEach(node => {
      if (node.nodeType === Node.ELEMENT_NODE && node.matches('.modal')) {
        trap.deactivate();
      }
    });
  }
});

observer.observe(document.body, { childList: true, subtree: true });

Такой подход гарантирует, что фокус не «зависнет» после удаления элемента.


Советы по производительности

  • Использовать селекторы максимально конкретные, чтобы не сканировать весь DOM.
  • Ограничивать наблюдение на конкретные контейнеры, а не на весь document.body.
  • При больших интерфейсах с динамическим контентом комбинировать childList и subtree, избегая attributes, если они не нужны.
  • Разбирать изменения пакетно в callback, чтобы минимизировать лишние активации focus-trap.

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

const traps = new Map();

const observer = new MutationObserver((mutationsList) => {
  mutationsList.forEach(mutation => {
    mutation.addedNodes.forEach(node => {
      if (node.nodeType === Node.ELEMENT_NODE && node.matches('.modal')) {
        const trap = createFocusTrap(node, {
          escapeDeactivates: true,
          clickOutsideDeactivates: true,
        });
        trap.activate();
        traps.set(node, trap);
      }
    });

    mutation.removedNodes.forEach(node => {
      if (traps.has(node)) {
        traps.get(node).deactivate();
        traps.delete(node);
      }
    });
  });
});

observer.observe(document.body, { childList: true, subtree: true });

В этом решении создаётся карта активных ловушек, автоматически активируются новые и деактивируются удалённые, что идеально подходит для SPA или динамически обновляемых интерфейсов.


Итоговые особенности интеграции

  • Автоматическая активация: ловушка фокуса включается при появлении элемента.
  • Динамическое обновление: пересканирование элементов при изменении содержимого.
  • Автоматическая деактивация: отключение фокуса при удалении элемента из DOM.
  • Производительность: минимизация работы через точные селекторы и ограниченные области наблюдения.

Такое сочетание Focus-trap и Mutation Observer обеспечивает стабильное управление фокусом даже в самых динамичных интерфейсах.