Циклическая навигация

Библиотека Tabbable предназначена для управления фокусом элементов на веб-странице. Основная её задача — определять, какие элементы могут быть фокусируемыми с помощью клавиши Tab или Shift + Tab, а также предоставлять информацию о порядке навигации. Tabbable работает с любыми элементами DOM, включая стандартные интерактивные элементы (input, button, select, textarea, a[href]) и кастомные компоненты, имеющие атрибут tabindex.

Определение фокусируемых элементов

Метод tabbable(container, options) возвращает массив элементов внутри контейнера, которые могут быть доступны для фокуса. Пример базового использования:

import { tabbable } from 'tabbable';

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

В массиве будут только элементы, доступные для навигации через Tab, исключая скрытые или отключенные элементы (display: none, visibility: hidden, disabled).

Опции функции tabbable позволяют уточнять поведение:

  • includeContainer — если true, сам контейнер проверяется на фокусируемость.
  • getShadowRoot — функция для работы с Web Components и теневыми DOM.
  • tabindex — учитывает ли элементы с отрицательным tabindex.

Порядок фокусировки

Tabbable автоматически учитывает натуральный порядок DOM и атрибут tabindex. Элементы с положительным tabindex получают приоритет перед стандартным порядком, что позволяет задавать кастомный путь навигации. Элементы с tabindex="0" включаются в стандартный поток, а элементы с отрицательным tabindex исключаются из навигации по Tab.

Пример сортировки:

const elements = tabbable(container);
// Элементы с tabindex > 0 идут первыми
elements.forEach(el => console.log(el, el.tabIndex));

Циклическая навигация

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

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

const modal = document.querySelector('#modal');
const focusable = tabbable(modal);

modal.addEventListener('keydown', (event) => {
  if (event.key === 'Tab') {
    const first = focusable[0];
    const last = focusable[focusable.length - 1];

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

В этом примере:

  • Если нажата Shift + Tab на первом элементе, фокус переходит на последний.
  • Если нажата обычная Tab на последнем элементе, фокус возвращается на первый.
  • event.preventDefault() предотвращает уход фокуса за пределы контейнера.

Работа с динамическим содержимым

Если внутри контейнера появляются новые интерактивные элементы, массив focusable необходимо обновлять, чтобы цикл оставался корректным:

function updateFocusable() {
  focusable = tabbable(modal);
}

const observer = new MutationObserver(updateFocusable);
observer.observe(modal, { childList: true, subtree: true });

Таким образом, любая динамическая вставка элементов автоматически учитывается в цикле.

Управление атрибутами tabindex для циклов

Для тонкой настройки можно изменять tabindex элементов вручную, чтобы исключать часть из них из цикла или менять порядок:

firstElement.tabIndex = 0;
lastElement.tabIndex = 0;
hiddenButton.tabIndex = -1; // исключаем из цикла

Важно, что Tabbable учитывает эти значения при формировании массива элементов для навигации.

Интеграция с компонентами и библиотеками UI

Tabbable хорошо работает с:

  • Модальными окнами — сохраняет фокус внутри окна.
  • Dropdown-меню — предотвращает уход фокуса за пределы меню.
  • Теневым DOM — опция getShadowRoot позволяет обрабатывать кастомные элементы внутри Web Components.

Пример с Web Component:

const focusable = tabbable(customElement, {
  getShadowRoot: (node) => node.shadowRoot
});

Практические рекомендации

  1. Всегда обновлять массив фокусируемых элементов при динамических изменениях DOM.
  2. Использовать event.preventDefault() при реализации циклической навигации.
  3. Проверять tabindex у всех интерактивных элементов.
  4. Для сложных компонентов предусматривать обход теневых DOM через getShadowRoot.
  5. Минимизировать элементы с положительным tabindex, чтобы не нарушать естественный порядок Tab.

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