@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
@@ -0,0 +1,72 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { Box } from "@sproutsocial/seeds-react-box";
3
+
4
+ import { TEST_BOOKS } from "../__fixtures__/menu-test-data";
5
+ import {
6
+ MenuItem,
7
+ SingleSelectMenu,
8
+ MenuToggleButton,
9
+ MenuContent,
10
+ } from "../index";
11
+
12
+ const meta: Meta<typeof MenuToggleButton> = {
13
+ title: "Under Development/Primitives/MenuToggleButton",
14
+ component: MenuToggleButton,
15
+ decorators: [
16
+ (Story) => (
17
+ <Box display="flex" flexDirection="column" gap="16px" maxWidth={300}>
18
+ <Story />
19
+ </Box>
20
+ ),
21
+ ],
22
+ argTypes: {
23
+ appearance: {
24
+ description: "All Button props are available",
25
+ options: [
26
+ "primary",
27
+ "secondary",
28
+ "pill",
29
+ "destructive",
30
+ "unstyled",
31
+ "placeholder",
32
+ ],
33
+ control: { type: "select" },
34
+ },
35
+ },
36
+ args: {
37
+ appearance: "primary",
38
+ disabled: false,
39
+ size: "default",
40
+ },
41
+ };
42
+
43
+ export default meta;
44
+ type Story = StoryObj<typeof MenuToggleButton>;
45
+
46
+ export const MenuTriggerOptions: Story = {
47
+ name: "Menu Trigger",
48
+
49
+ render: function MenuTriggerOptions(args) {
50
+ return (
51
+ <SingleSelectMenu
52
+ defaultIsOpen
53
+ initialIsOpen={false}
54
+ menuToggleElement={
55
+ <MenuToggleButton {...args} appearance={args.appearance}>
56
+ Select a book &#8595;
57
+ </MenuToggleButton>
58
+ }
59
+ >
60
+ <MenuContent>
61
+ {TEST_BOOKS.map((book) => {
62
+ return (
63
+ <MenuItem key={book.id} id={book.id}>
64
+ {book.title}
65
+ </MenuItem>
66
+ );
67
+ })}
68
+ </MenuContent>
69
+ </SingleSelectMenu>
70
+ );
71
+ },
72
+ };
@@ -1,6 +1,6 @@
1
- import React, { type ReactNode, useContext } from "react";
1
+ import React, { type ReactNode } from "react";
2
2
  import { Button, type TypeButtonProps } from "@sproutsocial/seeds-react-button";
3
- import { MenuContext } from "./MenuContext";
3
+ import { useMenuToggleContext } from "../MenuContext";
4
4
 
5
5
  export interface TypeMenuToggleButtonProps extends Partial<TypeButtonProps> {
6
6
  children: ReactNode;
@@ -10,15 +10,16 @@ export const MenuToggleButton = ({
10
10
  children,
11
11
  ...rest
12
12
  }: TypeMenuToggleButtonProps) => {
13
- const { getToggleButtonProps, isListbox, getDropdownProps } =
14
- useContext(MenuContext);
13
+ const { getToggleButtonProps, isListbox, getDropdownProps, ref, ariaProps } =
14
+ useMenuToggleContext();
15
15
  return (
16
16
  <Button
17
17
  appearance="secondary"
18
18
  {...getToggleButtonProps?.({
19
19
  // Decide if we want the preventKeyAction stuff. Maybe create a prop to allow for this to be overriden.
20
- ...getDropdownProps?.({ preventKeyAction: true }),
20
+ ...(getDropdownProps?.({ ref, preventKeyAction: true }) || { ref }),
21
21
  refKey: "innerRef",
22
+ ...ariaProps,
22
23
  ...(isListbox ? {} : { role: "button", "aria-haspopup": "menu" }),
23
24
  })}
24
25
  {...rest}
@@ -0,0 +1 @@
1
+ export * from "./MenuToggleButton";
@@ -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,38 +1,13 @@
1
1
  import React from "react";
2
- import {
3
- useSelect,
4
- useMultipleSelection,
5
- type UseSelectState,
6
- } from "downshift";
7
- import { useItemsFromChildren } from "../hooks/useItemsFromChildren";
8
- import {
9
- defaultUseSelectProps,
10
- useSelectStateReducer,
11
- } from "../reducers/useSelectStateReducer";
12
- import { itemToString } from "../utils/itemToString";
13
- import { MenuRoot } from "../MenuRoot";
2
+ import { useSelect, useMultipleSelection } from "downshift";
3
+ import { useMenuChildren } from "../hooks/useMenuChildren";
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
-
17
- const selectReducerForMultiSelect: TypeMultiSelectMenuProps["stateReducer"] = (
18
- state,
19
- actionAndChanges
20
- ) => {
21
- const { changes, type } = actionAndChanges;
22
- switch (type) {
23
- case useSelect.stateChangeTypes.ToggleButtonKeyDownEnter:
24
- case useSelect.stateChangeTypes.ToggleButtonKeyDownSpaceButton:
25
- case useSelect.stateChangeTypes.ItemClick:
26
- return {
27
- ...changes,
28
- isOpen: true, // keep the menu open after selection.
29
- // TODO: Confirm that keeping highlightedIndex the same is the desired UX
30
- // highlightedIndex: 0, // with the first option highlighted.
31
- highlightedIndex: state.highlightedIndex, // keep highlightedIndex the same instead of returning to 0
32
- };
33
- }
34
- return changes;
35
- };
10
+ import type { TypeInternalItemProps } from "../types";
36
11
 
37
12
  /**
38
13
  * @link https://seeds.sproutsocial.com/components/multiselect-menu/
@@ -42,29 +17,36 @@ const selectReducerForMultiSelect: TypeMultiSelectMenuProps["stateReducer"] = (
42
17
  * @see {@link https://seeds.sproutsocial.com/components/menu-group/ | MenuGroup}
43
18
  */
44
19
 
45
- export const MultiSelectMenu = ({
46
- children,
20
+ export const MultiSelectMenu = <
21
+ I extends TypeMenuItemProps = TypeMenuItemProps
22
+ >({
23
+ children: childrenProp,
24
+ menuItems,
25
+ MenuItemComponent,
26
+ onSelectedItemChange: externalOnSelectedItemChange,
47
27
  stateReducer: externalStateReducer,
48
28
  getA11yMultiStatusMessage,
49
29
  onMultiSelectStateChange,
50
30
  multiSelectStateReducer,
51
31
  ...useSelectProps
52
- }: TypeMultiSelectMenuProps) => {
53
- const { allMenuItems, menuItemsMap } = useItemsFromChildren({ children });
32
+ }: TypeMultiSelectMenuProps<I>) => {
33
+ const { allMenuItems, menuItemsMap, children } = useMenuChildren({
34
+ children: childrenProp,
35
+ menuItems,
36
+ MenuItemComponent,
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
+ });
54
49
 
55
- const multiSelectOverrides = Object.entries({
56
- ...useSelectProps,
57
- getA11yStatusMessage: getA11yMultiStatusMessage,
58
- onStateChange: onMultiSelectStateChange,
59
- stateReducer: multiSelectStateReducer,
60
- }).reduce(
61
- (a, [k, v]) => (v ? ((a[k] = v), a) : a),
62
- {} as {
63
- getA11yStatusMessage?: TypeMultiSelectMenuProps["getA11yMultiStatusMessage"];
64
- onStateChange?: TypeMultiSelectMenuProps["onMultiSelectStateChange"];
65
- stateReducer?: TypeMultiSelectMenuProps["multiSelectStateReducer"];
66
- }
67
- );
68
50
  const {
69
51
  getSelectedItemProps,
70
52
  addSelectedItem,
@@ -72,29 +54,20 @@ export const MultiSelectMenu = ({
72
54
  selectedItems,
73
55
  getDropdownProps,
74
56
  ...useMultipleSelectionReturnProps
75
- } = useMultipleSelection<TypeDefaultItemProps>(multiSelectOverrides);
57
+ } = useMultipleSelection<TypeInternalItemProps>(defaultDownshiftProps.multi);
76
58
 
77
59
  const { isOpen, ...useSelectReturnProps } = useSelect({
78
- ...defaultUseSelectProps,
60
+ ...defaultDownshiftProps.select,
79
61
  items: allMenuItems,
80
62
  itemToString,
81
63
  selectedItem: null,
82
64
  defaultHighlightedIndex: 0,
83
- stateReducer: (state, actionAndChanges) => {
84
- let newState: Partial<UseSelectState<TypeDefaultItemProps>>;
85
- newState = useSelectStateReducer(state, actionAndChanges);
86
- newState = selectReducerForMultiSelect(state, {
87
- ...actionAndChanges,
88
- changes: newState,
89
- });
90
- return externalStateReducer
91
- ? externalStateReducer(state, {
92
- ...actionAndChanges,
93
- changes: newState,
94
- })
95
- : newState;
96
- },
97
- onStateChange: ({ type, selectedItem: newSelectedItem }) => {
65
+ stateReducer: reduceReducers(
66
+ defaultDownshiftProps.select.stateReducer,
67
+ externalStateReducer
68
+ ),
69
+ onSelectedItemChange: (changes) => {
70
+ const { type, selectedItem: newSelectedItem } = changes;
98
71
  switch (type) {
99
72
  case useSelect.stateChangeTypes.ToggleButtonKeyDownEnter:
100
73
  case useSelect.stateChangeTypes.ToggleButtonKeyDownSpaceButton:
@@ -117,6 +90,9 @@ export const MultiSelectMenu = ({
117
90
  default:
118
91
  break;
119
92
  }
93
+
94
+ // Call the consumer's onSelectedItemChange callback
95
+ externalOnSelectedItemChange?.(changes);
120
96
  },
121
97
  ...useSelectProps,
122
98
  });
@@ -140,6 +116,7 @@ export const MultiSelectMenu = ({
140
116
  ...useSelectProps,
141
117
  ...useSelectReturnProps,
142
118
  ...useMultipleSelectionReturnProps,
119
+ ...restDefaults,
143
120
  }}
144
121
  >
145
122
  {children}
@@ -1,12 +1,16 @@
1
- import type { TypeMenuRootProps, 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";
7
+ import type { TypeChildrenOrItems } from "../types";
6
8
 
7
- export interface TypeMultiSelectMenuProps
9
+ export interface TypeMultiSelectMenuBaseProps
8
10
  extends TypeMenuRootOwnProps,
9
11
  TypeDownshiftSelectProps,
10
- TypeDownshiftMultiSelectProps {
11
- children: TypeMenuRootProps["children"];
12
- }
12
+ TypeDownshiftMultiSelectProps {}
13
+
14
+ export type TypeMultiSelectMenuProps<
15
+ I extends TypeMenuItemProps = TypeMenuItemProps
16
+ > = TypeMultiSelectMenuBaseProps & TypeChildrenOrItems<I>;