В современных веб-интерфейсах структура DOM изменяется динамически: элементы добавляются, удаляются, скрываются, перемещаются. Для библиотек, управляющих навигацией по интерфейсу с клавиатуры, подобные изменения имеют критическое значение. Любое изменение структуры может повлиять на порядок фокусируемых элементов и на корректность навигации клавишей Tab.
Библиотека Tabbable предназначена для определения элементов, которые могут получать фокус при последовательной навигации. Однако если структура документа изменяется после первоначального вычисления, полученный список элементов может устареть. В таких ситуациях используется механизм наблюдения за изменениями DOM — MutationObserver.
MutationObserver представляет собой встроенный API браузера, позволяющий отслеживать изменения в DOM-дереве. Он реагирует на:
Использование MutationObserver позволяет автоматически обнаруживать изменения, влияющие на доступность элементов для фокуса, и своевременно пересчитывать список tabbable-элементов.
Современные интерфейсы активно используют:
В результате структура DOM изменяется значительно чаще, чем в классических статических страницах.
Пример ситуации:
Если список tabbable-элементов вычислялся ранее, он не будет содержать элементы модального окна. Это нарушит ожидаемую последовательность фокуса.
MutationObserver позволяет обнаружить появление нового DOM-фрагмента и инициировать повторный анализ.
MutationObserver создаётся через конструктор:
const observer = new MutationObserver(callback);
Аргумент callback — функция, вызываемая при обнаружении
изменений.
function callback(mutationsList, observer) {
for (const mutation of mutationsList) {
console.log(mutation.type);
}
}
Каждый объект mutation описывает конкретное
изменение.
Основные типы мутаций:
childList — добавление или удаление дочерних узловattributes — изменение атрибутовcharacterData — изменение текстового содержимогоНаблюдение запускается методом observe:
observer.observe(targetNode, options);
Пример настроек:
observer.observe(document.body, {
childList: true,
subtree: true
});
Значение subtree: true означает отслеживание изменений
во всех вложенных элементах.
Для корректной работы навигации по фокусу важны следующие изменения DOM.
Появление новых элементов может изменить порядок табуляции.
Примеры:
MutationObserver позволяет обнаружить такие элементы сразу после их вставки в DOM.
Удаление интерактивных элементов может нарушить текущую позицию фокуса.
Например:
Observer фиксирует удаление узлов через тип мутации
childList.
Некоторые атрибуты напрямую влияют на возможность получения фокуса:
tabindexdisabledhiddenaria-hiddenstyle (например display: none)Пример изменения:
button.setAttribute("disabled", true);
После этого элемент перестаёт быть tabbable. MutationObserver позволяет обнаружить изменение атрибутов и обновить список элементов.
Типичная схема работы выглядит следующим образом:
Пример:
import { tabbable } from 'tabbable';
const container = document.querySelector('#app');
function updateTabbables() {
const elements = tabbable(container);
console.log(elements);
}
const observer = new MutationObserver(() => {
updateTabbables();
});
observer.observe(container, {
childList: true,
subtree: true,
attributes: true
});
updateTabbables();
Каждое изменение DOM приводит к обновлению списка доступных элементов.
MutationObserver может генерировать большое количество событий. Не каждое изменение требует пересчёта tabbable-элементов.
Эффективная реализация анализирует тип мутации.
Пример фильтрации:
const observer = new MutationObserver((mutations) => {
let shouldUpdate = false;
for (const mutation of mutations) {
if (mutation.type === "childList") {
shouldUpdate = true;
break;
}
if (mutation.type === "attributes") {
if (mutation.attributeName === "tabindex") {
shouldUpdate = true;
break;
}
}
}
if (shouldUpdate) {
updateTabbables();
}
});
Такой подход снижает нагрузку на производительность.
MutationObserver позволяет ограничить список отслеживаемых атрибутов.
observer.observe(container, {
attributes: true,
attributeFilter: ["tabindex", "disabled", "hidden"],
subtree: true
});
Это уменьшает количество ненужных уведомлений.
При активном изменении DOM пересчёт tabbable-элементов может выполняться слишком часто. Для предотвращения этого применяются методы оптимизации.
Обновление откладывается на короткий промежуток времени.
let timer;
const observer = new MutationObserver(() => {
clearTimeout(timer);
timer = setTimeout(() => {
updateTabbables();
}, 50);
});
Несколько изменений объединяются в одно обновление.
Иногда добавленный элемент не влияет на фокусируемость.
Например:
<div class="decorative"></div>
Такие элементы можно игнорировать.
Проверка может выполняться по тегу или наличию интерактивных атрибутов.
Наблюдение за document.body может быть дорогостоящим.
Лучше отслеживать только контейнер интерфейса.
observer.observe(modalElement, {...});
Это существенно снижает количество мутаций.
Одним из наиболее распространённых применений Tabbable является focus trapping — ограничение перемещения фокуса внутри модального окна.
MutationObserver используется для отслеживания:
Пример сценария:
Без наблюдения новые элементы не попадут в последовательность Tab.
В React, Vue или Svelte DOM часто обновляется через виртуальное дерево.
Пример:
MutationObserver фиксирует эти изменения автоматически.
Это делает интеграцию Tabbable независимой от используемого фреймворка.
Когда наблюдение больше не требуется, observer необходимо отключить.
observer.disconnect();
Причины отключения:
Это предотвращает утечки памяти.
Иногда наблюдение временно отключается и включается снова.
Пример:
observer.disconnect();
updateTabbables();
observer.observe(container, config);
Это может использоваться во время массовых изменений DOM, чтобы избежать большого количества мутаций.
Типичная архитектура работы MutationObserver вместе с Tabbable включает следующие элементы:
1. Контейнер интерфейса
const root = document.querySelector('#ui');
2. Функция вычисления tabbable
function compute() {
return tabbable(root);
}
3. Наблюдатель
const observer = new MutationObserver(handleMutations);
4. Анализ изменений
function handleMutations(mutations) {
for (const mutation of mutations) {
if (mutation.type === "childList") {
compute();
break;
}
}
}
5. Запуск наблюдения
observer.observe(root, {
childList: true,
subtree: true
});
Такая схема обеспечивает автоматическую синхронизацию DOM и логики навигации по фокусу.
Несмотря на эффективность, механизм имеет ряд ограничений.
MutationObserver не срабатывает мгновенно. Уведомления помещаются в очередь микрозадач и выполняются после завершения текущего JavaScript-цикла.
Это означает, что изменения DOM могут быть обработаны с небольшой задержкой.
При массовых изменениях DOM может возникать большое количество мутаций.
Например:
Без оптимизации это может вызвать проблемы с производительностью.
MutationObserver реагирует только на изменения DOM и атрибутов.
Он не отслеживает:
В таких случаях состояние tabbable-элементов может измениться без уведомления наблюдателя.
В контексте accessibility MutationObserver играет важную роль:
Особенно это важно для:
Наблюдение за изменениями DOM позволяет автоматически адаптировать систему фокусировки к текущему состоянию интерфейса.