@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,104 @@
1
+ import React from "react";
2
+ import styled from "styled-components";
3
+ import {
4
+ TokenInput,
5
+ type TypeTokenInputProps,
6
+ type TypeTokenSpec,
7
+ } from "@sproutsocial/seeds-react-token-input";
8
+ import type { TypeInternalItemProps } from "./types";
9
+ import { MenuContentContext, MenuToggleContext } from "./MenuContext";
10
+
11
+ const StyledTokenInputWrapper = styled.div`
12
+ > div {
13
+ padding: 4px;
14
+ }
15
+
16
+ input {
17
+ line-height: 21px;
18
+ margin: 0;
19
+ padding: 2px 4px;
20
+ }
21
+
22
+ .TokenInput-token button {
23
+ margin-top: 0;
24
+ padding: 1px 4px;
25
+ }
26
+ `;
27
+
28
+ export interface TypeMenuTokenInputProps extends TypeTokenInputProps {
29
+ /* A function that can be passed to override the default token props */
30
+ getTokenProps?: (
31
+ itemId: TypeInternalItemProps["id"]
32
+ ) => Partial<TypeTokenSpec>;
33
+ MenuContext?: typeof MenuToggleContext | typeof MenuContentContext;
34
+ }
35
+
36
+ export const MenuTokenInput = ({
37
+ inputProps = {},
38
+ getTokenProps,
39
+ onKeyDown,
40
+ MenuContext = MenuToggleContext,
41
+ ...tokenInputProps
42
+ }: TypeMenuTokenInputProps) => {
43
+ const {
44
+ itemToString,
45
+ getSelectedItemProps,
46
+ removeSelectedItem,
47
+ setActiveIndex,
48
+ selectedItems = [],
49
+ } = React.useContext(MenuContext);
50
+
51
+ const removeToken = (tokenId: string) => {
52
+ const tokenToRemove = selectedItems?.find((item) => item.id === tokenId);
53
+ tokenToRemove && removeSelectedItem?.(tokenToRemove);
54
+ };
55
+
56
+ return (
57
+ <StyledTokenInputWrapper>
58
+ <TokenInput
59
+ onKeyDown={(e, value) => {
60
+ // set active index to last item when arrow left is pressed and input is empty
61
+ if (e.key === "ArrowLeft" && !value && selectedItems.length) {
62
+ setActiveIndex?.(selectedItems.length - 1);
63
+ }
64
+ onKeyDown?.(e, value);
65
+ }}
66
+ inputProps={{
67
+ autoComplete: "off",
68
+ type: "search",
69
+ ...inputProps,
70
+ }}
71
+ tokens={selectedItems.map((item, index) => {
72
+ const {
73
+ tokenProps: { ref, onKeyDown, ...restTokenProps } = {
74
+ ref: undefined,
75
+ onKeyDown: undefined,
76
+ },
77
+ ...rest
78
+ } = getTokenProps?.(item.id) || {};
79
+ return {
80
+ id: item.id,
81
+ value: itemToString?.(item) || item.id,
82
+ tokenProps: {
83
+ ...getSelectedItemProps?.({
84
+ selectedItem: item,
85
+ index,
86
+ ref,
87
+ refKey: "innerRef",
88
+ onKeyDown,
89
+ }),
90
+ // nullify the onclick provided from downshift
91
+ // TokenInput deletes the token on click
92
+ onClick: undefined,
93
+ tabIndex: 0,
94
+ ...restTokenProps,
95
+ },
96
+ ...rest,
97
+ };
98
+ })}
99
+ onRemoveToken={removeToken}
100
+ {...tokenInputProps}
101
+ />
102
+ </StyledTokenInputWrapper>
103
+ );
104
+ };
@@ -0,0 +1,214 @@
1
+ /* eslint-disable react-hooks/rules-of-hooks */
2
+ import React from "react";
3
+
4
+ import { Box } from "@sproutsocial/seeds-react-box";
5
+ import type { Meta, StoryObj } from "@storybook/react";
6
+ import { StorybookMenuToggleButton } from "../storybookUtilities/menu-storybook-components";
7
+ import { TEST_BOOKS, EXTRA_BOOKS } from "../__fixtures__/menu-test-data";
8
+
9
+ import {
10
+ MultiSelectMenu,
11
+ MenuContent,
12
+ MenuGroup,
13
+ MenuHeader,
14
+ MenuItem,
15
+ MenuSearchTokenInput,
16
+ type TypeInternalItemProps,
17
+ type TypeMultiSelectMenuProps,
18
+ } from "../index";
19
+
20
+ type TypeMultiSelectMenuStoryProps = TypeMultiSelectMenuProps & {
21
+ inputType: "checkbox" | "switch";
22
+ };
23
+
24
+ const meta: Meta<TypeMultiSelectMenuStoryProps> = {
25
+ title: "Under Development/MultiSelectMenu",
26
+ component: MultiSelectMenu,
27
+ decorators: [
28
+ (Story) => (
29
+ <Box display="flex" flexDirection="column" gap="16px" maxWidth={300}>
30
+ <Story />
31
+ </Box>
32
+ ),
33
+ ],
34
+ args: {
35
+ menuToggleElement: <StorybookMenuToggleButton />,
36
+ },
37
+ argTypes: {
38
+ inputType: {
39
+ options: ["checkbox", "switch", "radio"],
40
+ control: { type: "select" },
41
+ },
42
+ },
43
+ };
44
+
45
+ export default meta;
46
+
47
+ type Story = StoryObj<TypeMultiSelectMenuStoryProps>;
48
+
49
+ const MultiSelectMenuStory = ({ inputType, ...args }) => {
50
+ return (
51
+ <MultiSelectMenu menuToggleElement={args.menuToggleElement} {...args}>
52
+ <MenuContent>
53
+ <MenuGroup id="books">
54
+ {TEST_BOOKS.map((itemsToRender) => (
55
+ <MenuItem
56
+ inputType={inputType}
57
+ key={itemsToRender.id}
58
+ id={itemsToRender.id}
59
+ >
60
+ {itemsToRender.title}
61
+ </MenuItem>
62
+ ))}
63
+ </MenuGroup>
64
+ </MenuContent>
65
+ </MultiSelectMenu>
66
+ );
67
+ };
68
+
69
+ export const MultiSelectMenuExample: Story = {
70
+ name: "Multi select menu",
71
+ args: {
72
+ inputType: "checkbox",
73
+ },
74
+ render: (args) => <MultiSelectMenuStory {...args} />,
75
+ };
76
+
77
+ export const MultiSelectMenuWithItemsAsProps: Story = {
78
+ name: "With Items as Props",
79
+ args: {
80
+ inputType: "checkbox",
81
+ },
82
+ render: ({ inputType, ...args }) => {
83
+ const menuItems = TEST_BOOKS.map((book) => ({
84
+ id: book.id,
85
+ children: book.title,
86
+ inputType,
87
+ }));
88
+ return (
89
+ <MultiSelectMenu
90
+ {...args}
91
+ menuToggleElement={args.menuToggleElement}
92
+ menuItems={menuItems}
93
+ />
94
+ );
95
+ },
96
+ };
97
+
98
+ export const MultiSelectMenuWithGroupsExample: Story = {
99
+ name: "Multi select menu with groups",
100
+ args: {
101
+ inputType: "checkbox",
102
+ },
103
+ render: ({ inputType, ...args }) => {
104
+ return (
105
+ <MultiSelectMenu
106
+ menuToggleElement={<StorybookMenuToggleButton />}
107
+ {...args}
108
+ >
109
+ <MenuContent>
110
+ <MenuGroup title="Classics" id="classics">
111
+ {TEST_BOOKS.slice(0, 5).map((itemsToRender) => (
112
+ <MenuItem
113
+ inputType={inputType}
114
+ key={itemsToRender.id}
115
+ id={itemsToRender.id}
116
+ >
117
+ {itemsToRender.title}
118
+ </MenuItem>
119
+ ))}
120
+ </MenuGroup>
121
+ <MenuGroup title="Essentials" id="essentials" isSingleSelect>
122
+ {EXTRA_BOOKS.slice(0, 5).map((itemsToRender) => (
123
+ <MenuItem
124
+ inputType="radio"
125
+ key={itemsToRender.id}
126
+ id={itemsToRender.id}
127
+ >
128
+ {itemsToRender.title}
129
+ </MenuItem>
130
+ ))}
131
+ </MenuGroup>
132
+ </MenuContent>
133
+ </MultiSelectMenu>
134
+ );
135
+ },
136
+ };
137
+
138
+ export const MultiSelectMenuControlled: Story = {
139
+ name: "Controlled State",
140
+ args: {
141
+ inputType: "checkbox",
142
+ },
143
+ render: ({ inputType, ...args }) => {
144
+ const [isOpen, setIsOpen] = React.useState<boolean>(false);
145
+ const [selectedItems, setSelectedItems] = React.useState<
146
+ TypeInternalItemProps[]
147
+ >([]);
148
+ return (
149
+ <MultiSelectMenu
150
+ isOpen={isOpen}
151
+ onIsOpenChange={({ isOpen }) => setIsOpen(isOpen)}
152
+ selectedItems={selectedItems}
153
+ onSelectedItemsChange={({ selectedItems: newSelectedItems }) => {
154
+ setSelectedItems(newSelectedItems);
155
+ }}
156
+ menuToggleElement={<StorybookMenuToggleButton />}
157
+ {...args}
158
+ >
159
+ <MenuContent>
160
+ <MenuGroup title="Classics" id="classics">
161
+ {TEST_BOOKS.slice(0, 5).map((itemsToRender) => (
162
+ <MenuItem
163
+ inputType={inputType}
164
+ key={itemsToRender.id}
165
+ id={itemsToRender.id}
166
+ >
167
+ {itemsToRender.title}
168
+ </MenuItem>
169
+ ))}
170
+ </MenuGroup>
171
+ <MenuGroup title="Essentials" id="essentials">
172
+ {EXTRA_BOOKS.slice(0, 5).map((itemsToRender) => (
173
+ <MenuItem
174
+ inputType={inputType}
175
+ key={itemsToRender.id}
176
+ id={itemsToRender.id}
177
+ >
178
+ {itemsToRender.title}
179
+ </MenuItem>
180
+ ))}
181
+ </MenuGroup>
182
+ </MenuContent>
183
+ </MultiSelectMenu>
184
+ );
185
+ },
186
+ };
187
+
188
+ export const MultiSelectMenuWithSearch: Story = {
189
+ name: "With Search Token Input",
190
+ render: ({ inputType, ...args }) => {
191
+ return (
192
+ <MultiSelectMenu
193
+ itemToString={(item) => (item ? TEST_BOOKS[item.index].title : "")}
194
+ menuToggleElement={<StorybookMenuToggleButton />}
195
+ {...(args as object)}
196
+ >
197
+ <MenuHeader title="Select Book">
198
+ <MenuSearchTokenInput
199
+ id="single-select-search"
200
+ name="search"
201
+ aria-label="Search Books"
202
+ />
203
+ </MenuHeader>
204
+ <MenuContent>
205
+ {TEST_BOOKS.map(({ id, title }) => (
206
+ <MenuItem key={id} id={id} inputType={inputType}>
207
+ {title}
208
+ </MenuItem>
209
+ ))}
210
+ </MenuContent>
211
+ </MultiSelectMenu>
212
+ );
213
+ },
214
+ };
@@ -1,39 +1,13 @@
1
1
  import React from "react";
2
- import {
3
- useSelect,
4
- useMultipleSelection,
5
- type UseSelectState,
6
- } from "downshift";
2
+ import { useSelect, useMultipleSelection } from "downshift";
7
3
  import { useMenuChildren } from "../hooks/useMenuChildren";
8
- import {
9
- defaultUseSelectProps,
10
- useSelectStateReducer,
11
- } from "../reducers/useSelectStateReducer";
12
- import { itemToString } from "../utils/itemToString";
13
- import { MenuRoot } from "../MenuRoot";
4
+ import { useDownshiftDefaults } from "../hooks/useDownshiftDefaults";
5
+ import { itemToString } from "../utils/downshiftDefaults";
6
+ import { reduceReducers } from "../utils/reduceReducers";
7
+ import { MenuRoot, type TypeMenuRootProps } from "../MenuRoot";
8
+ import { type TypeMenuItemProps } from "../MenuItem";
14
9
  import type { TypeMultiSelectMenuProps } from "./MultiSelectMenuTypes";
15
- import type { TypeDefaultItemProps } from "../types";
16
- import { MenuItem } from "../MenuItem/index";
17
-
18
- const selectReducerForMultiSelect: TypeMultiSelectMenuProps["stateReducer"] = (
19
- state,
20
- actionAndChanges
21
- ) => {
22
- const { changes, type } = actionAndChanges;
23
- switch (type) {
24
- case useSelect.stateChangeTypes.ToggleButtonKeyDownEnter:
25
- case useSelect.stateChangeTypes.ToggleButtonKeyDownSpaceButton:
26
- case useSelect.stateChangeTypes.ItemClick:
27
- return {
28
- ...changes,
29
- isOpen: true, // keep the menu open after selection.
30
- // TODO: Confirm that keeping highlightedIndex the same is the desired UX
31
- // highlightedIndex: 0, // with the first option highlighted.
32
- highlightedIndex: state.highlightedIndex, // keep highlightedIndex the same instead of returning to 0
33
- };
34
- }
35
- return changes;
36
- };
10
+ import type { TypeInternalItemProps } from "../types";
37
11
 
38
12
  /**
39
13
  * @link https://seeds.sproutsocial.com/components/multiselect-menu/
@@ -43,35 +17,36 @@ const selectReducerForMultiSelect: TypeMultiSelectMenuProps["stateReducer"] = (
43
17
  * @see {@link https://seeds.sproutsocial.com/components/menu-group/ | MenuGroup}
44
18
  */
45
19
 
46
- export const MultiSelectMenu = ({
20
+ export const MultiSelectMenu = <
21
+ I extends TypeMenuItemProps = TypeMenuItemProps
22
+ >({
47
23
  children: childrenProp,
48
- items,
49
- MenuItemComponent = MenuItem,
24
+ menuItems,
25
+ MenuItemComponent,
26
+ onSelectedItemChange: externalOnSelectedItemChange,
50
27
  stateReducer: externalStateReducer,
51
28
  getA11yMultiStatusMessage,
52
29
  onMultiSelectStateChange,
53
30
  multiSelectStateReducer,
54
31
  ...useSelectProps
55
- }: TypeMultiSelectMenuProps) => {
32
+ }: TypeMultiSelectMenuProps<I>) => {
56
33
  const { allMenuItems, menuItemsMap, children } = useMenuChildren({
57
34
  children: childrenProp,
58
- items,
35
+ menuItems,
59
36
  MenuItemComponent,
60
37
  });
38
+ const { defaultDownshiftProps, ...restDefaults } = useDownshiftDefaults({
39
+ isCombobox: false,
40
+ isMulti: true,
41
+ // handles the prop name mapping for multi select
42
+ multiSelectProps: {
43
+ getA11yMultiStatusMessage,
44
+ onMultiSelectStateChange,
45
+ multiSelectStateReducer,
46
+ ...useSelectProps,
47
+ },
48
+ });
61
49
 
62
- const multiSelectOverrides = Object.entries({
63
- ...useSelectProps,
64
- getA11yStatusMessage: getA11yMultiStatusMessage,
65
- onStateChange: onMultiSelectStateChange,
66
- stateReducer: multiSelectStateReducer,
67
- }).reduce(
68
- (a, [k, v]) => (v ? ((a[k] = v), a) : a),
69
- {} as {
70
- getA11yStatusMessage?: TypeMultiSelectMenuProps["getA11yMultiStatusMessage"];
71
- onStateChange?: TypeMultiSelectMenuProps["onMultiSelectStateChange"];
72
- stateReducer?: TypeMultiSelectMenuProps["multiSelectStateReducer"];
73
- }
74
- );
75
50
  const {
76
51
  getSelectedItemProps,
77
52
  addSelectedItem,
@@ -79,29 +54,20 @@ export const MultiSelectMenu = ({
79
54
  selectedItems,
80
55
  getDropdownProps,
81
56
  ...useMultipleSelectionReturnProps
82
- } = useMultipleSelection<TypeDefaultItemProps>(multiSelectOverrides);
57
+ } = useMultipleSelection<TypeInternalItemProps>(defaultDownshiftProps.multi);
83
58
 
84
59
  const { isOpen, ...useSelectReturnProps } = useSelect({
85
- ...defaultUseSelectProps,
60
+ ...defaultDownshiftProps.select,
86
61
  items: allMenuItems,
87
62
  itemToString,
88
63
  selectedItem: null,
89
64
  defaultHighlightedIndex: 0,
90
- stateReducer: (state, actionAndChanges) => {
91
- let newState: Partial<UseSelectState<TypeDefaultItemProps>>;
92
- newState = useSelectStateReducer(state, actionAndChanges);
93
- newState = selectReducerForMultiSelect(state, {
94
- ...actionAndChanges,
95
- changes: newState,
96
- });
97
- return externalStateReducer
98
- ? externalStateReducer(state, {
99
- ...actionAndChanges,
100
- changes: newState,
101
- })
102
- : newState;
103
- },
104
- onStateChange: ({ type, selectedItem: newSelectedItem }) => {
65
+ stateReducer: reduceReducers(
66
+ defaultDownshiftProps.select.stateReducer,
67
+ externalStateReducer
68
+ ),
69
+ onSelectedItemChange: (changes) => {
70
+ const { type, selectedItem: newSelectedItem } = changes;
105
71
  switch (type) {
106
72
  case useSelect.stateChangeTypes.ToggleButtonKeyDownEnter:
107
73
  case useSelect.stateChangeTypes.ToggleButtonKeyDownSpaceButton:
@@ -124,6 +90,9 @@ export const MultiSelectMenu = ({
124
90
  default:
125
91
  break;
126
92
  }
93
+
94
+ // Call the consumer's onSelectedItemChange callback
95
+ externalOnSelectedItemChange?.(changes);
127
96
  },
128
97
  ...useSelectProps,
129
98
  });
@@ -147,6 +116,7 @@ export const MultiSelectMenu = ({
147
116
  ...useSelectProps,
148
117
  ...useSelectReturnProps,
149
118
  ...useMultipleSelectionReturnProps,
119
+ ...restDefaults,
150
120
  }}
151
121
  >
152
122
  {children}
@@ -1,14 +1,16 @@
1
- import type { TypeMenuRootOwnProps } from "../MenuRoot";
1
+ import type { TypeMenuRootOwnProps, TypeMenuRootProps } from "../MenuRoot";
2
2
  import type {
3
3
  TypeDownshiftSelectProps,
4
4
  TypeDownshiftMultiSelectProps,
5
5
  } from "../MenuContext";
6
+ import type { TypeMenuItemProps } from "../MenuItem";
6
7
  import type { TypeChildrenOrItems } from "../types";
7
8
 
8
- interface TypeMultiSelectMenuBaseProps
9
+ export interface TypeMultiSelectMenuBaseProps
9
10
  extends TypeMenuRootOwnProps,
10
- Omit<TypeDownshiftSelectProps, "items">,
11
- Omit<TypeDownshiftMultiSelectProps, "items"> {}
11
+ TypeDownshiftSelectProps,
12
+ TypeDownshiftMultiSelectProps {}
12
13
 
13
- export type TypeMultiSelectMenuProps = TypeMultiSelectMenuBaseProps &
14
- TypeChildrenOrItems;
14
+ export type TypeMultiSelectMenuProps<
15
+ I extends TypeMenuItemProps = TypeMenuItemProps
16
+ > = TypeMultiSelectMenuBaseProps & TypeChildrenOrItems<I>;
@@ -11,6 +11,7 @@ import {
11
11
  screen,
12
12
  cleanup,
13
13
  waitFor,
14
+ act,
14
15
  } from "@sproutsocial/seeds-react-testing-library";
15
16
 
16
17
  const menuItems = [
@@ -53,7 +54,9 @@ describe("MultiSelectMenu", () => {
53
54
 
54
55
  const toggleButton = screen.getByRole("button", { name: "testable menu" });
55
56
 
56
- await user.click(toggleButton);
57
+ await act(async () => {
58
+ await user.click(toggleButton);
59
+ });
57
60
 
58
61
  const item1 = screen.getByRole("option", { name: "Item 1" });
59
62
  const item2 = screen.getByRole("option", { name: "Item 2" });
@@ -64,7 +67,9 @@ describe("MultiSelectMenu", () => {
64
67
  });
65
68
 
66
69
  // Select first item
67
- await user.click(item1);
70
+ await act(async () => {
71
+ await user.click(item1);
72
+ });
68
73
 
69
74
  await waitFor(() => {
70
75
  // Check that the first item is selected
@@ -72,7 +77,9 @@ describe("MultiSelectMenu", () => {
72
77
  });
73
78
 
74
79
  // Select second item
75
- await user.click(item2);
80
+ await act(async () => {
81
+ await user.click(item2);
82
+ });
76
83
 
77
84
  await waitFor(() => {
78
85
  // Check that the second item is selected
@@ -112,7 +119,9 @@ describe("MultiSelectMenu", () => {
112
119
  const toggleButton = screen.getByRole("button", { name: "testable menu" });
113
120
 
114
121
  // Open Menu
115
- await user.click(toggleButton);
122
+ await act(async () => {
123
+ await user.click(toggleButton);
124
+ });
116
125
 
117
126
  const item1 = screen.getByRole("option", { name: "Item 1" });
118
127
 
@@ -121,14 +130,18 @@ describe("MultiSelectMenu", () => {
121
130
  });
122
131
 
123
132
  // Select Item 1
124
- await user.click(item1);
133
+ await act(async () => {
134
+ await user.click(item1);
135
+ });
125
136
 
126
137
  await waitFor(() => {
127
138
  expect(item1).toHaveAttribute("aria-selected", "true");
128
139
  });
129
140
 
130
141
  // Close Menu (MultiSelect does not close automatically)
131
- await user.click(toggleButton);
142
+ await act(async () => {
143
+ await user.click(toggleButton);
144
+ });
132
145
 
133
146
  // Make sure the menu is closed
134
147
  await waitFor(() =>
@@ -136,7 +149,9 @@ describe("MultiSelectMenu", () => {
136
149
  );
137
150
 
138
151
  // Open menu again
139
- await user.click(toggleButton);
152
+ await act(async () => {
153
+ await user.click(toggleButton);
154
+ });
140
155
 
141
156
  // Make sure the menu is open
142
157
  await waitFor(() =>
@@ -173,22 +188,30 @@ describe("MultiSelectMenu", () => {
173
188
  );
174
189
 
175
190
  // Open Menu
176
- await user.keyboard("{Tab}{Enter}");
191
+ await act(async () => {
192
+ await user.keyboard("{Tab}{Enter}");
193
+ });
177
194
  expect(await screen.findByRole("listbox")).toBeInTheDocument();
178
195
 
179
196
  // Check that the first item is highlighted
180
- await user.keyboard("{ArrowDown}");
197
+ await act(async () => {
198
+ await user.keyboard("{ArrowDown}");
199
+ });
181
200
  expect(screen.getByRole("listbox")).toHaveAttribute(
182
201
  "aria-activedescendant",
183
202
  screen.getByRole("option", { name: "Item 1" }).getAttribute("id")
184
203
  );
185
204
 
186
205
  // Select the first item
187
- await user.keyboard("{Enter}");
206
+ await act(async () => {
207
+ await user.keyboard("{Enter}");
208
+ });
188
209
 
189
210
  // select the second item
190
- await user.keyboard("{ArrowDown}");
191
- await user.keyboard(" ");
211
+ await act(async () => {
212
+ await user.keyboard("{ArrowDown}");
213
+ await user.keyboard(" ");
214
+ });
192
215
 
193
216
  // Check that the second item is highlighted and selected
194
217
  const item2 = screen.getByRole("option", { name: "Item 2" });
@@ -237,7 +260,9 @@ describe("MultiSelectMenu", () => {
237
260
  });
238
261
 
239
262
  // Open Menu
240
- await user.click(toggleButton);
263
+ await act(async () => {
264
+ await user.click(toggleButton);
265
+ });
241
266
 
242
267
  const item1 = screen.getByRole("option", { name: "Item 1" });
243
268
  const item2 = screen.getByRole("option", { name: "Item 2" });
@@ -290,7 +315,9 @@ describe("MultiSelectMenu", () => {
290
315
  });
291
316
 
292
317
  // Open Menu
293
- await user.click(toggleButton);
318
+ await act(async () => {
319
+ await user.click(toggleButton);
320
+ });
294
321
 
295
322
  const item1 = screen.getByRole("option", { name: "Item 1" });
296
323
 
@@ -299,12 +326,14 @@ describe("MultiSelectMenu", () => {
299
326
  });
300
327
 
301
328
  // Select Item 1
302
- await user.click(item1);
329
+ await act(async () => {
330
+ await user.click(item1);
331
+ });
303
332
 
304
333
  // Check that the menu is still open
305
334
  expect(await screen.findByRole("listbox")).toBeInTheDocument();
306
335
 
307
- // Check that callback called
308
- expect(mockSelectedItemsChange).toHaveBeenCalledTimes(1);
336
+ // Check that callback was called at least once
337
+ expect(mockSelectedItemsChange).toHaveBeenCalled();
309
338
  });
310
339
  });
@@ -21,15 +21,15 @@ export const MultiSelectMenuTypeTest = () => (
21
21
  <MenuContent>Test</MenuContent>
22
22
  <MenuFooter>Test</MenuFooter>
23
23
  </MultiSelectMenu>
24
- {/* Valid MultiSelectMenu allows items to be passed using the items prop */}
24
+ {/* Valid MultiSelectMenu allows menuItems to be passed using the menuItems prop */}
25
25
  <MultiSelectMenu
26
26
  menuToggleElement={<>test</>}
27
- items={[{ children: "Item Name", id: "item-1" }]}
27
+ menuItems={[{ children: "Item Name", id: "item-1" }]}
28
28
  />
29
- {/* Valid MultiSelectMenu allows items prop to be rendered using a MenuItemComponent prop */}
29
+ {/* Valid MultiSelectMenu allows menuItems prop to be rendered using a MenuItemComponent prop */}
30
30
  <MultiSelectMenu
31
31
  menuToggleElement={<>test</>}
32
- items={[{ children: "Item Name", id: "item-1" }]}
32
+ menuItems={[{ children: "Item Name", id: "item-1" }]}
33
33
  MenuItemComponent={MenuItem}
34
34
  />
35
35
  {/* Valid MultiSelectMenu accepts Downshift props and functions for useSelect */}
@@ -70,8 +70,8 @@ export const MultiSelectMenuTypeTest = () => (
70
70
  <MultiSelectMenu menuToggleElement={<>test</>} MenuItemComponent={MenuItem}>
71
71
  Children
72
72
  </MultiSelectMenu>
73
- {/* @ts-expect-error - when both children and items provided */}
74
- <MultiSelectMenu menuToggleElement={<>test</>} items={[]}>
73
+ {/* @ts-expect-error - when both children and menuItems provided */}
74
+ <MultiSelectMenu menuToggleElement={<>test</>} menuItems={[]}>
75
75
  Children
76
76
  </MultiSelectMenu>
77
77
  <MultiSelectMenu