Skip to content

[experiment] Menu RFC playground#48823

Draft
LukasTy wants to merge 21 commits into
mui:masterfrom
LukasTy:menu-rfc-playground
Draft

[experiment] Menu RFC playground#48823
LukasTy wants to merge 21 commits into
mui:masterfrom
LukasTy:menu-rfc-playground

Conversation

@LukasTy

@LukasTy LukasTy commented Jul 16, 2026

Copy link
Copy Markdown
Member

Companion experiment for the upcoming "Menu: Base UI-based successor" RFC. Draft, not intended to merge as-is.

This branch:

  • Merges the menu proof of concept from [experiment] Popups on menu items #48663 (by @mj12albert) onto current master, with @base-ui/react aligned to ^1.6.0 (docs catalog + direct dependency of @mui/material).
  • Renames the family to the agreed lifecycle naming: public subpaths @mui/material/Unstable_Menu2* (one component per subpath, no Base UI-style short aliases), internal Menu2* identifiers, theme keys and classes MuiMenu2* (the name-matches-component lint rule derives keys from component names, matching the Grid2 precedent; keys survive the later Unstable_ -> stable rename).
  • Removes the docs/API tooling special-casing from the PoC (projectSettings carve-out, generated API pages, translations); the experiment is exercised via the non-public /experiments pages instead.
  • Pilots describeConformance on Menu2Item and Menu2Popup as the replacement for the bespoke theming/slots/classes hygiene tests (62 passing conformance tests; two documented Base UI-specific adaptations).
  • Adds a non-public playground at /experiments/menu-rfc with use-case demos and knobs for the RFC's open questions.

Playground knobs

  • Root behavior: modal, loopFocus, highlightItemOnHover
  • Submenus: openOnHover, delay, closeDelay, closeParentOnEsc
  • Positioning: side, align, sideOffset, alignOffset, keepMounted
  • Appearance / RFC open questions: elevation, default animation candidate (CSS approximation of Grow via data-starting-style/data-ending-style), dense items, item dividers, RTL direction

Sections

  • Kitchen sink: three-level submenus, groups with labels, checkbox/radio items, a disabled item, a visual-only selected item, a link item, plus an onOpenChange reason log
  • Classic vs successor: the same item set on both menus for visual parity checks
  • Classic-style controlled usage: external anchor + open/onOpenChange, no Trigger part
  • Typeahead and scrolling: long menu constrained via slotProps.paper

See also /experiments/menu-preview from #48663 for the Tooltip, PreviewCard, and ContextMenu recipes (updated to the new imports).

RFC: the living draft is tracked in this PR at packages/mui-material/src/Unstable_Menu2/RFC.md -- review comments welcome there. It moves to a public RFC issue once settled.

Verified locally: full Menu2 unit suite green (115 passed / 13 skipped across jsdom + browser projects), tsgo typecheck clean, fresh no-cache ESLint clean, and the playground exercised against the dev server (keyboard submenu navigation, Escape ordering, onOpenChange reasons, MuiMenu2* classes in the DOM).

Generated with Claude Code

mj12albert and others added 9 commits June 29, 2026 18:47
Brings in mj12albert's MenuPreview proof of concept from
mui#48663, rebased context onto
current master: @base-ui/react aligned to ^1.6.0 (catalog + direct
dependency of @mui/material), lockfile regenerated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Non-public experiments page accompanying the Menu successor RFC:
kitchen-sink demo with three-level submenus, groups, checkbox/radio
items, link items, and an onOpenChange reason log; knobs for root
behavior, submenu hover/delays, positioning, and the RFC open
questions (elevation, default animation candidate, dense, dividers,
RTL); classic-vs-successor parity section; classic-style controlled
anchor usage; typeahead + scrolling demo.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@code-infra-dashboard

code-infra-dashboard Bot commented Jul 16, 2026

Copy link
Copy Markdown

Deploy preview

https://deploy-preview-48823--material-ui.netlify.app/
QR code for https://deploy-preview-48823--material-ui.netlify.app/

Bundle size

Bundle Parsed size Gzip size
@mui/material 🔺+160B(+0.03%) 🔺+94B(+0.06%)
@mui/lab 0B(0.00%) 0B(0.00%)
@mui/private-theming 0B(0.00%) 0B(0.00%)
@mui/system 0B(0.00%) 0B(0.00%)
@mui/utils 0B(0.00%) 0B(0.00%)

Details of bundle changes


Check out the code infra dashboard for more information about this PR.

@github-actions github-actions Bot added the PR: out-of-date The pull request has merge conflicts and can't be merged. label Jul 20, 2026
@LukasTy LukasTy added type: new feature Expand the scope of the product to solve a new problem. scope: menu Changes related to the menu. RFC Request For Comments. labels Jul 22, 2026
@LukasTy LukasTy self-assigned this Jul 22, 2026
@github-actions github-actions Bot removed the PR: out-of-date The pull request has merge conflicts and can't be merged. label Jul 22, 2026
Lockfile regenerated after dependency bumps (pnpm 11.13.1, Vite 8).
MenuItem.js composes cleanly: upstream aria-checked for
menuitemcheckbox/menuitemradio (mui#48651) + the PoC style extraction.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@LukasTy
LukasTy force-pushed the menu-rfc-playground branch from b8d5972 to 905a982 Compare July 22, 2026 14:13
LukasTy and others added 11 commits July 24, 2026 11:00
Per the agreed standard, docs tooling is a fixed constraint: drop the
projectSettings/getComponentImports carve-out, the apiDocs.d.ts entry
point, the generated API reference pages and translations, and restore
the menus.md frontmatter. The experiment is exercised via the
non-public /experiments pages instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Aligns the experiment with the agreed naming lifecycle:
- public subpaths and directories use Unstable_Menu2* (one component
  per subpath); the root index no longer re-exports parts or Base
  UI-style short aliases
- internal identifiers and file names use clean Menu2* naming,
  following the Unstable_TrapFocus/Grid2 precedent enforced by the
  naming-convention lint rules
- theme keys and classes use MuiMenu2* (the name-matches-component
  lint rule derives keys from component names; matches Grid2, and
  keys survive the later Unstable_ -> stable rename)
- data attributes use data-mui-menu2-*

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Explores replacing the bespoke theming/slots/classes hygiene tests with
the standard conformance harness. Both parts pass the full suite (ref
forwarding, className merge, props spread, root class, component prop,
slot swaps with class assertions, theme defaultProps/styleOverrides/
variants) with two Base UI-specific adaptations:
- portalled roots sit behind Base UI focus-guard spans, so the render
  wrapper hands the harness a container satisfying its firstChild
  contract (candidate for a getRootElement option in the shared
  harness)
- componentProp is skipped on the popup: the host element swaps via
  slots.popup instead

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Applies the agreed prop-surface standard (extend Base UI types, Omit
what does not fit, declare only Material additions or changed
defaults):
- Menu2 and Menu2SubmenuRoot inherit the full Base UI Root/SubmenuRoot
  surface (gains openOnHover/delay/closeDelay/actionsRef and future
  props); children is curated to a plain ReactNode
- the popup's flattened positioner/portal surface is inherited via
  Pick; only side/align (changed defaults) and the Material elevation
  convenience prop stay locally declared
- the playground exercises the inherited surface: root openOnHover
  knob and the top-level elevation prop

Empirical finding recorded for the RFC: the proptypes generator does
not expand members declared in node_modules, so runtime PropTypes on
the renderless roots degrade to `children` (generator-stable; types
carry the full contract). Teaching the generator to expand external
heritage is the shared-infra follow-up.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The shared classic paper styles carry maxHeight: calc(100% - 96px),
which in the classic Menu resolves against the full-viewport Modal.
Inside the content-sized Base UI popup the percentage resolves against
the popup itself (browser-dependent), clamping the Paper ~96px shorter
than its content and clipping the end of the menu (trailing items and
the separator), with a visible reflow on open.

Override it for the successor with the positioner-provided
collision-aware space: min(calc(100vh - 96px), var(--available-height))
plus overflowY: auto so long menus scroll inside the surface. Classic
Menu keeps its original styles untouched.

Regression test asserts the paper's computed max-height is no longer
the classic percentage form and that the surface scrolls.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Opening a submenu mounts Base UI inline focus-guard and portal-anchor
nodes as siblings of the submenu trigger, which breaks the legacy
`[item] + .MuiDivider-root` adjacency rule carried by the shared item
styles: the following separator lost its 8px margins and the menu
visibly contracted below the trigger.

Own the spacing on Menu2Separator's root instead (same values, so the
closed state is unchanged where the adjacency rule also applies). Side
effect: a separator following a Group now gets the same margins, making
separator spacing uniform.

Regression test opens a submenu (defaultOpen) and asserts the separator
keeps its margins.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Restore docs/data/material/pagesApi.js to upstream: it still registered
  the 16 removed /material-ui/api/menu-preview* routes (kebab-case, so the
  earlier cleanup sweep missed it), breaking the docs build on Netlify.
- Narrow the JSDoc-typed classic paper styles before spreading
  (CSSInterpolation union fails TS2698 under tsconfig.build declaration
  emit, which broke the package build, publish dry run, and bundle
  monitor; the dev tsconfig tolerated it).
- Drop the dead api/menu-preview* links from the generated component
  JSDoc headers.
- Wire the playground hover-open knob to the trigger: Base UI puts
  openOnHover/delay on Menu.Trigger, not Menu.Root (caught by the docs
  typecheck once the barrel types were exercised).

Verified: @mui/material build with dependency graph (declaration emit)
exit 0, docs + package tsgo clean, proptypes regeneration churn-free,
Menu2 suite 117 passed / 15 skipped, eslint/prettier clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The docs tooling cleanup removed the generated API JSONs and
translations but left the sibling page components in
docs/pages/material-ui/api/, whose imports of the deleted JSONs broke
the Netlify docs build (webpack module-not-found).

Verified with the full Netlify-parity build locally: pnpm docs:build
(llms + next build + export + build-sw + link-check) exits 0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Unstable_ subpaths use default exports, so consumers naturally drop
the prefix at the import binding. The experiment pages and the type
spec now import Menu2* names directly (paths keep the Unstable_
subpaths) and the JSX reads like the future stable API; the local
alias blocks are gone.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A detached (virtual-anchor) menu has no trigger to return focus to on
close, and right-clicking text blurs the previously focused element to
body, so Base UI's focus manager fell back to its internal previously-
focused-element record -- typically the trigger of whichever menu was
used earlier on the page. Closing the context menu with Escape then
focused an unrelated demo's trigger.

Follow the APG context-menu pattern instead: the invoked surface gets
tabIndex={-1} and is passed as the popup's finalFocus.

The regression test seeds the stale record with another menu, opens
the detached menu via contextmenu, and asserts Escape restores focus
to the invoked surface (fails without finalFocus).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Versions the living RFC draft next to the component so revisions land
atomically with the discoveries that motivate them and reviewers can
comment line-by-line. Formatted per repo markdown conventions (vale
clean); the file is removed when the RFC is posted publicly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

RFC Request For Comments. scope: menu Changes related to the menu. type: new feature Expand the scope of the product to solve a new problem.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants