@sproutsocial/seeds-react-menu 0.5.0 → 1.0.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.
Files changed (104) hide show
  1. package/.turbo/turbo-build.log +10 -10
  2. package/CHANGELOG.md +72 -0
  3. package/dist/esm/index.js +1529 -440
  4. package/dist/esm/index.js.map +1 -1
  5. package/dist/index.d.mts +275 -92
  6. package/dist/index.d.ts +275 -92
  7. package/dist/index.js +1558 -444
  8. package/dist/index.js.map +1 -1
  9. package/package.json +15 -12
  10. package/src/ActionMenu/ActionMenu.stories.tsx +422 -0
  11. package/src/ActionMenu/ActionMenu.tsx +24 -27
  12. package/src/ActionMenu/ActionMenuTypes.ts +8 -7
  13. package/src/ActionMenu/__tests__/ActionMenu.test.tsx +17 -8
  14. package/src/ActionMenu/__tests__/ActionMenu.typetest.tsx +8 -8
  15. package/src/Autocomplete/Autocomplete.stories.tsx +38 -3
  16. package/src/Autocomplete/Autocomplete.tsx +15 -80
  17. package/src/Autocomplete/AutocompleteInput.tsx +18 -27
  18. package/src/Autocomplete/AutocompleteTokenInput.tsx +50 -0
  19. package/src/Autocomplete/AutocompleteTypes.ts +25 -12
  20. package/src/Autocomplete/MultiAutocomplete.tsx +188 -0
  21. package/src/Autocomplete/SingleAutocomplete.tsx +74 -0
  22. package/src/Autocomplete/__tests__/Autocomplete.test.tsx +207 -3
  23. package/src/Autocomplete/__tests__/Autocomplete.typetest.tsx +90 -8
  24. package/src/MenuContent/MenuContent.stories.tsx +164 -0
  25. package/src/MenuContent/MenuContent.tsx +28 -12
  26. package/src/MenuContent/MenuContentTypes.ts +6 -4
  27. package/src/MenuContent/__tests__/MenuContent.typetest.tsx +8 -8
  28. package/src/MenuContext.tsx +192 -37
  29. package/src/MenuFooter/MenuFooter.stories.tsx +202 -0
  30. package/src/MenuFooter/__tests__/MenuFooter.test.tsx +16 -11
  31. package/src/MenuFooter/styles.tsx +1 -1
  32. package/src/MenuGroup/MenuGroup.feature +16 -16
  33. package/src/MenuGroup/MenuGroup.stories.tsx +248 -0
  34. package/src/MenuGroup/MenuGroup.tsx +12 -7
  35. package/src/MenuGroup/MenuGroupTypes.ts +6 -2
  36. package/src/MenuGroup/__tests__/MenuGroup.test.tsx +6 -6
  37. package/src/MenuGroup/__tests__/MenuGroup.typetest.tsx +7 -7
  38. package/src/MenuGroup/styles.tsx +4 -2
  39. package/src/MenuHeader/MenuHeader.stories.tsx +276 -0
  40. package/src/MenuHeader/MenuHeader.tsx +12 -7
  41. package/src/MenuHeader/__tests__/MenuHeader.test.tsx +23 -13
  42. package/src/MenuHeader/styles.tsx +22 -14
  43. package/src/MenuItem/MenuItem.stories.tsx +507 -0
  44. package/src/MenuItem/MenuItem.tsx +23 -9
  45. package/src/MenuItem/MenuItemTypes.ts +9 -4
  46. package/src/MenuItem/__tests__/MenuItem.test.tsx +6 -6
  47. package/src/MenuItem/styles.tsx +23 -5
  48. package/src/MenuItem/useOptionProps.tsx +11 -9
  49. package/src/MenuLabel.tsx +2 -2
  50. package/src/MenuRoot/MenuRoot.tsx +52 -24
  51. package/src/MenuRoot/__tests__/MenuRoot.test.tsx +59 -32
  52. package/src/MenuSearchInput/MenuSearchInput.tsx +85 -0
  53. package/src/MenuSearchInput/__tests__/MenuSearchInput.test.tsx +157 -0
  54. package/src/MenuSearchInput/__tests__/MenuSearchInput.typetest.tsx +31 -0
  55. package/src/MenuSearchInput/index.ts +1 -0
  56. package/src/MenuSearchTokenInput/MenuSearchTokenInput.tsx +108 -0
  57. package/src/MenuSearchTokenInput/__tests__/MenuSearchTokenInput.test.tsx +247 -0
  58. package/src/MenuSearchTokenInput/__tests__/MenuSearchTokenInput.typetest.tsx +33 -0
  59. package/src/MenuSearchTokenInput/index.ts +1 -0
  60. package/src/MenuToggleButton/MenuToggleButton.stories.tsx +72 -0
  61. package/src/{MenuToggleButton.tsx → MenuToggleButton/MenuToggleButton.tsx} +6 -5
  62. package/src/MenuToggleButton/index.ts +1 -0
  63. package/src/MenuTokenInput.tsx +104 -0
  64. package/src/MultiSelectMenu/MultiSelectMenu.stories.tsx +222 -0
  65. package/src/MultiSelectMenu/MultiSelectMenu.tsx +38 -68
  66. package/src/MultiSelectMenu/MultiSelectMenuTypes.ts +8 -6
  67. package/src/MultiSelectMenu/__tests__/MultiSelectMenu.test.tsx +46 -17
  68. package/src/MultiSelectMenu/__tests__/MultiSelectMenu.typetest.tsx +6 -6
  69. package/src/NestedMenu/NestedMenu.feature +109 -0
  70. package/src/NestedMenu/NestedMenu.stories.tsx +157 -0
  71. package/src/NestedMenu/NestedMenu.tsx +102 -0
  72. package/src/NestedMenu/NestedMenuContent.tsx +75 -0
  73. package/src/NestedMenu/NestedMenuContext.tsx +56 -0
  74. package/src/NestedMenu/NestedMenuHeader.tsx +25 -0
  75. package/src/NestedMenu/NestedMenuItem.tsx +41 -0
  76. package/src/NestedMenu/NestedMenuTypes.ts +47 -0
  77. package/src/NestedMenu/__tests__/NestedMenu.test.tsx +31 -0
  78. package/src/NestedMenu/__tests__/NestedMenu.typetest.tsx +134 -0
  79. package/src/NestedMenu/index.ts +5 -0
  80. package/src/SingleSelectMenu/SingleSelectMenu.stories.tsx +233 -0
  81. package/src/SingleSelectMenu/SingleSelectMenu.tsx +23 -26
  82. package/src/SingleSelectMenu/SingleSelectMenuTypes.ts +7 -5
  83. package/src/SingleSelectMenu/__tests__/SingleSelectMenu.typetest.tsx +6 -6
  84. package/src/SubmenuItem/SubmenuItem.feature +82 -0
  85. package/src/SubmenuItem/SubmenuItem.stories.tsx +163 -0
  86. package/src/SubmenuItem/SubmenuItem.tsx +258 -0
  87. package/src/SubmenuItem/SubmenuItemTypes.ts +24 -0
  88. package/src/SubmenuItem/__tests__/SubmenuItem.test.tsx +3 -0
  89. package/src/SubmenuItem/__tests__/SubmenuItem.typetest.tsx +112 -0
  90. package/src/SubmenuItem/index.ts +2 -0
  91. package/src/hooks/useDownshiftDefaults.tsx +117 -0
  92. package/src/hooks/useItemFiltering.tsx +68 -0
  93. package/src/hooks/useMenuChildren.tsx +205 -131
  94. package/src/index.ts +6 -0
  95. package/src/menuTestingUtils.tsx +21 -10
  96. package/src/reducers/nestedMenuStateReducer.tsx +23 -0
  97. package/src/reducers/useComboboxStateReducer.tsx +26 -8
  98. package/src/reducers/useSelectStateReducer.tsx +24 -6
  99. package/src/storybookUtilities/menu-storybook-components.tsx +5 -3
  100. package/src/types.ts +38 -30
  101. package/src/utils/downshiftDefaults.ts +9 -0
  102. package/src/utils/getMultiSelectOverrides.ts +21 -0
  103. package/src/utils/reduceReducers.ts +19 -0
  104. package/src/utils/itemToString.ts +0 -5
@@ -0,0 +1,82 @@
1
+ Scenario: SubmenuItem is expandable via click
2
+ Given a SubmenuItem is not disabled
3
+ When the SubmenuItem is clicked
4
+ Then the submenu should be displayed
5
+
6
+ Scenario Outline: SubmenuItem can be expanded via keyboard
7
+ Given a SubmenuItem is focused
8
+ And the SubmenuItem is not disabled
9
+ When the SubmenuItem is triggered via <interaction>
10
+ Then the submenu should be displayed
11
+
12
+ Examples:
13
+ | interaction |
14
+ | pressing the Enter key |
15
+ | pressing the Space key |
16
+ | pressing the Right Arrow key |
17
+
18
+ Scenario Outline: SubmenuItem is disabled
19
+ Given a SubmenuItem is disabled
20
+ When the SubmenuItem is triggered via <interaction>
21
+ Then no events should fire
22
+ And the submenu should not be displayed
23
+
24
+ Examples:
25
+ | interaction |
26
+ | clicking |
27
+ | focusing and pressing the Enter key |
28
+ | focusing and pressing the Space key |
29
+ | focusing and pressing the Right Arrow key |
30
+
31
+ Scenario: SubmenuItem displays an arrow indicator
32
+ Given a SubmenuItem is rendered
33
+ Then it should display an arrow icon indicating a submenu
34
+
35
+ Scenario: SubmenuItem can be collapsed via keyboard
36
+ Given a SubmenuItem is expanded
37
+ And the submenu is displayed
38
+ When the Left Arrow key is pressed
39
+ Then the submenu should be hidden
40
+
41
+ Rule: API
42
+ Background:
43
+ Given an engineer is developing using the component
44
+
45
+ Scenario Outline: SubmenuItem accepts children
46
+ When <child component(s)> provided
47
+ Then the SubmenuItem should render the <component(s)>
48
+
49
+ Examples: child components
50
+ | child component(s) | component(s) |
51
+ | a Link is | Link |
52
+ | a Text is | Text |
53
+ | an Icon is | Icon |
54
+ | a Text then an Icon are | Text and Icon |
55
+ | an Icon and Text are | Icon and Text |
56
+
57
+ Scenario Outline: A SubmenuItem accepts system props
58
+ When a system prop from the category <systemPropCategory> is passed a value
59
+ Then the SubmenuItem will have <systemPropName> styling
60
+
61
+ Examples: System Props
62
+ | systemPropCategory | systemPropName |
63
+ | color | bg |
64
+ | border | borderColor |
65
+ | layout | display |
66
+ | position | zIndex |
67
+ | flexbox | flex |
68
+ | grid | gridGap |
69
+ | space | p |
70
+
71
+ Scenario Outline: SubmenuItem validates prop types
72
+ When any <propName> is passed to the SubmenuItem
73
+ Then the SubmenuItem should check that each <propName> is of the expected <propType>
74
+
75
+ Examples: Prop Types
76
+ | propName | propType |
77
+ | children | ReactNode |
78
+ | disabled | boolean |
79
+ | elevation | low, medium, or high |
80
+ | href | string |
81
+ | onClick | React.MouseEventHandler |
82
+ | role | string |
@@ -0,0 +1,163 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { action } from "@storybook/addon-actions";
3
+ import { Box } from "@sproutsocial/seeds-react-box";
4
+ import {
5
+ MenuItem,
6
+ MenuToggleButton,
7
+ MenuContent,
8
+ ActionMenu,
9
+ MenuGroup,
10
+ SubmenuItem,
11
+ type TypeMenuItemProps,
12
+ } from "../index";
13
+
14
+ type SubmenuItemStoryArgs = TypeMenuItemProps & {
15
+ text?: string;
16
+ };
17
+
18
+ type Story = StoryObj<SubmenuItemStoryArgs>;
19
+
20
+ const meta: Meta<SubmenuItemStoryArgs> = {
21
+ title: "Under Development/Submenu",
22
+ decorators: [(Story) => <Story />],
23
+ };
24
+
25
+ export default meta;
26
+
27
+ const StoryMenuToggleButton = ({ appearance, triggerText }) => {
28
+ return (
29
+ <MenuToggleButton appearance={appearance} alignSelf="flex-start">
30
+ {triggerText}
31
+ </MenuToggleButton>
32
+ );
33
+ };
34
+
35
+ const handleOnClick = (e: React.MouseEvent<HTMLElement>, itemName: string) => {
36
+ action(`${itemName}`)(e);
37
+ };
38
+
39
+ const submenuItems: TypeMenuItemProps[] = [
40
+ { id: "item1", children: "Option 1" },
41
+ { id: "item2", children: "Option 2" },
42
+ { id: "item3", children: "Option 3" },
43
+ ];
44
+
45
+ export const SubmenuItemStory: Story = {
46
+ name: "Simple Action Menu",
47
+ render: (args) => {
48
+ return (
49
+ <ActionMenu
50
+ {...args}
51
+ menuToggleElement={
52
+ <StoryMenuToggleButton
53
+ appearance={args["Trigger Appearance"]}
54
+ triggerText={"Button"}
55
+ />
56
+ }
57
+ onStateChange={(changes) => action("onStateChange")(changes)}
58
+ >
59
+ <MenuContent>
60
+ <MenuGroup id="actions">
61
+ <MenuItem onClick={(e) => handleOnClick(e, "Compose")} id="Compose">
62
+ Compose
63
+ </MenuItem>
64
+ <SubmenuItem
65
+ id="sj"
66
+ MenuComponent={ActionMenu}
67
+ menuProps={{
68
+ menuItems: submenuItems,
69
+ onStateChange: (changes) =>
70
+ action("submenu onStateChange")(changes),
71
+ }}
72
+ >
73
+ Basic Item
74
+ </SubmenuItem>
75
+ <MenuItem
76
+ onClick={(e) => handleOnClick(e, "View Messages")}
77
+ id="View Messages"
78
+ >
79
+ View Messages
80
+ </MenuItem>
81
+ <MenuItem
82
+ onClick={(e) => handleOnClick(e, "Analyze post")}
83
+ id="Analyze post"
84
+ >
85
+ Analyze post
86
+ </MenuItem>
87
+ <MenuItem
88
+ href="https://sproutsocial.com"
89
+ target="_blank"
90
+ id="settings"
91
+ >
92
+ This Opens a Link
93
+ </MenuItem>
94
+ </MenuGroup>
95
+ </MenuContent>
96
+ </ActionMenu>
97
+ );
98
+ },
99
+ };
100
+
101
+ const children = (
102
+ <MenuContent>
103
+ <MenuItem id="sub-menu-item-1">Item 1</MenuItem>
104
+ <SubmenuItem
105
+ id="subsub"
106
+ MenuComponent={ActionMenu}
107
+ menuProps={{ menuItems: submenuItems }}
108
+ >
109
+ Another one
110
+ </SubmenuItem>
111
+ </MenuContent>
112
+ );
113
+
114
+ export const NestedSubmenuItemStory: Story = {
115
+ name: "Nested Submenus",
116
+ render: (args) => {
117
+ return (
118
+ <ActionMenu
119
+ {...args}
120
+ menuToggleElement={
121
+ <StoryMenuToggleButton
122
+ appearance={args["Trigger Appearance"]}
123
+ triggerText={"Button"}
124
+ />
125
+ }
126
+ >
127
+ <MenuContent>
128
+ <MenuGroup id="actions">
129
+ <MenuItem onClick={(e) => handleOnClick(e, "Compose")} id="Compose">
130
+ Compose
131
+ </MenuItem>
132
+ <SubmenuItem
133
+ id="sj"
134
+ MenuComponent={ActionMenu}
135
+ menuProps={{ children }}
136
+ >
137
+ Basic Item
138
+ </SubmenuItem>
139
+ <MenuItem
140
+ onClick={(e) => handleOnClick(e, "View Messages")}
141
+ id="View Messages"
142
+ >
143
+ View Messages
144
+ </MenuItem>
145
+ <MenuItem
146
+ onClick={(e) => handleOnClick(e, "Analyze post")}
147
+ id="Analyze post"
148
+ >
149
+ Analyze post
150
+ </MenuItem>
151
+ <MenuItem
152
+ href="https://sproutsocial.com"
153
+ target="_blank"
154
+ id="settings"
155
+ >
156
+ This Opens a Link
157
+ </MenuItem>
158
+ </MenuGroup>
159
+ </MenuContent>
160
+ </ActionMenu>
161
+ );
162
+ },
163
+ };
@@ -0,0 +1,258 @@
1
+ import React from "react";
2
+ import { Icon } from "@sproutsocial/seeds-react-icon";
3
+ import { Box } from "@sproutsocial/seeds-react-box";
4
+ import { mergeRefs } from "@sproutsocial/seeds-react-utilities";
5
+ import type { TypeInternalItemProps } from "../types";
6
+ import { getChildrenFromMenuItems } from "../hooks/useMenuChildren";
7
+ import { useMenuContentContext, useMenuToggleContext } from "../MenuContext";
8
+ import {
9
+ MenuItem,
10
+ MenuItemActionRight,
11
+ type TypeMenuItemProps,
12
+ } from "../MenuItem";
13
+ import type {
14
+ TypeSubmenuItemProps,
15
+ TypeSubmenuValidMenuComponentProps,
16
+ } from "./SubmenuItemTypes";
17
+
18
+ const SubmenuItemTrigger = ({
19
+ id,
20
+ children,
21
+ innerRef,
22
+ ...rest
23
+ }: TypeMenuItemProps) => {
24
+ const { getToggleButtonProps, getDropdownProps, ref, ariaProps } =
25
+ useMenuToggleContext();
26
+ const toggleRef =
27
+ ref && innerRef
28
+ ? mergeRefs([ref, innerRef].filter(Boolean))
29
+ : ref || innerRef;
30
+ return (
31
+ <MenuItem
32
+ {...rest}
33
+ {...getToggleButtonProps?.(
34
+ {
35
+ ...(getDropdownProps?.({
36
+ ref: toggleRef,
37
+ preventKeyAction: true,
38
+ }) || { ref: toggleRef }),
39
+ refKey: "innerRef",
40
+ role: "menuitem",
41
+ ...ariaProps,
42
+ },
43
+ { suppressRefError: true }
44
+ )}
45
+ id={id}
46
+ >
47
+ <Box display="flex" justifyContent="space-between">
48
+ {children}
49
+ <MenuItemActionRight>
50
+ <Icon name="chevron-right-outline" />
51
+ </MenuItemActionRight>
52
+ </Box>
53
+ </MenuItem>
54
+ );
55
+ };
56
+
57
+ const doesRefContainEventTarget = (
58
+ ref: React.RefObject<HTMLElement | undefined>,
59
+ event: MouseEvent
60
+ ) => {
61
+ return (
62
+ ref.current &&
63
+ event.target instanceof Node &&
64
+ ref.current.contains(event.target)
65
+ );
66
+ };
67
+
68
+ const SubmenuItem = <
69
+ I extends TypeMenuItemProps,
70
+ P extends TypeSubmenuValidMenuComponentProps<I>
71
+ >({
72
+ MenuComponent,
73
+ menuProps: {
74
+ children: menuChildren,
75
+ menuItems,
76
+ MenuItemComponent,
77
+ popoutProps: { popperProps, ...popoutProps } = {},
78
+ onStateChange,
79
+ ...menuProps
80
+ },
81
+ // isOpen,
82
+ // onToggleSubmenu,
83
+ id,
84
+ children,
85
+ ...rest
86
+ }: TypeSubmenuItemProps<I, P>) => {
87
+ const {
88
+ highlightedIndex,
89
+ setHighlightedIndex,
90
+ itemsMap,
91
+ isOpen: parentIsOpen,
92
+ } = useMenuContentContext();
93
+ const [submenuIsFocused, setSubmenuIsFocused] = React.useState(false);
94
+ const [submenuIsOpen, setSubmenuIsOpen] = React.useState(false);
95
+ const [submenuIsActive, setSubmenuIsActive] = React.useState(false);
96
+ const submenuItemRef = React.useRef<HTMLElement>(null);
97
+ const submenuRef = React.useRef<HTMLDivElement>(null);
98
+
99
+ // get children for both use cases so it can be wrapped in a submenu div
100
+ const menuContent = menuItems
101
+ ? getChildrenFromMenuItems({ menuItems, MenuItemComponent })
102
+ : menuChildren;
103
+
104
+ const item = (itemsMap[id] || {
105
+ index: 0,
106
+ id,
107
+ }) as TypeInternalItemProps;
108
+
109
+ // React.useEffect(() => {
110
+ // debugger;
111
+ // // reset isOpen on render
112
+ // setSubmenuIsOpen(false);
113
+ // setSubmenuIsActive(false);
114
+ // }, []);
115
+
116
+ React.useEffect(() => {
117
+ if (highlightedIndex === item.index) {
118
+ // Open submenu when item becomes highlighted
119
+ !submenuIsOpen && setSubmenuIsOpen(true);
120
+ } else if (submenuIsOpen && highlightedIndex !== -1) {
121
+ // Close submenu when another item is highlighted
122
+ setSubmenuIsOpen(false);
123
+ setSubmenuIsActive(false);
124
+ }
125
+ }, [highlightedIndex, item.index, submenuIsOpen, setSubmenuIsOpen]);
126
+
127
+ const onSubmenuStateChange = React.useCallback(
128
+ (state) => {
129
+ if (state.isOpen !== undefined) {
130
+ // Ensures the menu opens and closes when the state changes
131
+ setSubmenuIsOpen(state.isOpen);
132
+ if (!state.isOpen) {
133
+ // Reset the submenu focus and highlighted index when the submenu closes
134
+ setSubmenuIsFocused(false);
135
+ setHighlightedIndex?.(-1);
136
+ }
137
+ }
138
+ onStateChange?.(state);
139
+ },
140
+ [onStateChange, setSubmenuIsOpen]
141
+ );
142
+
143
+ // Callback for navigating the submenu wit harrow keys
144
+ const onSubmenuKeydown = (e: KeyboardEvent): void => {
145
+ switch (e.key) {
146
+ case "ArrowRight":
147
+ case " ":
148
+ case "Enter":
149
+ // Focus the submenu if item highlighted and right arrow key is pressed
150
+ if (!submenuIsFocused && submenuIsOpen) {
151
+ // stop propagation to avoid interacting with other components
152
+ e.stopPropagation();
153
+ setSubmenuIsFocused(true);
154
+ }
155
+ break;
156
+ case "ArrowLeft":
157
+ // Return focus to the parent menu if submenu is focused and left arrow key is pressed
158
+ if (submenuIsFocused) {
159
+ // stop propagation to avoid interacting with other components
160
+ e.stopPropagation();
161
+ setSubmenuIsFocused(false);
162
+ }
163
+ break;
164
+ default:
165
+ break;
166
+ }
167
+ };
168
+
169
+ const onMenuItemClick = () => {
170
+ const newIsActive = !submenuIsActive;
171
+ // Lock the submenu in an active open state when clicked
172
+ // or close the menu and unlock if it is open
173
+ setSubmenuIsActive(newIsActive);
174
+ setSubmenuIsOpen(newIsActive);
175
+ };
176
+
177
+ const onBodyClick = (e: MouseEvent): void => {
178
+ // Prevent closing the parent menu when clicking on the submenu item
179
+ if (doesRefContainEventTarget(submenuItemRef, e)) {
180
+ e.stopPropagation();
181
+ onMenuItemClick();
182
+ }
183
+ // Prevent closing other popouts when clicking on inside the submenu
184
+ // This doesn't work because it's overridden by the logic in Popout the closes when clicked outside
185
+ // if (doesRefContainEventTarget(submenuRef, e)) {
186
+ // e.stopPropagation();
187
+ // }
188
+ };
189
+
190
+ React.useEffect(() => {
191
+ if (document && document.body) {
192
+ document.body.addEventListener("click", onBodyClick, {
193
+ capture: true,
194
+ });
195
+ document.body.addEventListener("keydown", onSubmenuKeydown, {
196
+ capture: true,
197
+ });
198
+ return () => {
199
+ document.body.removeEventListener("click", onBodyClick, {
200
+ capture: true,
201
+ });
202
+ document.body.removeEventListener("keydown", onSubmenuKeydown, {
203
+ capture: true,
204
+ });
205
+ };
206
+ }
207
+ }, [document, onSubmenuKeydown]);
208
+
209
+ const subMenuContent = (
210
+ <div
211
+ ref={submenuRef}
212
+ onKeyDown={(e) => {
213
+ // prevent keydown on submenu triggering the parent menu
214
+ e.stopPropagation();
215
+ }}
216
+ tabIndex={-1}
217
+ >
218
+ {menuContent}
219
+ </div>
220
+ );
221
+
222
+ return (
223
+ <MenuComponent
224
+ {...({
225
+ ...menuProps,
226
+ children: subMenuContent,
227
+ menuToggleElement: (
228
+ <SubmenuItemTrigger
229
+ innerRef={submenuItemRef}
230
+ id={id}
231
+ children={children}
232
+ active={submenuIsActive}
233
+ {...rest}
234
+ />
235
+ ),
236
+ isOpen: submenuIsOpen,
237
+ onStateChange: onSubmenuStateChange,
238
+ popoutProps: {
239
+ placement: "right",
240
+ popperProps: {
241
+ modifiers: {
242
+ offset: {
243
+ offset: "0,18",
244
+ },
245
+ },
246
+ ...popperProps,
247
+ },
248
+ focusOnContent: submenuIsFocused,
249
+ ...popoutProps,
250
+ },
251
+ } as P)}
252
+ />
253
+ );
254
+ };
255
+
256
+ SubmenuItem.__MENU_PRIMITIVE_TYPE = "MenuItem";
257
+
258
+ export { SubmenuItem };
@@ -0,0 +1,24 @@
1
+ import type { TypeActionMenuProps } from "../ActionMenu";
2
+ import type { TypeSingleSelectMenuProps } from "../SingleSelectMenu";
3
+ import type { TypeMultiSelectMenuProps } from "../MultiSelectMenu";
4
+ import type { TypeMenuItemProps } from "../MenuItem";
5
+ import type { TypeMenuRootProps } from "../MenuRoot";
6
+ import type { TypeChildrenOrItems } from "../types";
7
+
8
+ export type TypeSubmenuValidMenuComponentProps<I extends TypeMenuItemProps> =
9
+ TypeChildrenOrItems<I> &
10
+ (
11
+ | TypeActionMenuProps<I>
12
+ | TypeSingleSelectMenuProps<I>
13
+ | TypeMultiSelectMenuProps<I>
14
+ );
15
+
16
+ export interface TypeSubmenuItemProps<
17
+ I extends TypeMenuItemProps,
18
+ P extends TypeSubmenuValidMenuComponentProps<I>
19
+ > extends TypeMenuItemProps {
20
+ isOpen?: boolean;
21
+ onToggleSubmenu?: (isOpen: boolean) => void;
22
+ MenuComponent: React.ComponentType<P>;
23
+ menuProps: Omit<P, "menuToggleElement">;
24
+ }
@@ -0,0 +1,3 @@
1
+ describe("SubmenuItem", () => {
2
+ xit("placeholder", () => {});
3
+ });
@@ -0,0 +1,112 @@
1
+ import { SubmenuItem } from "../SubmenuItem";
2
+ import { ActionMenu } from "../../index";
3
+
4
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
5
+ function SubmenuItemTypetest() {
6
+ return (
7
+ <>
8
+ {/* Valid SubmenuItem with minimal required props */}
9
+ <SubmenuItem id="test" MenuComponent={ActionMenu} menuProps={{}}>
10
+ Test
11
+ </SubmenuItem>
12
+ {/* Valid SubmenuItem allows href */}
13
+ <SubmenuItem
14
+ id="test"
15
+ href="test.com"
16
+ MenuComponent={ActionMenu}
17
+ menuProps={{}}
18
+ >
19
+ Test
20
+ </SubmenuItem>
21
+ {/* Valid SubmenuItem allows active prop */}
22
+ <SubmenuItem id="test" active MenuComponent={ActionMenu} menuProps={{}}>
23
+ Test
24
+ </SubmenuItem>
25
+ {/* Valid SubmenuItem allows active prop false */}
26
+ <SubmenuItem
27
+ id="test"
28
+ active={false}
29
+ MenuComponent={ActionMenu}
30
+ menuProps={{}}
31
+ >
32
+ Test
33
+ </SubmenuItem>
34
+ {/* Valid SubmenuItem allows inputType prop to be checkbox */}
35
+ <SubmenuItem
36
+ id="test"
37
+ inputType="checkbox"
38
+ MenuComponent={ActionMenu}
39
+ menuProps={{}}
40
+ >
41
+ Test
42
+ </SubmenuItem>
43
+ {/* Valid SubmenuItem allows inputType prop to be radio */}
44
+ <SubmenuItem
45
+ id="test"
46
+ inputType="radio"
47
+ MenuComponent={ActionMenu}
48
+ menuProps={{}}
49
+ >
50
+ Test
51
+ </SubmenuItem>
52
+ {/* Valid SubmenuItem allows inputType prop to be switch */}
53
+ <SubmenuItem
54
+ id="test"
55
+ inputType="switch"
56
+ MenuComponent={ActionMenu}
57
+ menuProps={{}}
58
+ >
59
+ Test
60
+ </SubmenuItem>
61
+ {/* Valid SubmenuItem allows inputType prop to be icon */}
62
+ <SubmenuItem
63
+ id="test"
64
+ inputType="icon"
65
+ MenuComponent={ActionMenu}
66
+ menuProps={{}}
67
+ >
68
+ Test
69
+ </SubmenuItem>
70
+ {/* Valid SubmenuItem allows inputLabelId prop as a string */}
71
+ <SubmenuItem
72
+ id="test"
73
+ inputLabelId="Test Label"
74
+ MenuComponent={ActionMenu}
75
+ menuProps={{}}
76
+ >
77
+ Test
78
+ </SubmenuItem>
79
+
80
+ {/* @ts-expect-error - no children is invalid */}
81
+ <SubmenuItem id="foo" MenuComponent={ActionMenu} menuProps={{}} />
82
+ {/* @ts-expect-error - missing id is invalid */}
83
+ <SubmenuItem MenuComponent={ActionMenu} menuProps={{}}>
84
+ Test
85
+ </SubmenuItem>
86
+ {/* @ts-expect-error - missing MenuComponent is invalid */}
87
+ <SubmenuItem id="foo" menuProps={{}}>
88
+ Test
89
+ </SubmenuItem>
90
+ {/* @ts-expect-error - missing menuProps is invalid */}
91
+ <SubmenuItem id="foo" MenuComponent={ActionMenu}>
92
+ Test
93
+ </SubmenuItem>
94
+ <SubmenuItem
95
+ /* @ts-expect-error - id may not be a number */
96
+ id={0}
97
+ /* @ts-expect-error - active may not be a string */
98
+ active="true"
99
+ /* @ts-expect-error - inputType must be valid */
100
+ inputType="invalid"
101
+ /* @ts-expect-error - inputLabelId may not be a number */
102
+ inputLabelId={0}
103
+ /* @ts-expect-error - non-Menu component is invalid for MenuComponent */
104
+ MenuComponent={SubmenuItem}
105
+ /* @ts-expect-error - menuProps must be an object */
106
+ menuProps={false}
107
+ >
108
+ Test
109
+ </SubmenuItem>
110
+ </>
111
+ );
112
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./SubmenuItem";
2
+ export * from "./SubmenuItemTypes";