Управление tabindex в больших списках

Работа с большими списками интерактивных элементов в веб-интерфейсе требует аккуратного управления фокусом. Стандартный HTML-подход с tabindex быстро становится непрактичным при динамическом контенте, где количество элементов может меняться. Библиотека ally.js предоставляет набор инструментов для эффективного контроля фокусируемых элементов, позволяя управлять их доступностью и навигацией с клавиатуры.


Основные концепции

Фокусируемые элементы — это те элементы DOM, на которые можно перемещаться с помощью клавиши Tab или программно через JavaScript. В HTML к ним относятся a[href], button, input, select, textarea, а также элементы с явным атрибутом tabindex.

tabindex определяет порядок и возможность фокусировки:

  • tabindex="0" — элемент включён в естественный порядок табуляции.
  • tabindex="-1" — элемент исключён из табуляции, но доступен для программного фокуса.
  • tabindex="n" (положительное число) — устанавливает явный порядок, что не рекомендуется для больших динамических списков, так как требует постоянного обновления индексов.

Выбор фокусируемых элементов с ally.js

Для работы с элементами ally.js предоставляет функцию:

import {queryFocusable} from 'ally.js';

const focusableElements = queryFocusable({
  context: document.getElementById('list-container')
});

Параметры функции queryFocusable:

  • context — контейнер, в пределах которого ищутся фокусируемые элементы.
  • includeOnlyTabbable — если true, возвращает только элементы с положительным tabindex или без него.
  • filter — пользовательская функция для исключения элементов по критериям (например, скрытых элементов или disabled).

Результат — массив DOM-элементов, готовых к управлению фокусом.


Динамическое управление tabindex

В больших списках особенно важно управлять tabindex динамически, чтобы:

  1. Ограничить количество одновременно доступных элементов для клавиатурной навигации.
  2. Сохранять правильный порядок при добавлении или удалении элементов.
  3. Избежать проблем с производительностью при сотнях элементов.

Пример динамического управления:

import {focus} from 'ally.js';

function setActiveItem(index, container) {
  const items = queryFocusable({context: container});
  
  items.forEach((el, i) => {
    el.tabIndex = i === index ? 0 : -1;
  });

  focus(items[index]);
}

Пояснения:

  • Все элементы, кроме текущего активного, получают tabindex="-1".
  • Активный элемент получает tabindex="0" и программно фокусируется через focus().
  • Это позволяет ограничить фокус только одним элементом в списке, что особенно полезно для виртуализированных списков с динамической подгрузкой.

Поддержка клавиатурной навигации

Ally.js упрощает управление событиями клавиатуры и перемещением фокуса:

container.addEventListener('keydown', (event) => {
  const items = queryFocusable({context: container});
  const currentIndex = items.findIndex(el => el === document.activeElement);

  if (event.key === 'ArrowDown') {
    setActiveItem((currentIndex + 1) % items.length, container);
    event.preventDefault();
  } else if (event.key === 'ArrowUp') {
    setActiveItem((currentIndex - 1 + items.length) % items.length, container);
    event.preventDefault();
  }
});

Эта схема обеспечивает:

  • Циклическую навигацию по списку.
  • Обновление tabindex только при необходимости.
  • Минимальное количество DOM-операций для повышения производительности.

Управление скрытыми и неактивными элементами

Ally.js поддерживает фильтрацию невидимых и disabled элементов. При больших списках это предотвращает попадание в tabindex элементов, которые пользователю не доступны:

const visibleItems = queryFocusable({
  context: container,
  filter: (el) => el.offsetParent !== null && !el.disabled
});

Преимущества:

  • Исключение скрытых элементов из фокусируемых.
  • Поддержка динамического скрытия/показа элементов.
  • Снижение количества элементов, участвующих в табуляции, что облегчает управление.

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

Для больших списков, где элементы создаются по мере прокрутки (виртуализация), ally.js позволяет:

  • Обновлять список фокусируемых элементов после каждой отрисовки.
  • Сохранять текущий индекс фокуса и корректно переносить его на новый элемент.
  • Исключать невидимые элементы из массива queryFocusable без дополнительных проверок.

Пример обновления после рендера:

function updateFocusAfterRender(container, currentIndex) {
  const items = queryFocusable({context: container});
  const newIndex = Math.min(currentIndex, items.length - 1);
  setActiveItem(newIndex, container);
}

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

При сотнях элементов важно:

  • Использовать queryFocusable с узким context.
  • Фильтровать только необходимые элементы.
  • Избегать массового изменения tabindex без надобности — обновлять только активный и предыдущий элемент.
  • Интегрировать ally.js с виртуализацией списка для минимизации DOM-операций.

Работа с группами элементов

Для списков с группами или разделами (например, меню с подменю) полезно управлять tabindex на уровне контейнеров:

  • Фокус разрешён только внутри активной группы.
  • Неактивные группы получают aria-hidden="true" и tabindex="-1".
  • Ally.js позволяет легко выбрать все фокусируемые элементы в определённой секции и управлять их доступностью.
const activeSectionItems = queryFocusable({context: activeSection});
activeSectionItems.forEach(el => el.tabIndex = 0);

Такой подход предотвращает случайное попадание фокуса на скрытые или свернутые элементы.


Использование ally.js в больших списках позволяет выстраивать надежную, производительную и доступную навигацию, управляя tabindex динамически и минимизируя ручное вмешательство в порядок элементов.