@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sproutsocial/seeds-react-menu",
3
- "version": "0.5.0",
3
+ "version": "1.0.0",
4
4
  "description": "Seeds React Menu",
5
5
  "author": "Sprout Social, Inc.",
6
6
  "license": "MIT",
@@ -17,21 +17,24 @@
17
17
  "test:watch": "jest --watch --coverage=false"
18
18
  },
19
19
  "dependencies": {
20
- "@sproutsocial/seeds-react-box": "^1.1.1",
21
- "@sproutsocial/seeds-react-button": "^1.2.0",
22
- "@sproutsocial/seeds-react-checkbox": "^1.3.0",
23
- "@sproutsocial/seeds-react-icon": "^1.1.1",
24
- "@sproutsocial/seeds-react-input": "^1.3.1",
25
- "@sproutsocial/seeds-react-label": "^1.0.0",
26
- "@sproutsocial/seeds-react-popout": "^2.1.0",
27
- "@sproutsocial/seeds-react-radio": "^1.2.0",
28
- "@sproutsocial/seeds-react-switch": "^1.2.0",
29
- "@sproutsocial/seeds-react-system-props": "^3.0.1",
30
- "@sproutsocial/seeds-react-theme": "^2.2.0",
20
+ "@sproutsocial/seeds-react-box": "^1.1.2",
21
+ "@sproutsocial/seeds-react-button": "^1.2.1",
22
+ "@sproutsocial/seeds-react-checkbox": "^1.3.1",
23
+ "@sproutsocial/seeds-react-icon": "^1.1.2",
24
+ "@sproutsocial/seeds-react-input": "^1.4.1",
25
+ "@sproutsocial/seeds-react-label": "^1.0.1",
26
+ "@sproutsocial/seeds-react-popout": "^2.1.1",
27
+ "@sproutsocial/seeds-react-radio": "^1.3.0",
28
+ "@sproutsocial/seeds-react-switch": "^1.2.1",
29
+ "@sproutsocial/seeds-react-system-props": "^3.0.2",
30
+ "@sproutsocial/seeds-react-theme": "^2.3.0",
31
+ "@sproutsocial/seeds-react-token-input": "^1.3.0",
32
+ "@sproutsocial/seeds-react-utilities": "^4.2.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,422 @@
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
+ "circle-question-outline",
164
+ ],
165
+ "Trigger Appearance": "secondary",
166
+ },
167
+ render: (args) => {
168
+ return (
169
+ <ActionMenu
170
+ {...args}
171
+ menuToggleElement={
172
+ <StorybookMenuToggleButton
173
+ buttonText={args["Trigger Text"]}
174
+ buttonProps={{ appearance: args["Trigger Appearance"] }}
175
+ />
176
+ }
177
+ >
178
+ <MenuContent>
179
+ <MenuGroup id="actions">
180
+ <MenuItem
181
+ onClick={(e) => handleOnClick(e, "Compose")}
182
+ key={0}
183
+ id="Compose"
184
+ >
185
+ <Box display="flex" alignItems="center" gap="300">
186
+ <Icon name={args["icons"][0] as TypeIconName} />
187
+ Compose
188
+ </Box>
189
+ </MenuItem>
190
+ <MenuItem
191
+ onClick={(e) => handleOnClick(e, "View Messages")}
192
+ key={1}
193
+ id="View Messages"
194
+ >
195
+ <Box display="flex" alignItems="center" gap="300">
196
+ <Icon name={args["icons"][1] as TypeIconName} />
197
+ View Messages
198
+ </Box>
199
+ </MenuItem>
200
+ <MenuItem
201
+ onClick={(e) => handleOnClick(e, "Analyze post")}
202
+ key={2}
203
+ id="Analyze post"
204
+ >
205
+ <Box display="flex" alignItems="center" gap="300">
206
+ <Icon name={args["icons"][2] as TypeIconName} />
207
+ Analyze Post
208
+ </Box>
209
+ </MenuItem>
210
+ <MenuItem href="https://sproutsocial.com" target="_blank" id="help">
211
+ <Box display="flex" alignItems="center" gap="300">
212
+ <Icon name={args["icons"][3] as TypeIconName} />
213
+ Visit Help Center
214
+ <Icon
215
+ name="arrow-right-up"
216
+ aria-label="open in a new window"
217
+ style={{ marginLeft: "auto" }}
218
+ />
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 display="flex" alignItems="center" gap="300">
229
+ <Icon name="gear-outline" />
230
+ Settings
231
+ </Box>
232
+ </MenuItem>
233
+ </MenuGroup>
234
+ </MenuContent>
235
+ </ActionMenu>
236
+ );
237
+ },
238
+ };
239
+
240
+ export const ActionMenuWithHeaderFooter: Story = {
241
+ name: "With Header & Footer",
242
+ args: {
243
+ "Trigger Text": "ActionMenu",
244
+ "Trigger Appearance": "secondary",
245
+ },
246
+ render: (args) => {
247
+ const [highContrast, setHighContrast] = React.useState(false);
248
+ const [darkMode, setDarkMode] = React.useState(false);
249
+ return (
250
+ <ActionMenu
251
+ {...args}
252
+ menuToggleElement={
253
+ <StorybookMenuToggleButton
254
+ buttonText={args["Trigger Text"]}
255
+ buttonProps={{ appearance: args["Trigger Appearance"] }}
256
+ />
257
+ }
258
+ >
259
+ <MenuHeader title="Select one of the following" />
260
+ <MenuContent>
261
+ <MenuGroup id="actions">
262
+ <MenuItem
263
+ onClick={(e) => handleOnClick(e, "Compose")}
264
+ key={0}
265
+ id="Compose"
266
+ >
267
+ Compose
268
+ </MenuItem>
269
+ <MenuItem
270
+ onClick={(e) => handleOnClick(e, "View Messages")}
271
+ key={1}
272
+ id="View Messages"
273
+ >
274
+ View Messages
275
+ </MenuItem>
276
+ <MenuItem
277
+ onClick={(e) => handleOnClick(e, "Analyze post")}
278
+ key={2}
279
+ id="Analyze post"
280
+ >
281
+ Analyze post
282
+ </MenuItem>
283
+ </MenuGroup>
284
+ </MenuContent>
285
+ <MenuFooter>
286
+ <StyledList>
287
+ <li>
288
+ <Label htmlFor="menuSwitch1">High contrast</Label>
289
+ <Switch
290
+ mr={300}
291
+ id="menuSwitch1"
292
+ checked={highContrast}
293
+ onClick={() => setHighContrast(!highContrast)}
294
+ />
295
+ </li>
296
+ <li>
297
+ <Label htmlFor="menuSwitch2">Dark mode</Label>
298
+ <Switch
299
+ mr={300}
300
+ id="menuSwitch2"
301
+ checked={darkMode}
302
+ onClick={() => setDarkMode(!darkMode)}
303
+ />
304
+ </li>
305
+ <li>
306
+ <Link href="#" fontSize={200}>
307
+ Manage settings
308
+ </Link>
309
+ </li>
310
+ </StyledList>
311
+ </MenuFooter>
312
+ </ActionMenu>
313
+ );
314
+ },
315
+ };
316
+
317
+ export const ActionMenuControlled: Story = {
318
+ name: "Controlled State",
319
+ args: {
320
+ "Trigger Text": "ActionMenu",
321
+ "Trigger Appearance": "secondary",
322
+ },
323
+ render: (args) => {
324
+ const [isOpen, setIsOpen] = React.useState<boolean>(false);
325
+ return (
326
+ <ActionMenu
327
+ {...args}
328
+ isOpen={isOpen}
329
+ onIsOpenChange={({ isOpen }) => setIsOpen(isOpen)}
330
+ menuToggleElement={
331
+ <StorybookMenuToggleButton
332
+ buttonText={args["Trigger Text"]}
333
+ buttonProps={{ appearance: args["Trigger Appearance"] }}
334
+ />
335
+ }
336
+ >
337
+ <MenuContent>
338
+ <MenuGroup id="actions">
339
+ <MenuItem
340
+ onClick={(e) => handleOnClick(e, "Compose")}
341
+ key={0}
342
+ id="Compose"
343
+ >
344
+ Compose
345
+ </MenuItem>
346
+ <MenuItem
347
+ onClick={(e) => handleOnClick(e, "View Messages")}
348
+ key={1}
349
+ id="View Messages"
350
+ >
351
+ View Messages
352
+ </MenuItem>
353
+ <MenuItem
354
+ onClick={(e) => handleOnClick(e, "Analyze post")}
355
+ key={2}
356
+ id="Analyze post"
357
+ >
358
+ Analyze post
359
+ </MenuItem>
360
+ </MenuGroup>
361
+ </MenuContent>
362
+ </ActionMenu>
363
+ );
364
+ },
365
+ };
366
+
367
+ export const ActionMenuWithSearch: Story = {
368
+ name: "Action Menu with Search",
369
+ args: {
370
+ "Trigger Text": "ActionMenu",
371
+ "Trigger Appearance": "secondary",
372
+ },
373
+ render: (args) => {
374
+ return (
375
+ <ActionMenu
376
+ itemToString={(item) => `${item?.menuItemProps?.children || ""}`}
377
+ menuToggleElement={
378
+ <StorybookMenuToggleButton
379
+ buttonText={args["Trigger Text"]}
380
+ buttonProps={{ appearance: args["Trigger Appearance"] }}
381
+ />
382
+ }
383
+ onStateChange={(changes) => action("onStateChange")(changes)}
384
+ {...(args as object)}
385
+ >
386
+ <MenuHeader>
387
+ <MenuSearchInput
388
+ id="single-select-search"
389
+ name="search"
390
+ type="search"
391
+ aria-label="Search Menu"
392
+ />
393
+ </MenuHeader>
394
+ <MenuContent>
395
+ <MenuGroup id="actions">
396
+ <MenuItem
397
+ onClick={(e) => handleOnClick(e, "Compose")}
398
+ key={0}
399
+ id="Compose"
400
+ >
401
+ Compose
402
+ </MenuItem>
403
+ <MenuItem
404
+ onClick={(e) => handleOnClick(e, "View Messages")}
405
+ key={1}
406
+ id="View Messages"
407
+ >
408
+ View Messages
409
+ </MenuItem>
410
+ <MenuItem
411
+ onClick={(e) => handleOnClick(e, "Analyze post")}
412
+ key={2}
413
+ id="Analyze post"
414
+ >
415
+ Analyze post
416
+ </MenuItem>
417
+ </MenuGroup>
418
+ </MenuContent>
419
+ </ActionMenu>
420
+ );
421
+ },
422
+ };
@@ -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