useMenu для выпадающих меню

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

Хук возвращает объекты свойств, которые нужно распространять на корневой элемент меню и на элементы внутри меню, а также контролирует состояние меню через useMenuState.


Инициализация состояния меню

Перед использованием useMenu необходимо создать состояние меню с помощью хука useMenuState из @react-stately/menu.

import { useMenuState } from "@react-stately/menu";

const menuState = useMenuState({
  selectionMode: "single", // "single", "multiple" или "none"
  disallowEmptySelection: true,
  onSelectionChange: (key) => console.log(key)
});

Пояснения параметров:

  • selectionMode – определяет, как пользователь может выбирать элементы:

    • "single" — можно выбрать только один элемент;
    • "multiple" — множественный выбор;
    • "none" — выбор невозможен, меню используется только для навигации.
  • disallowEmptySelection — запрещает оставлять выбор пустым, актуально при "single" или "multiple".

  • onSelectionChange — колбэк при изменении выбранного элемента.


Использование useMenu

Хук useMenu принимает два аргумента:

  1. props – объект с параметрами меню (например, aria-label).
  2. state – объект состояния меню, возвращаемый useMenuState.

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

import { useMenu } from "@react-aria/menu";
import { useMenuState } from "@react-stately/menu";

function DropdownMenu({ items }) {
  const state = useMenuState({ items, selectionMode: "single" });
  const { menuProps } = useMenu({}, state);

  return (
    <ul {...menuProps}>
      {items.map(item => (
        <li key={item.key}>{item.name}</li>
      ))}
    </ul>
  );
}

menuProps содержит все необходимые ARIA-атрибуты и обработчики событий:

  • role="menu" — обозначает элемент как меню для скринридеров.
  • aria-labelledby или aria-label — для описания меню.
  • Обработчики клавиатуры (ArrowUp, ArrowDown, Home, End, Enter, Space), чтобы навигация была доступной с клавиатуры.

Управление элементами меню

Для каждого элемента меню рекомендуется использовать useMenuItem:

import { useMenuItem } from "@react-aria/menu";

function MenuItem({ item, state }) {
  const ref = React.useRef();
  const { menuItemProps, isFocused, isSelected } = useMenuItem(
    { key: item.key },
    state,
    ref
  );

  return (
    <li
      {...menuItemProps}
      ref={ref}
      style={{
        background: isFocused ? "lightgray" : "transparent",
        fontWeight: isSelected ? "bold" : "normal"
      }}
    >
      {item.name}
    </li>
  );
}

Основные свойства useMenuItem:

  • menuItemProps — ARIA-атрибуты и обработчики событий для элемента.
  • isFocused — состояние фокуса для визуального выделения.
  • isSelected — выбран ли элемент.

Пример полного меню с клавиатурной навигацией

import React from "react";
import { useMenu } from "@react-aria/menu";
import { useMenuState } from "@react-stately/menu";
import { useMenuItem } from "@react-aria/menu";

function Menu({ items }) {
  const state = useMenuState({ items, selectionMode: "single" });
  const { menuProps } = useMenu({}, state);

  return (
    <ul {...menuProps} style={{ border: "1px solid black", padding: "0", listStyle: "none" }}>
      {items.map(item => (
        <MenuItem key={item.key} item={item} state={state} />
      ))}
    </ul>
  );
}

function MenuItem({ item, state }) {
  const ref = React.useRef();
  const { menuItemProps, isFocused, isSelected } = useMenuItem(
    { key: item.key },
    state,
    ref
  );

  return (
    <li
      {...menuItemProps}
      ref={ref}
      style={{
        padding: "8px 16px",
        background: isFocused ? "#e0e0e0" : "white",
        fontWeight: isSelected ? "bold" : "normal",
        cursor: "pointer"
      }}
    >
      {item.name}
    </li>
  );
}

В этом примере реализована полная клавиатурная поддержка: стрелки перемещают фокус, Enter выбирает элемент, Esc закрывает меню (если оно обёрнуто в Popover).


Интеграция с Popover или Dropdown

useMenu часто используется вместе с usePopover для создания выпадающих списков:

import { useOverlay, usePreventScroll } from "@react-aria/overlays";

function Dropdown({ buttonLabel, items }) {
  const [isOpen, setIsOpen] = React.useState(false);
  const state = useMenuState({ items, selectionMode: "single" });
  const { menuProps } = useMenu({}, state);

  return (
    <div>
      <button onCl ick={() => setIsOpen(!isOpen)}>{buttonLabel}</button>
      {isOpen && (
        <div
          {...menuProps}
          style={{
            position: "absolute",
            background: "white",
            border: "1px solid black"
          }}
        >
          {items.map(item => (
            <MenuItem key={item.key} item={item} state={state} />
          ))}
        </div>
      )}
    </div>
  );
}

useMenu автоматически добавляет ARIA-атрибуты и обработчики для доступности, в том числе управление фокусом и клавиатурой, когда меню открыто внутри оверлея.


Настройка поведения клавиатуры

По умолчанию useMenu поддерживает:

  • ArrowUp / ArrowDown – перемещение фокуса по элементам.
  • Home / End – переход к первому/последнему элементу.
  • Enter / Space – выбор элемента.
  • Escape – закрытие меню (при интеграции с Popover).

Дополнительно можно настроить:

  • disabled элементы, чтобы они пропускались при навигации.
  • typeahead поиск, чтобы быстро найти элемент по первым буквам.

Важные рекомендации

  • Всегда использовать useMenuItem для элементов меню, чтобы фокус и выбранное состояние корректно синхронизировались с state.
  • Для динамических меню (items меняются) убедиться, что ключи элементов (key) уникальны.
  • Интеграция с useOverlay и usePopover повышает удобство и доступность меню.
  • Следить за контрастом выделения фокуса и выбранного элемента для визуальной доступности.

Таким образом, useMenu вместе с useMenuItem и useMenuState позволяет создавать полностью доступные и управляемые меню, с минимальными усилиями по реализации ARIA-совместимости и клавиатурной навигации.