abaabil

Menu

heaviest of the twenty-three, by js weight

The W3C APG menu button pattern, on top of the native Popover API. The browser supplies the surface; this supplies the menu.

Example (a11y tier)

Open it, then use Up and Down, Home and End, or type a letter. Tab closes it and carries on; Escape and a click outside close it too, and those two are the browser's doing, not ours.

Import

import Menu from 'abaabil/menu'         // structure only, no ARIA
import Menu from 'abaabil/menu/styled'  // + CSS
import Menu from 'abaabil/menu/a11y'    // + APG menu semantics and keyboard

When a menu is the wrong widget

role="menu" means a list of actions, in the application-menu sense: the things you find under File and Edit. A button that reveals a few navigation links is not a menu. Marking it up as one makes a screen reader announce a widget the user then cannot operate as one, and it takes the links out of the page's normal reading order for no benefit.

For links, use popover and put an ordinary list inside it. That is lighter, needs no JavaScript, and is what the content actually is.

What the browser does, and what is left

The Popover API gives the panel its top layer, dismissal on an outside click and dismissal on Escape. None of that is reimplemented here, which is why the lower two tiers are server-renderable and why the a11y tier is as small as it is for a pattern this involved.

What the browser does not know is that this panel is a menu:

  • aria-haspopup, and an aria-expanded that is kept in step with the panel's own toggle event rather than a second copy of the open state, because the browser can close the panel without telling the component.
  • role="menu" on the panel, role="menuitem" on each entry.
  • A roving tabindex, so Tab leaves the menu instead of walking every item in it.
  • Up and Down with wrapping, Home and End, skipping disabled items.
  • Multi-character typeahead, with the buffer resetting after half a second.
  • Focus moving to the first item on open, and back to the trigger on close.

Props

PropTypeDefaultTierDescription
idstring— (required)allPanel id; also wires the trigger. Not generated, for the same reason as popover: that would mean useId, and a client boundary.
triggerReactNodeallButton content.
itemsArray<{ key?, label, href?, onSelect?, disabled? }>allAn item with href renders an <a>, otherwise a <button>.
triggerPropsobjectallSpread onto the trigger.
labelstringa11yNames the menu itself. Without it the menu is named by its trigger.