Вложенные подменю

Библиотека Tabbable предназначена для определения элементов на странице, которые могут получать фокус с помощью клавиши Tab. Она анализирует DOM-структуру и возвращает массив элементов, доступных для табуляции, учитывая их видимость, атрибуты tabindex и состояния, такие как disabled или hidden. Основной метод библиотеки — tabbable(container), где container — корневой элемент, внутри которого производится поиск фокусируемых элементов.

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

import { tabbable } from 'tabbable';

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

В результате возвращается массив DOM-элементов в порядке, соответствующем естественной последовательности табуляции.


Фокусировка во вложенных подменю

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

Принципы работы с вложенными подменю:

  1. Изоляция табуляции При открытии подменю необходимо ограничивать табуляцию элементами только внутри него. Это позволяет создать эффект “фокусного контейнера”, предотвращая случайное переключение на элементы родительского меню или страницы.

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

    function updateSubmenuTabbable(submenu) {
      return tabbable(submenu);
    }

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

  3. Учёт видимости Элементы, скрытые с помощью display: none, visibility: hidden или aria-hidden="true", автоматически исключаются из списка tabbable, что предотвращает попадание фокуса на невидимые объекты.


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

Элементы меню могут иметь атрибут tabindex, который управляет порядком табуляции:

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

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


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

Открытие подменю: первый фокусируемый элемент подменю автоматически получает фокус, чтобы пользователь мог сразу перемещаться по нему с клавиатуры:

const submenuElements = tabbable(submenu);
if (submenuElements.length > 0) {
  submenuElements[0].focus();
}

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

triggerElement.focus();

Обработка сложных сценариев

  1. Вложенные уровни В меню с несколькими уровнями подменю каждый уровень управляет собственной областью tabbable. Для корректной работы часто используют рекурсивный подсчёт:
function getAllTabbable(menu) {
  let elements = tabbable(menu);
  const submenus = menu.querySelectorAll('.submenu');
  submenus.forEach(sub => {
    elements = elements.concat(getAllTabbable(sub));
  });
  return elements;
}
  1. Управление клавишами навигации Помимо табуляции, рекомендуется поддерживать навигацию стрелками. Tabbable предоставляет только определение фокусируемых элементов, поэтому обработка ArrowUp, ArrowDown, ArrowLeft, ArrowRight реализуется вручную, перемещая фокус между элементами массива.

  2. Динамическое обновление состояния При изменении состояния подменю (например, открытие/закрытие, изменение доступности элементов) необходимо повторно вычислять tabbable-массив. Это предотвращает ошибки, когда фокус остаётся на удалённом или скрытом элементе.


Практические советы

  • Всегда ограничивать область tabbable контейнером подменю при его активации.
  • Использовать tabindex="0" вместо положительных значений для предсказуемой навигации.
  • Обновлять массив tabbable при любом динамическом изменении DOM внутри подменю.
  • Обрабатывать возврат фокуса на вызывающий элемент при закрытии подменю, чтобы не нарушать естественный поток табуляции.
  • Для многоуровневых меню применять рекурсивное вычисление tabbable, чтобы контролировать каждый уровень отдельно.

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