@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.
- package/.turbo/turbo-build.log +10 -10
- package/CHANGELOG.md +32 -0
- package/dist/esm/index.js +1553 -423
- package/dist/esm/index.js.map +1 -1
- package/dist/index.d.mts +303 -122
- package/dist/index.d.ts +303 -122
- package/dist/index.js +1561 -405
- 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 +30 -26
- package/src/ActionMenu/ActionMenuTypes.ts +13 -8
- package/src/ActionMenu/__tests__/ActionMenu.test.tsx +17 -30
- package/src/ActionMenu/__tests__/ActionMenu.typetest.tsx +32 -3
- package/src/Autocomplete/Autocomplete.stories.tsx +61 -2
- package/src/Autocomplete/Autocomplete.tsx +15 -74
- package/src/Autocomplete/AutocompleteInput.tsx +18 -27
- package/src/Autocomplete/AutocompleteTokenInput.tsx +50 -0
- package/src/Autocomplete/AutocompleteTypes.ts +27 -11
- 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 +116 -3
- package/src/MenuContent/MenuContent.stories.tsx +164 -0
- package/src/MenuContent/MenuContent.tsx +35 -8
- package/src/MenuContent/MenuContentTypes.ts +7 -5
- package/src/MenuContent/__tests__/MenuContent.typetest.tsx +18 -2
- 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 +21 -21
- package/src/MenuGroup/MenuGroup.stories.tsx +248 -0
- package/src/MenuGroup/MenuGroup.tsx +9 -7
- package/src/MenuGroup/MenuGroupTypes.ts +8 -6
- package/src/MenuGroup/__tests__/MenuGroup.test.tsx +6 -6
- package/src/MenuGroup/__tests__/MenuGroup.typetest.tsx +23 -4
- 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 -9
- 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 +44 -67
- package/src/MultiSelectMenu/MultiSelectMenuTypes.ts +9 -5
- package/src/MultiSelectMenu/__tests__/MultiSelectMenu.test.tsx +46 -60
- package/src/MultiSelectMenu/__tests__/MultiSelectMenu.typetest.tsx +29 -2
- 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 +29 -25
- package/src/SingleSelectMenu/SingleSelectMenuTypes.ts +9 -5
- package/src/SingleSelectMenu/__tests__/SingleSelectMenu.typetest.tsx +32 -2
- 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/__tests__/Menu.test.tsx +38 -0
- package/src/hooks/useDownshiftDefaults.tsx +107 -0
- package/src/hooks/useItemFiltering.tsx +66 -0
- package/src/hooks/useMenuChildren.tsx +279 -0
- package/src/index.ts +7 -1
- 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 -5
- package/src/utils/downshiftDefaults.ts +9 -0
- package/src/utils/getMultiSelectOverrides.ts +21 -0
- package/src/utils/reduceReducers.ts +19 -0
- package/src/hooks/useItemsFromChildren.tsx +0 -131
- package/src/utils/itemToString.ts +0 -5
|
@@ -5,9 +5,10 @@ import {
|
|
|
5
5
|
waitFor,
|
|
6
6
|
act,
|
|
7
7
|
} from "@sproutsocial/seeds-react-testing-library";
|
|
8
|
-
import { MenuContent, MenuItem } from "../../index";
|
|
8
|
+
import { MenuContent, MenuItem, type TypeMenuRootProps } from "../../index";
|
|
9
9
|
import { Autocomplete } from "../Autocomplete";
|
|
10
10
|
import { AutocompleteInput } from "../AutocompleteInput";
|
|
11
|
+
import { AutocompleteTokenInput } from "../AutocompleteTokenInput";
|
|
11
12
|
import type { TypeAutocompleteProps } from "../AutocompleteTypes";
|
|
12
13
|
|
|
13
14
|
const menuItems = [
|
|
@@ -74,6 +75,7 @@ describe("Autocomplete Component", () => {
|
|
|
74
75
|
const suggestion = screen.getByRole("option", { name: menuItems[0].label });
|
|
75
76
|
await act(() => user.click(suggestion));
|
|
76
77
|
await waitFor(() => expect(inputElement.value).toBe(menuItems[0].label));
|
|
78
|
+
await act(async () => await user.click(inputElement));
|
|
77
79
|
expect(suggestion).toHaveAttribute("aria-selected", "true");
|
|
78
80
|
});
|
|
79
81
|
|
|
@@ -145,7 +147,8 @@ describe("Autocomplete Component", () => {
|
|
|
145
147
|
|
|
146
148
|
it("should use the custom filter function when getIsItemVisible is passed", async () => {
|
|
147
149
|
const customFilter: TypeAutocompleteProps["getIsItemVisible"] = jest.fn(
|
|
148
|
-
(item, inputValue) =>
|
|
150
|
+
(item, inputValue) =>
|
|
151
|
+
itemToString(item).toLowerCase().startsWith(inputValue.toLowerCase())
|
|
149
152
|
);
|
|
150
153
|
const { user } = render(
|
|
151
154
|
<Autocomplete
|
|
@@ -156,11 +159,176 @@ describe("Autocomplete Component", () => {
|
|
|
156
159
|
const inputElement = screen.getByRole("combobox");
|
|
157
160
|
await act(async () => {
|
|
158
161
|
await user.click(inputElement);
|
|
162
|
+
await user.type(inputElement, "item 0");
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
await waitFor(() => expect(screen.queryAllByRole("option").length).toBe(1));
|
|
166
|
+
|
|
167
|
+
await act(async () => {
|
|
168
|
+
await user.clear(inputElement);
|
|
159
169
|
await user.type(inputElement, "0");
|
|
160
170
|
});
|
|
161
171
|
|
|
162
|
-
await waitFor(() => expect(screen.
|
|
172
|
+
await waitFor(() => expect(screen.queryAllByRole("option").length).toBe(0));
|
|
163
173
|
|
|
164
174
|
expect(customFilter).toHaveBeenCalled();
|
|
165
175
|
});
|
|
176
|
+
|
|
177
|
+
describe("in multiSelect mode", () => {
|
|
178
|
+
it("should render multiple selected items", () => {
|
|
179
|
+
render(
|
|
180
|
+
<Autocomplete
|
|
181
|
+
{...defaultAutocompleteProps}
|
|
182
|
+
menuToggleElement={<AutocompleteTokenInput />}
|
|
183
|
+
initialSelectedItems={[menuItems[0], menuItems[1]]}
|
|
184
|
+
multiSelect
|
|
185
|
+
/>
|
|
186
|
+
);
|
|
187
|
+
const selectedItems = screen.getAllByRole("button");
|
|
188
|
+
expect(selectedItems.length).toBe(2);
|
|
189
|
+
expect(selectedItems[0]).toHaveTextContent(menuItems[0].label);
|
|
190
|
+
expect(selectedItems[1]).toHaveTextContent(menuItems[1].label);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("should add an item to the selected items list", async () => {
|
|
194
|
+
const { user } = render(
|
|
195
|
+
<Autocomplete
|
|
196
|
+
{...defaultAutocompleteProps}
|
|
197
|
+
menuToggleElement={<AutocompleteTokenInput />}
|
|
198
|
+
initialSelectedItems={[menuItems[0]]}
|
|
199
|
+
multiSelect
|
|
200
|
+
/>
|
|
201
|
+
);
|
|
202
|
+
const inputElement = screen.getByRole("combobox");
|
|
203
|
+
await act(async () => {
|
|
204
|
+
await user.click(inputElement);
|
|
205
|
+
await user.type(inputElement, "item 1");
|
|
206
|
+
});
|
|
207
|
+
const suggestion = screen.getByRole("option", {
|
|
208
|
+
name: menuItems[1].label,
|
|
209
|
+
});
|
|
210
|
+
await act(() => user.click(suggestion));
|
|
211
|
+
const selectedItems = screen.getAllByRole("button");
|
|
212
|
+
expect(selectedItems.length).toBe(2);
|
|
213
|
+
expect(selectedItems[1]).toHaveTextContent(menuItems[1].label);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("should remove an item from the selected items list", async () => {
|
|
217
|
+
const { user } = render(
|
|
218
|
+
<Autocomplete
|
|
219
|
+
{...defaultAutocompleteProps}
|
|
220
|
+
menuToggleElement={<AutocompleteTokenInput />}
|
|
221
|
+
initialSelectedItems={[menuItems[0], menuItems[1]]}
|
|
222
|
+
multiSelect
|
|
223
|
+
/>
|
|
224
|
+
);
|
|
225
|
+
const token = screen.getByRole("button", {
|
|
226
|
+
name: menuItems[0].label,
|
|
227
|
+
});
|
|
228
|
+
await act(() => user.click(token));
|
|
229
|
+
const selectedItems = screen.getAllByRole("button");
|
|
230
|
+
expect(selectedItems.length).toBe(1);
|
|
231
|
+
expect(selectedItems[0]).toHaveTextContent(menuItems[1].label);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("should call onSelectedItemsChange when an item is added", async () => {
|
|
235
|
+
const handleChange = jest.fn();
|
|
236
|
+
const { user } = render(
|
|
237
|
+
<Autocomplete
|
|
238
|
+
{...defaultAutocompleteProps}
|
|
239
|
+
menuToggleElement={<AutocompleteTokenInput />}
|
|
240
|
+
onSelectedItemsChange={handleChange}
|
|
241
|
+
multiSelect
|
|
242
|
+
/>
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
const testOption = menuItems[1];
|
|
246
|
+
const inputElement = screen.getByRole("combobox");
|
|
247
|
+
await act(async () => {
|
|
248
|
+
await user.click(inputElement);
|
|
249
|
+
await user.type(inputElement, testOption.label);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
const suggestion = screen.getByRole("option", {
|
|
253
|
+
name: testOption.label,
|
|
254
|
+
});
|
|
255
|
+
await act(() => user.click(suggestion));
|
|
256
|
+
await waitFor(() => expect(handleChange).toHaveBeenCalled());
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it("should call onSelectedItemsChange when an item is removed", async () => {
|
|
260
|
+
const testOption = menuItems[0];
|
|
261
|
+
const handleChange = jest.fn();
|
|
262
|
+
const { user } = render(
|
|
263
|
+
<Autocomplete
|
|
264
|
+
{...defaultAutocompleteProps}
|
|
265
|
+
menuToggleElement={<AutocompleteTokenInput />}
|
|
266
|
+
initialSelectedItems={[testOption]}
|
|
267
|
+
onSelectedItemsChange={handleChange}
|
|
268
|
+
multiSelect
|
|
269
|
+
/>
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
const token = screen.getByRole("button", {
|
|
273
|
+
name: testOption.label,
|
|
274
|
+
});
|
|
275
|
+
await act(() => user.click(token));
|
|
276
|
+
await waitFor(() => expect(handleChange).toHaveBeenCalled());
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it("hides selected items in the suggestions list", async () => {
|
|
280
|
+
const testOption = menuItems[0];
|
|
281
|
+
const { user } = render(
|
|
282
|
+
<Autocomplete
|
|
283
|
+
{...defaultAutocompleteProps}
|
|
284
|
+
menuToggleElement={<AutocompleteTokenInput />}
|
|
285
|
+
initialSelectedItems={[testOption]}
|
|
286
|
+
multiSelect
|
|
287
|
+
/>
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
const inputElement = screen.getByRole("combobox");
|
|
291
|
+
await act(async () => {
|
|
292
|
+
await user.click(inputElement);
|
|
293
|
+
await user.type(inputElement, "item");
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
waitFor(() =>
|
|
297
|
+
expect(screen.getAllByRole("option")).toHaveLength(menuItems.length - 1)
|
|
298
|
+
);
|
|
299
|
+
expect(
|
|
300
|
+
screen.queryByRole("option", {
|
|
301
|
+
name: testOption.label,
|
|
302
|
+
})
|
|
303
|
+
).not.toBeInTheDocument();
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it("shows selected items in the suggestions list when showSelectedItemsInDropdown is true", async () => {
|
|
307
|
+
const testOption = menuItems[0];
|
|
308
|
+
const { user } = render(
|
|
309
|
+
<Autocomplete
|
|
310
|
+
{...defaultAutocompleteProps}
|
|
311
|
+
menuToggleElement={<AutocompleteTokenInput />}
|
|
312
|
+
initialSelectedItems={[testOption]}
|
|
313
|
+
multiSelect
|
|
314
|
+
showSelectedItemsInDropdown
|
|
315
|
+
/>
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
const inputElement = screen.getByRole("combobox");
|
|
319
|
+
await act(async () => {
|
|
320
|
+
await user.click(inputElement);
|
|
321
|
+
await user.type(inputElement, "item");
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
waitFor(() =>
|
|
325
|
+
expect(screen.getAllByRole("option")).toHaveLength(menuItems.length)
|
|
326
|
+
);
|
|
327
|
+
expect(
|
|
328
|
+
screen.getByRole("option", {
|
|
329
|
+
name: testOption.label,
|
|
330
|
+
})
|
|
331
|
+
).toBeInTheDocument();
|
|
332
|
+
});
|
|
333
|
+
});
|
|
166
334
|
});
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
Autocomplete,
|
|
4
|
+
MenuContent,
|
|
5
|
+
MenuHeader,
|
|
6
|
+
MenuFooter,
|
|
7
|
+
MenuItem,
|
|
8
|
+
type TypeMenuItemProps,
|
|
9
|
+
} from "../../index";
|
|
3
10
|
|
|
4
11
|
export const AutocompleteTypeTest = () => (
|
|
5
12
|
<>
|
|
@@ -15,6 +22,17 @@ export const AutocompleteTypeTest = () => (
|
|
|
15
22
|
<MenuContent>Test</MenuContent>
|
|
16
23
|
<MenuFooter>Test</MenuFooter>
|
|
17
24
|
</Autocomplete>
|
|
25
|
+
{/* Valid Autocomplete allows menuItems to be passed using the menuItems prop */}
|
|
26
|
+
<Autocomplete
|
|
27
|
+
menuToggleElement={<>test</>}
|
|
28
|
+
menuItems={[{ children: "Item Name", id: "item-1" }]}
|
|
29
|
+
/>
|
|
30
|
+
{/* Valid Autocomplete allows menuItems to be rendered using MenuItemComponent */}
|
|
31
|
+
<Autocomplete
|
|
32
|
+
menuToggleElement={<>test</>}
|
|
33
|
+
menuItems={[{ children: "Item Name", id: "item-1" }]}
|
|
34
|
+
MenuItemComponent={MenuItem}
|
|
35
|
+
/>
|
|
18
36
|
{/* Valid Autocomplete allows getIsItemVisible function */}
|
|
19
37
|
<Autocomplete menuToggleElement={<>test</>} getIsItemVisible={() => true}>
|
|
20
38
|
Test
|
|
@@ -31,6 +49,27 @@ export const AutocompleteTypeTest = () => (
|
|
|
31
49
|
>
|
|
32
50
|
Test
|
|
33
51
|
</Autocomplete>
|
|
52
|
+
{/* Valid Autocomplete allows multiSelect false */}
|
|
53
|
+
<Autocomplete menuToggleElement={<>test</>} multiSelect={false}>
|
|
54
|
+
Test
|
|
55
|
+
</Autocomplete>
|
|
56
|
+
{/* Valid Autocomplete allows multiSelect true */}
|
|
57
|
+
<Autocomplete menuToggleElement={<>test</>} multiSelect={true}>
|
|
58
|
+
Test
|
|
59
|
+
</Autocomplete>
|
|
60
|
+
{/* Valid Autocomplete allows multiSelect with valid multiSelect props */}
|
|
61
|
+
<Autocomplete
|
|
62
|
+
menuToggleElement={<>test</>}
|
|
63
|
+
multiSelect
|
|
64
|
+
showSelectedItemsInDropdown
|
|
65
|
+
onSelectedItemsChange={() => {}}
|
|
66
|
+
selectedItems={[]}
|
|
67
|
+
multiSelectStateReducer={(c) => c}
|
|
68
|
+
onMultiSelectStateChange={() => {}}
|
|
69
|
+
getA11yMultiStatusMessage={() => ""}
|
|
70
|
+
>
|
|
71
|
+
Test
|
|
72
|
+
</Autocomplete>
|
|
34
73
|
{/* @ts-expect-error - menuToggleElement is a required prop */}
|
|
35
74
|
<Autocomplete>Test</Autocomplete>
|
|
36
75
|
<Autocomplete
|
|
@@ -39,8 +78,25 @@ export const AutocompleteTypeTest = () => (
|
|
|
39
78
|
>
|
|
40
79
|
Test
|
|
41
80
|
</Autocomplete>
|
|
42
|
-
{/* @ts-expect-error - when no children provided */}
|
|
43
|
-
<Autocomplete menuToggleElement=
|
|
81
|
+
{/* @ts-expect-error - when no children or items provided */}
|
|
82
|
+
<Autocomplete menuToggleElement={<>test</>} />
|
|
83
|
+
{/* @ts-expect-error - when both children and menuItems provided */}
|
|
84
|
+
<Autocomplete menuToggleElement={<>test</>} menuItems={[]}>
|
|
85
|
+
Test
|
|
86
|
+
</Autocomplete>
|
|
87
|
+
{/* @ts-expect-error - when MenuItemComponent is passed with children */}
|
|
88
|
+
<Autocomplete menuToggleElement={<>test</>} MenuItemComponent={MenuItem}>
|
|
89
|
+
Test
|
|
90
|
+
</Autocomplete>
|
|
91
|
+
<Autocomplete
|
|
92
|
+
menuToggleElement={<>test</>}
|
|
93
|
+
menuItems={[] as TypeMenuItemProps[]}
|
|
94
|
+
/* @ts-expect-error - when MenuItemComponent has the wrong type */
|
|
95
|
+
MenuItemComponent={true}
|
|
96
|
+
// TODO: Fix TypeChildrenOrItems so this fails
|
|
97
|
+
// MenuItemComponent={MenuFooter}
|
|
98
|
+
></Autocomplete>
|
|
99
|
+
{/* Valid Autocomplete rejects invalid props when not multiSelect */}
|
|
44
100
|
<Autocomplete
|
|
45
101
|
menuToggleElement={<>test</>}
|
|
46
102
|
/* @ts-expect-error - isOpen can't be a string */
|
|
@@ -56,5 +112,62 @@ export const AutocompleteTypeTest = () => (
|
|
|
56
112
|
>
|
|
57
113
|
Test
|
|
58
114
|
</Autocomplete>
|
|
115
|
+
<Autocomplete
|
|
116
|
+
menuToggleElement={<>test</>}
|
|
117
|
+
multiSelect={false}
|
|
118
|
+
/* @ts-expect-error - onSelectedItemsChange is not valid for multiSelect false */
|
|
119
|
+
onSelectedItemsChange={() => {}}
|
|
120
|
+
>
|
|
121
|
+
Test
|
|
122
|
+
</Autocomplete>
|
|
123
|
+
<Autocomplete
|
|
124
|
+
menuToggleElement={<>test</>}
|
|
125
|
+
multiSelect={false}
|
|
126
|
+
/* @ts-expect-error - selectedItems is not valid for multiSelect false */
|
|
127
|
+
selectedItems={[]}
|
|
128
|
+
>
|
|
129
|
+
Test
|
|
130
|
+
</Autocomplete>
|
|
131
|
+
<Autocomplete
|
|
132
|
+
menuToggleElement={<>test</>}
|
|
133
|
+
multiSelect={false}
|
|
134
|
+
/* @ts-expect-error - multiSelectStateReducer is not valid for multiSelect false */
|
|
135
|
+
multiSelectStateReducer={(c) => c}
|
|
136
|
+
>
|
|
137
|
+
Test
|
|
138
|
+
</Autocomplete>
|
|
139
|
+
<Autocomplete
|
|
140
|
+
menuToggleElement={<>test</>}
|
|
141
|
+
multiSelect={false}
|
|
142
|
+
/* @ts-expect-error - onMultiSelectStateChange is not valid for multiSelect false */
|
|
143
|
+
onMultiSelectStateChange={() => {}}
|
|
144
|
+
>
|
|
145
|
+
Test
|
|
146
|
+
</Autocomplete>
|
|
147
|
+
<Autocomplete
|
|
148
|
+
menuToggleElement={<>test</>}
|
|
149
|
+
multiSelect={false}
|
|
150
|
+
/* @ts-expect-error - getA11yMultiStatusMessage is not valid for multiSelect false */
|
|
151
|
+
getA11yMultiStatusMessage={() => ""}
|
|
152
|
+
>
|
|
153
|
+
Test
|
|
154
|
+
</Autocomplete>
|
|
155
|
+
{/* Valid Autocomplete rejects invalid props for multiSelect */}
|
|
156
|
+
<Autocomplete
|
|
157
|
+
menuToggleElement={<>test</>}
|
|
158
|
+
multiSelect
|
|
159
|
+
/* @ts-expect-error - showSelectedItemsInDropdown can't be a string */
|
|
160
|
+
showSelectedItemsInDropdown="true"
|
|
161
|
+
/* @ts-expect-error - onSelectedItemsChange can't be a string */
|
|
162
|
+
onSelectedItemsChange="test"
|
|
163
|
+
/* @ts-expect-error - selectedItems can't null */
|
|
164
|
+
selectedItems={null}
|
|
165
|
+
/* @ts-expect-error - multiSelectStateReducer must have a return value */
|
|
166
|
+
multiSelectStateReducer={() => {}}
|
|
167
|
+
/* @ts-expect-error - onMultiSelectStateChange can't be a string */
|
|
168
|
+
onMultiSelectStateChange="test"
|
|
169
|
+
>
|
|
170
|
+
Test
|
|
171
|
+
</Autocomplete>
|
|
59
172
|
</>
|
|
60
173
|
);
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { Box } from "@sproutsocial/seeds-react-box";
|
|
3
|
+
import { Button } from "@sproutsocial/seeds-react-button";
|
|
4
|
+
import { Icon } from "@sproutsocial/seeds-react-icon";
|
|
5
|
+
import { Link } from "@sproutsocial/seeds-react-link";
|
|
6
|
+
import { Text } from "@sproutsocial/seeds-react-text";
|
|
7
|
+
import { VisuallyHidden } from "@sproutsocial/seeds-react-visually-hidden";
|
|
8
|
+
|
|
9
|
+
import { StorybookMenuToggleButton } from "../storybookUtilities/menu-storybook-components";
|
|
10
|
+
import {
|
|
11
|
+
MenuHeader,
|
|
12
|
+
MenuLabel,
|
|
13
|
+
MenuContent,
|
|
14
|
+
MenuItem,
|
|
15
|
+
SingleSelectMenu,
|
|
16
|
+
MenuFooter,
|
|
17
|
+
MenuGroup,
|
|
18
|
+
} from "../index";
|
|
19
|
+
|
|
20
|
+
const meta: Meta<typeof MenuContent> = {
|
|
21
|
+
title: "Under Development/Primitives/MenuContent",
|
|
22
|
+
component: MenuContent,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export default meta;
|
|
26
|
+
type Story = StoryObj<typeof MenuContent>;
|
|
27
|
+
|
|
28
|
+
interface TypeBook {
|
|
29
|
+
id: string;
|
|
30
|
+
author: string;
|
|
31
|
+
title: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const books: TypeBook[] = [
|
|
35
|
+
{ id: "book-1", author: "Harper Lee", title: "To Kill a Mockingbird" },
|
|
36
|
+
{ id: "book-2", author: "Lev Tolstoy", title: "War and Peace" },
|
|
37
|
+
{ id: "book-3", author: "Fyodor Dostoyevsy", title: "The Idiot" },
|
|
38
|
+
{ id: "book-4", author: "Oscar Wilde", title: "A Picture of Dorian Gray" },
|
|
39
|
+
{ id: "book-5", author: "George Orwell", title: "1984" },
|
|
40
|
+
{ id: "book-6", author: "Jane Austen", title: "Pride and Prejudice" },
|
|
41
|
+
{ id: "book-7", author: "Marcus Aurelius", title: "Meditations" },
|
|
42
|
+
{
|
|
43
|
+
id: "book-8",
|
|
44
|
+
author: "Fyodor Dostoevsky",
|
|
45
|
+
title: "The Brothers Karamazov",
|
|
46
|
+
},
|
|
47
|
+
{ id: "book-9", author: "Lev Tolstoy", title: "Anna Karenina" },
|
|
48
|
+
{
|
|
49
|
+
id: "book-10",
|
|
50
|
+
author: "Fyodor Dostoevsky",
|
|
51
|
+
title: "Crime and Punishment",
|
|
52
|
+
},
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
export const MenuContentStory: Story = {
|
|
56
|
+
name: "Standard Content",
|
|
57
|
+
args: {},
|
|
58
|
+
render: function MenuContentStory() {
|
|
59
|
+
return (
|
|
60
|
+
<Box display="flex" gap="16px" flexDirection="column" maxWidth={300}>
|
|
61
|
+
<SingleSelectMenu
|
|
62
|
+
defaultIsOpen
|
|
63
|
+
menuToggleElement={
|
|
64
|
+
<>
|
|
65
|
+
<VisuallyHidden>
|
|
66
|
+
<MenuLabel>Select your favorite book:</MenuLabel>
|
|
67
|
+
</VisuallyHidden>
|
|
68
|
+
<StorybookMenuToggleButton />
|
|
69
|
+
</>
|
|
70
|
+
}
|
|
71
|
+
>
|
|
72
|
+
{/* Header */}
|
|
73
|
+
<MenuHeader
|
|
74
|
+
leftAction={
|
|
75
|
+
<Button
|
|
76
|
+
width="24px"
|
|
77
|
+
height="24px"
|
|
78
|
+
display="flex"
|
|
79
|
+
justifyContent="center"
|
|
80
|
+
alignItems="center"
|
|
81
|
+
>
|
|
82
|
+
<Icon name="arrow-left" />
|
|
83
|
+
</Button>
|
|
84
|
+
}
|
|
85
|
+
rightAction={<Link href="#">Help</Link>}
|
|
86
|
+
title="Sprout menu"
|
|
87
|
+
/>
|
|
88
|
+
{/* All Menu List */}
|
|
89
|
+
<MenuContent>
|
|
90
|
+
<MenuGroup id="books">
|
|
91
|
+
{books.map((book) => (
|
|
92
|
+
<MenuItem key={book.id} id={book.id}>
|
|
93
|
+
{book.title}
|
|
94
|
+
</MenuItem>
|
|
95
|
+
))}
|
|
96
|
+
</MenuGroup>
|
|
97
|
+
</MenuContent>
|
|
98
|
+
{/* Footer */}
|
|
99
|
+
<MenuFooter display="flex" alignItems="right" justifyContent="right">
|
|
100
|
+
<Button display="flex" justifyContent="center" alignItems="center">
|
|
101
|
+
<Text color="text.body" mr={200}>
|
|
102
|
+
Do Something
|
|
103
|
+
</Text>
|
|
104
|
+
<Icon name="arrow-right" />
|
|
105
|
+
</Button>
|
|
106
|
+
</MenuFooter>
|
|
107
|
+
</SingleSelectMenu>
|
|
108
|
+
</Box>
|
|
109
|
+
);
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
export const MenuContentWithItemsAsProps: Story = {
|
|
114
|
+
name: "With Items as Props",
|
|
115
|
+
render: function MenuContentWithItemsAsProps() {
|
|
116
|
+
const menuItems = books.map((book) => ({
|
|
117
|
+
id: book.id,
|
|
118
|
+
children: book.title,
|
|
119
|
+
}));
|
|
120
|
+
return (
|
|
121
|
+
<Box display="flex" gap="16px" flexDirection="column" maxWidth={300}>
|
|
122
|
+
<SingleSelectMenu
|
|
123
|
+
defaultIsOpen
|
|
124
|
+
menuToggleElement={
|
|
125
|
+
<>
|
|
126
|
+
<VisuallyHidden>
|
|
127
|
+
<MenuLabel>Select your favorite book:</MenuLabel>
|
|
128
|
+
</VisuallyHidden>
|
|
129
|
+
<StorybookMenuToggleButton />
|
|
130
|
+
</>
|
|
131
|
+
}
|
|
132
|
+
>
|
|
133
|
+
{/* Header */}
|
|
134
|
+
<MenuHeader
|
|
135
|
+
leftAction={
|
|
136
|
+
<Button
|
|
137
|
+
width="24px"
|
|
138
|
+
height="24px"
|
|
139
|
+
display="flex"
|
|
140
|
+
justifyContent="center"
|
|
141
|
+
alignItems="center"
|
|
142
|
+
>
|
|
143
|
+
<Icon name="arrow-left" />
|
|
144
|
+
</Button>
|
|
145
|
+
}
|
|
146
|
+
rightAction={<Link href="#">Help</Link>}
|
|
147
|
+
title="Sprout menu"
|
|
148
|
+
/>
|
|
149
|
+
{/* All Menu List */}
|
|
150
|
+
<MenuContent menuItems={menuItems} />
|
|
151
|
+
{/* Footer */}
|
|
152
|
+
<MenuFooter display="flex" alignItems="right" justifyContent="right">
|
|
153
|
+
<Button display="flex" justifyContent="center" alignItems="center">
|
|
154
|
+
<Text color="text.body" mr={200}>
|
|
155
|
+
Do Something
|
|
156
|
+
</Text>
|
|
157
|
+
<Icon name="arrow-right" />
|
|
158
|
+
</Button>
|
|
159
|
+
</MenuFooter>
|
|
160
|
+
</SingleSelectMenu>
|
|
161
|
+
</Box>
|
|
162
|
+
);
|
|
163
|
+
},
|
|
164
|
+
};
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import type { TypeMenuContentProps } from "./MenuContentTypes";
|
|
2
2
|
import { StyledMenuContent } from "./styles";
|
|
3
|
-
import {
|
|
3
|
+
import { useMenuContentContext } from "../MenuContext";
|
|
4
|
+
import { mapMenuItems } from "../hooks/useMenuChildren";
|
|
5
|
+
import { useCallback } from "react";
|
|
6
|
+
import type { TypeMenuItemProps } from "../MenuItem";
|
|
4
7
|
|
|
5
8
|
/**
|
|
6
9
|
* @link https://seeds.sproutsocial.com/components/menu-content/
|
|
@@ -25,12 +28,36 @@ import { useMenuContext } from "../MenuContext";
|
|
|
25
28
|
* @see {@link https://seeds.sproutsocial.com/components/menu-group/ | MenuGroup}
|
|
26
29
|
*/
|
|
27
30
|
|
|
28
|
-
const MenuContent = ({
|
|
31
|
+
const MenuContent = <I extends TypeMenuItemProps = TypeMenuItemProps>({
|
|
29
32
|
id = "menu_content",
|
|
30
|
-
children,
|
|
33
|
+
children: childrenProp,
|
|
34
|
+
menuItems,
|
|
35
|
+
MenuItemComponent,
|
|
36
|
+
onKeyDown,
|
|
37
|
+
innerRef = null,
|
|
31
38
|
...rest
|
|
32
|
-
}: TypeMenuContentProps) => {
|
|
33
|
-
const { getToggleButtonProps, getMenuProps, isListbox } =
|
|
39
|
+
}: TypeMenuContentProps<I>) => {
|
|
40
|
+
const { getToggleButtonProps, getMenuProps, isListbox } =
|
|
41
|
+
useMenuContentContext();
|
|
42
|
+
const toggleButtonProps = getToggleButtonProps?.();
|
|
43
|
+
|
|
44
|
+
const children =
|
|
45
|
+
childrenProp || !menuItems || !menuItems.length
|
|
46
|
+
? childrenProp
|
|
47
|
+
: mapMenuItems({ menuItems, MenuItemComponent });
|
|
48
|
+
|
|
49
|
+
if (!children) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Memoize the onKeyDown handler to prevent unnecessary re-renders
|
|
54
|
+
const handleKeyDown = useCallback(
|
|
55
|
+
(e) => {
|
|
56
|
+
toggleButtonProps?.["onKeyDown"]?.(e);
|
|
57
|
+
onKeyDown?.(e);
|
|
58
|
+
},
|
|
59
|
+
[onKeyDown, toggleButtonProps?.["onKeyDown"]]
|
|
60
|
+
);
|
|
34
61
|
|
|
35
62
|
return (
|
|
36
63
|
<StyledMenuContent
|
|
@@ -38,12 +65,12 @@ const MenuContent = ({
|
|
|
38
65
|
id={id}
|
|
39
66
|
tabIndex={0}
|
|
40
67
|
// apply onKeyDown logic for downshift keyboard accessibility support
|
|
41
|
-
onKeyDown={
|
|
68
|
+
onKeyDown={handleKeyDown}
|
|
42
69
|
// apply active descendent updates to container for proper accessibility
|
|
43
|
-
aria-activedescendant={
|
|
70
|
+
aria-activedescendant={toggleButtonProps?.["aria-activedescendant"]}
|
|
44
71
|
{...getMenuProps?.(
|
|
45
72
|
{
|
|
46
|
-
...(isListbox ? {} : { role: "menu" }),
|
|
73
|
+
...(isListbox ? { ref: innerRef } : { ref: innerRef, role: "menu" }),
|
|
47
74
|
},
|
|
48
75
|
// Suppress ref error for now
|
|
49
76
|
{ suppressRefError: true }
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import React from "react";
|
|
2
1
|
import { type HTMLMotionProps } from "motion/react";
|
|
3
|
-
import type {
|
|
2
|
+
import type { TypeMenuItemProps } from "../MenuItem";
|
|
3
|
+
import type { TypeChildrenOrItems } from "../types";
|
|
4
4
|
|
|
5
|
-
export
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
export type TypeMenuContentProps<
|
|
6
|
+
I extends TypeMenuItemProps = TypeMenuItemProps
|
|
7
|
+
> = Omit<HTMLMotionProps<"ul">, "children"> & {
|
|
8
|
+
innerRef?: React.Ref<HTMLUListElement>;
|
|
9
|
+
} & TypeChildrenOrItems<I>;
|
|
@@ -1,9 +1,16 @@
|
|
|
1
|
-
import { MenuContent, MenuItem } from "../../index";
|
|
1
|
+
import { MenuContent, MenuItem, type TypeMenuItemProps } from "../../index";
|
|
2
2
|
|
|
3
3
|
export const MenuContentTypeTest = () => (
|
|
4
4
|
<>
|
|
5
5
|
{/* MenuContent allows children */}
|
|
6
6
|
<MenuContent>Test</MenuContent>
|
|
7
|
+
{/* MenuContent allows menuItems prop*/}
|
|
8
|
+
<MenuContent menuItems={[{ id: "1", children: "1" }]} />
|
|
9
|
+
{/* MenuContent allows menuItems prop with MenuItemComponent*/}
|
|
10
|
+
<MenuContent
|
|
11
|
+
menuItems={[{ id: "1", children: "1" }]}
|
|
12
|
+
MenuItemComponent={MenuItem}
|
|
13
|
+
/>
|
|
7
14
|
{/* MenuContent allows children with MenuItems */}
|
|
8
15
|
<MenuContent>
|
|
9
16
|
<MenuItem id="1">Test 1</MenuItem>
|
|
@@ -12,7 +19,16 @@ export const MenuContentTypeTest = () => (
|
|
|
12
19
|
{/* MenuContent accepts ul props */}
|
|
13
20
|
<MenuContent aria-label="test label">Test</MenuContent>
|
|
14
21
|
|
|
15
|
-
{/* @ts-expect-error - MenuContent requires children */}
|
|
22
|
+
{/* @ts-expect-error - MenuContent requires children or items */}
|
|
16
23
|
<MenuContent />
|
|
24
|
+
{/* @ts-expect-error - when both children and menuItems provided */}
|
|
25
|
+
<MenuContent menuItems={[]}>Test</MenuContent>
|
|
26
|
+
{/* @ts-expect-error - when MenuItemComponent is passed with children */}
|
|
27
|
+
<MenuContent MenuItemComponent={MenuItem}>Test</MenuContent>
|
|
28
|
+
<MenuContent
|
|
29
|
+
menuItems={[] as TypeMenuItemProps[]}
|
|
30
|
+
/* @ts-expect-error - when MenuItemComponent has the wrong type */
|
|
31
|
+
MenuItemComponent={({ text }) => <div>{text}</div>}
|
|
32
|
+
></MenuContent>
|
|
17
33
|
</>
|
|
18
34
|
);
|