Навигация стрелками

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

Основная функция библиотеки возвращает массив элементов, доступных для табуляции. Эти элементы включают стандартные интерактивные теги (<a>, <button>, <input>, <textarea>, <select>) и элементы с установленным атрибутом tabindex, где tabindex >= 0.

import tabbable from 'tabbable';

const elements = tabbable(document.body);
console.log(elements); // массив элементов доступных для табуляции

Важно: tabindex="-1" делает элемент фокусируемым программно, но исключает из последовательности Tab.


Навигация стрелками внутри фокусируемых областей

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

Получение фокусируемых элементов

const container = document.querySelector('.menu');
const focusableItems = tabbable(container);

focusableItems будет упорядочен в порядке естественной табуляции по DOM. Для навигации стрелками необходимо хранить индекс текущего элемента:

let currentIndex = 0;
focusableItems[currentIndex].focus();

Реакция на события клавиатуры

Навигация стрелками реализуется через обработчик события keydown. Для примера списка вертикальной навигации:

container.addEventListener('keydown', (event) => {
  const key = event.key;

  if (key === 'ArrowDown') {
    currentIndex = (currentIndex + 1) % focusableItems.length;
    focusableItems[currentIndex].focus();
    event.preventDefault();
  }

  if (key === 'ArrowUp') {
    currentIndex = (currentIndex - 1 + focusableItems.length) % focusableItems.length;
    focusableItems[currentIndex].focus();
    event.preventDefault();
  }
});

Особенности:

  • Используется модульная арифметика для циклической навигации.
  • event.preventDefault() предотвращает стандартное поведение браузера (например, прокрутку страницы).

Горизонтальная и комбинированная навигация

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

if (key === 'ArrowRight') {
  currentIndex = (currentIndex + 1) % focusableItems.length;
  focusableItems[currentIndex].focus();
}

if (key === 'ArrowLeft') {
  currentIndex = (currentIndex - 1 + focusableItems.length) % focusableItems.length;
  focusableItems[currentIndex].focus();
}

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


Управление фокусом при динамическом контенте

Если DOM изменяется (добавляются или удаляются элементы), массив элементов Tabbable становится устаревшим. Необходимо обновлять его при каждом изменении:

function updateFocusable() {
  focusableItems = tabbable(container);
  if (currentIndex >= focusableItems.length) {
    currentIndex = focusableItems.length - 1;
  }
}

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


Игнорирование элементов и условная табуляция

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

const visibleFocusable = tabbable(container, { includeOnlyTabbable: true });

Параметр includeOnlyTabbable гарантирует исключение элементов с tabindex=-1 и скрытых элементов (display: none или visibility: hidden).


Совместное использование Tab и стрелок

Стандартная навигация клавишей Tab может сочетаться с управлением стрелками. Рекомендуется:

  • Хранить индекс текущего элемента в массиве Tabbable.
  • Обновлять индекс при фокусе через Tab (focusin событие).
  • Обновлять массив элементов при изменении DOM.

Пример отслеживания текущего элемента при Tab:

container.addEventListener('focusin', (event) => {
  currentIndex = focusableItems.indexOf(event.target);
});

Это позволяет стрелкам работать корректно независимо от того, был ли фокус установлен через Tab или программно.


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

  • Использовать Tabbable только для контейнеров с интерактивными элементами. Не включать весь документ без необходимости.
  • При сложной навигации по сеткам лучше заранее строить структуру элементов, чтобы индексация соответствовала визуальному расположению.
  • Обновлять массив элементов после любых динамических изменений интерфейса.
  • Для доступности избегать изменения стандартного поведения Tab без веских причин, ограничивая управление стрелками рамками конкретного компонента.