Keyboard navigation

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


Основы навигации с клавиатуры

Навигация с клавиатуры обычно строится вокруг следующих событий:

  1. keydown — основное событие, которое позволяет перехватывать нажатия клавиш.
  2. focus и blur — позволяют отслеживать активный элемент.
  3. tabindex — атрибут HTML, определяющий порядок обхода элементов клавишей Tab.

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

  • открытие элемента при нажатии Enter или Space на триггере;
  • перемещение фокуса между элементами внутри Popper с помощью Tab и Shift+Tab;
  • закрытие Popper при нажатии Esc.

Пример базовой клавиатурной логики

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

const button = document.querySelector('#dropdownButton');
const menu = document.querySelector('#dropdownMenu');

const popperInstance = createPopper(button, menu, {
  placement: 'bottom-start',
});

function toggleMenu() {
  menu.classList.toggle('show');
  if (menu.classList.contains('show')) {
    menu.querySelector('button, [tabindex]:not([tabindex="-1"])').focus();
  }
}

button.addEventListener('keydown', (event) => {
  if (event.key === 'Enter' || event.key === ' ') {
    event.preventDefault();
    toggleMenu();
  }
});

menu.addEventListener('keydown', (event) => {
  const focusableElements = menu.querySelectorAll('button, [tabindex]:not([tabindex="-1"])');
  const focusArray = Array.from(focusableElements);
  const currentIndex = focusArray.indexOf(document.activeElement);

  if (event.key === 'ArrowDown') {
    event.preventDefault();
    const nextIndex = (currentIndex + 1) % focusArray.length;
    focusArray[nextIndex].focus();
  }

  if (event.key === 'ArrowUp') {
    event.preventDefault();
    const prevIndex = (currentIndex - 1 + focusArray.length) % focusArray.length;
    focusArray[prevIndex].focus();
  }

  if (event.key === 'Escape') {
    menu.classList.remove('show');
    button.focus();
  }
});

Учет видимости и позиции Popper

Popper.js управляет позиционированием и может динамически изменять расположение всплывающего элемента с помощью модификаторов (modifiers). При реализации клавиатурного взаимодействия важно учитывать:

  • dynamic placement: положение Popper может измениться в зависимости от видимости экрана, поэтому фокус должен быть назначен на правильный элемент после изменения позиции.
  • preventOverflow: если Popper сдвигается автоматически, элементы внутри него остаются доступными для фокуса.
  • eventListeners: Popper использует слушатели событий для пересчета позиции, но клавиатурные события должны обрабатываться отдельно.

Пример добавления модификатора для постоянного обновления позиции при открытии:

const popperInstance = createPopper(button, menu, {
  placement: 'bottom-start',
  modifiers: [
    {
      name: 'eventListeners',
      enabled: true,
    },
  ],
});

Рекомендации по доступности

  1. Роль и атрибуты Для выпадающих элементов необходимо использовать role="menu" и role="menuitem" для кнопок внутри, чтобы экранные читалки корректно воспринимали структуру.
<div id="dropdownMenu" role="menu">
  <button role="menuitem">Опция 1</button>
  <button role="menuitem">Опция 2</button>
</div>
  1. Tabindex Использовать tabindex="0" для элементов, которые должны быть доступны через Tab, и tabindex="-1" для элементов, которые не должны быть фокусируемыми по умолчанию.

  2. Фокус при открытии/закрытии При открытии Popper фокус нужно переводить на первый элемент меню. При закрытии возвращать фокус на триггер.

  3. Обработка Esc и Arrow Keys Esc закрывает меню и возвращает фокус на триггер. Стрелки ArrowDown и ArrowUp перемещают фокус между элементами.


Продвинутые сценарии

  • Группировка элементов: если Popper содержит несколько групп элементов, можно использовать ArrowRight и ArrowLeft для перехода между группами.
  • Динамическое добавление элементов: при добавлении новых элементов в Popper необходимо обновлять массив фокусируемых элементов, чтобы навигация оставалась корректной.
  • Контроль видимости через состояние: использование классов .show или aria-expanded="true/false" помогает синхронизировать видимость Popper с доступностью.
menu.setAttribute('aria-hidden', !menu.classList.contains('show'));
button.setAttribute('aria-expanded', menu.classList.contains('show'));

Интеграция с другими библиотеками

Для сложных интерфейсов Popper.js часто интегрируют с библиотеками управления состоянием (React, Vue, Svelte). В этих случаях клавиатурная навигация реализуется через:

  • хранение состояния isOpen;
  • управление фокусом через рефы и useEffect (React) или watch (Vue);
  • вызов popperInstance.update() при изменении содержимого Popper для корректного позиционирования перед назначением фокуса.

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