Focus-trap — это библиотека для управления фокусом в веб-приложениях. Она позволяет создать ловушку фокуса, удерживая его внутри определённого элемента (например, модального окна или панели навигации), пока не будет выполнено явное действие выхода. Для динамических интерфейсов, где элементы DOM могут появляться или исчезать, интеграция с 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 активируется один раз на элементе. В динамическом интерфейсе, например, при подгрузке модального окна через 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 });
В интерфейсах с несколькими модальными окнами или панелями важно правильно включать и отключать фокус:
Для автоматизации деактивации через 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 });
Такой подход гарантирует, что фокус не «зависнет» после удаления элемента.
document.body.childList и subtree, избегая
attributes, если они не нужны.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 или динамически обновляемых интерфейсов.
Такое сочетание Focus-trap и Mutation Observer обеспечивает стабильное управление фокусом даже в самых динамичных интерфейсах.