Mosmox UI
Current version
GitHubnpmjs.com
Navigation & Selection

Menu

Відкрите меню для команд, вибору режимів та контекстних дій.

Menu — це базовий компонент уже відкритого меню. Він не містить тригера або логіки позиціонування: його можна вбудовувати в ButtonMenu, ProfileMenu, ContextMenu або інший компонент, який керує видимістю та позицією меню.

Базове використання

Передайте масив entries. Звичайний пункт має обов’язкові key і label. Записи з type використовуються для службового вмісту: розділювача, заголовка групи або довільного React-вмісту.

<Menu
  aria-label="File actions"
  entries={[
    { type: 'label', label: 'Actions' },
    { key: 'edit', label: 'Edit' },
    { type: 'divider' },
    { key: 'delete', label: 'Delete', danger: true },
  ]}
/>

Пункт меню рендериться як кнопка з ARIA-роллю menuitem або option, залежно від значення role. Menu не має власного тригера — для відкриття та позиціонування використовуйте компонент-обгортку.

Вміст пунктів

MenuItem підтримує іконку, зображення, опис, гарячу клавішу, стани disabled, danger, checked і локальний обробник onClick.

<Menu
  entries={[
    {
      key: 'open',
      label: 'Open project',
      description: 'Choose a workspace',
      icon: <FolderIcon />,
      shortcut: '⌘ O',
    },
    { key: 'archive', label: 'Archive', disabled: true },
    { key: 'remove', label: 'Remove', danger: true, onClick: removeProject },
    { key: 'grid', label: 'Grid view', checked: true },
  ]}
/>

label, description, shortcut та icon приймають ReactNode, тому замість простого тексту можна передати власний React-вміст. image приймає URL зображення і відображає його як кругле зображення зліва.

Розділювачі та заголовки груп

<Menu
  entries={[
    { type: 'label', key: 'workspace-label', label: 'Workspace' },
    { key: 'new', label: 'New document' },
    { key: 'open', label: 'Open project' },
    { type: 'divider', key: 'workspace-divider' },
    { key: 'settings', label: 'Settings' },
  ]}
/>

key для divider і label необов’язковий. Він корисний, якщо запис формується динамічно або потрібен стабільний React-ключ.

Вибір пунктів

Для меню налаштувань увімкніть selectable. Без selectedKeys стан є неконтрольованим і змінюється всередині компонента. defaultSelectedKeys задає початкові вибрані ключі. За потреби стан можна контролювати через selectedKeys.

<Menu
  aria-label="View mode"
  selectable
  defaultSelectedKeys={['grid']}
  onSelect={(key, item) => console.log('Selected:', key, item)}
  entries={[
    { key: 'list', label: 'List view', icon: <ListIcon /> },
    { key: 'grid', label: 'Grid view', icon: <GridIcon /> },
  ]}
/>

Контрольований варіант:

const [selectedKeys, setSelectedKeys] = useState(['grid']);

<Menu
  selectable
  selectedKeys={selectedKeys}
  onSelect={(key) => {
    setSelectedKeys((current) =>
      current.includes(key)
        ? current.filter((itemKey) => itemKey !== key)
        : [...current, key],
    );
  }}
  entries={viewEntries}
/>

Компонент підтримує кілька вибраних ключів. onSelect отримує (key, item) і викликається для звичайних пунктів, але не для divider, label або content.

Підменю

Пункт може містити submenu з таким самим набором записів MenuEntry, включно з вкладеними підменю будь-якої глибини. Підменю відкривається натисканням або наведенням на пункт, а справа відображається стрілка.

<Menu
  aria-label="Project actions"
  entries={[
    {
      key: 'file',
      label: 'File',
      icon: <FileIcon />,
      submenu: [
        { key: 'new', label: 'New document', shortcut: '⌘ N' },
        { key: 'open', label: 'Open project', shortcut: '⌘ O' },
        { type: 'divider' },
        {
          key: 'recent',
          label: 'Recent files',
          submenu: [
            { key: 'report', label: 'Quarterly report' },
            { key: 'notes', label: 'Meeting notes' },
          ],
        },
      ],
    },
    { key: 'settings', label: 'Settings' },
  ]}
/>

Відкриті ключі можна задати через defaultOpenKeys або контролювати через openKeys. onOpenChange отримує новий масив відкритих ключів.

const [openKeys, setOpenKeys] = useState(['file']);

<Menu
  openKeys={openKeys}
  onOpenChange={setOpenKeys}
  entries={entriesWithSubmenus}
/>

Клік по пункту з submenu перемикає його відкритий стан і не викликає onSelect. Для звичайного пункту всередині підменю викликаються його onClick та onSelect.

Закриття при кліку поза меню

Якщо відкриті підменю, клік або pointer-натискання поза межами всього Menu закриває всі рівні. Для контрольованого openKeys потрібно оновити стан через onOpenChange.

Автоматичне закриття після виходу курсора

Властивість closeSubmenusOnMouseLeave вмикає закриття всіх відкритих підменю після виходу курсора за межі меню. Затримка задається через submenuCloseDelay у мілісекундах. За замовчуванням затримка становить 10000, але автозакриття вимкнене, щоб не змінювати стандартну поведінку.

<Menu
  closeSubmenusOnMouseLeave
  submenuCloseDelay={3000}
  defaultOpenKeys={['file', 'recent']}
  entries={entriesWithSubmenus}
/>

Повернення курсора в область меню до завершення затримки скасовує закриття. Щоб залишити меню відкритим після виходу курсора, не передавайте closeSubmenusOnMouseLeave або встановіть його в false.

Довільний невзаємодіючий вміст

Для вставки довільного React-компонента або текстового блоку використовуйте запис { type: 'content', content }. Він не є пунктом меню: не отримує роль menuitem, не реагує на hover-стилі Menu, не бере участі в клавіатурній навігації та не викликає onSelect. Внутрішні елементи можуть мати власну взаємодію.

<Menu
  entries={[
    {
      type: 'content',
      key: 'account-summary',
      content: (
        <div>
          <strong>Alex Johnson</strong>
          <span>[email protected]</span>
        </div>
      ),
    },
    { type: 'divider' },
    { key: 'settings', label: 'Account settings' },
  ]}
/>

Клавіатурна навігація

Контейнер Menu має tabIndex={0} і role="menu" за замовчуванням. Підтримуються такі клавіші:

КлавішаПоведінка
ArrowDownПерехід до наступного активного пункту.
ArrowUpПерехід до попереднього активного пункту.
HomeПерехід до першого активного пункту.
EndПерехід до останнього активного пункту.
Enter або SpaceАктивація активного пункту.

Disabled-пункти, divider, label і content пропускаються. Для сценарію списку вибору можна встановити role="listbox"; тоді пункти отримують роль option.

Розміри, варіанти та розміщення

size визначає щільність пунктів:

  • sm — компактна типографіка та відступи.
  • default — стандартний розмір.
  • lg — збільшені пункти.

variant визначає поверхню меню:

  • default — поверхня з рамкою.
  • compact — щільніші внутрішні відступи.
  • filled — заповнена поверхня на основі токена control.

Menu не має власної властивості placement і не виконує автоматичне визначення вільного місця у viewport. Позиціювання виконує компонент-обгортка, наприклад ButtonMenu.

<Menu
  size="lg"
  variant="filled"
  width={360}
  maxHeight={320}
  className="project-menu"
  footer="Use Arrow keys to navigate"
  entries={items}
/>

width приймає число в пікселях або CSS-значення, наприклад "24rem". maxHeight також приймає число в пікселях або CSS-значення. className додається до кореневого елемента для локального стилювання, а footer рендериться в окремій нижній секції.

API Reference

ВластивістьТипЗа замовчуваннямПризначення
entriesMenuEntry[]—Записи меню: пункти, підменю, довільний вміст, розділювачі та заголовки.
aria-labelstring"Menu"Доступна назва кореневого меню.
classNamestring—Додатковий CSS-клас кореневого елемента.
closeSubmenusOnMouseLeavebooleanfalseВмикає автозакриття всіх відкритих підменю після виходу курсора.
submenuCloseDelaynumber10000Затримка автозакриття в мілісекундах. Використовується разом із closeSubmenusOnMouseLeave.
defaultSelectedKeysstring[][]Початкові вибрані ключі для неконтрольованого меню.
selectedKeysstring[]—Контрольований масив вибраних ключів.
selectablebooleanfalseДозволяє перемикати вибраний стан пунктів. Підтримує кілька ключів.
defaultOpenKeysstring[][]Початкові відкриті ключі підменю для неконтрольованого меню.
openKeysstring[]—Контрольований масив відкритих ключів підменю.
onOpenChange(keys: string[]) => void—Викликається після зміни відкритих підменю, включно із закриттям поза меню та автозакриттям.
onSelect(key: string, item: MenuItem) => void—Викликається після активації звичайного пункту меню. Не викликається для службових записів і пунктів із підменю.
footerReactNode—Довільний вміст у нижній секції меню.
maxHeightnumber | string—Максимальна висота кореневого меню. Число інтерпретується як пікселі.
widthnumber | string280Ширина меню. Число інтерпретується як пікселі.
role'menu' | 'listbox''menu'ARIA-роль кореневого контейнера.
size'sm' | 'default' | 'lg''default'Розмір і щільність пунктів.
variant'default' | 'compact' | 'filled''default'Візуальний варіант поверхні меню.
ВластивістьТипЗа замовчуваннямПризначення
keystring—Унікальний ключ пункту.
labelReactNode—Основний текст або React-вміст пункту.
descriptionReactNode—Додатковий рядок під основним текстом.
iconReactNode—Іконка зліва.
imagestring—URL зображення зліва.
shortcutReactNode—Позначення гарячої клавіші справа.
disabledbooleanfalseВимикає пункт і виключає його з клавіатурної навігації.
dangerbooleanfalseЗастосовує семантичний стиль небезпечної дії.
checkedbooleanfalseВідображає пункт як вибраний незалежно від selectable.
onClick() => void—Локальна дія пункту.
submenuMenuEntry[]—Вкладені записи меню. Підтримує підменю будь-якої глибини.
ВластивістьТипЗа замовчуваннямПризначення
type'content'—Позначає довільний невзаємодіючий запис.
contentReactNode—Будь-який текст або React-компонент.
keystring—Необов’язковий ключ запису.

Інші типи MenuEntry

type MenuEntry =
  | MenuItem
  | { type: 'divider'; key?: string }
  | { type: 'label'; key?: string; label: ReactNode }
  | MenuContentEntry;

MenuSize має значення 'sm' | 'default' | 'lg', а MenuVariant — 'default' | 'compact' | 'filled'.