@sproutsocial/seeds-react-menu 0.4.1 → 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 (105) hide show
  1. package/.turbo/turbo-build.log +10 -10
  2. package/CHANGELOG.md +32 -0
  3. package/dist/esm/index.js +1553 -423
  4. package/dist/esm/index.js.map +1 -1
  5. package/dist/index.d.mts +303 -122
  6. package/dist/index.d.ts +303 -122
  7. package/dist/index.js +1561 -405
  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 +30 -26
  12. package/src/ActionMenu/ActionMenuTypes.ts +13 -8
  13. package/src/ActionMenu/__tests__/ActionMenu.test.tsx +17 -30
  14. package/src/ActionMenu/__tests__/ActionMenu.typetest.tsx +32 -3
  15. package/src/Autocomplete/Autocomplete.stories.tsx +61 -2
  16. package/src/Autocomplete/Autocomplete.tsx +15 -74
  17. package/src/Autocomplete/AutocompleteInput.tsx +18 -27
  18. package/src/Autocomplete/AutocompleteTokenInput.tsx +50 -0
  19. package/src/Autocomplete/AutocompleteTypes.ts +27 -11
  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 +116 -3
  24. package/src/MenuContent/MenuContent.stories.tsx +164 -0
  25. package/src/MenuContent/MenuContent.tsx +35 -8
  26. package/src/MenuContent/MenuContentTypes.ts +7 -5
  27. package/src/MenuContent/__tests__/MenuContent.typetest.tsx +18 -2
  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 +21 -21
  32. package/src/MenuGroup/MenuGroup.stories.tsx +248 -0
  33. package/src/MenuGroup/MenuGroup.tsx +9 -7
  34. package/src/MenuGroup/MenuGroupTypes.ts +8 -6
  35. package/src/MenuGroup/__tests__/MenuGroup.test.tsx +6 -6
  36. package/src/MenuGroup/__tests__/MenuGroup.typetest.tsx +23 -4
  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 -9
  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 +44 -67
  65. package/src/MultiSelectMenu/MultiSelectMenuTypes.ts +9 -5
  66. package/src/MultiSelectMenu/__tests__/MultiSelectMenu.test.tsx +46 -60
  67. package/src/MultiSelectMenu/__tests__/MultiSelectMenu.typetest.tsx +29 -2
  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 +29 -25
  81. package/src/SingleSelectMenu/SingleSelectMenuTypes.ts +9 -5
  82. package/src/SingleSelectMenu/__tests__/SingleSelectMenu.typetest.tsx +32 -2
  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/__tests__/Menu.test.tsx +38 -0
  91. package/src/hooks/useDownshiftDefaults.tsx +107 -0
  92. package/src/hooks/useItemFiltering.tsx +66 -0
  93. package/src/hooks/useMenuChildren.tsx +279 -0
  94. package/src/index.ts +7 -1
  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 -5
  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/hooks/useItemsFromChildren.tsx +0 -131
  105. package/src/utils/itemToString.ts +0 -5
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sproutsocial/seeds-react-menu",
3
- "version": "0.4.1",
3
+ "version": "0.6.0",
4
4
  "description": "Seeds React Menu",
5
5
  "author": "Sprout Social, Inc.",
6
6
  "license": "MIT",
@@ -28,10 +28,13 @@
28
28
  "@sproutsocial/seeds-react-switch": "^1.2.0",
29
29
  "@sproutsocial/seeds-react-system-props": "^3.0.1",
30
30
  "@sproutsocial/seeds-react-theme": "^2.2.0",
31
+ "@sproutsocial/seeds-react-token-input": "^1.0.1",
32
+ "@sproutsocial/seeds-react-utilities": "^4.1.0",
31
33
  "downshift": "9.0.4",
32
34
  "motion": "^11.11.17"
33
35
  },
34
36
  "devDependencies": {
37
+ "@sproutsocial/seeds-react-tooltip": "^1.0.1",
35
38
  "@sproutsocial/eslint-config-seeds": "*",
36
39
  "@sproutsocial/seeds-react-testing-library": "*",
37
40
  "@sproutsocial/seeds-testing": "*",
@@ -0,0 +1,426 @@
1
+ /* eslint-disable react-hooks/rules-of-hooks */
2
+ import React from "react";
3
+ import styled from "styled-components";
4
+ import type { Meta, StoryObj } from "@storybook/react";
5
+ import { action } from "@storybook/addon-actions";
6
+
7
+ import { Box } from "@sproutsocial/seeds-react-box";
8
+ import { Icon, type TypeIconName } from "@sproutsocial/seeds-react-icon";
9
+ import { Switch } from "@sproutsocial/seeds-react-switch";
10
+ import { Label } from "@sproutsocial/seeds-react-label";
11
+ import { Link } from "@sproutsocial/seeds-react-link";
12
+ import { StorybookMenuToggleButton } from "../storybookUtilities/menu-storybook-components";
13
+ import {
14
+ ActionMenu,
15
+ MenuContent,
16
+ MenuFooter,
17
+ MenuGroup,
18
+ MenuHeader,
19
+ MenuItem,
20
+ MenuSearchInput,
21
+ type TypeActionMenuProps,
22
+ } from "../index";
23
+
24
+ const StyledList = styled.ul`
25
+ list-style: none;
26
+ padding: 0px;
27
+ margin: 0px;
28
+ width: 240px;
29
+
30
+ li {
31
+ display: flex;
32
+ flex-direction: row-reverse;
33
+ padding: 4px 0px 4px 0px;
34
+ justify-content: left;
35
+ }
36
+ `;
37
+ type ActionMenuStoryArgs = TypeActionMenuProps & {
38
+ "Trigger Text"?: string;
39
+ "Trigger Appearance"?: "primary" | "secondary";
40
+ icons: Array<string>;
41
+ };
42
+ type Story = StoryObj<ActionMenuStoryArgs>;
43
+
44
+ const meta: Meta<ActionMenuStoryArgs> = {
45
+ title: "Under Development/ActionMenu",
46
+ component: ActionMenu,
47
+ argTypes: {
48
+ "Trigger Appearance": {
49
+ options: ["primary", "secondary"],
50
+ control: { type: "select" },
51
+ },
52
+ },
53
+ decorators: [
54
+ (Story) => (
55
+ <Box display="flex" gap="16px" flexDirection="column" maxWidth={300}>
56
+ <Story />
57
+ </Box>
58
+ ),
59
+ ],
60
+ };
61
+
62
+ export default meta;
63
+
64
+ const handleOnClick = (e: React.MouseEvent<HTMLElement>, itemName: string) => {
65
+ action(`${itemName}`)(e);
66
+ };
67
+
68
+ export const ActionMenuSimple: Story = {
69
+ name: "Simple Action Menu",
70
+ args: {
71
+ "Trigger Text": "ActionMenu",
72
+ "Trigger Appearance": "secondary",
73
+ },
74
+ render: (args) => {
75
+ return (
76
+ <ActionMenu
77
+ {...args}
78
+ menuToggleElement={
79
+ <StorybookMenuToggleButton
80
+ buttonText={args["Trigger Text"]}
81
+ buttonProps={{ appearance: args["Trigger Appearance"] }}
82
+ />
83
+ }
84
+ >
85
+ <MenuContent>
86
+ <MenuGroup id="actions">
87
+ <MenuItem onClick={(e) => handleOnClick(e, "Compose")} id="Compose">
88
+ Compose
89
+ </MenuItem>
90
+ <MenuItem
91
+ onClick={(e) => handleOnClick(e, "View Messages")}
92
+ id="View Messages"
93
+ >
94
+ View Messages
95
+ </MenuItem>
96
+ <MenuItem
97
+ onClick={(e) => handleOnClick(e, "Analyze post")}
98
+ id="Analyze post"
99
+ >
100
+ Analyze post
101
+ </MenuItem>
102
+ <MenuItem
103
+ href="https://sproutsocial.com"
104
+ target="_blank"
105
+ id="settings"
106
+ >
107
+ This Opens a Link
108
+ </MenuItem>
109
+ </MenuGroup>
110
+ </MenuContent>
111
+ </ActionMenu>
112
+ );
113
+ },
114
+ };
115
+
116
+ const itemsAsProp = [
117
+ {
118
+ id: "compose",
119
+ onClick: (e) => handleOnClick(e, "Compose"),
120
+ children: "Compose",
121
+ },
122
+ {
123
+ id: "view-messages",
124
+ onClick: (e) => handleOnClick(e, "View Messages"),
125
+ children: "View Messages",
126
+ },
127
+ {
128
+ id: "analyze-post",
129
+ onClick: (e) => handleOnClick(e, "Analyze post"),
130
+ children: "Analyze post",
131
+ },
132
+ ];
133
+ export const ActionMenuWithItemsAsProps: Story = {
134
+ name: "With Items as Props",
135
+ args: {
136
+ "Trigger Text": "ActionMenu",
137
+ "Trigger Appearance": "secondary",
138
+ },
139
+ render: (args) => {
140
+ return (
141
+ <ActionMenu
142
+ {...args}
143
+ menuToggleElement={
144
+ <StorybookMenuToggleButton
145
+ buttonText={args["Trigger Text"]}
146
+ buttonProps={{ appearance: args["Trigger Appearance"] }}
147
+ />
148
+ }
149
+ menuItems={itemsAsProp}
150
+ />
151
+ );
152
+ },
153
+ };
154
+
155
+ export const ActionMenuWithIcons: Story = {
156
+ name: "Using Icons",
157
+ args: {
158
+ "Trigger Text": "ActionMenu",
159
+ icons: [
160
+ "pencil-to-square-outline",
161
+ "envelope-outline",
162
+ "chart-area-outline",
163
+ ],
164
+ "Trigger Appearance": "secondary",
165
+ },
166
+ render: (args) => {
167
+ return (
168
+ <ActionMenu
169
+ {...args}
170
+ menuToggleElement={
171
+ <StorybookMenuToggleButton
172
+ buttonText={args["Trigger Text"]}
173
+ buttonProps={{ appearance: args["Trigger Appearance"] }}
174
+ />
175
+ }
176
+ >
177
+ <MenuContent>
178
+ <MenuGroup id="actions">
179
+ <MenuItem
180
+ onClick={(e) => handleOnClick(e, "Compose")}
181
+ key={0}
182
+ id="Compose"
183
+ >
184
+ <Box
185
+ display="flex"
186
+ alignItems="center"
187
+ justifyContent="space-between"
188
+ >
189
+ Compose
190
+ <Icon name={args["icons"][0] as TypeIconName} />
191
+ </Box>
192
+ </MenuItem>
193
+ <MenuItem
194
+ onClick={(e) => handleOnClick(e, "View Messages")}
195
+ key={1}
196
+ id="View Messages"
197
+ >
198
+ <Box
199
+ display="flex"
200
+ alignItems="center"
201
+ justifyContent="space-between"
202
+ >
203
+ View Messages
204
+ <Icon name={args["icons"][1] as TypeIconName} />
205
+ </Box>
206
+ </MenuItem>
207
+ <MenuItem
208
+ onClick={(e) => handleOnClick(e, "Analyze post")}
209
+ key={2}
210
+ id="Analyze post"
211
+ >
212
+ <Box
213
+ display="flex"
214
+ alignItems="center"
215
+ justifyContent="space-between"
216
+ >
217
+ Analyze Post
218
+ <Icon name={args["icons"][2] as TypeIconName} />
219
+ </Box>
220
+ </MenuItem>
221
+ </MenuGroup>
222
+ <MenuGroup id="settings">
223
+ <MenuItem
224
+ onClick={(e) => handleOnClick(e, "Settings")}
225
+ key={3}
226
+ id="Settings"
227
+ >
228
+ <Box
229
+ display="flex"
230
+ alignItems="center"
231
+ justifyContent="space-between"
232
+ >
233
+ Settings
234
+ <Icon name="gear-outline" />
235
+ </Box>
236
+ </MenuItem>
237
+ </MenuGroup>
238
+ </MenuContent>
239
+ </ActionMenu>
240
+ );
241
+ },
242
+ };
243
+
244
+ export const ActionMenuWithHeaderFooter: Story = {
245
+ name: "With Header & Footer",
246
+ args: {
247
+ "Trigger Text": "ActionMenu",
248
+ "Trigger Appearance": "secondary",
249
+ },
250
+ render: (args) => {
251
+ const [highContrast, setHighContrast] = React.useState(false);
252
+ const [darkMode, setDarkMode] = React.useState(false);
253
+ return (
254
+ <ActionMenu
255
+ {...args}
256
+ menuToggleElement={
257
+ <StorybookMenuToggleButton
258
+ buttonText={args["Trigger Text"]}
259
+ buttonProps={{ appearance: args["Trigger Appearance"] }}
260
+ />
261
+ }
262
+ >
263
+ <MenuHeader title="Select one of the following" />
264
+ <MenuContent>
265
+ <MenuGroup id="actions">
266
+ <MenuItem
267
+ onClick={(e) => handleOnClick(e, "Compose")}
268
+ key={0}
269
+ id="Compose"
270
+ >
271
+ Compose
272
+ </MenuItem>
273
+ <MenuItem
274
+ onClick={(e) => handleOnClick(e, "View Messages")}
275
+ key={1}
276
+ id="View Messages"
277
+ >
278
+ View Messages
279
+ </MenuItem>
280
+ <MenuItem
281
+ onClick={(e) => handleOnClick(e, "Analyze post")}
282
+ key={2}
283
+ id="Analyze post"
284
+ >
285
+ Analyze post
286
+ </MenuItem>
287
+ </MenuGroup>
288
+ </MenuContent>
289
+ <MenuFooter>
290
+ <StyledList>
291
+ <li>
292
+ <Label htmlFor="menuSwitch1">High contrast</Label>
293
+ <Switch
294
+ mr={300}
295
+ id="menuSwitch1"
296
+ checked={highContrast}
297
+ onClick={() => setHighContrast(!highContrast)}
298
+ />
299
+ </li>
300
+ <li>
301
+ <Label htmlFor="menuSwitch2">Dark mode</Label>
302
+ <Switch
303
+ mr={300}
304
+ id="menuSwitch2"
305
+ checked={darkMode}
306
+ onClick={() => setDarkMode(!darkMode)}
307
+ />
308
+ </li>
309
+ <li>
310
+ <Link href="#" fontSize={200}>
311
+ Manage settings
312
+ </Link>
313
+ </li>
314
+ </StyledList>
315
+ </MenuFooter>
316
+ </ActionMenu>
317
+ );
318
+ },
319
+ };
320
+
321
+ export const ActionMenuControlled: Story = {
322
+ name: "Controlled State",
323
+ args: {
324
+ "Trigger Text": "ActionMenu",
325
+ "Trigger Appearance": "secondary",
326
+ },
327
+ render: (args) => {
328
+ const [isOpen, setIsOpen] = React.useState<boolean>(false);
329
+ return (
330
+ <ActionMenu
331
+ {...args}
332
+ isOpen={isOpen}
333
+ onIsOpenChange={({ isOpen }) => setIsOpen(isOpen)}
334
+ menuToggleElement={
335
+ <StorybookMenuToggleButton
336
+ buttonText={args["Trigger Text"]}
337
+ buttonProps={{ appearance: args["Trigger Appearance"] }}
338
+ />
339
+ }
340
+ >
341
+ <MenuContent>
342
+ <MenuGroup id="actions">
343
+ <MenuItem
344
+ onClick={(e) => handleOnClick(e, "Compose")}
345
+ key={0}
346
+ id="Compose"
347
+ >
348
+ Compose
349
+ </MenuItem>
350
+ <MenuItem
351
+ onClick={(e) => handleOnClick(e, "View Messages")}
352
+ key={1}
353
+ id="View Messages"
354
+ >
355
+ View Messages
356
+ </MenuItem>
357
+ <MenuItem
358
+ onClick={(e) => handleOnClick(e, "Analyze post")}
359
+ key={2}
360
+ id="Analyze post"
361
+ >
362
+ Analyze post
363
+ </MenuItem>
364
+ </MenuGroup>
365
+ </MenuContent>
366
+ </ActionMenu>
367
+ );
368
+ },
369
+ };
370
+
371
+ export const ActionMenuWithSearch: Story = {
372
+ name: "Action Menu with Search",
373
+ args: {
374
+ "Trigger Text": "ActionMenu",
375
+ "Trigger Appearance": "secondary",
376
+ },
377
+ render: (args) => {
378
+ return (
379
+ <ActionMenu
380
+ itemToString={(item) => `${item?.menuItemProps?.children || ""}`}
381
+ menuToggleElement={
382
+ <StorybookMenuToggleButton
383
+ buttonText={args["Trigger Text"]}
384
+ buttonProps={{ appearance: args["Trigger Appearance"] }}
385
+ />
386
+ }
387
+ onStateChange={(changes) => action("onStateChange")(changes)}
388
+ {...(args as object)}
389
+ >
390
+ <MenuHeader>
391
+ <MenuSearchInput
392
+ id="single-select-search"
393
+ name="search"
394
+ type="search"
395
+ aria-label="Search Menu"
396
+ />
397
+ </MenuHeader>
398
+ <MenuContent>
399
+ <MenuGroup id="actions">
400
+ <MenuItem
401
+ onClick={(e) => handleOnClick(e, "Compose")}
402
+ key={0}
403
+ id="Compose"
404
+ >
405
+ Compose
406
+ </MenuItem>
407
+ <MenuItem
408
+ onClick={(e) => handleOnClick(e, "View Messages")}
409
+ key={1}
410
+ id="View Messages"
411
+ >
412
+ View Messages
413
+ </MenuItem>
414
+ <MenuItem
415
+ onClick={(e) => handleOnClick(e, "Analyze post")}
416
+ key={2}
417
+ id="Analyze post"
418
+ >
419
+ Analyze post
420
+ </MenuItem>
421
+ </MenuGroup>
422
+ </MenuContent>
423
+ </ActionMenu>
424
+ );
425
+ },
426
+ };
@@ -4,20 +4,20 @@ import {
4
4
  type UseSelectState,
5
5
  type UseSelectStateChange,
6
6
  } from "downshift";
7
- import type { TypeDefaultItemProps } from "../types";
8
- import { useItemsFromChildren } from "../hooks/useItemsFromChildren";
9
- import {
10
- defaultUseSelectProps,
11
- useSelectStateReducer,
12
- } from "../reducers/useSelectStateReducer";
13
- import { MenuRoot } from "../MenuRoot";
14
- import { itemToString } from "../utils/itemToString";
7
+ import type { TypeInternalItemProps } from "../types";
8
+ import { useMenuChildren } from "../hooks/useMenuChildren";
9
+ import { useDownshiftDefaults } from "../hooks/useDownshiftDefaults";
10
+ import { reduceReducers } from "../utils/reduceReducers";
11
+ import { itemToString } from "../utils/downshiftDefaults";
12
+ import { MenuRoot, type TypeMenuRootProps } from "../MenuRoot";
13
+ import type { TypeMenuItemProps } from "../MenuItem";
14
+
15
15
  import type { TypeActionMenuProps } from "./ActionMenuTypes";
16
16
 
17
17
  // Manually fire the onClick handler when Enter or Space is pressed
18
18
  const handleStateChange = (
19
- changes: UseSelectStateChange<TypeDefaultItemProps>,
20
- currentSelectedItem: UseSelectState<TypeDefaultItemProps>["selectedItem"]
19
+ changes: UseSelectStateChange<TypeInternalItemProps>,
20
+ currentSelectedItem: UseSelectState<TypeInternalItemProps>["selectedItem"]
21
21
  ) => {
22
22
  // if not Enter or Space key, do nothing
23
23
  if (
@@ -81,30 +81,33 @@ const handleStateChange = (
81
81
  * @see {@link https://github.com/sproutsocial/seeds/tree/dev/seeds-react/seeds-react-menu/src/SingleSelectMenu | SingleSelectMenu}
82
82
  * @see {@link https://github.com/sproutsocial/seeds/tree/dev/seeds-react/seeds-react-menu/src/MultiSelectMenu | MultiSelectMenu}
83
83
  */
84
- export const ActionMenu = ({
85
- children,
84
+ export const ActionMenu = <I extends TypeMenuItemProps = TypeMenuItemProps>({
85
+ children: childrenProp,
86
+ menuItems,
87
+ MenuItemComponent,
86
88
  stateReducer: externalStateReducer,
87
89
  ...useSelectProps
88
- }: TypeActionMenuProps) => {
89
- const { allMenuItems, menuItemsMap } = useItemsFromChildren({ children });
90
+ }: TypeActionMenuProps<I>) => {
91
+ const { allMenuItems, menuItemsMap, children } = useMenuChildren({
92
+ children: childrenProp,
93
+ menuItems,
94
+ MenuItemComponent,
95
+ });
96
+ const { defaultDownshiftProps, ...restDefaults } = useDownshiftDefaults({
97
+ isCombobox: false,
98
+ });
90
99
  const [currentSelectedItem, setCurrentSelectedItem] =
91
- React.useState<UseSelectState<TypeDefaultItemProps>["selectedItem"]>(null);
100
+ React.useState<UseSelectState<TypeInternalItemProps>["selectedItem"]>(null);
92
101
 
93
102
  const { isOpen, ...useSelectReturnProps } = useSelect({
94
- ...defaultUseSelectProps,
103
+ ...defaultDownshiftProps.select,
95
104
  items: allMenuItems,
96
105
  itemToString,
97
106
  onStateChange: (changes) => handleStateChange(changes, currentSelectedItem),
98
- stateReducer: (state, actionAndChanges) => {
99
- let newState: Partial<UseSelectState<TypeDefaultItemProps>>;
100
- newState = useSelectStateReducer(state, actionAndChanges);
101
- return externalStateReducer
102
- ? externalStateReducer(state, {
103
- ...actionAndChanges,
104
- changes: newState,
105
- })
106
- : newState;
107
- },
107
+ stateReducer: reduceReducers(
108
+ defaultDownshiftProps.select.stateReducer,
109
+ externalStateReducer
110
+ ),
108
111
  ...useSelectProps,
109
112
  });
110
113
 
@@ -122,6 +125,7 @@ export const ActionMenu = ({
122
125
  isOpen,
123
126
  ...useSelectProps,
124
127
  ...useSelectReturnProps,
128
+ ...restDefaults,
125
129
  }}
126
130
  >
127
131
  {children}
@@ -1,12 +1,17 @@
1
- import { type TypeMenuRootProps, type TypeMenuRootOwnProps } from "../MenuRoot";
1
+ import { type TypeMenuRootOwnProps } from "../MenuRoot";
2
2
  import type { TypeDownshiftSelectProps } from "../MenuContext";
3
+ import type { TypeMenuRootProps } from "../MenuRoot";
4
+ import type { TypeMenuItemProps } from "../MenuItem";
5
+ import type { TypeChildrenOrItems } from "../types";
3
6
 
4
7
  type TypeInvalidMenuProps =
5
- | "initialSelectedItem"
6
- | "defaultSelectedItem"
7
- | "selectedItem";
8
- export interface TypeActionMenuProps
8
+ // There is no selection state on action menus.
9
+ "initialSelectedItem" | "defaultSelectedItem" | "selectedItem";
10
+
11
+ export interface TypeActionMenuBaseProps
9
12
  extends TypeMenuRootOwnProps,
10
- Omit<TypeDownshiftSelectProps, TypeInvalidMenuProps> {
11
- children: TypeMenuRootProps["children"];
12
- }
13
+ Omit<TypeDownshiftSelectProps, TypeInvalidMenuProps> {}
14
+
15
+ export type TypeActionMenuProps<
16
+ I extends TypeMenuItemProps = TypeMenuItemProps
17
+ > = TypeActionMenuBaseProps & TypeChildrenOrItems<I>;
@@ -4,6 +4,7 @@ import {
4
4
  render,
5
5
  screen,
6
6
  waitFor,
7
+ act,
7
8
  } from "@sproutsocial/seeds-react-testing-library";
8
9
  import { PointerEventsCheckLevel } from "@testing-library/user-event";
9
10
  import { type UserEvent } from "@testing-library/user-event";
@@ -11,28 +12,6 @@ import { ActionMenu } from "../ActionMenu";
11
12
  import { MenuContent, MenuItem, MenuToggleButton } from "../../index";
12
13
 
13
14
  describe("ActionMenu", () => {
14
- it("should render the MenuItems when provided as children", async () => {
15
- render(
16
- <ActionMenu
17
- menuToggleElement={<MenuToggleButton>Test Menu</MenuToggleButton>}
18
- >
19
- <MenuContent>
20
- <MenuItem id="test-item-1">Item 1</MenuItem>
21
- <MenuItem id="test-item-2">Item 2</MenuItem>
22
- </MenuContent>
23
- </ActionMenu>
24
- );
25
-
26
- expect(screen.queryByRole("menu")).not.toBeInTheDocument();
27
-
28
- const toggleBtn = screen.getByRole("button", { name: "Test Menu" });
29
- fireEvent.click(toggleBtn);
30
-
31
- await waitFor(() => expect(screen.getByRole("menu")).toBeVisible());
32
- expect(screen.getByRole("menuitem", { name: "Item 1" })).toBeVisible();
33
- expect(screen.getByRole("menuitem", { name: "Item 2" })).toBeVisible();
34
- });
35
-
36
15
  test.each([
37
16
  {
38
17
  action: "click",
@@ -40,23 +19,29 @@ describe("ActionMenu", () => {
40
19
  const firstItem = screen.getByRole("menuitem", {
41
20
  name: "Item 1",
42
21
  });
43
- await user
44
- .setup({
45
- pointerEventsCheck: PointerEventsCheckLevel.Never,
46
- })
47
- .click(firstItem);
22
+ await act(async () => {
23
+ await user
24
+ .setup({
25
+ pointerEventsCheck: PointerEventsCheckLevel.Never,
26
+ })
27
+ .click(firstItem);
28
+ });
48
29
  },
49
30
  },
50
31
  {
51
32
  action: "pressed the Enter key",
52
33
  activate: async (user: UserEvent) => {
53
- await user.keyboard("{ArrowDown}{Enter}");
34
+ await act(async () => {
35
+ await user.keyboard("{ArrowDown}{Enter}");
36
+ });
54
37
  },
55
38
  },
56
39
  {
57
40
  action: "pressed the Space key",
58
41
  activate: async (user: UserEvent) => {
59
- await user.keyboard("{ArrowDown} ");
42
+ await act(async () => {
43
+ await user.keyboard("{ArrowDown} ");
44
+ });
60
45
  },
61
46
  },
62
47
  ])("should close after a MenuItem is $action", async ({ activate }) => {
@@ -75,7 +60,9 @@ describe("ActionMenu", () => {
75
60
  );
76
61
 
77
62
  const toggleBtn = screen.getByRole("button", { name: "Test Menu" });
78
- await user.click(toggleBtn);
63
+ await act(async () => {
64
+ await user.click(toggleBtn);
65
+ });
79
66
 
80
67
  await waitFor(() => expect(screen.getByRole("menu")).toBeVisible());
81
68