В библиотеке Floating UI управление фокусом реализуется через концепцию portal-aware positioning и контролируемых взаимодействий с элементами. Основная цель — корректное распределение фокуса между триггером, плавающим элементом и внутренними интерактивными компонентами, обеспечивая доступность и предсказуемое поведение.
Фокус можно условно разделить на несколько типов:
Floating UI не накладывает строгих правил, но предоставляет утилиты и 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,
библиотека автоматически вернет фокус на элемент, с которого было
инициировано открытие.
focusTrap, чтобы табуляция не
выводила фокус за пределы tooltip.Floating UI предоставляет лишь позиционирование, но интеграция с клавиатурными событиями критична для управления фокусом:
Пример обработки стрелок в выпадающем меню:
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();
}
});
Floating UI рекомендует применять следующие практики:
role="menu",
role="menuitem", aria-haspopup="true".aria-expanded
для триггера, aria-hidden для floating-элемента при
скрытии.Эти элементы обеспечивают правильное поведение для пользователей с клавиатурой и вспомогательными технологиями.
В 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-элементами, полностью соответствующее требованиям современных интерфейсов.