@sproutsocial/seeds-react-menu 0.5.0 → 0.6.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 (103) hide show
  1. package/.turbo/turbo-build.log +10 -10
  2. package/CHANGELOG.md +15 -0
  3. package/dist/esm/index.js +1486 -440
  4. package/dist/esm/index.js.map +1 -1
  5. package/dist/index.d.mts +248 -92
  6. package/dist/index.d.ts +248 -92
  7. package/dist/index.js +1500 -429
  8. package/dist/index.js.map +1 -1
  9. package/package.json +4 -1
  10. package/src/ActionMenu/ActionMenu.stories.tsx +426 -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 +189 -0
  21. package/src/Autocomplete/SingleAutocomplete.tsx +74 -0
  22. package/src/Autocomplete/__tests__/Autocomplete.test.tsx +171 -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 +26 -12
  26. package/src/MenuContent/MenuContentTypes.ts +6 -4
  27. package/src/MenuContent/__tests__/MenuContent.typetest.tsx +8 -8
  28. package/src/MenuContext.tsx +191 -37
  29. package/src/MenuFooter/MenuFooter.stories.tsx +202 -0
  30. package/src/MenuFooter/__tests__/MenuFooter.test.tsx +16 -11
  31. package/src/MenuGroup/MenuGroup.feature +16 -16
  32. package/src/MenuGroup/MenuGroup.stories.tsx +248 -0
  33. package/src/MenuGroup/MenuGroup.tsx +8 -7
  34. package/src/MenuGroup/MenuGroupTypes.ts +6 -2
  35. package/src/MenuGroup/__tests__/MenuGroup.test.tsx +6 -6
  36. package/src/MenuGroup/__tests__/MenuGroup.typetest.tsx +7 -7
  37. package/src/MenuGroup/styles.tsx +4 -2
  38. package/src/MenuHeader/MenuHeader.stories.tsx +276 -0
  39. package/src/MenuHeader/MenuHeader.tsx +3 -1
  40. package/src/MenuHeader/__tests__/MenuHeader.test.tsx +23 -13
  41. package/src/MenuHeader/styles.tsx +18 -14
  42. package/src/MenuItem/MenuItem.stories.tsx +507 -0
  43. package/src/MenuItem/MenuItem.tsx +18 -8
  44. package/src/MenuItem/MenuItemTypes.ts +9 -4
  45. package/src/MenuItem/__tests__/MenuItem.test.tsx +6 -6
  46. package/src/MenuItem/styles.tsx +21 -5
  47. package/src/MenuItem/useOptionProps.tsx +10 -8
  48. package/src/MenuLabel.tsx +2 -2
  49. package/src/MenuRoot/MenuRoot.tsx +37 -23
  50. package/src/MenuRoot/__tests__/MenuRoot.test.tsx +59 -32
  51. package/src/MenuSearchInput/MenuSearchInput.tsx +63 -0
  52. package/src/MenuSearchInput/__tests__/MenuSearchInput.test.tsx +157 -0
  53. package/src/MenuSearchInput/__tests__/MenuSearchInput.typetest.tsx +31 -0
  54. package/src/MenuSearchInput/index.ts +1 -0
  55. package/src/MenuSearchTokenInput/MenuSearchTokenInput.tsx +98 -0
  56. package/src/MenuSearchTokenInput/__tests__/MenuSearchTokenInput.test.tsx +247 -0
  57. package/src/MenuSearchTokenInput/__tests__/MenuSearchTokenInput.typetest.tsx +33 -0
  58. package/src/MenuSearchTokenInput/index.ts +1 -0
  59. package/src/MenuToggleButton/MenuToggleButton.stories.tsx +72 -0
  60. package/src/{MenuToggleButton.tsx → MenuToggleButton/MenuToggleButton.tsx} +6 -5
  61. package/src/MenuToggleButton/index.ts +1 -0
  62. package/src/MenuTokenInput.tsx +104 -0
  63. package/src/MultiSelectMenu/MultiSelectMenu.stories.tsx +214 -0
  64. package/src/MultiSelectMenu/MultiSelectMenu.tsx +38 -68
  65. package/src/MultiSelectMenu/MultiSelectMenuTypes.ts +8 -6
  66. package/src/MultiSelectMenu/__tests__/MultiSelectMenu.test.tsx +46 -17
  67. package/src/MultiSelectMenu/__tests__/MultiSelectMenu.typetest.tsx +6 -6
  68. package/src/NestedMenu/NestedMenu.feature +109 -0
  69. package/src/NestedMenu/NestedMenu.stories.tsx +157 -0
  70. package/src/NestedMenu/NestedMenu.tsx +102 -0
  71. package/src/NestedMenu/NestedMenuContent.tsx +71 -0
  72. package/src/NestedMenu/NestedMenuContext.tsx +56 -0
  73. package/src/NestedMenu/NestedMenuHeader.tsx +25 -0
  74. package/src/NestedMenu/NestedMenuItem.tsx +37 -0
  75. package/src/NestedMenu/NestedMenuTypes.ts +47 -0
  76. package/src/NestedMenu/__tests__/NestedMenu.test.tsx +31 -0
  77. package/src/NestedMenu/__tests__/NestedMenu.typetest.tsx +134 -0
  78. package/src/NestedMenu/index.ts +5 -0
  79. package/src/SingleSelectMenu/SingleSelectMenu.stories.tsx +227 -0
  80. package/src/SingleSelectMenu/SingleSelectMenu.tsx +23 -26
  81. package/src/SingleSelectMenu/SingleSelectMenuTypes.ts +7 -5
  82. package/src/SingleSelectMenu/__tests__/SingleSelectMenu.typetest.tsx +6 -6
  83. package/src/SubmenuItem/SubmenuItem.feature +82 -0
  84. package/src/SubmenuItem/SubmenuItem.stories.tsx +163 -0
  85. package/src/SubmenuItem/SubmenuItem.tsx +254 -0
  86. package/src/SubmenuItem/SubmenuItemTypes.ts +24 -0
  87. package/src/SubmenuItem/__tests__/SubmenuItem.test.tsx +3 -0
  88. package/src/SubmenuItem/__tests__/SubmenuItem.typetest.tsx +112 -0
  89. package/src/SubmenuItem/index.ts +2 -0
  90. package/src/hooks/useDownshiftDefaults.tsx +107 -0
  91. package/src/hooks/useItemFiltering.tsx +66 -0
  92. package/src/hooks/useMenuChildren.tsx +197 -128
  93. package/src/index.ts +6 -0
  94. package/src/menuTestingUtils.tsx +21 -10
  95. package/src/reducers/nestedMenuStateReducer.tsx +23 -0
  96. package/src/reducers/useComboboxStateReducer.tsx +26 -8
  97. package/src/reducers/useSelectStateReducer.tsx +24 -6
  98. package/src/storybookUtilities/menu-storybook-components.tsx +5 -3
  99. package/src/types.ts +38 -30
  100. package/src/utils/downshiftDefaults.ts +9 -0
  101. package/src/utils/getMultiSelectOverrides.ts +21 -0
  102. package/src/utils/reduceReducers.ts +19 -0
  103. package/src/utils/itemToString.ts +0 -5
@@ -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,254 @@
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
+ export 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
+ };
@@ -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";
@@ -0,0 +1,107 @@
1
+ import React from "react";
2
+ import type {
3
+ UseComboboxProps,
4
+ UseSelectProps,
5
+ UseMultipleSelectionProps,
6
+ } from "downshift";
7
+ import type { TypeInternalItemProps } from "../types";
8
+ import type {
9
+ TypeMenuContextBaseProps,
10
+ TypeDownshiftMultiSelectProps,
11
+ } from "../MenuContext";
12
+ import {
13
+ useComboboxStateReducer,
14
+ useComboboxStateReducerForMulti,
15
+ } from "../reducers/useComboboxStateReducer";
16
+ import {
17
+ useSelectStateReducer,
18
+ useSelectStateReducerForMulti,
19
+ } from "../reducers/useSelectStateReducer";
20
+ import { getMultiSelectOverrides } from "../utils/getMultiSelectOverrides";
21
+
22
+ export const defaultUseSelectProps: Partial<
23
+ UseSelectProps<TypeInternalItemProps>
24
+ > = {
25
+ stateReducer: useSelectStateReducer,
26
+ };
27
+
28
+ export const defaultUseComboboxProps: Partial<
29
+ UseComboboxProps<TypeInternalItemProps>
30
+ > = {
31
+ stateReducer: useComboboxStateReducer,
32
+ };
33
+
34
+ export const defaultUseSelectPropsForMulti: Partial<
35
+ UseSelectProps<TypeInternalItemProps>
36
+ > = {
37
+ stateReducer: useSelectStateReducerForMulti,
38
+ };
39
+
40
+ export const defaultUseComboboxPropsForMulti: Partial<
41
+ UseComboboxProps<TypeInternalItemProps>
42
+ > = {
43
+ stateReducer: useComboboxStateReducerForMulti,
44
+ };
45
+
46
+ export interface TypeUseDownshiftDefaultsReturn
47
+ extends Pick<
48
+ Required<TypeMenuContextBaseProps>,
49
+ "hiddenItemsMap" | "setHiddenItemsMap"
50
+ > {
51
+ defaultDownshiftProps: {
52
+ select: Partial<UseSelectProps<TypeInternalItemProps>>;
53
+ combobox: Partial<UseComboboxProps<TypeInternalItemProps>>;
54
+ multi: Partial<UseMultipleSelectionProps<TypeInternalItemProps>>;
55
+ };
56
+ }
57
+
58
+ export const useDownshiftDefaults = ({
59
+ isCombobox = false,
60
+ isMulti = false,
61
+ multiSelectProps = {},
62
+ }: {
63
+ isCombobox?: boolean;
64
+ isMulti?: boolean;
65
+ multiSelectProps?: TypeDownshiftMultiSelectProps;
66
+ } = {}): TypeUseDownshiftDefaultsReturn => {
67
+ const [hiddenItemsMap, setHiddenItemsMap] = React.useState<
68
+ Record<string, boolean>
69
+ >({});
70
+ const isItemDisabled = React.useCallback<
71
+ Required<UseSelectProps<TypeInternalItemProps>>["isItemDisabled"]
72
+ >(
73
+ (item) => Boolean(item.disabled || hiddenItemsMap[item.id]),
74
+ [hiddenItemsMap]
75
+ );
76
+
77
+ return {
78
+ hiddenItemsMap,
79
+ setHiddenItemsMap,
80
+ defaultDownshiftProps: {
81
+ select: !isCombobox
82
+ ? {
83
+ // disabling while item.hidden so that hidden items are ignored by downshift
84
+ isItemDisabled,
85
+ ...(isMulti
86
+ ? defaultUseSelectPropsForMulti
87
+ : defaultUseSelectProps),
88
+ }
89
+ : {},
90
+ combobox: isCombobox
91
+ ? {
92
+ // disabling while item.hidden so that hidden items are ignored by downshift
93
+ isItemDisabled,
94
+ ...(isMulti
95
+ ? defaultUseComboboxPropsForMulti
96
+ : defaultUseComboboxProps),
97
+ }
98
+ : {},
99
+ multi: isMulti
100
+ ? {
101
+ // handles the prop name mapping for multi select
102
+ ...getMultiSelectOverrides(multiSelectProps),
103
+ }
104
+ : {},
105
+ },
106
+ };
107
+ };