Skip to content
Draft
Show file tree
Hide file tree
Changes from 37 commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
1c342d4
menu preview
mj12albert Jun 17, 2026
e591fa6
add preview card with popover
mj12albert Jun 17, 2026
edcecab
do not flip submenu tooltips
mj12albert Jun 18, 2026
a922a5f
dedupe styles
mj12albert Jun 22, 2026
65c4777
fixes
mj12albert Jun 29, 2026
6bdd72c
Add context menu recipe
mj12albert Jun 29, 2026
62aa760
fix ci
mj12albert Jun 29, 2026
98300e9
Merge Base UI menu PoC (#48663) as RFC experiment substrate
LukasTy Jul 16, 2026
a93d303
Add /experiments/menu-rfc playground page
LukasTy Jul 16, 2026
905a982
Merge upstream master
LukasTy Jul 22, 2026
1650ef4
Remove docs/API tooling special-casing for the menu experiment
LukasTy Jul 24, 2026
a9c5e15
Rename MenuPreview family to Unstable_Menu2
LukasTy Jul 24, 2026
62c9b19
Pilot describeConformance on Menu2Item and Menu2Popup
LukasTy Jul 24, 2026
9c5b891
Inherit Base UI prop surfaces via Omit/Pick on Menu2 roots and popup
LukasTy Jul 24, 2026
84de2d4
Fix popup surface clipping from inherited classic maxHeight
LukasTy Jul 24, 2026
7296bb6
Fix separator spacing collapse when a submenu opens
LukasTy Jul 24, 2026
e802107
Fix Netlify and CI failures from the tooling cleanup and styles spread
LukasTy Jul 24, 2026
e4c6809
Remove leftover menu-preview API page components
LukasTy Jul 24, 2026
b5a80ed
Use Menu2 bindings in demo and spec JSX
LukasTy Jul 24, 2026
d1414db
Fix context menu recipe restoring focus to a stale menu trigger
LukasTy Jul 24, 2026
b6b2b2b
Track the Menu successor RFC draft in the PR
LukasTy Jul 24, 2026
4b0248b
Render the RFC draft as an experiment and align names on Menu2
LukasTy Jul 27, 2026
b515922
Resolve the inset question: composition already covers it
LukasTy Jul 27, 2026
3cef988
Merge upstream master
LukasTy Jul 27, 2026
1147bde
Migrate Menu2 slot plumbing to the @mui/utils utilities
LukasTy Jul 27, 2026
41ad207
Roll describeConformance out to every rendering Menu2 part
LukasTy Jul 28, 2026
a552e06
Run the behavior benchmark and record its results in the RFC
LukasTy Jul 28, 2026
aa9d4e7
Correct the behavior benchmark: Tab and placement were false divergences
LukasTy Jul 28, 2026
526edff
Decide pointer-open focus: keep the Base UI behavior
LukasTy Jul 28, 2026
573a7fa
Fix RFC and playground issues found by an audit pass
LukasTy Jul 28, 2026
2128fbe
Pin why variant="selectedMenu" cannot be carried over
LukasTy Jul 29, 2026
50e220f
Record that Base UI omits Menu initialFocus on purpose
LukasTy Jul 29, 2026
6dba4b0
Record why animation cannot simply be wrapped
LukasTy Jul 29, 2026
2910cbf
Ship a default menu animation and surface the backdrop slot
LukasTy Jul 29, 2026
fb44442
Record four confirmations and repair the requirements list
LukasTy Jul 29, 2026
942e7de
Shorten the RFC and use plainer wording
LukasTy Jul 29, 2026
e14f562
Drop the Select dependency and ground two open questions
LukasTy Jul 29, 2026
4f021a7
Cover the Menu2 items in the high contrast enhancer
LukasTy Jul 31, 2026
4b55454
Fix four defects in the Menu2 forced-colors coverage
LukasTy Jul 31, 2026
26352a6
Build the indicator into the checkbox and radio items
LukasTy Jul 31, 2026
c9ae437
Collapse the menu containers, at both levels
LukasTy Jul 31, 2026
379eed9
Make the composed parts internal and migrate the rest
LukasTy Aug 3, 2026
64f8d61
Record the settled API shape in the RFC
LukasTy Aug 3, 2026
70dda7f
Use index.ts and drop the Unstable_ prefix from identifiers
LukasTy Aug 3, 2026
1b7e08b
prettier
LukasTy Aug 3, 2026
0c38222
Fix the CI failures: stale proptypes and a mid-animation measurement
LukasTy Aug 3, 2026
26f32a3
Fix the behavioral slots, the lost ref, and the stale RFC
LukasTy Aug 3, 2026
6ba9ab1
Collapse the theme API onto MuiMenu2 and MuiMenu2Submenu
LukasTy Aug 4, 2026
38ac755
Rewrite the RFC in Simplified Technical English
LukasTy Aug 4, 2026
2635cd1
Overlap the submenu with its parent menu by default
LukasTy Aug 4, 2026
03f1560
Restructure the RFC for a human reader
LukasTy Aug 4, 2026
1361367
Record four RFC decisions and correct the Tooltip claim
LukasTy Aug 5, 2026
94e2d9d
Give the menu items a ripple, with disableRipple
LukasTy Aug 7, 2026
2637e04
Accept every node kind as the root trigger
LukasTy Aug 11, 2026
12aa959
Require an element for the trigger, at both levels
LukasTy Aug 11, 2026
2aeda50
Pin the submenu open-state cascade and update the RFC
LukasTy Aug 11, 2026
fd1a85a
Merge remote-tracking branch 'upstream/master' into claude/menu-subme…
LukasTy Aug 13, 2026
b576383
Fix the submenu triggers in the docs, and warn on a fragment
LukasTy Aug 13, 2026
fc2f47b
Fix the submenu props that the docs sent to the wrong place
LukasTy Aug 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
632 changes: 632 additions & 0 deletions docs/pages/experiments/menu2-playground.tsx

Large diffs are not rendered by default.

697 changes: 697 additions & 0 deletions docs/pages/experiments/menu2-recipes.tsx

Large diffs are not rendered by default.

27 changes: 27 additions & 0 deletions docs/pages/experiments/menu2-rfc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import * as React from 'react';
import Container from '@mui/material/Container';
import CssBaseline from '@mui/material/CssBaseline';
import { ThemeProvider, createTheme } from '@mui/material/styles';
import { MarkdownElement } from '@mui/internal-core-docs/MarkdownDocs';
import { AppLayoutHead as Head } from '@mui/internal-core-docs/AppLayout';
import { docs } from './menu2-rfc.md?muiMarkdown';

const theme = createTheme({});

export default function Menu2RfcPage() {
const localizedDoc = docs.en;

return (
<ThemeProvider theme={theme}>
<CssBaseline />
<Head title="Menu2 RFC draft" description={localizedDoc.description} />
<Container maxWidth="md" sx={{ pt: 4, pb: 8 }}>
{localizedDoc.rendered.map((chunk, index) =>
typeof chunk === 'string' ? (
<MarkdownElement key={index} renderedMarkdown={chunk} />
) : null,
)}
</Container>
</ThemeProvider>
);
}
375 changes: 375 additions & 0 deletions docs/pages/experiments/menu2-rfc.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages-internal/core-docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
"next": "15.5.20"
},
"peerDependencies": {
"@base-ui/react": "^1",
"@base-ui/react": "^1.5.0",
"@docsearch/react": "catalog:docs",
"@emotion/cache": "catalog:docs",
"@emotion/react": "catalog:docs",
Expand Down
1 change: 1 addition & 0 deletions packages/mui-material/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
},
"dependencies": {
"@babel/runtime": "^7.29.7",
"@base-ui/react": "^1.6.0",
"@mui/core-downloads-tracker": "workspace:^",
"@mui/system": "workspace:^",
"@mui/types": "workspace:^",
Expand Down
15 changes: 3 additions & 12 deletions packages/mui-material/src/Menu/Menu.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { styled } from '../zero-styled';
import { useDefaultProps } from '../DefaultPropsProvider';
import { getMenuUtilityClass } from './menuClasses';
import useSlot from '../utils/useSlot';
import { menuListStyles, menuPaperStyles } from './menuStyles';

const RTL_ORIGIN = {
vertical: 'top',
Expand Down Expand Up @@ -44,22 +45,12 @@ const MenuRoot = styled(Popover, {
export const MenuPaper = styled(PopoverPaper, {
name: 'MuiMenu',
slot: 'Paper',
})({
// specZ: The maximum height of a simple menu should be one or more rows less than the view
// height. This ensures a tappable area outside of the simple menu with which to dismiss
// the menu.
maxHeight: 'calc(100% - 96px)',
// Add iOS momentum scrolling for iOS < 13.0
WebkitOverflowScrolling: 'touch',
});
})(menuPaperStyles);

const MenuMenuList = styled(MenuList, {
name: 'MuiMenu',
slot: 'List',
})({
// We disable the focus ring for mouse, touch and keyboard users.
outline: 0,
});
})(menuListStyles);

const Menu = React.forwardRef(function Menu(inProps, ref) {
const props = useDefaultProps({ props: inProps, name: 'MuiMenu' });
Expand Down
15 changes: 15 additions & 0 deletions packages/mui-material/src/Menu/menuStyles.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/** @type {import('@mui/system').CSSInterpolation} */
export const menuPaperStyles = {
// specZ: The maximum height of a simple menu should be one or more rows less than the view
// height. This ensures a tappable area outside of the simple menu with which to dismiss
// the menu.
maxHeight: 'calc(100% - 96px)',
// Add iOS momentum scrolling for iOS < 13.0
WebkitOverflowScrolling: 'touch',
};

/** @type {import('@mui/system').CSSInterpolation} */
export const menuListStyles = {
// We disable the focus ring for mouse, touch and keyboard users.
outline: 0,
};
123 changes: 3 additions & 120 deletions packages/mui-material/src/MenuItem/MenuItem.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,12 @@ import focusWithVisible from '../utils/focusWithVisible';
import useForkRef from '../utils/useForkRef';
import useId from '../utils/useId';
import { useRovingTabIndexItem } from '../utils/useRovingTabIndex';
import { dividerClasses } from '../Divider';
import { listItemIconClasses } from '../ListItemIcon';
import { listItemTextClasses } from '../ListItemText';
import { useMenuListContext } from '../MenuList/MenuListContext';
import { useSelectFocusSource } from '../Select/utils';
import menuItemClasses, { getMenuItemUtilityClass } from './menuItemClasses';
import { getMenuItemRootStyles, menuItemOverridesResolver } from './menuItemStyles';

export const overridesResolver = (props, styles) => {
const { ownerState } = props;

return [
styles.root,
ownerState.dense && styles.dense,
ownerState.divider && styles.divider,
!ownerState.disableGutters && styles.gutters,
];
};
export const overridesResolver = menuItemOverridesResolver;

const useUtilityClasses = (ownerState) => {
const { disabled, dense, divider, disableGutters, selected, classes } = ownerState;
Expand Down Expand Up @@ -58,113 +47,7 @@ const MenuItemRoot = styled(ButtonBase, {
name: 'MuiMenuItem',
slot: 'Root',
overridesResolver,
})(
memoTheme(({ theme }) => ({
...theme.typography.body1,
display: 'flex',
justifyContent: 'flex-start',
alignItems: 'center',
position: 'relative',
textDecoration: 'none',
minHeight: 48,
paddingTop: 6,
paddingBottom: 6,
boxSizing: 'border-box',
whiteSpace: 'nowrap',
'&:hover': {
textDecoration: 'none',
backgroundColor: (theme.vars || theme).palette.action.hover,
// Reset on touch devices, it doesn't add specificity
'@media (hover: none)': {
backgroundColor: 'transparent',
},
},
[`&.${menuItemClasses.selected}`]: {
backgroundColor: theme.alpha(
(theme.vars || theme).palette.primary.main,
(theme.vars || theme).palette.action.selectedOpacity,
),
[`&.${menuItemClasses.focusVisible}`]: {
backgroundColor: theme.alpha(
(theme.vars || theme).palette.primary.main,
`${(theme.vars || theme).palette.action.selectedOpacity} + ${(theme.vars || theme).palette.action.focusOpacity}`,
),
},
},
[`&.${menuItemClasses.selected}:hover`]: {
backgroundColor: theme.alpha(
(theme.vars || theme).palette.primary.main,
`${(theme.vars || theme).palette.action.selectedOpacity} + ${(theme.vars || theme).palette.action.hoverOpacity}`,
),
// Reset on touch devices, it doesn't add specificity
'@media (hover: none)': {
backgroundColor: theme.alpha(
(theme.vars || theme).palette.primary.main,
(theme.vars || theme).palette.action.selectedOpacity,
),
},
},
[`&.${menuItemClasses.focusVisible}`]: {
backgroundColor: (theme.vars || theme).palette.action.focus,
},
[`&.${menuItemClasses.disabled}`]: {
opacity: (theme.vars || theme).palette.action.disabledOpacity,
},
[`& + .${dividerClasses.root}`]: {
marginTop: theme.spacing(1),
marginBottom: theme.spacing(1),
},
[`& + .${dividerClasses.inset}`]: {
marginLeft: 52,
},
[`& .${listItemTextClasses.root}`]: {
marginTop: 0,
marginBottom: 0,
},
[`& .${listItemTextClasses.inset}`]: {
paddingLeft: 36,
},
[`& .${listItemIconClasses.root}`]: {
minWidth: 36,
},
variants: [
{
props: ({ ownerState }) => !ownerState.disableGutters,
style: {
paddingLeft: 16,
paddingRight: 16,
},
},
{
props: ({ ownerState }) => ownerState.divider,
style: {
borderBottom: `1px solid ${(theme.vars || theme).palette.divider}`,
backgroundClip: 'padding-box',
},
},
{
props: ({ ownerState }) => !ownerState.dense,
style: {
[theme.breakpoints.up('sm')]: {
minHeight: 'auto',
},
},
},
{
props: ({ ownerState }) => ownerState.dense,
style: {
minHeight: 32, // https://m2.material.io/components/menus#specs > Dense
paddingTop: 4,
paddingBottom: 4,
...theme.typography.body2,
[`& .${listItemIconClasses.root} svg`]: {
fontSize: '1.25rem',
},
},
},
],
})),
);
})(memoTheme(({ theme }) => getMenuItemRootStyles(theme, menuItemClasses)));

const MenuItem = React.forwardRef(function MenuItem(inProps, ref) {
const props = useDefaultProps({ props: inProps, name: 'MuiMenuItem' });
Expand Down
137 changes: 137 additions & 0 deletions packages/mui-material/src/MenuItem/menuItemStyles.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { dividerClasses } from '../Divider';
import { listItemIconClasses } from '../ListItemIcon';
import { listItemTextClasses } from '../ListItemText';

export const menuItemOverridesResolver = (props, styles) => {
const { ownerState } = props;

return [
styles.root,
ownerState.dense && styles.dense,
ownerState.divider && styles.divider,
!ownerState.disableGutters && styles.gutters,
];
};

export function getMenuItemRootStyles(theme, classes, options = {}) {
const focusVisibleClass = options.focusVisibleClass ?? classes.focusVisible;
const disabledPointerEvents = options.disabledPointerEvents ?? false;

return {
...theme.typography.body1,
display: 'flex',
justifyContent: 'flex-start',
alignItems: 'center',
position: 'relative',
textDecoration: 'none',
minHeight: 48,
paddingTop: 6,
paddingBottom: 6,
boxSizing: 'border-box',
whiteSpace: 'nowrap',
'&:hover': {
textDecoration: 'none',
backgroundColor: (theme.vars || theme).palette.action.hover,
// Reset on touch devices, it doesn't add specificity
'@media (hover: none)': {
backgroundColor: 'transparent',
},
},
[`&.${classes.selected}`]: {
backgroundColor: theme.alpha(
(theme.vars || theme).palette.primary.main,
(theme.vars || theme).palette.action.selectedOpacity,
),
...(focusVisibleClass && {
[`&.${focusVisibleClass}`]: {
backgroundColor: theme.alpha(
(theme.vars || theme).palette.primary.main,
`${(theme.vars || theme).palette.action.selectedOpacity} + ${
(theme.vars || theme).palette.action.focusOpacity
}`,
),
},
}),
},
[`&.${classes.selected}:hover`]: {
backgroundColor: theme.alpha(
(theme.vars || theme).palette.primary.main,
`${(theme.vars || theme).palette.action.selectedOpacity} + ${
(theme.vars || theme).palette.action.hoverOpacity
}`,
),
// Reset on touch devices, it doesn't add specificity
'@media (hover: none)': {
backgroundColor: theme.alpha(
(theme.vars || theme).palette.primary.main,
(theme.vars || theme).palette.action.selectedOpacity,
),
},
},
...(focusVisibleClass && {
[`&.${focusVisibleClass}`]: {
backgroundColor: (theme.vars || theme).palette.action.focus,
},
}),
[`&.${classes.disabled}`]: {
opacity: (theme.vars || theme).palette.action.disabledOpacity,
...(disabledPointerEvents && {
pointerEvents: 'none',
cursor: 'default',
}),
},
[`& + .${dividerClasses.root}`]: {
marginTop: theme.spacing(1),
marginBottom: theme.spacing(1),
},
[`& + .${dividerClasses.inset}`]: {
marginLeft: 52,
},
[`& .${listItemTextClasses.root}`]: {
marginTop: 0,
marginBottom: 0,
},
[`& .${listItemTextClasses.inset}`]: {
paddingLeft: 36,
},
[`& .${listItemIconClasses.root}`]: {
minWidth: 36,
},
variants: [
{
props: ({ ownerState }) => !ownerState.disableGutters,
style: {
paddingLeft: 16,
paddingRight: 16,
},
},
{
props: ({ ownerState }) => ownerState.divider,
style: {
borderBottom: `1px solid ${(theme.vars || theme).palette.divider}`,
backgroundClip: 'padding-box',
},
},
{
props: ({ ownerState }) => !ownerState.dense,
style: {
[theme.breakpoints.up('sm')]: {
minHeight: 'auto',
},
},
},
{
props: ({ ownerState }) => ownerState.dense,
style: {
minHeight: 32, // https://m2.material.io/components/menus#specs > Dense
paddingTop: 4,
paddingBottom: 4,
...theme.typography.body2,
[`& .${listItemIconClasses.root} svg`]: {
fontSize: '1.25rem',
},
},
},
],
};
}
Loading
Loading