Focus management

Popper.js изначально разрабатывался как библиотека для динамического позиционирования всплывающих элементов, таких как тултипы, всплывающие окна и выпадающие меню. Однако управление фокусом является критическим аспектом при работе с интерактивными элементами, особенно для доступности (accessibility) и правильного взаимодействия с пользователем. Важно понимать, как Popper.js взаимодействует с фокусируемыми элементами и какие методы предоставляет для контроля поведения фокуса.


Позиционирование и фокусируемые элементы

Popper.js создаёт popper-элемент относительно reference-элемента. Фокусировка на reference-элементе или popper-элементе влияет на его видимость и поведение:

  • Reference-элемент — элемент, относительно которого позиционируется popper. Например, кнопка, которая открывает меню.
  • Popper-элемент — всплывающий элемент, который может содержать интерактивные элементы, такие как ссылки, кнопки или поля ввода.

Popper.js не управляет фокусом напрямую, но важно правильно интегрировать его с механизмами фокусировки:

import { createPopper } from '@popperjs/core';

const button = document.querySelector('#button');
const tooltip = document.querySelector('#tooltip');

const popperInstance = createPopper(button, tooltip, {
  placement: 'bottom',
  modifiers: [
    { name: 'offset', options: { offset: [0, 8] } },
  ],
});

В этом примере popper создаётся и позиционируется относительно кнопки. Для корректного управления фокусом необходимо учитывать два момента:

  1. Передача фокуса при открытии popper.
  2. Возврат фокуса после закрытия popper.

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

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

button.addEventListener('click', () => {
  tooltip.setAttribute('data-show', '');
  tooltip.focus(); // Передача фокуса на popper
});

Если popper содержит несколько интерактивных элементов (например, меню с несколькими ссылками), рекомендуется устанавливать фокус на первый фокусируемый элемент:

const firstFocusable = tooltip.querySelector('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
if (firstFocusable) firstFocusable.focus();

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

После закрытия popper важно вернуть фокус на reference-элемент, чтобы не потерять контекст взаимодействия:

function closePopper() {
  tooltip.removeAttribute('data-show');
  button.focus(); // Возврат фокуса на reference-элемент
}

Это особенно важно для всплывающих окон, меню и диалогов, чтобы соблюсти ARIA-рекомендации и корректную навигацию с клавиатуры.


Модификаторы и фокус

Popper.js поддерживает систему модификаторов, которые можно использовать для интеграции с управлением фокусом. Наиболее полезные:

  • eventListeners — позволяет динамически слушать события scroll и resize для корректного позиционирования popper, что предотвращает потерю фокуса при изменении положения страницы.
  • preventOverflow — гарантирует, что popper не выходит за границы видимой области, что важно для видимости и доступности фокусируемых элементов.
  • flip — автоматически меняет сторону появления popper при нехватке места, предотвращая скрытие элементов, на которые может быть передан фокус.

Пример использования модификатора preventOverflow с focus management:

const popperInstance = createPopper(button, tooltip, {
  placement: 'bottom-start',
  modifiers: [
    {
      name: 'preventOverflow',
      options: { padding: 8 },
    },
    {
      name: 'focusTrap',
      enabled: true,
      fn({ state }) {
        const focusable = state.elements.popper.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
        focusable.forEach(el => el.addEventListener('keydown', trapFocus));
      }
    }
  ],
});

В этом примере модификатор focusTrap реализует базовую логику удержания фокуса внутри popper.


Создание “focus trap”

Для сложных интерфейсов (модальные окна, тултипы с формами) рекомендуется создавать focus trap, чтобы фокус не уходил за пределы popper, пока он активен:

function trapFocus(event) {
  const focusableElements = tooltip.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
  const firstEl = focusableElements[0];
  const lastEl = focusableElements[focusableElements.length - 1];

  if (event.key === 'Tab') {
    if (event.shiftKey && document.activeElement === firstEl) {
      event.preventDefault();
      lastEl.focus();
    } else if (!event.shiftKey && document.activeElement === lastEl) {
      event.preventDefault();
      firstEl.focus();
    }
  }
}

Focus trap совместно с Popper.js позволяет создавать полностью управляемые интерактивные элементы, которые корректно реагируют на клавиатурную навигацию.


Доступность (ARIA) и фокус

При управлении фокусом необходимо синхронизировать ARIA-атрибуты с состоянием popper:

  • aria-haspopup="true" на reference-элементе.
  • aria-expanded="true/false" для индикации видимости popper.
  • aria-controls с указанием id popper-элемента.
  • Установка role="tooltip" или role="menu" на popper-элементе в зависимости от его назначения.

Пример:

button.setAttribute('aria-haspopup', 'true');
button.setAttribute('aria-expanded', tooltip.hasAttribute('data-show'));
button.setAttribute('aria-controls', 'tooltip-id');
tooltip.setAttribute('role', 'tooltip');

Такое управление позволяет пользователям с ассистивными технологиями корректно ориентироваться между reference и popper-элементами.


Рекомендации по интеграции

  • Использовать Popper.js только для позиционирования, а управление фокусом реализовывать отдельно, чтобы сохранить контроль над доступностью.
  • Для сложных интерфейсов реализовывать focus trap и автоматическое восстановление фокуса.
  • Синхронизировать состояние popper с ARIA-атрибутами.
  • Проверять поведение на клавиатуре и с ассистивными технологиями.

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