В интерфейсах с динамическим содержимым элементы могут появляться и исчезать в процессе работы приложения. Диалоговые окна закрываются, списки обновляются, формы перестраиваются. Библиотека Tabbable должна корректно реагировать на такие изменения, поскольку порядок перемещения фокуса напрямую зависит от актуального набора интерактивных элементов.
Удаление элементов из DOM влияет на результаты функций
tabbable, focusable и isTabbable.
При изменении структуры документа необходимо понимать, каким образом
библиотека пересчитывает список доступных для фокусировки узлов и какие
последствия возникают для клавиатурной навигации.
Библиотека определяет tabbable-элементы на основании текущего состояния DOM. При каждом вызове функции выполняется обход дерева и формируется новый список подходящих узлов.
Если элемент удалён из документа:
Пример структуры:
<div id="container">
<button id="first">Первый</button>
<button id="second">Второй</button>
<button id="third">Третий</button>
</div>
Получение списка tabbable:
import { tabbable } from 'tabbable';
const container = document.getElementById('container');
const elements = tabbable(container);
console.log(elements);
Результат:
[firstButton, secondButton, thirdButton]
Удаление второго элемента:
document.getElementById('second').remove();
Повторный вызов:
const updated = tabbable(container);
console.log(updated);
Результат:
[firstButton, thirdButton]
Библиотека не хранит внутреннего состояния списка элементов. Каждая операция вычисления выполняется заново, поэтому удалённые узлы автоматически исчезают из результатов.
Особая ситуация возникает, когда удаляется элемент, на котором в данный момент находится фокус.
Поведение определяется браузером:
document.activeElement обычно становится
body.Пример:
const button = document.getElementById('delete-me');
button.focus();
button.remove();
После удаления:
console.log(document.activeElement);
Вывод:
<body>
С точки зрения Tabbable это означает, что следующая операция поиска tabbable-элементов не будет учитывать удалённый узел, а фокус нужно установить вручную, если требуется сохранить логичную навигацию.
В большинстве интерфейсов удаление элементов происходит в рамках реактивных обновлений интерфейса. В таких случаях рекомендуется пересчитывать список tabbable-узлов после изменения DOM.
Типичный сценарий:
import { tabbable } from 'tabbable';
function updateFocusables(container) {
return tabbable(container);
}
После удаления элемента:
item.remove();
const focusableElements = updateFocusables(container);
Такой подход гарантирует:
Удаление родительского узла автоматически исключает из документа всех его потомков. Для Tabbable это означает, что весь набор элементов внутри контейнера исчезает из tabbable-результата.
Пример:
<div id="dialog">
<button>OK</button>
<button>Cancel</button>
</div>
Удаление диалога:
document.getElementById('dialog').remove();
Любой последующий вызов:
tabbable(document.body);
уже не включает кнопки из удалённого диалога.
Этот механизм особенно важен при закрытии модальных окон, выпадающих меню и панелей интерфейса.
Библиотека Tabbable часто используется совместно с
системами управления фокусом, например с библиотекой
focus-trap. В таких системах список tabbable-элементов
ограничивает область навигации клавишей Tab.
Удаление элемента внутри такой области требует пересчёта списка.
Пример логики:
const trapElements = tabbable(container);
После удаления:
removedElement.remove();
необходимо повторно выполнить:
const updatedTrap = tabbable(container);
Без пересчёта возможны проблемы:
Элементы с положительным tabindex имеют приоритет в
порядке табуляции. При их удалении изменяется логика перехода между
элементами.
Исходная разметка:
<button tabindex="3">A</button>
<button tabindex="1">B</button>
<button tabindex="2">C</button>
<button>D</button>
Порядок табуляции:
B → C → A → D
Удаление элемента C:
document.querySelector('[tabindex="2"]').remove();
Новый порядок:
B → A → D
Библиотека автоматически перестраивает список, учитывая оставшиеся
значения tabindex.
Некоторые элементы удаляются не физически, а через изменение
состояния отображения (display: none,
visibility: hidden, hidden). В таких случаях
Tabbable также исключает их из результата, поскольку
проверяет видимость элементов.
Однако при фактическом удалении:
element.remove();
проверка становится быстрее, поскольку библиотека не анализирует стили отсутствующего узла.
В современных приложениях элементы чаще удаляются через фреймворки, использующие виртуальный DOM:
При ререндере компонент может полностью удалить узел из DOM. После
завершения обновления интерфейса вызов tabbable возвращает
новый список элементов.
Пример React-компонента:
function List({ items }) {
return (
<div>
{items.map(item => (
<button key={item.id}>{item.label}</button>
))}
</div>
);
}
Если элемент исчезает из массива items, соответствующая
кнопка удаляется из DOM.
Следующий вызов tabbable(container) автоматически
отражает новое состояние интерфейса.
В сложных интерфейсах используется MutationObserver,
позволяющий отслеживать изменения DOM и автоматически пересчитывать
tabbable-элементы.
Пример наблюдателя:
const observer = new MutationObserver(() => {
focusableElements = tabbable(container);
});
observer.observe(container, {
childList: true,
subtree: true
});
Когда элемент удаляется:
node.remove();
наблюдатель получает уведомление и выполняет обновление списка.
Иногда элемент удаляется в момент обработки события клавиатуры. Например, кнопка закрывает диалог и удаляет себя из DOM.
Пример:
button.addEventListener('keydown', (event) => {
if (event.key === 'Tab') {
button.remove();
}
});
В этом случае:
Для корректного поведения часто применяется ручная установка фокуса:
nextElement.focus();
Перед выполнением операций с элементом, полученным из списка tabbable, полезно проверять, существует ли он в документе.
Пример проверки:
if (document.contains(element)) {
element.focus();
}
Это предотвращает ситуации, когда код пытается работать с уже удалённым DOM-узлом.
В интерфейсах с большим количеством динамических операций (например, редакторы, таблицы, канбан-доски) элементы могут удаляться очень часто.
Поскольку Tabbable не кэширует список элементов, частые вызовы функции могут создавать дополнительную нагрузку.
Оптимизационные подходы:
document.body.Пример:
const focusable = tabbable(modalElement);
вместо
const focusable = tabbable(document.body);
Это значительно уменьшает объём обхода DOM.
const list = tabbable(container);
// элемент удалён
element.remove();
// попытка использовать старый список
list[1].focus();
Если list[1] был удалён, возникнет ошибка.
Удаление элемента с активным фокусом без переназначения нового может приводить к непредсказуемой навигации.
Старый список элементов внутри модального окна может нарушить циклическую табуляцию.
<ul id="todo">
<li><button>Задача 1</button></li>
<li><button>Задача 2</button></li>
<li><button>Задача 3</button></li>
</ul>
Получение tabbable-элементов:
const container = document.getElementById('todo');
let buttons = tabbable(container);
Удаление задачи:
container.children[1].remove();
Обновление:
buttons = tabbable(container);
Теперь список содержит только две кнопки.
Tabbable при удалении элементов:
Эти свойства делают библиотеку устойчивой к изменениям DOM и удобной для использования в интерфейсах с интенсивной динамикой элементов.