Menu
Список команд, привязанный к тому, что его открыло. Третье, что висит на place(), и отличие от двух других — в том, что читатель с ним делает: тултип читают, в поповере работают, а из меню запускают команду и оно исчезает, поэтому любая строка его закрывает. Это и не Select: селект сообщает выбранное значение и объявляет его выбранным, меню же запускает и не сообщает ничего — потому здесь ни одна строка не бывает aria-selected, а в списке нет галочки. Фокус — вторая половина того же отличия. Селект держит фокус на триггере и указывает оттуда aria-activedescendant; меню переносит фокус внутрь списка, а Tab его закрывает, а не гоняет по кругу: меню, из которого нельзя выйти вперёд, — это ловушка, а не меню. Передайте point, и оно откроется по координатам, а не рядом с триггером, — это и есть контекстное меню.
Код
<button type="button" class="aura-btn aura-btn--secondary"
aria-haspopup="menu" aria-expanded="true" aria-controls="m1-menu">Actions</button>
<div data-state="open" class="aura-popup aura-menu">
<ul id="m1-menu" role="menu" aria-label="Actions" aria-activedescendant="m1-item-0" tabindex="-1"
class="aura-listbox aura-scroll">
<li id="m1-item-0" role="menuitem" data-active="true" class="aura-listbox__option">
<span class="aura-listbox__label">Open</span>
</li>
<li id="m1-item-1" role="menuitem" data-active="false" class="aura-listbox__option">
<span class="aura-listbox__label">Rename<span class="aura-listbox__description">Give it another name</span></span>
</li>
<li role="separator" class="aura-listbox__separator"></li>
<li id="m1-item-2" role="menuitem" data-active="false" class="aura-listbox__option aura-listbox__option--danger">
<span class="aura-listbox__label">Delete</span>
</li>
</ul>
</div>import { Button, Menu, type MenuItem } from '@ilomee/aura-react';
const items: MenuItem[] = [
{ value: 'open', label: 'Open' },
{ value: 'rename', label: 'Rename', description: 'Give it another name' },
{ value: 'duplicate', label: 'Duplicate' },
{ value: 'archive', label: 'Archive', disabled: true },
{ value: 'delete', label: 'Delete', danger: true, separatorBefore: true },
];
/**
* Нажмите на триггер и походите стрелками. Фокус переходит внутрь списка, курсор виртуальный — один
* элемент в фокусе, а `aria-activedescendant` указывает на строку, — и набор буквы перескакивает на
* строку, которая с неё начинается. Enter запускает строку, Escape закрывает, ничего не запуская, а
* Tab выходит из меню совсем, а не гоняет по кругу внутри. Отключённую строку стрелки перешагивают,
* а не встают на неё, а опасная отделена от остальных чертой.
*/
export default function Demo() {
return (
<Menu items={items} label="Actions" onSelect={(value) => console.log(value)}>
<Button variant="secondary">Actions</Button>
</Menu>
);
}<script setup lang="ts">
import { Menu, Button } from '@ilomee/aura-vue';
const items = [
{ value: 'open', label: 'Open' },
{ value: 'rename', label: 'Rename', description: 'Give it another name' },
{ value: 'delete', label: 'Delete', danger: true, separatorBefore: true },
];
</script>
<template>
<Menu :items="items" label="Actions" @select="run">
<Button variant="secondary">Actions</Button>
</Menu>
</template><script lang="ts">
import { Menu } from '@ilomee/aura-svelte';
const items = [
{ value: 'open', label: 'Open' },
{ value: 'delete', label: 'Delete', danger: true, separatorBefore: true },
];
</script>
<Menu {items} label="Actions" onselect={run}>
{#snippet trigger(props)}
<button type="button" class="aura-btn aura-btn--secondary" {...props}>Actions</button>
{/snippet}
</Menu>Используется как [auraMenu] — директива, которую вы вешаете на свой элемент.
<button auraButton variant="secondary" [auraMenu]="items" label="Actions" (select)="run($event)">
Actions
</button>Панель строится через Renderer2, а не шаблоном: содержимое — это данные, а не разметка автора, и компонент, вставленный в body, поставил бы собственный host-элемент туда, где остальные три рисуют <div class="aura-popup">. Директива снимает собственный атрибут auraMenu в ngOnInit, как и auraPopover. Шаблона строки нет: строка — это подпись и пояснение, а шаблонный вход ради двух строк никому не нужен.
Пропсы
| Имя | Тип | По умолчанию | Примечания |
|---|---|---|---|
| items | MenuItem[] | null | — | Команды. value опознаёт команду в обработчике, label — её текст; description, disabled, danger, group и separatorBefore — остальное. null рисует пустой список, а не бросает исключение. |
| children | ReactElement | — | Открывающий контрол. Клонируется, а не оборачивается. Необязателен: с point триггера нет вовсе. |
| label | string | — | Доступное имя меню. role="menu" без имени объявляется как группа без подписи. |
| openдвусторонний | boolean | — | |
| defaultOpen | boolean | false | |
| onOpenChangeсобытие | (open: boolean) => void | — | |
| onSelectсобытие | (value: string) => void | — | Запущенная строка. Меню закрывается само — этим оно и является меню. |
| point | { x: number; y: number } | null | — | Открыть по этим координатам вьюпорта, а не рядом с триггером, — clientX/clientY, не смещения страницы. |
| side | 'top' | 'right' | 'bottom' | 'left' | 'bottom' | |
| align | 'start' | 'center' | 'end' | 'start' | |
| emptyLabel | string | 'No actions' | |
| width | number | string | — | Ширина панели — пиксели или любая CSS-длина. Без неё панель по ширине самой длинной строки, но не уже 200px и не шире 320px. Это никогда не ширина триггера: меню часто открывают из чего-то более узкого, чем оно само, а по координатам триггера нет вовсе. |
| renderItem | (item, state) => ReactNode | — | Заменяет содержимое строки, но не её <li>. |
| Имя | Тип | По умолчанию | Примечания |
|---|---|---|---|
| items | MenuItem[] | null | — | |
| defaultслот | slot | — | Триггер. Ровно один элемент, клонируется. С point необязателен. |
| itemслот | slot | — | Заменяет содержимое строки. Получает { item, index, active, disabled }. |
| label | string | — | |
| openдвусторонний | boolean | — | |
| update:openсобытие | (open: boolean) => void | — | |
| selectсобытие | (value: string) => void | — | |
| point | { x: number; y: number } | null | — | |
| side | 'top' | 'right' | 'bottom' | 'left' | 'bottom' | |
| align | 'start' | 'center' | 'end' | 'start' | |
| emptyLabel | string | 'No actions' | |
| width | number | string | — |
| Имя | Тип | По умолчанию | Примечания |
|---|---|---|---|
| items | AuraMenuItem[] | null | — | |
| trigger | Snippet<[TriggerProps]> | — | Получает пропсы для раскрытия на контроле. С point необязателен. |
| item | Snippet<[item, state]> | — | Заменяет содержимое строки. |
| label | string | — | |
| openдвусторонний | boolean | — | |
| onselectсобытие | (value: string) => void | — | |
| point | { x: number; y: number } | null | — | |
| side | 'top' | 'right' | 'bottom' | 'left' | 'bottom' | |
| align | 'start' | 'center' | 'end' | 'start' | |
| emptyLabel | string | 'No actions' | |
| width | number | string | — |
| Имя | Тип | По умолчанию | Примечания |
|---|---|---|---|
| auraMenu | AuraMenuItem[] | null | — | Команды; названы по селектору, чтобы привязка читалась прямо на триггере. |
| label | string | — | |
| openдвусторонний | boolean | — | |
| openChangeсобытие | (open: boolean) => void | — | |
| selectсобытие | (value: string) => void | — | |
| point | { x: number; y: number } | null | — | |
| side | 'top' | 'right' | 'bottom' | 'left' | 'bottom' | |
| align | 'start' | 'center' | 'end' | 'start' | |
| emptyLabel | string | 'No actions' | |
| width | number | string | — |
Какие классы отдаёт
Все фреймворки отдают одни и те же классы — именно поэтому четыре библиотеки выглядят одинаково. Их можно использовать напрямую, вообще без библиотеки компонентов.
| Класс | Роль | Примечания |
|---|---|---|
| .aura-popup | база | Обязательный. Без него модификатор не работает. |
| .aura-menu | база | Обязательный. Без него модификатор не работает. |
| .aura-listbox | база | Обязательный. Без него модификатор не работает. |
| .aura-listbox__option | элемент | |
| .aura-listbox__separator | элемент |
Токены и правила
components.css- слой
- var(--z-dropdown) — under a modal, on purpose
- ширина
- its rows, floored at 200px and capped at 320px — or var(--menu-w)
- высота
- min(320px, 60vh), then it scrolls — .aura-listbox’s own
- опасная строка
- var(--danger), inverted under the cursor