MenuTreeController
Nested submenu coordination: timers, keyboard and popovers.
Reach for it for a menu button or menubar with submenus.
Import
import { MenuTreeController, createMenuTreeController } from '@ecopages/radiant-ui/controllers';Try it
The submenu is the menu that follows its menuitem. Click Open recent, or focus it and press ArrowRight or Enter, to open it; choosing a leaf item reports it.
Nothing chosen
<div data-example="menu-tree">
<div role="menu" aria-label="File">
<button type="button" role="menuitem">New</button>
<button type="button" role="menuitem">Open recent</button>
<div role="menu" aria-label="Open recent">
<button type="button" role="menuitem">Report.md</button>
<button type="button" role="menuitem">Notes.md</button>
</div>
</div>
<p data-status>Nothing chosen</p>
</div>import { MenuTreeController } from '@ecopages/radiant-ui/controllers';
export function mount(root: HTMLElement): () => void {
const menu = root.querySelector<HTMLElement>('[role="menu"]')!;
const status = root.querySelector('[data-status]')!;
const tree = new MenuTreeController({
getRoot: () => root,
getRootMenu: () => menu,
onActivate: (item) => (status.textContent = `Chose ${item.textContent}`),
onCloseRoot: () => tree.closeAll(),
});
tree.attach();
tree.sync();
menu.addEventListener('click', (event) => tree.handleClick(event));
menu.addEventListener('keydown', (event) => tree.handleKeydown(event));
return () => tree.destroy();
}mount runs once for each example element on the page, and the function it returns undoes it; see how the examples run.
Config
| Option | Description |
|---|---|
getRoot | The element whose descendants hold the menus, read on every event. |
getRootMenu | The top-level menu. |
onActivate | Called with a leaf item the user activates by click, Enter or Space. |
onCloseRoot | Called for Escape (true, return focus to the trigger) and Tab (false). |
Lifecycle
The controller holds no listener of its own: attach() has nothing to start, and the owner routes events to handlePointerOver, handlePointerOut, handleFocusOut, handleKeydown and handleClick. Call sync() after the menus render to set aria-haspopup, aria-expanded, aria-controls and hidden on the items with a submenu. destroy() closes every open branch and releases its popover and timers; closeAll() closes them on demand.
With a host, createMenuTreeController(host, config) does the same on connect and disconnect; see bindController.
Protected hooks
None. The key map and timers are fixed; behavior that differs belongs in onActivate and onCloseRoot.