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
MenuProps
| Властивість | Тип | За замовчуванням | Призначення |
|---|---|---|---|
entries | MenuEntry[] | — | Записи меню: пункти, підменю, довільний вміст, розділювачі та заголовки. |
aria-label | string | "Menu" | Доступна назва кореневого меню. |
className | string | — | Додатковий CSS-клас кореневого елемента. |
closeSubmenusOnMouseLeave | boolean | false | Вмикає автозакриття всіх відкритих підменю після виходу курсора. |
submenuCloseDelay | number | 10000 | Затримка автозакриття в мілісекундах. Використовується разом із closeSubmenusOnMouseLeave. |
defaultSelectedKeys | string[] | [] | Початкові вибрані ключі для неконтрольованого меню. |
selectedKeys | string[] | — | Контрольований масив вибраних ключів. |
selectable | boolean | false | Дозволяє перемикати вибраний стан пунктів. Підтримує кілька ключів. |
defaultOpenKeys | string[] | [] | Початкові відкриті ключі підменю для неконтрольованого меню. |
openKeys | string[] | — | Контрольований масив відкритих ключів підменю. |
onOpenChange | (keys: string[]) => void | — | Викликається після зміни відкритих підменю, включно із закриттям поза меню та автозакриттям. |
onSelect | (key: string, item: MenuItem) => void | — | Викликається після активації звичайного пункту меню. Не викликається для службових записів і пунктів із підменю. |
footer | ReactNode | — | Довільний вміст у нижній секції меню. |
maxHeight | number | string | — | Максимальна висота кореневого меню. Число інтерпретується як пікселі. |
width | number | string | 280 | Ширина меню. Число інтерпретується як пікселі. |
role | 'menu' | 'listbox' | 'menu' | ARIA-роль кореневого контейнера. |
size | 'sm' | 'default' | 'lg' | 'default' | Розмір і щільність пунктів. |
variant | 'default' | 'compact' | 'filled' | 'default' | Візуальний варіант поверхні меню. |
MenuItem
| Властивість | Тип | За замовчуванням | Призначення |
|---|---|---|---|
key | string | — | Унікальний ключ пункту. |
label | ReactNode | — | Основний текст або React-вміст пункту. |
description | ReactNode | — | Додатковий рядок під основним текстом. |
icon | ReactNode | — | Іконка зліва. |
image | string | — | URL зображення зліва. |
shortcut | ReactNode | — | Позначення гарячої клавіші справа. |
disabled | boolean | false | Вимикає пункт і виключає його з клавіатурної навігації. |
danger | boolean | false | Застосовує семантичний стиль небезпечної дії. |
checked | boolean | false | Відображає пункт як вибраний незалежно від selectable. |
onClick | () => void | — | Локальна дія пункту. |
submenu | MenuEntry[] | — | Вкладені записи меню. Підтримує підменю будь-якої глибини. |
MenuContentEntry
| Властивість | Тип | За замовчуванням | Призначення |
|---|---|---|---|
type | 'content' | — | Позначає довільний невзаємодіючий запис. |
content | ReactNode | — | Будь-який текст або React-компонент. |
key | string | — | Необов’язковий ключ запису. |
Інші типи MenuEntry
type MenuEntry =
| MenuItem
| { type: 'divider'; key?: string }
| { type: 'label'; key?: string; label: ReactNode }
| MenuContentEntry;MenuSize має значення 'sm' | 'default' | 'lg', а MenuVariant — 'default' | 'compact' | 'filled'.