@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.
- package/.turbo/turbo-build.log +10 -10
- package/CHANGELOG.md +15 -0
- package/dist/esm/index.js +1486 -440
- package/dist/esm/index.js.map +1 -1
- package/dist/index.d.mts +248 -92
- package/dist/index.d.ts +248 -92
- package/dist/index.js +1500 -429
- package/dist/index.js.map +1 -1
- package/package.json +4 -1
- package/src/ActionMenu/ActionMenu.stories.tsx +426 -0
- package/src/ActionMenu/ActionMenu.tsx +24 -27
- package/src/ActionMenu/ActionMenuTypes.ts +8 -7
- package/src/ActionMenu/__tests__/ActionMenu.test.tsx +17 -8
- package/src/ActionMenu/__tests__/ActionMenu.typetest.tsx +8 -8
- package/src/Autocomplete/Autocomplete.stories.tsx +38 -3
- package/src/Autocomplete/Autocomplete.tsx +15 -80
- package/src/Autocomplete/AutocompleteInput.tsx +18 -27
- package/src/Autocomplete/AutocompleteTokenInput.tsx +50 -0
- package/src/Autocomplete/AutocompleteTypes.ts +25 -12
- package/src/Autocomplete/MultiAutocomplete.tsx +189 -0
- package/src/Autocomplete/SingleAutocomplete.tsx +74 -0
- package/src/Autocomplete/__tests__/Autocomplete.test.tsx +171 -3
- package/src/Autocomplete/__tests__/Autocomplete.typetest.tsx +90 -8
- package/src/MenuContent/MenuContent.stories.tsx +164 -0
- package/src/MenuContent/MenuContent.tsx +26 -12
- package/src/MenuContent/MenuContentTypes.ts +6 -4
- package/src/MenuContent/__tests__/MenuContent.typetest.tsx +8 -8
- package/src/MenuContext.tsx +191 -37
- package/src/MenuFooter/MenuFooter.stories.tsx +202 -0
- package/src/MenuFooter/__tests__/MenuFooter.test.tsx +16 -11
- package/src/MenuGroup/MenuGroup.feature +16 -16
- package/src/MenuGroup/MenuGroup.stories.tsx +248 -0
- package/src/MenuGroup/MenuGroup.tsx +8 -7
- package/src/MenuGroup/MenuGroupTypes.ts +6 -2
- package/src/MenuGroup/__tests__/MenuGroup.test.tsx +6 -6
- package/src/MenuGroup/__tests__/MenuGroup.typetest.tsx +7 -7
- package/src/MenuGroup/styles.tsx +4 -2
- package/src/MenuHeader/MenuHeader.stories.tsx +276 -0
- package/src/MenuHeader/MenuHeader.tsx +3 -1
- package/src/MenuHeader/__tests__/MenuHeader.test.tsx +23 -13
- package/src/MenuHeader/styles.tsx +18 -14
- package/src/MenuItem/MenuItem.stories.tsx +507 -0
- package/src/MenuItem/MenuItem.tsx +18 -8
- package/src/MenuItem/MenuItemTypes.ts +9 -4
- package/src/MenuItem/__tests__/MenuItem.test.tsx +6 -6
- package/src/MenuItem/styles.tsx +21 -5
- package/src/MenuItem/useOptionProps.tsx +10 -8
- package/src/MenuLabel.tsx +2 -2
- package/src/MenuRoot/MenuRoot.tsx +37 -23
- package/src/MenuRoot/__tests__/MenuRoot.test.tsx +59 -32
- package/src/MenuSearchInput/MenuSearchInput.tsx +63 -0
- package/src/MenuSearchInput/__tests__/MenuSearchInput.test.tsx +157 -0
- package/src/MenuSearchInput/__tests__/MenuSearchInput.typetest.tsx +31 -0
- package/src/MenuSearchInput/index.ts +1 -0
- package/src/MenuSearchTokenInput/MenuSearchTokenInput.tsx +98 -0
- package/src/MenuSearchTokenInput/__tests__/MenuSearchTokenInput.test.tsx +247 -0
- package/src/MenuSearchTokenInput/__tests__/MenuSearchTokenInput.typetest.tsx +33 -0
- package/src/MenuSearchTokenInput/index.ts +1 -0
- package/src/MenuToggleButton/MenuToggleButton.stories.tsx +72 -0
- package/src/{MenuToggleButton.tsx → MenuToggleButton/MenuToggleButton.tsx} +6 -5
- package/src/MenuToggleButton/index.ts +1 -0
- package/src/MenuTokenInput.tsx +104 -0
- package/src/MultiSelectMenu/MultiSelectMenu.stories.tsx +214 -0
- package/src/MultiSelectMenu/MultiSelectMenu.tsx +38 -68
- package/src/MultiSelectMenu/MultiSelectMenuTypes.ts +8 -6
- package/src/MultiSelectMenu/__tests__/MultiSelectMenu.test.tsx +46 -17
- package/src/MultiSelectMenu/__tests__/MultiSelectMenu.typetest.tsx +6 -6
- package/src/NestedMenu/NestedMenu.feature +109 -0
- package/src/NestedMenu/NestedMenu.stories.tsx +157 -0
- package/src/NestedMenu/NestedMenu.tsx +102 -0
- package/src/NestedMenu/NestedMenuContent.tsx +71 -0
- package/src/NestedMenu/NestedMenuContext.tsx +56 -0
- package/src/NestedMenu/NestedMenuHeader.tsx +25 -0
- package/src/NestedMenu/NestedMenuItem.tsx +37 -0
- package/src/NestedMenu/NestedMenuTypes.ts +47 -0
- package/src/NestedMenu/__tests__/NestedMenu.test.tsx +31 -0
- package/src/NestedMenu/__tests__/NestedMenu.typetest.tsx +134 -0
- package/src/NestedMenu/index.ts +5 -0
- package/src/SingleSelectMenu/SingleSelectMenu.stories.tsx +227 -0
- package/src/SingleSelectMenu/SingleSelectMenu.tsx +23 -26
- package/src/SingleSelectMenu/SingleSelectMenuTypes.ts +7 -5
- package/src/SingleSelectMenu/__tests__/SingleSelectMenu.typetest.tsx +6 -6
- package/src/SubmenuItem/SubmenuItem.feature +82 -0
- package/src/SubmenuItem/SubmenuItem.stories.tsx +163 -0
- package/src/SubmenuItem/SubmenuItem.tsx +254 -0
- package/src/SubmenuItem/SubmenuItemTypes.ts +24 -0
- package/src/SubmenuItem/__tests__/SubmenuItem.test.tsx +3 -0
- package/src/SubmenuItem/__tests__/SubmenuItem.typetest.tsx +112 -0
- package/src/SubmenuItem/index.ts +2 -0
- package/src/hooks/useDownshiftDefaults.tsx +107 -0
- package/src/hooks/useItemFiltering.tsx +66 -0
- package/src/hooks/useMenuChildren.tsx +197 -128
- package/src/index.ts +6 -0
- package/src/menuTestingUtils.tsx +21 -10
- package/src/reducers/nestedMenuStateReducer.tsx +23 -0
- package/src/reducers/useComboboxStateReducer.tsx +26 -8
- package/src/reducers/useSelectStateReducer.tsx +24 -6
- package/src/storybookUtilities/menu-storybook-components.tsx +5 -3
- package/src/types.ts +38 -30
- package/src/utils/downshiftDefaults.ts +9 -0
- package/src/utils/getMultiSelectOverrides.ts +21 -0
- package/src/utils/reduceReducers.ts +19 -0
- 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
|
-
|
|
10
|
-
|
|
11
|
-
} from "../
|
|
12
|
-
import {
|
|
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 {
|
|
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
|
-
|
|
49
|
-
MenuItemComponent
|
|
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
|
-
|
|
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<
|
|
57
|
+
} = useMultipleSelection<TypeInternalItemProps>(defaultDownshiftProps.multi);
|
|
83
58
|
|
|
84
59
|
const { isOpen, ...useSelectReturnProps } = useSelect({
|
|
85
|
-
...
|
|
60
|
+
...defaultDownshiftProps.select,
|
|
86
61
|
items: allMenuItems,
|
|
87
62
|
itemToString,
|
|
88
63
|
selectedItem: null,
|
|
89
64
|
defaultHighlightedIndex: 0,
|
|
90
|
-
stateReducer: (
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
11
|
-
|
|
11
|
+
TypeDownshiftSelectProps,
|
|
12
|
+
TypeDownshiftMultiSelectProps {}
|
|
12
13
|
|
|
13
|
-
export type TypeMultiSelectMenuProps
|
|
14
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
206
|
+
await act(async () => {
|
|
207
|
+
await user.keyboard("{Enter}");
|
|
208
|
+
});
|
|
188
209
|
|
|
189
210
|
// select the second item
|
|
190
|
-
await
|
|
191
|
-
|
|
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
|
|
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
|
|
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
|
|
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).
|
|
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
|
|
24
|
+
{/* Valid MultiSelectMenu allows menuItems to be passed using the menuItems prop */}
|
|
25
25
|
<MultiSelectMenu
|
|
26
26
|
menuToggleElement={<>test</>}
|
|
27
|
-
|
|
27
|
+
menuItems={[{ children: "Item Name", id: "item-1" }]}
|
|
28
28
|
/>
|
|
29
|
-
{/* Valid MultiSelectMenu allows
|
|
29
|
+
{/* Valid MultiSelectMenu allows menuItems prop to be rendered using a MenuItemComponent prop */}
|
|
30
30
|
<MultiSelectMenu
|
|
31
31
|
menuToggleElement={<>test</>}
|
|
32
|
-
|
|
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
|
|
74
|
-
<MultiSelectMenu menuToggleElement={<>test</>}
|
|
73
|
+
{/* @ts-expect-error - when both children and menuItems provided */}
|
|
74
|
+
<MultiSelectMenu menuToggleElement={<>test</>} menuItems={[]}>
|
|
75
75
|
Children
|
|
76
76
|
</MultiSelectMenu>
|
|
77
77
|
<MultiSelectMenu
|