Динамическое изменение структуры документа — обычная практика современных веб-приложений. Элементы интерфейса могут появляться, удаляться или изменяться в результате пользовательских действий, сетевых запросов или логики компонентов. В таких условиях особое значение приобретает корректная обработка фокусируемых и tabbable-элементов, поскольку навигация с клавиатуры должна оставаться последовательной и предсказуемой.
Библиотека Tabbable предназначена для определения
элементов, которые могут получать фокус при навигации клавишей
Tab. При добавлении новых узлов в DOM необходимо учитывать,
как библиотека определяет такие элементы и каким образом повторно
анализирует структуру документа.
Tabbable не хранит постоянное состояние элементов. Она выполняет анализ текущего DOM-дерева в момент вызова функций. Это означает, что при появлении новых элементов в документе библиотека начинает учитывать их автоматически — при следующем обращении к API.
Основные функции библиотеки:
tabbable(container)focusable(container)isTabbable(element)isFocusable(element)Каждая из них работает на основе текущего состояния DOM. Если в контейнер были добавлены новые элементы, они будут обнаружены при следующем вызове.
Пример:
import { tabbable } from 'tabbable';
const container = document.querySelector('#dialog');
let elements = tabbable(container);
console.log(elements.length);
После добавления новых элементов список изменится.
Динамическое создание интерфейса часто выполняется через
appendChild, insertAdjacentHTML,
innerHTML или современные фреймворки.
Пример создания нового tabbable-элемента:
const button = document.createElement('button');
button.textContent = 'Новая кнопка';
const container = document.querySelector('#panel');
container.appendChild(button);
Кнопка автоматически становится tabbable-элементом, поскольку:
<button> фокусируемый по умолчанию;tabindex="-1";При повторном анализе:
import { tabbable } from 'tabbable';
const elements = tabbable(container);
новая кнопка будет включена в результат.
Элемент можно сделать tabbable вручную при помощи
tabindex.
const div = document.createElement('div');
div.setAttribute('tabindex', '0');
div.textContent = 'Фокусируемый блок';
container.appendChild(div);
Значения tabindex:
| Значение | Поведение |
|---|---|
0 |
элемент участвует в обычной последовательности Tab |
>0 |
задаёт приоритет в порядке фокуса |
-1 |
элемент можно сфокусировать программно, но не через Tab |
Tabbable учитывает эти правила при формировании списка элементов.
После динамического создания элемента может потребоваться проверить его статус.
import { isTabbable } from 'tabbable';
const element = document.querySelector('.dynamic');
console.log(isTabbable(element));
Функция возвращает:
true — элемент участвует в Tab-навигации;false — элемент не входит в последовательность.Проверка особенно полезна в интерфейсах, где поведение элементов зависит от условий.
В интерфейсах часто создаются целые блоки элементов: формы, списки действий, диалоги.
Пример динамической формы:
const form = document.createElement('form');
form.innerHTML = `
<input type="text" placeholder="Имя">
<input type="email" placeholder="Email">
<button type="submit">Отправить</button>
`;
document.body.appendChild(form);
Все элементы формы автоматически становятся tabbable, если:
disabled);display: none,
visibility: hidden);Получение списка:
import { tabbable } from 'tabbable';
const fields = tabbable(form);
Порядок элементов в DOM напрямую влияет на порядок навигации клавишей
Tab.
Пример вставки:
const newInput = document.createElement('input');
const reference = document.querySelector('#email');
reference.after(newInput);
Теперь последовательность фокуса изменится:
#emailinputTabbable учитывает реальный порядок узлов в DOM,
если tabindex не переопределяет его.
Если элемент добавляется в контейнер, который скрыт стилями, он не считается tabbable.
Пример:
const panel = document.querySelector('#hiddenPanel');
panel.style.display = 'none';
const input = document.createElement('input');
panel.appendChild(input);
Проверка:
tabbable(panel)
вернёт пустой список.
Причина: элементы внутри display: none не участвуют в
навигации.
После отображения:
panel.style.display = 'block';
элементы становятся доступными для Tab.
При динамическом добавлении элементов необходимо учитывать состояние
disabled.
const button = document.createElement('button');
button.disabled = true;
container.appendChild(button);
Такой элемент:
Проверка:
isTabbable(button); // false
После удаления атрибута:
button.disabled = false;
элемент начинает участвовать в Tab-навигации.
Современные компоненты часто используют Shadow DOM. Tabbable поддерживает такие структуры, если они доступны для обхода.
Пример компонента:
const host = document.querySelector('#component');
const shadow = host.attachShadow({ mode: 'open' });
shadow.innerHTML = `
<button>Shadow Button</button>
`;
При использовании:
tabbable(host)
результат зависит от конфигурации обхода Shadow DOM.
В сложных интерфейсах необходимо учитывать:
При активной динамике интерфейса часто применяется
MutationObserver. Он позволяет отслеживать изменения DOM и
выполнять повторный анализ tabbable-элементов.
Пример:
const observer = new MutationObserver(() => {
const elements = tabbable(document.body);
console.log('Tabbable elements:', elements.length);
});
observer.observe(document.body, {
childList: true,
subtree: true
});
Теперь при каждом добавлении элемента список обновляется автоматически.
Модальные окна часто создаются динамически и должны ограничивать
навигацию клавишей Tab.
Пример создания:
const modal = document.createElement('div');
modal.innerHTML = `
<button>Закрыть</button>
<input type="text">
<button>Сохранить</button>
`;
document.body.appendChild(modal);
Получение списка tabbable:
const elements = tabbable(modal);
Эти элементы формируют циклическую навигацию внутри модального окна, которая используется в библиотеках управления фокусом (например, focus-trap).
При удалении узлов из DOM они автоматически исчезают из списка tabbable-элементов.
Пример:
button.remove();
После повторного добавления:
container.appendChild(button);
элемент снова будет найден функцией tabbable.
Это происходит потому, что библиотека не кеширует результаты, а каждый раз анализирует DOM заново.
В сложных интерфейсах может существовать большое число фокусируемых
элементов. Повторные вызовы tabbable() на всём документе
могут создавать лишнюю нагрузку.
Практика оптимизации:
Пример ограниченного анализа:
const dialog = document.querySelector('#dialog');
const elements = tabbable(dialog);
Это значительно быстрее, чем анализ всего
document.body.
Во фреймворках (React, Vue, Angular) элементы добавляются через виртуальный DOM. Tabbable взаимодействует с реальным DOM, поэтому анализ выполняется после завершения рендера.
Типичный сценарий:
tabbable(container)В React это часто выполняется внутри useEffect.
Элементы могут появляться после сетевых запросов.
Пример:
fetch('/data')
.then(r => r.json())
.then(data => {
const button = document.createElement('button');
button.textContent = data.label;
container.appendChild(button);
});
После выполнения запроса новый элемент становится частью навигации.
Для обновления списка:
const elements = tabbable(container);
Если элемент добавляется в контейнер, который сам не находится в документе, он не будет учитываться в навигации.
Пример:
const wrapper = document.createElement('div');
const input = document.createElement('input');
wrapper.appendChild(input);
Пока контейнер не добавлен:
document.body.appendChild(wrapper);
элементы внутри него не участвуют в таб-навигации страницы.
Порядок Tab формируется из трёх факторов:
tabindexПри добавлении элементов важно учитывать, что изменение структуры DOM может полностью изменить порядок фокуса.
Tabbable воспроизводит именно тот порядок, который будет использовать браузер.
Динамическое добавление элементов в DOM не требует специальной интеграции с Tabbable. Достаточно повторного вызова функций библиотеки после изменения структуры документа.