Виртуализация и ленивая загрузка

Tabbable — это утилита для JavaScript, позволяющая точно определять, какие элементы на странице могут получать фокус при навигации с помощью клавиши Tab. Она используется для управления доступностью интерфейсов, особенно в сложных веб-приложениях, где элементы динамически появляются, исчезают или находятся внутри модальных окон и вложенных контейнеров.

Главная задача библиотеки — определить табулируемые элементы (tabbable elements) и элементы, которые фокусируемы, но не включаются в порядок табуляции (focusable but not tabbable).

Пример базового использования:

import { tabbable } from 'tabbable';

const container = document.querySelector('#form');
const focusableElements = tabbable(container);
console.log(focusableElements);

В результате возвращается массив элементов, которые будут фокусироваться при навигации с клавишей Tab в указанном контейнере.


Разделение tabbable и focusable элементов

Focusable элементы — это все элементы, на которых можно установить фокус программно или с помощью мыши (.focus()), включая скрытые элементы с tabindex="-1".

Tabbable элементы — это subset focusable элементов, которые участвуют в стандартной табуляции (tabindex="0" или естественно фокусируемые элементы: <input>, <button>, <a href>).

import { focusable } from 'tabbable';

const allFocusable = focusable(document.body);
console.log(allFocusable); // Включает элементы с tabindex=-1

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


Настройка табуляции

Tabbable позволяет учитывать пользовательские атрибуты и кастомные правила:

  • tabindex="-1" — исключает элемент из табуляции, но оставляет фокусируемым.
  • tabindex="0" — включает элемент в стандартный порядок.
  • tabindex="1..n" — задаёт приоритетный порядок (не рекомендуется использовать слишком часто, нарушает естественную навигацию).
const options = { 
  includeContainer: true,  // включить сам контейнер в проверку
  displayCheck: 'full'      // учитывать только видимые элементы
};

const elements = tabbable(document.body, options);

displayCheck может принимать значения:

  • 'full' — проверяет все CSS свойства (display, visibility, opacity) для определения видимости.
  • 'legacy-full' — старая версия проверки, учитывает также элементы с clip-path и overflow.
  • 'none' — игнорирует визуальные свойства, возвращает все потенциально фокусируемые элементы.

Виртуализация и ленивое вычисление tabbable элементов

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

Решение — виртуализация и ленивый подсчёт:

  1. Виртуализация DOM: рендерить в DOM только видимые элементы (например, в списках с бесконечной прокруткой).
  2. Ленивая табуляция: вычислять tabbable элементы только для видимого сегмента DOM, не обходя скрытые узлы.

Пример реализации ленивой табуляции:

function getVisibleTabbable(container) {
  const visibleNodes = Array.from(container.children).filter(el => el.offsetParent !== null);
  return visibleNodes.flatMap(el => tabbable(el));
}

const visibleTabbable = getVisibleTabbable(document.querySelector('#virtual-list'));

Преимущество: уменьшение вычислительной нагрузки и предотвращение “залипания” интерфейса при работе с длинными списками.


Работа с динамически меняющимся DOM

Если элементы добавляются или удаляются, массив tabbable элементов устаревает. Для корректной работы необходимо:

  • Использовать MutationObserver для отслеживания изменений в контейнере.
  • Пересчитывать tabbable элементы только при необходимости.
const container = document.querySelector('#dynamic-form');

const observer = new MutationObserver(() => {
  const updatedTabbable = tabbable(container);
  console.log('Обновлённый список:', updatedTabbable);
});

observer.observe(container, { childList: true, subtree: true });

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


Интеграция с управлением фокусом

Библиотека Tabbable часто используется вместе с утилитами для управления фокусом:

  • Фокусировка первого или последнего элемента в модальном окне:
const modal = document.querySelector('#modal');
const elements = tabbable(modal);

elements[0].focus();  // первый tabbable элемент
elements[elements.length - 1].focus(); // последний
  • Циклическая табуляция (Focus Trap):
modal.addEventListener('keydown', (e) => {
  if (e.key === 'Tab') {
    const elements = tabbable(modal);
    const first = elements[0];
    const last = elements[elements.length - 1];

    if (e.shiftKey && document.activeElement === first) {
      e.preventDefault();
      last.focus();
    } else if (!e.shiftKey && document.activeElement === last) {
      e.preventDefault();
      first.focus();
    }
  }
});

Такой подход гарантирует, что пользователь не выйдет за пределы модального окна при навигации клавишей Tab.


Продвинутые опции и кастомизация

  • includeContainer — включает сам контейнер в проверку.
  • tabbableOptions — передача параметров для кастомной логики проверки.
  • displayCheck — контроль видимости элементов.
  • getShadowRoot — поддержка web components и shadow DOM.

Пример с Shadow DOM:

const shadowHost = document.querySelector('#shadow-host');
const shadowRoot = shadowHost.shadowRoot;

const elements = tabbable(shadowRoot, { getShadowRoot: true });

Рекомендации по производительности

  1. Использовать ленивый подсчёт для больших списков.
  2. Ограничивать глубину поиска (subtree) для контейнеров с большим количеством элементов.
  3. Совмещать с виртуализированными списками (например, с библиотекой react-virtualized) для минимизации DOM операций.
  4. Кэшировать результаты tabbable для статичных контейнеров и обновлять только при изменениях DOM.

Итоговые свойства Tabbable

  • Определяет порядок табуляции с учётом tabindex и видимости.
  • Позволяет управлять фокусом в модальных окнах и динамических интерфейсах.
  • Поддерживает ленивую загрузку и виртуализированные элементы.
  • Совместим с Shadow DOM и кастомными компонентами.
  • Уменьшает нагрузку на браузер при больших DOM-структурах.

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