Обновление списка tabbable

Список tabbable-элементов представляет собой набор DOM-узлов, которые могут получить фокус при перемещении клавишей Tab. Библиотека Tabbable вычисляет такие элементы динамически, анализируя структуру DOM, атрибуты элементов, состояние видимости и доступности. В реальных интерфейсах структура документа часто изменяется: появляются модальные окна, раскрываются панели, добавляются новые кнопки, поля ввода или ссылки. В таких условиях возникает необходимость повторно вычислять список tabbable-элементов.

Обновление списка tabbable — это процесс повторного определения элементов, доступных для фокусировки клавишей Tab, после изменений в DOM или изменениях состояния элементов.


Причины обновления списка tabbable

В интерфейсах с динамическим содержимым список фокусируемых элементов не является постоянным. Несколько распространённых сценариев требуют его пересчёта.

Динамическое добавление элементов

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, но влияют на возможность фокусировки:

  • изменение tabindex
  • добавление disabled
  • изменение display или visibility
  • добавление hidden
  • изменение aria-hidden

Пример:

button.setAttribute('tabindex', '-1');

После такого изменения кнопка перестаёт быть tabbable, однако сохранённый список этого не отражает.


Появление и скрытие интерфейсных компонентов

Типичные элементы интерфейса:

  • модальные окна
  • выпадающие меню
  • аккордеоны
  • всплывающие панели
  • навигационные панели

Когда компонент становится видимым, он добавляет новые 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();

Использование вместе с focus-менеджерами

Многие интерфейсы используют логику управления фокусом: циклическую навигацию, фокус-ловушки или управление фокусом внутри диалогов.

В таких системах список tabbable элементов используется для:

  • определения первого элемента
  • определения последнего элемента
  • реализации циклического перехода

Пример:

const elements = tabbable(container);

const first = elements[0];
const last = elements[elements.length - 1];

Если элементы изменились, границы навигации становятся неверными. Поэтому перед обработкой навигации список должен обновляться.


Автоматическое обновление через MutationObserver

Для интерфейсов с частыми изменениями DOM возможно автоматическое обновление списка с использованием MutationObserver.

Наблюдение за DOM

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

Библиотеки управления фокусом (например, 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 автоматически исключает такие элементы, но сохранённый список может содержать их до следующего обновления.


Практическая схема обновления

Один из распространённых шаблонов:

  1. хранение текущего списка tabbable-элементов
  2. пересчёт после изменений интерфейса
  3. использование обновлённого списка для навигации

Пример реализации:

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-элементов как часть архитектуры управления фокусом.