Список tabbable-элементов представляет собой набор DOM-узлов, которые могут получить фокус при перемещении клавишей Tab. Библиотека Tabbable вычисляет такие элементы динамически, анализируя структуру DOM, атрибуты элементов, состояние видимости и доступности. В реальных интерфейсах структура документа часто изменяется: появляются модальные окна, раскрываются панели, добавляются новые кнопки, поля ввода или ссылки. В таких условиях возникает необходимость повторно вычислять список tabbable-элементов.
Обновление списка tabbable — это процесс повторного определения элементов, доступных для фокусировки клавишей Tab, после изменений в DOM или изменениях состояния элементов.
В интерфейсах с динамическим содержимым список фокусируемых элементов не является постоянным. Несколько распространённых сценариев требуют его пересчёта.
JavaScript-приложения часто создают элементы программно:
После добавления таких элементов в DOM они не будут автоматически включены в уже сохранённый список tabbable-элементов.
Пример:
import { tabbable } from 'tabbable';
const container = document.querySelector('#app');
let tabbables = tabbable(container);
// добавление нового поля
const input = document.createElement('input');
container.appendChild(input);
// список устарел
Переменная tabbables по-прежнему содержит старый набор
элементов.
Удаление узлов также делает список устаревшим.
container.removeChild(container.querySelector('button'));
Если удалённый элемент присутствовал в списке tabbable, сохранённый массив будет содержать ссылку на DOM-узел, которого уже нет в документе.
Некоторые изменения не затрагивают структуру DOM, но влияют на возможность фокусировки:
tabindexdisableddisplay или visibilityhiddenaria-hiddenПример:
button.setAttribute('tabindex', '-1');
После такого изменения кнопка перестаёт быть tabbable, однако сохранённый список этого не отражает.
Типичные элементы интерфейса:
Когда компонент становится видимым, он добавляет новые tabbable-элементы. При скрытии — наоборот.
Библиотека Tabbable не хранит глобального состояния. Каждое обращение
к функции tabbable() выполняет новый анализ DOM.
const tabbables = tabbable(container);
Поэтому обновление списка выполняется простым повторным вызовом функции.
import { tabbable } from 'tabbable';
const container = document.querySelector('#container');
function updateTabbables() {
return tabbable(container);
}
let tabbables = updateTabbables();
После изменений DOM:
container.appendChild(document.createElement('button'));
tabbables = updateTabbables();
Многие интерфейсы используют логику управления фокусом: циклическую навигацию, фокус-ловушки или управление фокусом внутри диалогов.
В таких системах список tabbable элементов используется для:
Пример:
const elements = tabbable(container);
const first = elements[0];
const last = elements[elements.length - 1];
Если элементы изменились, границы навигации становятся неверными. Поэтому перед обработкой навигации список должен обновляться.
Для интерфейсов с частыми изменениями DOM возможно автоматическое
обновление списка с использованием MutationObserver.
import { tabbable } from 'tabbable';
const container = document.querySelector('#container');
let tabbables = tabbable(container);
const observer = new MutationObserver(() => {
tabbables = tabbable(container);
});
Подключение наблюдателя:
observer.observe(container, {
childList: true,
subtree: true,
attributes: true
});
Теперь список будет пересчитываться при:
Для повышения производительности рекомендуется наблюдать только ту часть DOM, где происходят изменения.
observer.observe(modalElement, {
childList: true,
subtree: true
});
Это особенно важно в крупных приложениях с большим количеством DOM-узлов.
Пересчёт tabbable-элементов требует обхода DOM. В больших интерфейсах это может быть затратной операцией.
Если изменения происходят редко, список можно хранить и обновлять только при необходимости.
let cachedTabbables = null;
function getTabbables() {
if (!cachedTabbables) {
cachedTabbables = tabbable(container);
}
return cachedTabbables;
}
После изменения DOM кэш сбрасывается.
function invalidateTabbables() {
cachedTabbables = null;
}
Вместо постоянного наблюдения DOM можно обновлять список при событиях интерфейса.
Пример:
menuButton.addEventListener('click', () => {
menu.classList.toggle('open');
tabbables = tabbable(menu);
});
Этот подход снижает нагрузку на систему.
В некоторых случаях пересчёт всей области не требуется. Возможно ограничить поиск определённым контейнером.
const modalTabbables = tabbable(modal);
При закрытии модального окна:
const pageTabbables = tabbable(document.body);
Такой подход применяется при переключении контекстов фокусировки.
В сложных интерфейсах контейнеры могут содержать другие контейнеры с собственными списками tabbable-элементов.
Пример структуры:
app
├ sidebar
├ main
│ ├ form
│ └ modal
└ footer
Каждый контейнер может иметь свой список tabbable.
const sidebarTabbables = tabbable(sidebar);
const formTabbables = tabbable(form);
При обновлении конкретной части интерфейса нет необходимости пересчитывать весь документ.
Библиотеки управления фокусом (например, focus-trap) используют Tabbable для вычисления элементов фокусировки. Внутри фокус-ловушки список элементов также обновляется при изменении DOM.
Если элементы добавляются динамически внутри ловушки фокуса, необходимо инициировать обновление.
Пример:
trap.updateContainerElements(container);
После этого выполняется повторное вычисление tabbable-элементов.
Иногда требуется убедиться, что элемент из сохранённого списка всё ещё присутствует в DOM.
document.contains(element)
Если возвращается false, элемент был удалён и список
необходимо пересчитать.
const elements = tabbable(container);
// DOM изменился
elements[0].focus();
Если первый элемент был удалён, вызов focus() может
привести к ошибкам или непредсказуемому поведению.
Пересчёт при каждом изменении атрибута может создавать нагрузку.
Плохой пример:
observer.observe(document.body, {
subtree: true,
attributes: true
});
Это может вызывать тысячи пересчётов.
Элемент может оставаться в DOM, но быть скрытым.
display: none;
Tabbable автоматически исключает такие элементы, но сохранённый список может содержать их до следующего обновления.
Один из распространённых шаблонов:
Пример реализации:
import { tabbable } from 'tabbable';
const container = document.querySelector('#app');
let tabbables = [];
function refreshTabbables() {
tabbables = tabbable(container);
}
refreshTabbables();
document.addEventListener('ui:update', refreshTabbables);
При изменениях интерфейса вызывается пользовательское событие:
document.dispatchEvent(new Event('ui:update'));
Корректный список tabbable-элементов играет ключевую роль в доступности:
Без регулярного обновления списка навигация клавишей Tab может:
Поэтому динамические интерфейсы должны учитывать обновление tabbable-элементов как часть архитектуры управления фокусом.