Menu
A list of actions or options that opens from a button, with the keyboard, a pointer or a screen reader.
Last action: nothing yet. Sorted by name.
<script>
import { $bind, $signal } from 'brumate/signals';
import Button from '@brumate/ui/Button.brm';
import Header from '@brumate/ui/Header.brm';
import Menu from '@brumate/ui/Menu.brm';
import MenuItem from '@brumate/ui/MenuItem.brm';
import MenuSection from '@brumate/ui/MenuSection.brm';
import MenuTrigger from '@brumate/ui/MenuTrigger.brm';
import Popover from '@brumate/ui/Popover.brm';
import Separator from '@brumate/ui/Separator.brm';
import SubmenuTrigger from '@brumate/ui/SubmenuTrigger.brm';
import type { Key } from '@brumate/ui/collection.ts';
let last = $signal<Key>('nothing yet');
let sort = $signal<Key[]>(['name']);
</script>
<div class="ui-row">
<MenuTrigger>
<Button class="ui-button">Edit</Button>
<Popover class="ui-popover" placement="bottom start">
<Menu class="ui-menu" onAction=((key) => (last = key))>
<MenuItem id="cut" class="ui-menu-item">Cut</MenuItem>
<MenuItem id="copy" class="ui-menu-item">Copy</MenuItem>
<MenuItem id="paste" class="ui-menu-item" isDisabled>Paste</MenuItem>
<Separator />
<SubmenuTrigger>
<MenuItem id="share" class="ui-menu-item">Share</MenuItem>
<Popover class="ui-popover" offset=(-4)>
<Menu class="ui-menu" onAction=((key) => (last = key))>
<MenuItem id="email" class="ui-menu-item">Email</MenuItem>
<MenuItem id="messages" class="ui-menu-item">Messages</MenuItem>
</Menu>
</Popover>
</SubmenuTrigger>
</Menu>
</Popover>
</MenuTrigger>
<MenuTrigger>
<Button class="ui-button">Sort</Button>
<Popover class="ui-popover" placement="bottom start">
<Menu class="ui-menu" selectionMode="single" selectedKeys=$bind(sort)>
<MenuSection>
<Header class="ui-listbox-header">Sort by</Header>
<MenuItem id="name" class="ui-menu-item">Name</MenuItem>
<MenuItem id="date" class="ui-menu-item">Date</MenuItem>
<MenuItem id="size" class="ui-menu-item">Size</MenuItem>
</MenuSection>
</Menu>
</Popover>
</MenuTrigger>
</div>
<p>Last action: {last}. Sorted by {sort[0]}.</p>
<MenuTrigger>
<Button>Edit</Button>
<Popover>
<Menu onAction=((key) => run(key))>
<MenuItem id="cut">Cut</MenuItem>
<MenuItem id="copy">Copy</MenuItem>
</Menu>
</Popover>
</MenuTrigger>
- A
MenuTriggerholds aButtonand aPopoverwith aMenu. The Button getsaria-haspopup="menu",aria-expandedandaria-controls, and the Menu is named by it. - A
MenuholdsMenuItemelements, each with anid, its key. Write them out, or render them from a#for. onActionon the Menu gets the key of the item, andonActionon the item runs too.disabledKeys, orisDisabledon an item, keeps items from focus and action.isOpenalone starts the menu open, andisOpen=$bind(signal)writes the caller's.onOpenChangegets each change.
Opening and closing
- A press of the Button opens the menu. From a key, focus goes to the first item. From a pointer, focus goes to the menu, and an arrow key moves to an item.
- ArrowDown on the Button opens the menu at its first item, and ArrowUp at its last.
- An action closes the menu, and focus goes back to the Button.
shouldCloseOnSelect=(false)keeps it open. - Escape, Tab and a press outside close it too.
Keyboard
- The arrow keys move focus, and go from the last item to the first.
shouldFocusWrap=(false)stops at the ends. - Home and End move to the first and the last item.
- Enter and Space act on the item that has focus.
- Typing moves to the item whose text starts with what was typed, in the second after each letter.
textValuesets that text for an item whose content is more than text. - The pointer moves focus to the item under it, as a menu of the system does.
Selection
selectionMode="single" makes the items radios, and "multiple" checkboxes. Each item is then a menuitemradio or a menuitemcheckbox, with aria-checked.
selectedKeysis the list of the keys of the checked items. See State.onSelectionChangegets the keys after each change.- A single selection closes the menu. A multiple selection keeps it open, so several items can change.
disallowEmptySelectionkeeps one item checked.
Submenus
A SubmenuTrigger holds a MenuItem and a Popover with the Menu of the submenu.
<SubmenuTrigger>
<MenuItem id="share">Share</MenuItem>
<Popover>
<Menu onAction=((key) => share(key))>
<MenuItem id="email">Email</MenuItem>
</Menu>
</Popover>
</SubmenuTrigger>
- The item gets
aria-haspopup="menu"andaria-expanded. - A press, Enter, Space or ArrowRight opens the submenu, with focus on its first item. Right to left, ArrowLeft opens it.
- The pointer resting on the item opens it after
delay, 200 milliseconds by default, and focus stays on the item. - ArrowLeft and Escape close it, and focus goes back to the item. Focus that moves to another item of the parent menu closes it too.
- The submenu is not modal, so the parent menu stays usable. It is placed at the end of the item, with its top at the top of the item.
- An action in the submenu closes every menu.
Sections
MenuSection groups items, and a Header inside names the group. A section with no Header needs aria-label. A Separator draws a line between items.
Items
An item shows data-selected, data-disabled, data-pressed, data-hovered, data-focused, data-focus-visible, and data-open while its submenu is open. A Text with slot="label" names it, and one with slot="description" describes it, with hasLabel and hasDescription on the item.
A Menu reads its items from the page, as a ListBox does. A Menu that stands on the page, with no trigger, takes Tab once and passes focus to its first item.