Фокус и tab order

Floating UI — это современная библиотека для позиционирования всплывающих элементов в веб-приложениях. Помимо точного позиционирования, важным аспектом является управление фокусом и порядком табуляции для обеспечения доступности и корректного взаимодействия с пользователем.


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

Когда всплывающий элемент появляется на странице, необходимо определить, куда будет установлен фокус. Floating UI не управляет фокусом автоматически, но предоставляет инструменты для интеграции с библиотеками вроде React, Vue или нативными DOM-методами.

  • Первичный фокус Обычно фокус устанавливается на первый интерактивный элемент внутри floating-элемента (кнопка, ссылка, input). Это позволяет пользователю сразу взаимодействовать с элементом через клавиатуру.

    const floatingRef = document.querySelector('#tooltip');
    const firstFocusable = floatingRef.querySelector('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
    firstFocusable?.focus();
  • Фокус после закрытия После закрытия всплывающего элемента рекомендуется возвращать фокус на элемент-триггер, который вызвал открытие. Это предотвращает потерю контекста для пользователя.

    const trigger = document.querySelector('#open-tooltip');
    trigger.focus();
  • Фокус-контейнер Для сложных floating-элементов (например, меню или модальные окна) удобно использовать focus trap — ограничение фокуса внутри контейнера. Floating UI интегрируется с библиотеками вроде focus-trap или можно реализовать самостоятельно:

    floatingRef.addEventListener('keydown', (e) => {
      if (e.key === 'Tab') {
        const focusableElements = Array.from(floatingRef.querySelectorAll(
          'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
        ));
        const first = focusableElements[0];
        const last = focusableElements[focusableElements.length - 1];
    
        if (!e.shiftKey && document.activeElement === last) {
          e.preventDefault();
          first.focus();
        }
    
        if (e.shiftKey && document.activeElement === first) {
          e.preventDefault();
          last.focus();
        }
      }
    });

Порядок табуляции (Tab Order)

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

  • Встраивание в документ Floating UI позиционирует элементы в DOM рядом с корнем (document.body) для предотвращения переполнения контейнеров. Чтобы сохранить логический порядок табуляции, нужно явно управлять tabindex:

    <div id="menu" tabindex="-1">
      <button>Пункт 1</button>
      <button>Пункт 2</button>
    </div>

    Здесь tabindex="-1" предотвращает случайное попадание фокуса на контейнер, сохраняя фокус на интерактивных элементах.

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

  • Управление клавишами Для сложных компонентов, таких как дропдауны или контекстные меню, полезно обрабатывать клавиши стрелок, Enter и Escape, чтобы пользователь мог навигировать без мыши. Floating UI предоставляет middleware для определения размеров, позиции и видимости, что помогает корректно позиционировать элементы при перемещении фокуса.


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

В React можно комбинировать Floating UI с хуками для управления фокусом:

import { useFloating } from '@floating-ui/react-dom';

function Dropdown() {
  const { x, y, reference, floating, strategy } = useFloating();
  const [isOpen, setIsOpen] = React.useState(false);

  React.useEffect(() => {
    if (isOpen) {
      const firstFocusable = floating.current?.querySelector('button, [tabindex]:not([tabindex="-1"])');
      firstFocusable?.focus();
    }
  }, [isOpen]);

  return (
    <>
      <button ref={reference} onCl ick={() => setIsOpen(prev => !prev)}>Меню</button>
      {isOpen && (
        <div ref={floating} style={{ position: strategy, top: y ?? 0, left: x ?? 0 }}>
          <button>Пункт 1</button>
          <button>Пункт 2</button>
        </div>
      )}
    </>
  );
}
  • Фокус устанавливается автоматически на первый интерактивный элемент при открытии.
  • После закрытия можно возвращать фокус на кнопку-триггер.

Доступность (Accessibility)

  • ARIA-атрибуты Всплывающие элементы должны иметь role и aria-* атрибуты для корректной работы с экранными считывателями. Например:

    <div role="menu" aria-labelledby="menu-button">
      <button role="menuitem">Пункт 1</button>
      <button role="menuitem">Пункт 2</button>
    </div>
  • Esc для закрытия Обработка клавиши Escape позволяет пользователю быстро закрыть floating-элемент и вернуть фокус на триггер.

  • Tab trap Обеспечивает фокус только внутри всплывающего элемента, предотвращая переход на элементы страницы, которые временно недоступны.


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

  • Всегда устанавливать фокус на первый интерактивный элемент при открытии floating-элемента.
  • Возвращать фокус на триггер после закрытия.
  • Использовать tabindex и focus trap для корректной последовательности фокуса.
  • Добавлять ARIA-атрибуты и обработку клавиш для повышения доступности.
  • Интегрировать Floating UI с существующими фреймворками через рефы и хуки, чтобы сохранять полный контроль над фокусом и табуляцией.

Эти подходы позволяют создавать полностью интерактивные, доступные и предсказуемые floating-элементы в любых веб-приложениях.