@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sproutsocial/seeds-react-menu",
3
- "version": "0.5.0",
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,21 +4,20 @@ import {
4
4
  type UseSelectState,
5
5
  type UseSelectStateChange,
6
6
  } from "downshift";
7
- import type { TypeDefaultItemProps } from "../types";
7
+ import type { TypeInternalItemProps } from "../types";
8
8
  import { useMenuChildren } from "../hooks/useMenuChildren";
9
- import {
10
- defaultUseSelectProps,
11
- useSelectStateReducer,
12
- } from "../reducers/useSelectStateReducer";
13
- import { MenuRoot } from "../MenuRoot";
14
- import { itemToString } from "../utils/itemToString";
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
- import { MenuItem } from "../MenuItem/index";
17
16
 
18
17
  // Manually fire the onClick handler when Enter or Space is pressed
19
18
  const handleStateChange = (
20
- changes: UseSelectStateChange<TypeDefaultItemProps>,
21
- currentSelectedItem: UseSelectState<TypeDefaultItemProps>["selectedItem"]
19
+ changes: UseSelectStateChange<TypeInternalItemProps>,
20
+ currentSelectedItem: UseSelectState<TypeInternalItemProps>["selectedItem"]
22
21
  ) => {
23
22
  // if not Enter or Space key, do nothing
24
23
  if (
@@ -82,36 +81,33 @@ const handleStateChange = (
82
81
  * @see {@link https://github.com/sproutsocial/seeds/tree/dev/seeds-react/seeds-react-menu/src/SingleSelectMenu | SingleSelectMenu}
83
82
  * @see {@link https://github.com/sproutsocial/seeds/tree/dev/seeds-react/seeds-react-menu/src/MultiSelectMenu | MultiSelectMenu}
84
83
  */
85
- export const ActionMenu = ({
84
+ export const ActionMenu = <I extends TypeMenuItemProps = TypeMenuItemProps>({
86
85
  children: childrenProp,
87
- items,
88
- MenuItemComponent = MenuItem,
86
+ menuItems,
87
+ MenuItemComponent,
89
88
  stateReducer: externalStateReducer,
90
89
  ...useSelectProps
91
- }: TypeActionMenuProps) => {
90
+ }: TypeActionMenuProps<I>) => {
92
91
  const { allMenuItems, menuItemsMap, children } = useMenuChildren({
93
92
  children: childrenProp,
94
- items,
93
+ menuItems,
95
94
  MenuItemComponent,
96
95
  });
96
+ const { defaultDownshiftProps, ...restDefaults } = useDownshiftDefaults({
97
+ isCombobox: false,
98
+ });
97
99
  const [currentSelectedItem, setCurrentSelectedItem] =
98
- React.useState<UseSelectState<TypeDefaultItemProps>["selectedItem"]>(null);
100
+ React.useState<UseSelectState<TypeInternalItemProps>["selectedItem"]>(null);
99
101
 
100
102
  const { isOpen, ...useSelectReturnProps } = useSelect({
101
- ...defaultUseSelectProps,
103
+ ...defaultDownshiftProps.select,
102
104
  items: allMenuItems,
103
105
  itemToString,
104
106
  onStateChange: (changes) => handleStateChange(changes, currentSelectedItem),
105
- stateReducer: (state, actionAndChanges) => {
106
- let newState: Partial<UseSelectState<TypeDefaultItemProps>>;
107
- newState = useSelectStateReducer(state, actionAndChanges);
108
- return externalStateReducer
109
- ? externalStateReducer(state, {
110
- ...actionAndChanges,
111
- changes: newState,
112
- })
113
- : newState;
114
- },
107
+ stateReducer: reduceReducers(
108
+ defaultDownshiftProps.select.stateReducer,
109
+ externalStateReducer
110
+ ),
115
111
  ...useSelectProps,
116
112
  });
117
113
 
@@ -129,6 +125,7 @@ export const ActionMenu = ({
129
125
  isOpen,
130
126
  ...useSelectProps,
131
127
  ...useSelectReturnProps,
128
+ ...restDefaults,
132
129
  }}
133
130
  >
134
131
  {children}
@@ -1,16 +1,17 @@
1
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";
3
5
  import type { TypeChildrenOrItems } from "../types";
4
6
 
5
7
  type TypeInvalidMenuProps =
6
- | "initialSelectedItem"
7
- | "defaultSelectedItem"
8
- | "selectedItem"
9
- // We omit this so we can retype it.
10
- | "items";
8
+ // There is no selection state on action menus.
9
+ "initialSelectedItem" | "defaultSelectedItem" | "selectedItem";
11
10
 
12
- interface TypeActionMenuBaseProps
11
+ export interface TypeActionMenuBaseProps
13
12
  extends TypeMenuRootOwnProps,
14
13
  Omit<TypeDownshiftSelectProps, TypeInvalidMenuProps> {}
15
14
 
16
- export type TypeActionMenuProps = TypeActionMenuBaseProps & TypeChildrenOrItems;
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";
@@ -18,23 +19,29 @@ describe("ActionMenu", () => {
18
19
  const firstItem = screen.getByRole("menuitem", {
19
20
  name: "Item 1",
20
21
  });
21
- await user
22
- .setup({
23
- pointerEventsCheck: PointerEventsCheckLevel.Never,
24
- })
25
- .click(firstItem);
22
+ await act(async () => {
23
+ await user
24
+ .setup({
25
+ pointerEventsCheck: PointerEventsCheckLevel.Never,
26
+ })
27
+ .click(firstItem);
28
+ });
26
29
  },
27
30
  },
28
31
  {
29
32
  action: "pressed the Enter key",
30
33
  activate: async (user: UserEvent) => {
31
- await user.keyboard("{ArrowDown}{Enter}");
34
+ await act(async () => {
35
+ await user.keyboard("{ArrowDown}{Enter}");
36
+ });
32
37
  },
33
38
  },
34
39
  {
35
40
  action: "pressed the Space key",
36
41
  activate: async (user: UserEvent) => {
37
- await user.keyboard("{ArrowDown} ");
42
+ await act(async () => {
43
+ await user.keyboard("{ArrowDown} ");
44
+ });
38
45
  },
39
46
  },
40
47
  ])("should close after a MenuItem is $action", async ({ activate }) => {
@@ -53,7 +60,9 @@ describe("ActionMenu", () => {
53
60
  );
54
61
 
55
62
  const toggleBtn = screen.getByRole("button", { name: "Test Menu" });
56
- await user.click(toggleBtn);
63
+ await act(async () => {
64
+ await user.click(toggleBtn);
65
+ });
57
66
 
58
67
  await waitFor(() => expect(screen.getByRole("menu")).toBeVisible());
59
68
 
@@ -20,15 +20,15 @@ export const ActionMenuTypeTest = () => (
20
20
  <MenuContent>Test</MenuContent>
21
21
  <MenuFooter>Test</MenuFooter>
22
22
  </ActionMenu>
23
- {/* Valid ActionMenu allows items to be passed using the items prop */}
23
+ {/* Valid ActionMenu allows menuItems to be passed using the menuItems prop */}
24
24
  <ActionMenu
25
25
  menuToggleElement={<>test</>}
26
- items={[{ children: "Item Name", id: "item-1" }]}
26
+ menuItems={[{ children: "Item Name", id: "item-1" }]}
27
27
  />
28
- {/* Valid ActionMenu allows items prop to be rendered using a MenuItemComponent prop */}
28
+ {/* Valid ActionMenu allows menuItems prop to be rendered using a MenuItemComponent prop */}
29
29
  <ActionMenu
30
30
  menuToggleElement={<>test</>}
31
- items={[{ children: "Item Name", id: "item-1" }]}
31
+ menuItems={[{ children: "Item Name", id: "item-1" }]}
32
32
  MenuItemComponent={MenuItem}
33
33
  />
34
34
  {/* Valid ActionMenu accepts Downshift props and functions for useSelect other than selectedItem */}
@@ -51,16 +51,16 @@ export const ActionMenuTypeTest = () => (
51
51
  >
52
52
  Test
53
53
  </ActionMenu>
54
- {/* @ts-expect-error - when no children or items provided */}
55
- <ActionMenu menuToggleElement={<>test</>} />
56
54
  {/* @ts-expect-error - when both children and MenuItemComponent provided */}
57
55
  <ActionMenu menuToggleElement={<>test</>} MenuItemComponent={MenuItem}>
58
56
  Children
59
57
  </ActionMenu>
60
- {/* @ts-expect-error - when both children and items provided */}
61
- <ActionMenu menuToggleElement={<>test</>} items={[]}>
58
+ {/* @ts-expect-error - when both children and menuItems provided */}
59
+ <ActionMenu menuToggleElement={<>test</>} menuItems={[]}>
62
60
  Children
63
61
  </ActionMenu>
62
+ {/* @ts-expect-error - when no children or items provided */}
63
+ <ActionMenu menuToggleElement={<>test</>} />
64
64
  {/* @ts-expect-error - when MenuItemComponent is of the wrong type */}
65
65
  <ActionMenu menuToggleElement={<>test</>} MenuItemComponent={MenuContent}>
66
66
  Children