Skip to main content

Breaking: MenuList renamed to MenuContent, plus new submenu and MenuTree APIs

@trackunit/react-components's MenuList claimed Up/Down keyboard navigation and nested-menu support in its docs that it never actually implemented — it was just a role="list" div with click handlers. It's now been rewritten with real WAI-ARIA menu semantics and renamed to MenuContent to reflect that. MenuItem also gains a new submenu prop for building nested menus, and the MenuTree/useMenuTree tree-coordination primitive that powers it is now public API.

Breaking change: MenuList renamed to MenuContent

MenuList and MenuListProps have been renamed to MenuContent and MenuContentProps. The API is otherwise unchanged, but every consumer now also inherits the new keyboard navigation described below, so it's worth re-testing rather than treating this as a pure find-and-replace.

-import { MenuList } from "@trackunit/react-components";
+import { MenuContent } from "@trackunit/react-components";

-<MenuList>
+<MenuContent>
<MenuItem id="edit" label="Edit" />
-</MenuList>
+</MenuContent>

A codemod is available to automate the rename — see Codemods below.

New: keyboard navigation on MenuContent

MenuContent now implements real WAI-ARIA menu semantics instead of a plain role="list" div:

  • Roving tabindex with ArrowUp/ArrowDown, wrapping from the last item back to the first (and vice versa)
  • Home/End jump to the first/last enabled item
  • Typeahead: typing a letter jumps to the next item whose label starts with it (~500 ms reset window)
  • Disabled items are skipped during navigation
  • ArrowRight/ArrowLeft open and close a MenuItem's submenu (see below) and move focus in and out of it
  • Non-MenuItem children (e.g. a search input rendered inside a MenuContent) are left untouched — they don't participate in roving tabindex or typeahead, so native typing and focus behavior keeps working

MenuContent reads keyboard/focus context from an ambient Popover when rendered inside one, and also works standalone (e.g. inside a Collapse) with no ambient Popover present.

New: submenu prop on MenuItem

MenuItem now accepts a submenu prop for building nested menus without hand-rolling a trigger row:

import { MenuContent, MenuItem } from "@trackunit/react-components";

const StatusMenu = () => (
<MenuContent>
<MenuItem
label="Status"
submenu={
<MenuContent>
<MenuItem id="active" label="Active" />
<MenuItem id="idle" label="Idle" />
</MenuContent>
}
/>
</MenuContent>
);

When submenu is provided, MenuItem internally wraps itself in a Popover joined to the nearest MenuTree and, unless a suffix is explicitly provided, renders a chevron affordance automatically. The submenu opens on click, Enter/Space, or ArrowRight; ArrowLeft inside the submenu closes it and returns focus to the triggering item. A new submenuSizing prop forwards size constraints to the internal Popover.

New: MenuTree / useMenuTree public API

MenuTree and useMenuTree — previously internal — are now exported from @trackunit/react-components for consumers who need to build fully custom nested-menu UI on top of Popover rather than using MenuItem's submenu prop.

Wrapping a family of Popovers in <MenuTree> opts them into coordinated behavior: opening one closes its open siblings, Escape dismisses one level at a time instead of the whole branch, outside-press is scoped per node, and hover between nested rows uses a safe-polygon so the pointer can travel diagonally into a submenu without it closing. Popovers outside a MenuTree are unaffected.

import { MenuTree, Popover, PopoverContent, PopoverTrigger } from "@trackunit/react-components";

const NestedMenu = () => (
<MenuTree>
<Popover placement="bottom-start">
<PopoverTrigger>Open</PopoverTrigger>
<PopoverContent>
<Popover activation={{ click: true, hover: { delayed: true } }} placement="right-start">
<PopoverTrigger>Row A</PopoverTrigger>
<PopoverContent>Row A's flyout</PopoverContent>
</Popover>
</PopoverContent>
</Popover>
</MenuTree>
);

MenuTree is opt-in — plain Popover usages elsewhere are unaffected, since joining a tree requires explicitly wrapping in <MenuTree>.

Codemods

This release ships an automated codemod for the MenuListMenuContent rename, orchestrated by the @trackunit/migrations coordinator (modeled after nx migrate).

Running the codemod

Run the two-step flow from your project root:

  1. Bump @trackunit/* versions in package.json and collect pending migrations into trackunit-migrations.json
    • nx g @trackunit/migrations:migrate
  2. Review trackunit-migrations.json, then execute the pending migrations
    • nx g @trackunit/migrations:run-migrations

Previously-applied migrations are tracked in trackunit-migrations.json and won't re-run.

Included migration

  • v3-0-0-menulist-rename-to-menucontent — renames MenuList/MenuListProps to MenuContent/MenuContentProps across import specifiers, JSX tags, and type references (e.g. Omit<MenuListProps, "children">), including aliased imports.