@sproutsocial/seeds-react-menu 1.12.2 → 1.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +13 -13
- package/CHANGELOG.md +16 -0
- package/dist/esm/v3/index.js +517 -58
- package/dist/esm/v3/index.js.map +1 -1
- package/dist/index.d.mts +22 -22
- package/dist/index.d.ts +22 -22
- package/dist/v3/index.d.mts +160 -50
- package/dist/v3/index.d.ts +160 -50
- package/dist/v3/index.js +522 -57
- package/dist/v3/index.js.map +1 -1
- package/package.json +1 -1
- package/src/v3/ActionMenu.stories.tsx +260 -0
- package/src/v3/ActionMenu.tsx +345 -0
- package/src/v3/ActionMenuStyles.tsx +153 -0
- package/src/v3/Common/SelectStyles.tsx +4 -3
- package/src/v3/ModalStories.stories.tsx +159 -0
- package/src/v3/MultiCombobox.stories.tsx +17 -0
- package/src/v3/MultiCombobox.tsx +24 -13
- package/src/v3/MultiSearchableSelect.stories.tsx +18 -0
- package/src/v3/MultiSearchableSelect.tsx +214 -55
- package/src/v3/MultiSelect.stories.tsx +17 -0
- package/src/v3/MultiSelect.tsx +3 -2
- package/src/v3/SingleCombobox.stories.tsx +17 -0
- package/src/v3/SingleCombobox.tsx +3 -1
- package/src/v3/SingleSearchableSelect.stories.tsx +18 -0
- package/src/v3/SingleSearchableSelect.tsx +3 -1
- package/src/v3/SingleSelect.stories.tsx +17 -0
- package/src/v3/SingleSelect.tsx +3 -2
- package/src/v3/index.ts +1 -0
package/package.json
CHANGED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
3
|
+
import { action } from "@storybook/addon-actions";
|
|
4
|
+
import { Icon } from "@sproutsocial/seeds-react-icon";
|
|
5
|
+
import { Box } from "@sproutsocial/seeds-react-box";
|
|
6
|
+
import { Button } from "@sproutsocial/seeds-react-button";
|
|
7
|
+
import {
|
|
8
|
+
ActionMenu,
|
|
9
|
+
MenuGroup,
|
|
10
|
+
MenuItem,
|
|
11
|
+
MenuRadioGroup,
|
|
12
|
+
MenuSub,
|
|
13
|
+
MenuSubTrigger,
|
|
14
|
+
} from "./ActionMenu";
|
|
15
|
+
|
|
16
|
+
const meta: Meta<typeof ActionMenu> = {
|
|
17
|
+
title: "Really Under Development/V3/ActionMenu",
|
|
18
|
+
component: ActionMenu,
|
|
19
|
+
decorators: [
|
|
20
|
+
(Story) => (
|
|
21
|
+
<Box>
|
|
22
|
+
<Story />
|
|
23
|
+
</Box>
|
|
24
|
+
),
|
|
25
|
+
],
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export default meta;
|
|
29
|
+
type Story = StoryObj<typeof ActionMenu>;
|
|
30
|
+
|
|
31
|
+
// ─── Basic ────────────────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
export const Basic: Story = {
|
|
34
|
+
name: "Basic",
|
|
35
|
+
render: () => (
|
|
36
|
+
<ActionMenu trigger={<Button appearance="secondary">Open Menu</Button>}>
|
|
37
|
+
<MenuItem onClick={action("Compose")}>Compose</MenuItem>
|
|
38
|
+
<MenuItem onClick={action("View Messages")}>View Messages</MenuItem>
|
|
39
|
+
<MenuItem onClick={action("Analyze post")}>Analyze post</MenuItem>
|
|
40
|
+
</ActionMenu>
|
|
41
|
+
),
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// ─── With Groups ──────────────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
export const WithGroups: Story = {
|
|
47
|
+
name: "With Groups",
|
|
48
|
+
render: () => (
|
|
49
|
+
<ActionMenu trigger={<Button appearance="secondary">Open Menu</Button>}>
|
|
50
|
+
<MenuGroup label="Actions" showSeparator>
|
|
51
|
+
<MenuItem onClick={action("Compose")}>Compose</MenuItem>
|
|
52
|
+
<MenuItem onClick={action("View Messages")}>View Messages</MenuItem>
|
|
53
|
+
</MenuGroup>
|
|
54
|
+
<MenuGroup label="Settings">
|
|
55
|
+
<MenuItem onClick={action("Settings")}>Settings</MenuItem>
|
|
56
|
+
<MenuItem onClick={action("Help")}>Help</MenuItem>
|
|
57
|
+
</MenuGroup>
|
|
58
|
+
</ActionMenu>
|
|
59
|
+
),
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// ─── With Icons ───────────────────────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
export const WithIcons: Story = {
|
|
65
|
+
name: "With Icons",
|
|
66
|
+
render: () => (
|
|
67
|
+
<ActionMenu trigger={<Button appearance="secondary">Open Menu</Button>}>
|
|
68
|
+
<MenuItem onClick={action("Compose")}>
|
|
69
|
+
<Box display="flex" alignItems="center" gap="300">
|
|
70
|
+
<Icon name="pencil-to-square-outline" aria-hidden />
|
|
71
|
+
Compose
|
|
72
|
+
</Box>
|
|
73
|
+
</MenuItem>
|
|
74
|
+
<MenuItem onClick={action("View Messages")}>
|
|
75
|
+
<Box display="flex" alignItems="center" gap="300">
|
|
76
|
+
<Icon name="envelope-outline" aria-hidden />
|
|
77
|
+
View Messages
|
|
78
|
+
</Box>
|
|
79
|
+
</MenuItem>
|
|
80
|
+
<MenuItem href="https://sproutsocial.com" target="_blank">
|
|
81
|
+
<Box display="flex" alignItems="center" gap="300">
|
|
82
|
+
<Icon name="circle-question-outline" aria-hidden />
|
|
83
|
+
Visit Help Center
|
|
84
|
+
<Icon
|
|
85
|
+
name="arrow-right-up"
|
|
86
|
+
aria-label="open in a new window"
|
|
87
|
+
style={{ marginLeft: "auto" }}
|
|
88
|
+
/>
|
|
89
|
+
</Box>
|
|
90
|
+
</MenuItem>
|
|
91
|
+
</ActionMenu>
|
|
92
|
+
),
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
// ─── With Disabled Items ──────────────────────────────────────────────────────
|
|
96
|
+
|
|
97
|
+
export const WithDisabledItems: Story = {
|
|
98
|
+
name: "With Disabled Items",
|
|
99
|
+
render: () => (
|
|
100
|
+
<ActionMenu trigger={<Button appearance="secondary">Open Menu</Button>}>
|
|
101
|
+
<MenuItem onClick={action("Compose")}>Compose</MenuItem>
|
|
102
|
+
<MenuItem onClick={action("View Messages")} disabled>
|
|
103
|
+
View Messages (disabled)
|
|
104
|
+
</MenuItem>
|
|
105
|
+
<MenuItem onClick={action("Analyze post")}>Analyze post</MenuItem>
|
|
106
|
+
</ActionMenu>
|
|
107
|
+
),
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// ─── Checkbox Items ───────────────────────────────────────────────────────────
|
|
111
|
+
|
|
112
|
+
export const CheckboxItems: Story = {
|
|
113
|
+
name: "Checkbox Items",
|
|
114
|
+
render: () => {
|
|
115
|
+
const [highContrast, setHighContrast] = React.useState(false);
|
|
116
|
+
const [darkMode, setDarkMode] = React.useState(false);
|
|
117
|
+
const [notifications, setNotifications] = React.useState(true);
|
|
118
|
+
return (
|
|
119
|
+
<ActionMenu trigger={<Button appearance="secondary">Preferences</Button>}>
|
|
120
|
+
<MenuItem
|
|
121
|
+
type="checkbox"
|
|
122
|
+
checked={highContrast}
|
|
123
|
+
onCheckedChange={setHighContrast}
|
|
124
|
+
>
|
|
125
|
+
High contrast
|
|
126
|
+
</MenuItem>
|
|
127
|
+
<MenuItem
|
|
128
|
+
type="checkbox"
|
|
129
|
+
checked={darkMode}
|
|
130
|
+
onCheckedChange={setDarkMode}
|
|
131
|
+
>
|
|
132
|
+
Dark mode
|
|
133
|
+
</MenuItem>
|
|
134
|
+
<MenuItem
|
|
135
|
+
type="checkbox"
|
|
136
|
+
checked={notifications}
|
|
137
|
+
onCheckedChange={setNotifications}
|
|
138
|
+
>
|
|
139
|
+
Notifications
|
|
140
|
+
</MenuItem>
|
|
141
|
+
</ActionMenu>
|
|
142
|
+
);
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
// ─── Radio Items ──────────────────────────────────────────────────────────────
|
|
147
|
+
|
|
148
|
+
export const RadioItems: Story = {
|
|
149
|
+
name: "Radio Items",
|
|
150
|
+
render: () => {
|
|
151
|
+
const [theme, setTheme] = React.useState("system");
|
|
152
|
+
return (
|
|
153
|
+
<ActionMenu trigger={<Button appearance="secondary">Theme</Button>}>
|
|
154
|
+
<MenuGroup label="Appearance">
|
|
155
|
+
<MenuRadioGroup value={theme} onValueChange={setTheme}>
|
|
156
|
+
<MenuItem type="radio" value="light">
|
|
157
|
+
Light
|
|
158
|
+
</MenuItem>
|
|
159
|
+
<MenuItem type="radio" value="dark">
|
|
160
|
+
Dark
|
|
161
|
+
</MenuItem>
|
|
162
|
+
<MenuItem type="radio" value="system">
|
|
163
|
+
System default
|
|
164
|
+
</MenuItem>
|
|
165
|
+
</MenuRadioGroup>
|
|
166
|
+
</MenuGroup>
|
|
167
|
+
</ActionMenu>
|
|
168
|
+
);
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
// ─── Controlled Open ──────────────────────────────────────────────────────────
|
|
173
|
+
|
|
174
|
+
export const ControlledOpen: Story = {
|
|
175
|
+
name: "Controlled Open",
|
|
176
|
+
render: () => {
|
|
177
|
+
const [open, setOpen] = React.useState(false);
|
|
178
|
+
return (
|
|
179
|
+
<Box>
|
|
180
|
+
<Button appearance="primary" onClick={() => setOpen(true)} mb={300}>
|
|
181
|
+
Open programmatically
|
|
182
|
+
</Button>
|
|
183
|
+
<ActionMenu
|
|
184
|
+
trigger={<Button appearance="secondary">Menu</Button>}
|
|
185
|
+
open={open}
|
|
186
|
+
onOpenChange={(isOpen) => {
|
|
187
|
+
setOpen(isOpen);
|
|
188
|
+
}}
|
|
189
|
+
>
|
|
190
|
+
<MenuItem onClick={action("Compose")}>Compose</MenuItem>
|
|
191
|
+
<MenuItem onClick={action("View Messages")}>View Messages</MenuItem>
|
|
192
|
+
<MenuItem onClick={action("Analyze post")}>Analyze post</MenuItem>
|
|
193
|
+
</ActionMenu>
|
|
194
|
+
</Box>
|
|
195
|
+
);
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
// ─── Nested Submenu ───────────────────────────────────────────────────────────
|
|
200
|
+
|
|
201
|
+
export const NestedSubmenu: Story = {
|
|
202
|
+
name: "Nested Submenu",
|
|
203
|
+
render: () => (
|
|
204
|
+
<ActionMenu trigger={<Button appearance="secondary">Open Menu</Button>}>
|
|
205
|
+
<MenuItem onClick={action("Compose")}>Compose</MenuItem>
|
|
206
|
+
<MenuItem onClick={action("View Messages")}>View Messages</MenuItem>
|
|
207
|
+
<MenuSub trigger={<MenuSubTrigger>Share</MenuSubTrigger>}>
|
|
208
|
+
<MenuItem onClick={action("Share to Twitter")}>Twitter</MenuItem>
|
|
209
|
+
<MenuItem onClick={action("Share to LinkedIn")}>LinkedIn</MenuItem>
|
|
210
|
+
<MenuSub trigger={<MenuSubTrigger>More platforms</MenuSubTrigger>}>
|
|
211
|
+
<MenuItem onClick={action("Share to Facebook")}>Facebook</MenuItem>
|
|
212
|
+
<MenuItem onClick={action("Share to Instagram")}>Instagram</MenuItem>
|
|
213
|
+
</MenuSub>
|
|
214
|
+
</MenuSub>
|
|
215
|
+
<MenuItem onClick={action("Delete")} disabled>
|
|
216
|
+
Delete
|
|
217
|
+
</MenuItem>
|
|
218
|
+
</ActionMenu>
|
|
219
|
+
),
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
// ─── Mixed Item Types ─────────────────────────────────────────────────────────
|
|
223
|
+
|
|
224
|
+
export const MixedItemTypes: Story = {
|
|
225
|
+
name: "Mixed Item Types",
|
|
226
|
+
render: () => {
|
|
227
|
+
const [notifications, setNotifications] = React.useState(true);
|
|
228
|
+
const [theme, setTheme] = React.useState("system");
|
|
229
|
+
return (
|
|
230
|
+
<ActionMenu trigger={<Button appearance="secondary">Settings</Button>}>
|
|
231
|
+
<MenuGroup label="Actions" showSeparator>
|
|
232
|
+
<MenuItem onClick={action("Edit profile")}>Edit profile</MenuItem>
|
|
233
|
+
<MenuItem onClick={action("Manage team")}>Manage team</MenuItem>
|
|
234
|
+
</MenuGroup>
|
|
235
|
+
<MenuGroup label="Preferences" showSeparator>
|
|
236
|
+
<MenuItem
|
|
237
|
+
type="checkbox"
|
|
238
|
+
checked={notifications}
|
|
239
|
+
onCheckedChange={setNotifications}
|
|
240
|
+
>
|
|
241
|
+
Email notifications
|
|
242
|
+
</MenuItem>
|
|
243
|
+
</MenuGroup>
|
|
244
|
+
<MenuGroup label="Appearance">
|
|
245
|
+
<MenuRadioGroup value={theme} onValueChange={setTheme}>
|
|
246
|
+
<MenuItem type="radio" value="light">
|
|
247
|
+
Light
|
|
248
|
+
</MenuItem>
|
|
249
|
+
<MenuItem type="radio" value="dark">
|
|
250
|
+
Dark
|
|
251
|
+
</MenuItem>
|
|
252
|
+
<MenuItem type="radio" value="system">
|
|
253
|
+
System
|
|
254
|
+
</MenuItem>
|
|
255
|
+
</MenuRadioGroup>
|
|
256
|
+
</MenuGroup>
|
|
257
|
+
</ActionMenu>
|
|
258
|
+
);
|
|
259
|
+
},
|
|
260
|
+
};
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { Menu, type MenuRootChangeEventDetails } from "@base-ui/react/menu";
|
|
3
|
+
import Radio from "@sproutsocial/seeds-react-radio";
|
|
4
|
+
import Checkbox from "@sproutsocial/seeds-react-checkbox";
|
|
5
|
+
import { Icon } from "@sproutsocial/seeds-react-icon";
|
|
6
|
+
import { useSeedsPortalContainer } from "./SeedsPortal";
|
|
7
|
+
import {
|
|
8
|
+
ActionMenuTrigger,
|
|
9
|
+
ActionMenuPositioner,
|
|
10
|
+
ActionMenuPopup,
|
|
11
|
+
ActionMenuGroup,
|
|
12
|
+
ActionMenuGroupLabel,
|
|
13
|
+
ActionMenuSeparator,
|
|
14
|
+
StyledMenuItem,
|
|
15
|
+
StyledCheckboxItem,
|
|
16
|
+
StyledCheckboxItemIndicator,
|
|
17
|
+
StyledRadioItem,
|
|
18
|
+
StyledRadioItemIndicator,
|
|
19
|
+
StyledLinkItem,
|
|
20
|
+
StyledSubmenuTrigger,
|
|
21
|
+
} from "./ActionMenuStyles";
|
|
22
|
+
|
|
23
|
+
// ─── ActionMenu ───────────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
export interface ActionMenuProps {
|
|
26
|
+
/** The button element that opens the menu */
|
|
27
|
+
trigger: React.ReactElement;
|
|
28
|
+
/** Menu items and groups */
|
|
29
|
+
children: React.ReactNode;
|
|
30
|
+
/** Controlled open state */
|
|
31
|
+
open?: boolean;
|
|
32
|
+
/** Uncontrolled default open state */
|
|
33
|
+
defaultOpen?: boolean;
|
|
34
|
+
/** Called when open state changes */
|
|
35
|
+
onOpenChange?: (
|
|
36
|
+
open: boolean,
|
|
37
|
+
eventDetails: MenuRootChangeEventDetails
|
|
38
|
+
) => void;
|
|
39
|
+
/** Disable the trigger */
|
|
40
|
+
disabled?: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function ActionMenu({
|
|
44
|
+
trigger,
|
|
45
|
+
children,
|
|
46
|
+
open,
|
|
47
|
+
defaultOpen,
|
|
48
|
+
onOpenChange,
|
|
49
|
+
disabled,
|
|
50
|
+
}: ActionMenuProps) {
|
|
51
|
+
const { containerRef, portalContainer } = useSeedsPortalContainer();
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<Menu.Root
|
|
55
|
+
open={open}
|
|
56
|
+
defaultOpen={defaultOpen}
|
|
57
|
+
onOpenChange={onOpenChange}
|
|
58
|
+
>
|
|
59
|
+
<div ref={containerRef} style={{ position: "relative" }} />
|
|
60
|
+
<ActionMenuTrigger render={trigger} disabled={disabled} />
|
|
61
|
+
<Menu.Portal container={portalContainer}>
|
|
62
|
+
<ActionMenuPositioner sideOffset={8}>
|
|
63
|
+
<ActionMenuPopup>{children}</ActionMenuPopup>
|
|
64
|
+
</ActionMenuPositioner>
|
|
65
|
+
</Menu.Portal>
|
|
66
|
+
</Menu.Root>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ─── MenuGroup ────────────────────────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
export interface MenuGroupProps {
|
|
73
|
+
children: React.ReactNode;
|
|
74
|
+
/** Optional group label shown above items */
|
|
75
|
+
label?: string;
|
|
76
|
+
/** Render a separator line after this group */
|
|
77
|
+
showSeparator?: boolean;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function MenuGroup({
|
|
81
|
+
children,
|
|
82
|
+
label,
|
|
83
|
+
showSeparator = false,
|
|
84
|
+
}: MenuGroupProps) {
|
|
85
|
+
return (
|
|
86
|
+
<>
|
|
87
|
+
<ActionMenuGroup>
|
|
88
|
+
{label && <ActionMenuGroupLabel>{label}</ActionMenuGroupLabel>}
|
|
89
|
+
{children}
|
|
90
|
+
</ActionMenuGroup>
|
|
91
|
+
{showSeparator && <ActionMenuSeparator />}
|
|
92
|
+
</>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ─── MenuRadioGroup ───────────────────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
export interface MenuRadioGroupProps {
|
|
99
|
+
children: React.ReactNode;
|
|
100
|
+
/** Controlled selected value */
|
|
101
|
+
value?: string;
|
|
102
|
+
/** Uncontrolled default value */
|
|
103
|
+
defaultValue?: string;
|
|
104
|
+
/** Called when selected value changes */
|
|
105
|
+
onValueChange?: (value: string) => void;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function MenuRadioGroup({
|
|
109
|
+
children,
|
|
110
|
+
value,
|
|
111
|
+
defaultValue,
|
|
112
|
+
onValueChange,
|
|
113
|
+
}: MenuRadioGroupProps) {
|
|
114
|
+
return (
|
|
115
|
+
<Menu.RadioGroup
|
|
116
|
+
value={value}
|
|
117
|
+
defaultValue={defaultValue}
|
|
118
|
+
onValueChange={onValueChange}
|
|
119
|
+
>
|
|
120
|
+
{children}
|
|
121
|
+
</Menu.RadioGroup>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ─── MenuItem ─────────────────────────────────────────────────────────────────
|
|
126
|
+
|
|
127
|
+
interface MenuItemActionProps {
|
|
128
|
+
type?: "action";
|
|
129
|
+
children: React.ReactNode;
|
|
130
|
+
onClick?: React.MouseEventHandler<HTMLElement>;
|
|
131
|
+
disabled?: boolean;
|
|
132
|
+
/** Close the menu when this item is clicked. Defaults to true. */
|
|
133
|
+
closeOnClick?: boolean;
|
|
134
|
+
/** Render as a link */
|
|
135
|
+
href?: string;
|
|
136
|
+
target?: string;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
interface MenuItemCheckboxProps {
|
|
140
|
+
type: "checkbox";
|
|
141
|
+
children: React.ReactNode;
|
|
142
|
+
/** Controlled checked state */
|
|
143
|
+
checked?: boolean;
|
|
144
|
+
/** Uncontrolled default checked state */
|
|
145
|
+
defaultChecked?: boolean;
|
|
146
|
+
onCheckedChange?: (checked: boolean) => void;
|
|
147
|
+
disabled?: boolean;
|
|
148
|
+
/** Close the menu when clicked. Defaults to false for checkbox items. */
|
|
149
|
+
closeOnClick?: boolean;
|
|
150
|
+
id?: string;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
interface MenuItemRadioProps {
|
|
154
|
+
type: "radio";
|
|
155
|
+
/** Value submitted to the parent MenuRadioGroup */
|
|
156
|
+
value: string;
|
|
157
|
+
children: React.ReactNode;
|
|
158
|
+
disabled?: boolean;
|
|
159
|
+
/** Close the menu when clicked. Defaults to false for radio items. */
|
|
160
|
+
closeOnClick?: boolean;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export type MenuItemProps =
|
|
164
|
+
| MenuItemActionProps
|
|
165
|
+
| MenuItemCheckboxProps
|
|
166
|
+
| MenuItemRadioProps;
|
|
167
|
+
|
|
168
|
+
export function MenuItem(props: MenuItemProps) {
|
|
169
|
+
const generatedId = React.useId();
|
|
170
|
+
|
|
171
|
+
if (props.type === "checkbox") {
|
|
172
|
+
const {
|
|
173
|
+
children,
|
|
174
|
+
checked,
|
|
175
|
+
defaultChecked,
|
|
176
|
+
onCheckedChange,
|
|
177
|
+
disabled,
|
|
178
|
+
closeOnClick = false,
|
|
179
|
+
id,
|
|
180
|
+
} = props;
|
|
181
|
+
const itemId = id ?? `checkbox-menu-item-${generatedId}`;
|
|
182
|
+
return (
|
|
183
|
+
<StyledCheckboxItem
|
|
184
|
+
checked={checked}
|
|
185
|
+
defaultChecked={defaultChecked}
|
|
186
|
+
onCheckedChange={onCheckedChange}
|
|
187
|
+
disabled={disabled}
|
|
188
|
+
closeOnClick={closeOnClick}
|
|
189
|
+
>
|
|
190
|
+
<StyledCheckboxItemIndicator
|
|
191
|
+
keepMounted
|
|
192
|
+
render={(_indicatorProps, state) => (
|
|
193
|
+
<Checkbox
|
|
194
|
+
checked={state.checked}
|
|
195
|
+
onChange={() => {}}
|
|
196
|
+
tabIndex={-1}
|
|
197
|
+
id={itemId}
|
|
198
|
+
name="menu-checkbox-item"
|
|
199
|
+
/>
|
|
200
|
+
)}
|
|
201
|
+
/>
|
|
202
|
+
{children}
|
|
203
|
+
</StyledCheckboxItem>
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (props.type === "radio") {
|
|
208
|
+
const { children, value, disabled, closeOnClick = false } = props;
|
|
209
|
+
return (
|
|
210
|
+
<StyledRadioItem
|
|
211
|
+
value={value}
|
|
212
|
+
disabled={disabled}
|
|
213
|
+
closeOnClick={closeOnClick}
|
|
214
|
+
>
|
|
215
|
+
<StyledRadioItemIndicator
|
|
216
|
+
keepMounted
|
|
217
|
+
render={(_indicatorProps, state) => (
|
|
218
|
+
<Radio
|
|
219
|
+
checked={state.checked}
|
|
220
|
+
onChange={() => {}}
|
|
221
|
+
tabIndex={-1}
|
|
222
|
+
id={`radio-menu-item-${value}`}
|
|
223
|
+
name="menu-radio-item"
|
|
224
|
+
/>
|
|
225
|
+
)}
|
|
226
|
+
/>
|
|
227
|
+
{children}
|
|
228
|
+
</StyledRadioItem>
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Default: action item
|
|
233
|
+
const {
|
|
234
|
+
children,
|
|
235
|
+
onClick,
|
|
236
|
+
disabled,
|
|
237
|
+
closeOnClick = true,
|
|
238
|
+
href,
|
|
239
|
+
target,
|
|
240
|
+
} = props as MenuItemActionProps;
|
|
241
|
+
|
|
242
|
+
if (href) {
|
|
243
|
+
return (
|
|
244
|
+
<StyledLinkItem href={href} target={target} closeOnClick={closeOnClick}>
|
|
245
|
+
{children}
|
|
246
|
+
</StyledLinkItem>
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return (
|
|
251
|
+
<StyledMenuItem
|
|
252
|
+
onClick={onClick}
|
|
253
|
+
disabled={disabled}
|
|
254
|
+
closeOnClick={closeOnClick}
|
|
255
|
+
>
|
|
256
|
+
{children}
|
|
257
|
+
</StyledMenuItem>
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// ─── MenuSub ──────────────────────────────────────────────────────────────────
|
|
262
|
+
|
|
263
|
+
export interface MenuSubProps {
|
|
264
|
+
/** The MenuSubTrigger element that opens the submenu */
|
|
265
|
+
trigger: React.ReactNode;
|
|
266
|
+
/** Submenu items */
|
|
267
|
+
children: React.ReactNode;
|
|
268
|
+
/** Controlled open state */
|
|
269
|
+
open?: boolean;
|
|
270
|
+
/** Uncontrolled default open state */
|
|
271
|
+
defaultOpen?: boolean;
|
|
272
|
+
/** Called when open state changes */
|
|
273
|
+
onOpenChange?: (
|
|
274
|
+
open: boolean,
|
|
275
|
+
eventDetails: MenuRootChangeEventDetails
|
|
276
|
+
) => void;
|
|
277
|
+
/**
|
|
278
|
+
* When true, pressing Escape closes the entire menu tree.
|
|
279
|
+
* When false (default), Escape only closes the submenu.
|
|
280
|
+
*/
|
|
281
|
+
closeParentOnEsc?: boolean;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export function MenuSub({
|
|
285
|
+
trigger,
|
|
286
|
+
children,
|
|
287
|
+
open,
|
|
288
|
+
defaultOpen,
|
|
289
|
+
onOpenChange,
|
|
290
|
+
closeParentOnEsc = false,
|
|
291
|
+
}: MenuSubProps) {
|
|
292
|
+
const { containerRef, portalContainer } = useSeedsPortalContainer();
|
|
293
|
+
|
|
294
|
+
return (
|
|
295
|
+
<Menu.SubmenuRoot
|
|
296
|
+
open={open}
|
|
297
|
+
defaultOpen={defaultOpen}
|
|
298
|
+
onOpenChange={onOpenChange}
|
|
299
|
+
closeParentOnEsc={closeParentOnEsc}
|
|
300
|
+
>
|
|
301
|
+
<div ref={containerRef} style={{ position: "relative" }} />
|
|
302
|
+
{trigger}
|
|
303
|
+
<Menu.Portal container={portalContainer}>
|
|
304
|
+
<ActionMenuPositioner sideOffset={getOffset} alignOffset={getOffset}>
|
|
305
|
+
<ActionMenuPopup>{children}</ActionMenuPopup>
|
|
306
|
+
</ActionMenuPositioner>
|
|
307
|
+
</Menu.Portal>
|
|
308
|
+
</Menu.SubmenuRoot>
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
function getOffset({ side }: { side: Menu.Positioner.Props["side"] }) {
|
|
312
|
+
return side === "top" || side === "bottom" ? 4 : -8;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// ─── MenuSubTrigger ───────────────────────────────────────────────────────────
|
|
316
|
+
|
|
317
|
+
export interface MenuSubTriggerProps {
|
|
318
|
+
children: React.ReactNode;
|
|
319
|
+
disabled?: boolean;
|
|
320
|
+
/** Custom label for keyboard text navigation */
|
|
321
|
+
label?: string;
|
|
322
|
+
/**
|
|
323
|
+
* Whether the submenu opens on hover.
|
|
324
|
+
* @default true
|
|
325
|
+
*/
|
|
326
|
+
openOnHover?: boolean;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export function MenuSubTrigger({
|
|
330
|
+
children,
|
|
331
|
+
disabled,
|
|
332
|
+
label,
|
|
333
|
+
openOnHover = true,
|
|
334
|
+
}: MenuSubTriggerProps) {
|
|
335
|
+
return (
|
|
336
|
+
<StyledSubmenuTrigger
|
|
337
|
+
disabled={disabled}
|
|
338
|
+
label={label}
|
|
339
|
+
openOnHover={openOnHover}
|
|
340
|
+
>
|
|
341
|
+
{children}
|
|
342
|
+
<Icon name="chevron-right-outline" fixedWidth aria-hidden />
|
|
343
|
+
</StyledSubmenuTrigger>
|
|
344
|
+
);
|
|
345
|
+
}
|