[experiment] Menu RFC playground#48823
Draft
LukasTy wants to merge 21 commits into
Draft
Conversation
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>
Deploy previewBundle size
Check out the code infra dashboard for more information about this PR. |
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
force-pushed
the
menu-rfc-playground
branch
from
July 22, 2026 14:13
b8d5972 to
905a982
Compare
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Companion experiment for the upcoming "Menu: Base UI-based successor" RFC. Draft, not intended to merge as-is.
This branch:
@base-ui/reactaligned to^1.6.0(docs catalog + direct dependency of@mui/material).@mui/material/Unstable_Menu2*(one component per subpath, no Base UI-style short aliases), internalMenu2*identifiers, theme keys and classesMuiMenu2*(the name-matches-component lint rule derives keys from component names, matching the Grid2 precedent; keys survive the laterUnstable_-> stable rename).projectSettingscarve-out, generated API pages, translations); the experiment is exercised via the non-public/experimentspages instead.describeConformanceonMenu2ItemandMenu2Popupas the replacement for the bespoke theming/slots/classes hygiene tests (62 passing conformance tests; two documented Base UI-specific adaptations)./experiments/menu-rfcwith use-case demos and knobs for the RFC's open questions.Playground knobs
modal,loopFocus,highlightItemOnHoveropenOnHover,delay,closeDelay,closeParentOnEscside,align,sideOffset,alignOffset,keepMountedelevation, default animation candidate (CSS approximation ofGrowviadata-starting-style/data-ending-style), dense items, item dividers, RTL directionSections
onOpenChangereason logopen/onOpenChange, no Trigger partslotProps.paperSee also
/experiments/menu-previewfrom #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),
tsgotypecheck clean, fresh no-cache ESLint clean, and the playground exercised against the dev server (keyboard submenu navigation, Escape ordering,onOpenChangereasons,MuiMenu2*classes in the DOM).Generated with Claude Code