Skip to content
Merged
Show file tree
Hide file tree
Changes from 47 commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
5e61e25
Introduce MenuHandle and add Payload to MenuStore
michaldudak Oct 29, 2025
7693585
Support detached triggers WIP
michaldudak Oct 30, 2025
868c657
Move interaction hooks to triggers
michaldudak Oct 31, 2025
126453b
Fix tests
michaldudak Nov 3, 2025
095f336
Fix unstable dependencies in useInteractions
michaldudak Nov 3, 2025
b00d6a3
Fix perf tests
michaldudak Nov 3, 2025
5092add
Add handle to Trigger
michaldudak Nov 3, 2025
615d28b
FloatingTree: do not require context
michaldudak Nov 4, 2025
ac8e5c4
Set payload in MenuTrigger
michaldudak Nov 4, 2025
060396d
Add experiment
michaldudak Nov 4, 2025
ea9b191
Fix multi-level menus not closing on outside click
michaldudak Nov 4, 2025
316cc55
Prevent rendering errors
michaldudak Nov 4, 2025
7980a74
Add tests
michaldudak Nov 5, 2025
c10ec08
Making detached triggers work with Toolbar and Menubar
michaldudak Nov 5, 2025
6c45078
Fix submenu hover
michaldudak Nov 5, 2025
6d99859
Fix menubar triggers
michaldudak Nov 6, 2025
a3391ed
ReactStore: useDebugValue
michaldudak Nov 6, 2025
97e691d
Remove unnecessary store subscriptions
michaldudak Nov 6, 2025
c7463df
Remove modal prop
michaldudak Nov 6, 2025
76ee10c
Fix inactive triggers updating the store
michaldudak Nov 6, 2025
6833a2e
Fix menubar's menu closing
michaldudak Nov 6, 2025
2b73d81
More Menubar fixes
michaldudak Nov 7, 2025
eb3ed12
Move useFocus to triggers
michaldudak Nov 7, 2025
36d7c5c
Fix keyboard navigation in Menubar
michaldudak Nov 7, 2025
002bf4f
Fix reversed opening by focus condition
michaldudak Nov 7, 2025
7c354f2
Update tests
michaldudak Nov 7, 2025
855a953
Move stickIfOpen to the trigger
michaldudak Nov 7, 2025
8e9a29e
Fix TS errors
michaldudak Nov 7, 2025
8c9caad
Fix Context menu positioning
michaldudak Nov 8, 2025
4947b68
Fix nested context menu
michaldudak Nov 9, 2025
2453fe1
API docs
michaldudak Nov 9, 2025
84347fa
Add Menu to Toolbar triggers experiment
michaldudak Nov 9, 2025
d9ba602
Fix menubar submenu keyboard navigation
michaldudak Nov 10, 2025
4975a1f
Error codes
michaldudak Nov 10, 2025
5e0f05b
Nested menu tests
michaldudak Nov 10, 2025
73790e8
Run all menubar tests for contained, detached and multiple contained …
michaldudak Nov 10, 2025
63a242a
Dedupe
michaldudak Nov 10, 2025
6b16ef0
Lint
michaldudak Nov 10, 2025
e1c05c6
Include detached triggers in MenuRoot tests
michaldudak Nov 10, 2025
578ef33
Docs
michaldudak Nov 11, 2025
faa9818
Dedupe
michaldudak Nov 11, 2025
a3cbcf0
Fix closing dialog issue
michaldudak Nov 11, 2025
d66bf2b
Imperative close in actionsRef
michaldudak Nov 11, 2025
a9e8c06
Fix aria-expanded
michaldudak Nov 11, 2025
1b6a0c0
FloatingTree changes docs
michaldudak Nov 11, 2025
c865231
Don't label the floating element with all the triggers
michaldudak Nov 11, 2025
27a02d1
Fix menubar tests
michaldudak Nov 11, 2025
3036695
Add failing Menubar tests
michaldudak Nov 12, 2025
96dccac
Split useHover into reference and floating hooks
michaldudak Nov 12, 2025
70d1eaa
Merge remote-tracking branch 'upstream/master' into menu-detached-tri…
michaldudak Nov 12, 2025
a5834d3
Comments in tests
michaldudak Nov 12, 2025
9a06cb3
Fix tab stop issues
michaldudak Nov 12, 2025
65c2d25
Fix returning focus timing
michaldudak Nov 13, 2025
7969c67
Fix safePolygon
michaldudak Nov 13, 2025
853b48e
Fix JSDOM tests
michaldudak Nov 13, 2025
b0b2e3f
Fix focus regression in menubar
michaldudak Nov 13, 2025
e34b1bb
Fix shift-tabbing out of menubar
michaldudak Nov 13, 2025
e06f293
docs
michaldudak Nov 13, 2025
a90cbf8
Merge remote-tracking branch 'upstream/master' into menu-detached-tri…
michaldudak Nov 13, 2025
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
23 changes: 19 additions & 4 deletions docs/reference/generated/context-menu-root.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
},
"actionsRef": {
"type": "RefObject<Menu.Root.Actions>",
"description": "A ref to imperative actions.\n- `unmount`: When specified, the menu will not be unmounted when closed.\nInstead, the `unmount` function must be called to unmount the menu manually.\nUseful when the menu's animation is controlled by an external library.",
"description": "A ref to imperative actions.\n- `unmount`: When specified, the menu will not be unmounted when closed.\n Instead, the `unmount` function must be called to unmount the menu manually.\n Useful when the menu's animation is controlled by an external library.\n- `close`: When specified, the menu can be closed imperatively.",
"detailedType": "React.RefObject<Menu.Root.Actions> | undefined"
},
"closeParentOnEsc": {
Expand All @@ -29,11 +29,26 @@
"description": "When in a submenu, determines whether pressing the Escape key\ncloses the entire menu, or only the current child menu.",
"detailedType": "boolean | undefined"
},
"defaultTriggerId": {
"type": "string | null",
"description": "ID of the trigger that the popover is associated with.\nThis is useful in conjuntion with the `defaultOpen` prop to create an initially open popover.",
"detailedType": "string | null | undefined"
},
"handle": {
"type": "Menu.Handle<unknown>",
"description": "A handle to associate the popover with a trigger.\nIf specified, allows external triggers to control the popover's open state.",
"detailedType": "{} | undefined"
},
"onOpenChangeComplete": {
"type": "((open: boolean) => void)",
"description": "Event handler called after any animations complete when the menu is closed.",
"detailedType": "((open: boolean) => void) | undefined"
},
"triggerId": {
"type": "string | null",
"description": "ID of the trigger that the popover is associated with.\nThis is useful in conjuntion with the `open` prop to create a controlled popover.\nThere's no need to specify this prop when the popover is uncontrolled (i.e. when the `open` prop is not set).",
"detailedType": "string | null | undefined"
},
"disabled": {
"type": "boolean",
"default": "false",
Expand All @@ -53,9 +68,9 @@
"detailedType": "'horizontal' | 'vertical' | undefined"
},
"children": {
"type": "ReactNode",
"required": true,
"detailedType": "React.ReactNode"
"type": "ReactNode | PayloadChildRenderFunction<unknown>",
"description": "The content of the popover.\nThis can be a regular React node or a render function that receives the `payload` of the active trigger.",
"detailedType": "| React.ReactNode\n| ((arg: { payload: unknown }) => ReactNode)"
}
},
"dataAttributes": {},
Expand Down
40 changes: 19 additions & 21 deletions docs/reference/generated/menu-root.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
},
"actionsRef": {
"type": "RefObject<Menu.Root.Actions>",
"description": "A ref to imperative actions.\n- `unmount`: When specified, the menu will not be unmounted when closed.\nInstead, the `unmount` function must be called to unmount the menu manually.\nUseful when the menu's animation is controlled by an external library.",
"description": "A ref to imperative actions.\n- `unmount`: When specified, the menu will not be unmounted when closed.\n Instead, the `unmount` function must be called to unmount the menu manually.\n Useful when the menu's animation is controlled by an external library.\n- `close`: When specified, the menu can be closed imperatively.",
"detailedType": "React.RefObject<Menu.Root.Actions> | undefined"
},
"closeParentOnEsc": {
Expand All @@ -29,6 +29,16 @@
"description": "When in a submenu, determines whether pressing the Escape key\ncloses the entire menu, or only the current child menu.",
"detailedType": "boolean | undefined"
},
"defaultTriggerId": {
"type": "string | null",
"description": "ID of the trigger that the popover is associated with.\nThis is useful in conjuntion with the `defaultOpen` prop to create an initially open popover.",
"detailedType": "string | null | undefined"
},
"handle": {
"type": "Menu.Handle<Payload>",
"description": "A handle to associate the popover with a trigger.\nIf specified, allows external triggers to control the popover's open state.",
"detailedType": "{} | undefined"
},
"modal": {
"type": "boolean",
"default": "true",
Expand All @@ -40,29 +50,17 @@
"description": "Event handler called after any animations complete when the menu is closed.",
"detailedType": "((open: boolean) => void) | undefined"
},
"triggerId": {
"type": "string | null",
"description": "ID of the trigger that the popover is associated with.\nThis is useful in conjuntion with the `open` prop to create a controlled popover.\nThere's no need to specify this prop when the popover is uncontrolled (i.e. when the `open` prop is not set).",
"detailedType": "string | null | undefined"
},
"disabled": {
"type": "boolean",
"default": "false",
"description": "Whether the component should ignore user interaction.",
"detailedType": "boolean | undefined"
},
"openOnHover": {
"type": "boolean",
"description": "Whether the menu should also open when the trigger is hovered.",
"detailedType": "boolean | undefined"
},
"delay": {
"type": "number",
"default": "100",
"description": "How long to wait before the menu may be opened on hover. Specified in milliseconds.\n\nRequires the `openOnHover` prop.",
"detailedType": "number | undefined"
},
"closeDelay": {
"type": "number",
"default": "0",
"description": "How long to wait before closing the menu that was opened on hover.\nSpecified in milliseconds.\n\nRequires the `openOnHover` prop.",
"detailedType": "number | undefined"
},
"loop": {
"type": "boolean",
"default": "true",
Expand All @@ -76,9 +74,9 @@
"detailedType": "'horizontal' | 'vertical' | undefined"
},
"children": {
"type": "ReactNode",
"required": true,
"detailedType": "React.ReactNode"
"type": "ReactNode | PayloadChildRenderFunction<Payload>",
"description": "The content of the popover.\nThis can be a regular React node or a render function that receives the `payload` of the active trigger.",
"detailedType": "| React.ReactNode\n| ((arg: { payload: Payload | undefined }) => ReactNode)"
}
},
"dataAttributes": {},
Expand Down
41 changes: 19 additions & 22 deletions docs/reference/generated/menu-submenu-root.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
},
"actionsRef": {
"type": "RefObject<Menu.Root.Actions>",
"description": "A ref to imperative actions.\n- `unmount`: When specified, the menu will not be unmounted when closed.\nInstead, the `unmount` function must be called to unmount the menu manually.\nUseful when the menu's animation is controlled by an external library.",
"description": "A ref to imperative actions.\n- `unmount`: When specified, the menu will not be unmounted when closed.\n Instead, the `unmount` function must be called to unmount the menu manually.\n Useful when the menu's animation is controlled by an external library.\n- `close`: When specified, the menu can be closed imperatively.",
"detailedType": "React.RefObject<Menu.Root.Actions> | undefined"
},
"closeParentOnEsc": {
Expand All @@ -29,35 +29,32 @@
"description": "When in a submenu, determines whether pressing the Escape key\ncloses the entire menu, or only the current child menu.",
"detailedType": "boolean | undefined"
},
"defaultTriggerId": {
"type": "string | null",
"description": "ID of the trigger that the popover is associated with.\nThis is useful in conjuntion with the `defaultOpen` prop to create an initially open popover.",
"detailedType": "string | null | undefined"
},
"handle": {
"type": "Menu.Handle<unknown>",
"description": "A handle to associate the popover with a trigger.\nIf specified, allows external triggers to control the popover's open state.",
"detailedType": "{} | undefined"
},
"onOpenChangeComplete": {
"type": "((open: boolean) => void)",
"description": "Event handler called after any animations complete when the menu is closed.",
"detailedType": "((open: boolean) => void) | undefined"
},
"triggerId": {
"type": "string | null",
"description": "ID of the trigger that the popover is associated with.\nThis is useful in conjuntion with the `open` prop to create a controlled popover.\nThere's no need to specify this prop when the popover is uncontrolled (i.e. when the `open` prop is not set).",
"detailedType": "string | null | undefined"
},
"disabled": {
"type": "boolean",
"default": "false",
"description": "Whether the component should ignore user interaction.",
"detailedType": "boolean | undefined"
},
"openOnHover": {
"type": "boolean",
"default": "true",
"description": "Whether the submenu should open when the trigger is hovered.",
"detailedType": "boolean | undefined"
},
"delay": {
"type": "number",
"default": "100",
"description": "How long to wait before the menu may be opened on hover. Specified in milliseconds.\n\nRequires the `openOnHover` prop.",
"detailedType": "number | undefined"
},
"closeDelay": {
"type": "number",
"default": "0",
"description": "How long to wait before closing the menu that was opened on hover.\nSpecified in milliseconds.\n\nRequires the `openOnHover` prop.",
"detailedType": "number | undefined"
},
"loop": {
"type": "boolean",
"default": "true",
Expand All @@ -71,9 +68,9 @@
"detailedType": "'horizontal' | 'vertical' | undefined"
},
"children": {
"type": "ReactNode",
"required": true,
"detailedType": "React.ReactNode"
"type": "ReactNode | PayloadChildRenderFunction<unknown>",
"description": "The content of the popover.\nThis can be a regular React node or a render function that receives the `payload` of the active trigger.",
"detailedType": "| React.ReactNode\n| ((arg: { payload: unknown }) => ReactNode)"
}
},
"dataAttributes": {},
Expand Down
23 changes: 23 additions & 0 deletions docs/reference/generated/menu-submenu-trigger.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,29 @@
"description": "Whether the component renders a native `<button>` element when replacing it\nvia the `render` prop.\nSet to `true` if the rendered element is a native button.",
"detailedType": "boolean | undefined"
},
"disabled": {
"type": "boolean",
"default": "false",
"description": "Whether the component should ignore user interaction.",
"detailedType": "boolean | undefined"
},
"openOnHover": {
"type": "boolean",
"description": "Whether the menu should also open when the trigger is hovered.",
"detailedType": "boolean | undefined"
},
"delay": {
"type": "number",
"default": "100",
"description": "How long to wait before the menu may be opened on hover. Specified in milliseconds.\n\nRequires the `openOnHover` prop.",
"detailedType": "number | undefined"
},
"closeDelay": {
"type": "number",
"default": "0",
"description": "How long to wait before closing the menu that was opened on hover.\nSpecified in milliseconds.\n\nRequires the `openOnHover` prop.",
"detailedType": "number | undefined"
},
"id": {
"type": "string",
"detailedType": "string | undefined"
Expand Down
27 changes: 27 additions & 0 deletions docs/reference/generated/menu-trigger.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,45 @@
"name": "MenuTrigger",
"description": "A button that opens the menu.\nRenders a `<button>` element.",
"props": {
"handle": {
"type": "Menu.Handle<Payload>",
"description": "A handle to associate the trigger with a menu.",
"detailedType": "{} | undefined"
},
"nativeButton": {
"type": "boolean",
"default": "true",
"description": "Whether the component renders a native `<button>` element when replacing it\nvia the `render` prop.\nSet to `false` if the rendered element is not a button (e.g. `<div>`).",
"detailedType": "boolean | undefined"
},
"payload": {
"type": "Payload",
"description": "A payload to pass to the menu when it is opened.",
"detailedType": "Payload | undefined"
},
"disabled": {
"type": "boolean",
"default": "false",
"description": "Whether the component should ignore user interaction.",
"detailedType": "boolean | undefined"
},
"openOnHover": {
"type": "boolean",
"description": "Whether the menu should also open when the trigger is hovered.",
"detailedType": "boolean | undefined"
},
"delay": {
"type": "number",
"default": "100",
"description": "How long to wait before the menu may be opened on hover. Specified in milliseconds.\n\nRequires the `openOnHover` prop.",
"detailedType": "number | undefined"
},
"closeDelay": {
"type": "number",
"default": "0",
"description": "How long to wait before closing the menu that was opened on hover.\nSpecified in milliseconds.\n\nRequires the `openOnHover` prop.",
"detailedType": "number | undefined"
},
"children": {
"type": "ReactNode",
"detailedType": "React.ReactNode"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,12 @@ export default function MenuFullyFeatured() {
return (
<div>
<h1>Fully featured menu</h1>
<Menu.Root
openOnHover={settings.openOnHover}
modal={settings.modal}
disabled={settings.disabled}
>
<Menu.Root modal={settings.modal} disabled={settings.disabled}>
<Menu.Trigger
className={classes.Button}
render={triggerRender}
nativeButton={triggerRender === undefined}
openOnHover={settings.openOnHover}
>
Menu <ChevronDownIcon className={classes.ButtonIcon} />
</Menu.Trigger>
Expand Down
10 changes: 6 additions & 4 deletions docs/src/app/(private)/experiments/menu/menu-horizontal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ export default function NestedMenu() {
<Menu.Portal>
<Menu.Positioner side="bottom" align="start" sideOffset={6} anchor={containerRef}>
<Menu.Popup className={styles.MenuRootPopup}>
<Menu.SubmenuRoot openOnHover={false}>
<Menu.SubmenuTrigger className={styles.SubmenuTrigger}>
<Menu.SubmenuRoot>
<Menu.SubmenuTrigger openOnHover={false} className={styles.SubmenuTrigger}>
Text color
</Menu.SubmenuTrigger>
<Menu.Portal>
Expand Down Expand Up @@ -49,8 +49,10 @@ export default function NestedMenu() {
</Menu.Portal>
</Menu.SubmenuRoot>

<Menu.SubmenuRoot openOnHover={false}>
<Menu.SubmenuTrigger className={styles.SubmenuTrigger}>Style</Menu.SubmenuTrigger>
<Menu.SubmenuRoot>
<Menu.SubmenuTrigger openOnHover={false} className={styles.SubmenuTrigger}>
Style
</Menu.SubmenuTrigger>
<Menu.Portal>
<Menu.Positioner align="start" side="bottom" sideOffset={12}>
<Menu.Popup className={styles.MenuPopup}>
Expand Down
Loading
Loading