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

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

Фокус можно условно разделить на несколько типов:

  • Фокус на триггере – элемент, который открывает плавающий компонент.
  • Фокус внутри floating-элемента – интерактивные элементы внутри всплывающего окна.
  • Фокус обратный к триггеру – возвращение фокуса после закрытия floating-элемента.

Floating UI не накладывает строгих правил, но предоставляет утилиты и middleware, которые помогают управлять этими сценариями.


Middleware для контроля фокуса

autoUpdate

Функция autoUpdate автоматически пересчитывает позицию floating-элемента при изменении размеров окна или содержимого. При этом важно, что она не управляет фокусом напрямую, но обеспечивает, чтобы элемент оставался видимым при изменении контекста.

import { autoUpdate, computePosition } from '@floating-ui/dom';

const cleanup = autoUpdate(referenceElement, floatingElement, () => {
  computePosition(referenceElement, floatingElement, {
    placement: 'bottom-start',
  }).then(({ x, y }) => {
    Object.assign(floatingElement.style, {
      left: `${x}px`,
      top: `${y}px`,
    });
  });
});

focus и focusTrap

Для управления фокусом внутри floating-элемента используются custom hooks или утилиты сторонних библиотек (focus-trap, tabbable), интегрируемые с Floating UI. Идея заключается в том, чтобы перехватывать табуляцию и оставлять пользователя внутри всплывающего окна до закрытия.

Пример простого focusTrap:

import { createFocusTrap } from 'focus-trap';

const trap = createFocusTrap(floatingElement, {
  onActivate: () => floatingElement.style.display = 'block',
  onDeactivate: () => floatingElement.style.display = 'none',
  initialFocus: floatingElement.querySelector('button'),
  escapeDeactivates: true,
});

triggerElement.addEventListener('click', () => trap.activate());
  • initialFocus – задает элемент, который получает фокус при открытии.
  • escapeDeactivates – позволяет закрывать floating-элемент клавишей Escape.

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

После закрытия floating-элемента часто требуется вернуть фокус на исходный триггер. Это особенно важно для доступности.

trap.deactivate({
  returnFocus: true,
});

Если returnFocus установлен в true, библиотека автоматически вернет фокус на элемент, с которого было инициировано открытие.


Сценарии использования

Выпадающие меню

  • Триггер: кнопка открытия меню.
  • Floating: меню с элементами, доступными для клавиатурной навигации.
  • Поведение: при открытии фокус перемещается на первый элемент меню; Escape возвращает фокус на кнопку.

Подсказки (Tooltip с интерактивным контентом)

  • Если подсказка содержит интерактивные элементы (например, форму), необходимо использовать focusTrap, чтобы табуляция не выводила фокус за пределы tooltip.
  • Для подсказок с чисто информационным содержимым фокус часто остается на триггере.

Модальные окна

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

Работа с клавиатурой

Floating UI предоставляет лишь позиционирование, но интеграция с клавиатурными событиями критична для управления фокусом:

  • Tab / Shift+Tab – цикл внутри floating-элемента при активном focus trap.
  • Escape – закрытие floating-элемента с возвратом фокуса.
  • Arrow keys – навигация по интерактивным элементам, например, в меню или списках.

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

floatingElement.addEventListener('keydown', (event) => {
  const items = Array.from(floatingElement.querySelectorAll('button'));
  const index = items.indexOf(document.activeElement);

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

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

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

Floating UI рекомендует применять следующие практики:

  • ARIA роли: role="menu", role="menuitem", aria-haspopup="true".
  • ARIA-атрибуты состояния: aria-expanded для триггера, aria-hidden для floating-элемента при скрытии.
  • Фокус-менеджмент: использование focus trap, корректное возвращение фокуса на триггер, обработка клавиш навигации.

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


Совмещение с React

В React удобно интегрировать Floating UI через useFloating, а фокус-менеджмент — через useEffect и ref.

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

const { x, y, reference, floating } = useFloating({
  placement: 'bottom-start',
});

useEffect(() => {
  if(open) {
    floating.current?.focus();
  }
}, [open]);
  • reference – ref для триггера.
  • floating – ref для floating-элемента.
  • Управление фокусом через useEffect обеспечивает корректное выставление начального фокуса при открытии.

Итоговые рекомендации по фокусу

  • Всегда задавать начальный фокус для интерактивного floating-элемента.
  • Возвращать фокус на триггер после закрытия.
  • Использовать focus trap для всплывающих окон с интерактивным контентом.
  • Обрабатывать клавиши навигации (Tab, Shift+Tab, Escape, стрелки).
  • Поддерживать ARIA-атрибуты для доступности.

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