Возврат фокуса при закрытии

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

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

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

import { tabbable } from 'tabbable';

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

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


Возврат фокуса после закрытия модального окна

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

Сохранение текущего фокуса

Перед открытием модального окна необходимо сохранить текущий активный элемент:

const previouslyFocusedElement = document.activeElement;

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


Ограничение фокуса внутри модального окна

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

const modal = document.getElementById('modal');
const focusableElements = tabbable(modal);

let firstElement = focusableElements[0];
let lastElement = focusableElements[focusableElements.length - 1];

modal.addEventListener('keydown', (event) => {
  if (event.key === 'Tab') {
    if (event.shiftKey) { // Shift + Tab
      if (document.activeElement === firstElement) {
        event.preventDefault();
        lastElement.focus();
      }
    } else { // Tab
      if (document.activeElement === lastElement) {
        event.preventDefault();
        firstElement.focus();
      }
    }
  }
});

Такой подход предотвращает уход фокуса за пределы модального окна, создавая замкнутый фокусный контур.


Восстановление фокуса после закрытия

После закрытия модального окна фокус возвращается на сохранённый элемент:

function closeModal() {
  modal.style.display = 'none';
  if (previouslyFocusedElement) {
    previouslyFocusedElement.focus();
  }
}

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


Управление динамически изменяемым контентом

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

function updateFocusableElements() {
  focusableElements = tabbable(modal);
  firstElement = focusableElements[0];
  lastElement = focusableElements[focusableElements.length - 1];
}

Этот метод обеспечивает корректное поведение Tab даже при изменении структуры DOM внутри модального окна.


Исключения и тонкости использования

  • Скрытые элементы (display: none или visibility: hidden) автоматически исключаются из списка Tabbable.
  • Элементы с tabindex="-1" также игнорируются библиотекой.
  • Для кастомных интерактивных компонентов (например, кнопок, ссылок или input, созданных через canvas или SVG) нужно вручную контролировать атрибуты tabindex, чтобы они корректно попадали в список фокусируемых элементов.

Интеграция с современными фреймворками

При использовании Tabbable в React, Vue или Svelte рекомендуется:

  • Хранить ссылку на активный элемент через ref или reactive state.
  • Пересчитывать фокусируемые элементы после монтирования или обновления компонента.
  • Использовать useEffect (React) или onMount (Svelte) для добавления обработчиков клавиатуры и восстановления фокуса.

Пример для React:

import { useEffect, useRef } from 'react';
import { tabbable } from 'tabbable';

export function Modal({ isOpen, onClose }) {
  const modalRef = useRef(null);
  const previouslyFocused = useRef(null);

  useEffect(() => {
    if (isOpen) {
      previouslyFocused.current = document.activeElement;
      const elements = tabbable(modalRef.current);
      elements[0]?.focus();
    } else {
      previouslyFocused.current?.focus();
    }
  }, [isOpen]);

  return (
    <div ref={modalRef} style={{ display: isOpen ? 'block' : 'none' }}>
      {/* контент модального окна */}
    </div>
  );
}

Этот подход гарантирует плавное взаимодействие с клавиатурой и возвращение фокуса на исходный элемент после закрытия модального окна.


Рекомендации по доступности

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

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