Keyboard navigation

Floating UI предоставляет расширенные возможности для позиционирования всплывающих элементов, таких как тултипы, модальные окна, меню и панели. Одним из ключевых аспектов интерактивности является поддержка управления с клавиатуры, что критично для доступности (accessibility) и удобства использования.

Фокусировка и навигация

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

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

Обработка клавиш

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

  • Escape – стандартное закрытие всплывающих окон.
  • Tab и Shift+Tab – циклическая навигация по интерактивным элементам внутри всплывающего окна. Важно предотвращать уход фокуса за пределы компонента.
  • Arrow Keys – для меню и списков поддержка навигации стрелками. Например, при открытом контекстном меню стрелки вверх и вниз должны переключать активный элемент.

Пример обработки клавиш в контексте Floating UI:

function handleKeyDown(event, floatingElement) {
  switch(event.key) {
    case 'Escape':
      closeFloating(floatingElement);
      break;
    case 'ArrowDown':
      focusNextItem(floatingElement);
      break;
    case 'ArrowUp':
      focusPrevItem(floatingElement);
      break;
    case 'Tab':
      trapFocus(event, floatingElement);
      break;
  }
}

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

Floating UI интегрируется с концепцией focus trapping, когда пользователь не может случайно выйти из модального окна с помощью Tab. Реализация включает:

  • Определение первого и последнего фокусируемого элемента.
  • Перенаправление фокуса при попытке выйти за пределы.
  • Поддержка Shift+Tab для обратного обхода.
function trapFocus(event, floatingElement) {
  const focusableElements = floatingElement.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
  const firstElement = focusableElements[0];
  const lastElement = focusableElements[focusableElements.length - 1];

  if (event.shiftKey && document.activeElement === firstElement) {
    lastElement.focus();
    event.preventDefault();
  } else if (!event.shiftKey && document.activeElement === lastElement) {
    firstElement.focus();
    event.preventDefault();
  }
}

Интеграция с Floating UI Middleware

Для удобства навигации часто используют middleware Floating UI, которые управляют позиционированием и состоянием элементов. Некоторые полезные middleware:

  • offset – добавляет смещение всплывающего окна, что предотвращает перекрытие кнопок.
  • flip – автоматически меняет сторону отображения при нехватке места.
  • autoUpdate – следит за изменениями размера или положения элемента и корректирует позицию.

Эти middleware помогают поддерживать правильное отображение элементов при работе с клавиатурой, особенно при перемещении фокуса между элементами.

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

Для корректной работы с клавиатурой крайне важно использовать ARIA-атрибуты:

  • aria-haspopup – указывает, что элемент открывает всплывающее окно.
  • aria-expanded – отражает состояние (открыто/закрыто).
  • role="menu" и role="menuitem" – для контекстных меню.
  • aria-activedescendant – указывает на текущий выбранный элемент при навигации стрелками.

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

<button id="menuButton" aria-haspopup="true" aria-expanded="false">Меню</button>
<ul role="menu" id="menuList">
  <li role="menuitem" tabindex="-1">Элемент 1</li>
  <li role="menuitem" tabindex="-1">Элемент 2</li>
  <li role="menuitem" tabindex="-1">Элемент 3</li>
</ul>

Советы по реализации

  • Всегда отслеживать последний активный элемент для корректного возврата фокуса.
  • Обрабатывать события keydown на уровне документа или родительского контейнера для предотвращения конфликтов с другими обработчиками.
  • Тестировать навигацию с клавиатуры на всех интерактивных элементах: кнопки, ссылки, чекбоксы, поля ввода.
  • Сочетать middleware Floating UI с focus management для плавной и предсказуемой навигации.

Пример полного кейса

Комбинируя autoUpdate, offset и focus trapping, можно реализовать всплывающее меню, которое:

  1. Автоматически позиционируется под кнопкой.
  2. Переносит фокус на первый элемент при открытии.
  3. Закрывается по Escape и возвращает фокус на кнопку.
  4. Поддерживает циклическую навигацию через Tab и стрелки.
import { computePosition, offset, flip, autoUpdate } from '@floating-ui/dom';

const button = document.querySelector('#menuButton');
const menu = document.querySelector('#menuList');

button.addEventListener('click', () => {
  menu.style.display = 'block';
  menu.querySelector('[role="menuitem"]').focus();
  
  const cleanup = autoUpdate(button, menu, () => {
    computePosition(button, menu, { middleware: [offset(4), flip()] })
      .then(({ x, y }) => {
        Object.assign(menu.style, { left: `${x}px`, top: `${y}px` });
      });
  });

  function closeMenu() {
    menu.style.display = 'none';
    button.focus();
    cleanup();
  }

  menu.addEventListener('keydown', (e) => handleKeyDown(e, menu, closeMenu));
});

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