@sproutsocial/seeds-react-menu 0.2.3 → 0.4.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 +27 -0
- package/dist/esm/index.js +243 -75
- package/dist/esm/index.js.map +1 -1
- package/dist/index.d.mts +81 -49
- package/dist/index.d.ts +81 -49
- package/dist/index.js +237 -67
- package/dist/index.js.map +1 -1
- package/package.json +3 -1
- package/src/Autocomplete/Autocomplete.feature +14 -43
- package/src/Autocomplete/Autocomplete.stories.tsx +128 -0
- package/src/Autocomplete/Autocomplete.tsx +29 -11
- package/src/Autocomplete/AutocompleteInput.tsx +51 -0
- package/src/Autocomplete/AutocompleteTypes.ts +8 -1
- package/src/Autocomplete/__tests__/Autocomplete.test.tsx +142 -89
- package/src/Autocomplete/__tests__/Autocomplete.typetest.tsx +25 -43
- package/src/Autocomplete/index.ts +1 -0
- package/src/MenuContent/MenuContent.tsx +2 -2
- package/src/MenuContext.tsx +11 -5
- package/src/MenuGroup/MenuGroup.feature +19 -37
- package/src/MenuGroup/MenuGroup.tsx +18 -3
- package/src/MenuGroup/MenuGroupTypes.ts +6 -1
- package/src/MenuGroup/__tests__/MenuGroup.test.tsx +167 -9
- package/src/MenuGroup/__tests__/MenuGroup.typetest.tsx +27 -13
- package/src/MenuItem/MenuItem.tsx +7 -4
- package/src/MenuLabel.tsx +4 -4
- package/src/MenuRoot/MenuRoot.tsx +5 -3
- package/src/MenuRoot/__tests__/MenuRoot.test.tsx +1 -1
- package/src/MultiSelectMenu/MultiSelectMenu.tsx +8 -0
- package/src/MultiSelectMenu/__tests__/MultiSelectMenu.test.tsx +6 -6
- package/src/SingleSelectMenu/SingleSelectMenu.tsx +2 -2
- package/src/SingleSelectMenu/__tests__/SingleSelectMenu.test.tsx +3 -3
- package/src/__fixtures__/menu-test-data.ts +36 -0
- package/src/__tests__/Menu.test.tsx +29 -96
- package/src/hooks/useItemsFromChildren.tsx +71 -21
- package/src/index.ts +1 -0
- package/src/reducers/useComboboxStateReducer.tsx +11 -8
- package/src/storybookUtilities/menu-storybook-components.tsx +39 -0
- package/src/types.ts +3 -0
|
@@ -12,7 +12,8 @@ export interface TypeMenuRootOwnProps {
|
|
|
12
12
|
width?: TypePopoutProps["width"];
|
|
13
13
|
}
|
|
14
14
|
export interface TypeMenuRootProps
|
|
15
|
-
extends Partial<TypeMenuProviderProps>,
|
|
15
|
+
extends Partial<TypeMenuProviderProps["menuProps"]>,
|
|
16
|
+
Pick<TypeMenuProviderProps, "children">,
|
|
16
17
|
TypeMenuRootOwnProps {}
|
|
17
18
|
|
|
18
19
|
const CustomPopoutContent = styled(Popout.Content)`
|
|
@@ -30,7 +31,7 @@ const CustomPopoutContent = styled(Popout.Content)`
|
|
|
30
31
|
export const MenuRoot = ({
|
|
31
32
|
children,
|
|
32
33
|
menuToggleElement,
|
|
33
|
-
popoutProps,
|
|
34
|
+
popoutProps: { focusLockProps, ...popoutProps } = {},
|
|
34
35
|
width = "300px",
|
|
35
36
|
...contextProps
|
|
36
37
|
}: TypeMenuRootProps) => {
|
|
@@ -43,7 +44,7 @@ export const MenuRoot = ({
|
|
|
43
44
|
);
|
|
44
45
|
|
|
45
46
|
return (
|
|
46
|
-
<MenuProvider {
|
|
47
|
+
<MenuProvider menuProps={contextProps}>
|
|
47
48
|
<Popout
|
|
48
49
|
menuPopout
|
|
49
50
|
isOpen={!!isOpen}
|
|
@@ -55,6 +56,7 @@ export const MenuRoot = ({
|
|
|
55
56
|
// correct jerking motion when scrolling while the Popout is open
|
|
56
57
|
// returnFocus: false,
|
|
57
58
|
crossFrame: false,
|
|
59
|
+
...focusLockProps,
|
|
58
60
|
}}
|
|
59
61
|
disableWrapperAria
|
|
60
62
|
{...popoutProps}
|
|
@@ -219,7 +219,7 @@ describe("MenuRoot", () => {
|
|
|
219
219
|
>
|
|
220
220
|
<MenuHeader title="Menu Header" />
|
|
221
221
|
<div>Custom Banner</div>
|
|
222
|
-
<MenuGroup title="Menu Group">
|
|
222
|
+
<MenuGroup title="Menu Group" id="group">
|
|
223
223
|
<MenuItem id="1">Item 1</MenuItem>
|
|
224
224
|
</MenuGroup>
|
|
225
225
|
<MenuFooter>Menu Footer</MenuFooter>
|
|
@@ -105,6 +105,14 @@ export const MultiSelectMenu = ({
|
|
|
105
105
|
} else {
|
|
106
106
|
addSelectedItem(newSelectedItem);
|
|
107
107
|
}
|
|
108
|
+
// If the newly selected item's group is single select, remove any other selected items in that group.
|
|
109
|
+
if (newSelectedItem.menuGroupProps?.isSingleSelect) {
|
|
110
|
+
const groupItems = selectedItems.filter(
|
|
111
|
+
(item) =>
|
|
112
|
+
item.menuGroupProps?.id === newSelectedItem.menuGroupProps?.id
|
|
113
|
+
);
|
|
114
|
+
groupItems.forEach((item) => removeSelectedItem(item));
|
|
115
|
+
}
|
|
108
116
|
break;
|
|
109
117
|
default:
|
|
110
118
|
break;
|
|
@@ -35,7 +35,7 @@ describe("MultiSelectMenu", () => {
|
|
|
35
35
|
}
|
|
36
36
|
>
|
|
37
37
|
<MenuContent>
|
|
38
|
-
<MenuGroup>
|
|
38
|
+
<MenuGroup id="group">
|
|
39
39
|
{menuItems.map((item) => (
|
|
40
40
|
<MenuItem key={item.id} id={item.id} label={item.label}>
|
|
41
41
|
{item.label}
|
|
@@ -78,7 +78,7 @@ describe("MultiSelectMenu", () => {
|
|
|
78
78
|
}
|
|
79
79
|
>
|
|
80
80
|
<MenuContent>
|
|
81
|
-
<MenuGroup>
|
|
81
|
+
<MenuGroup id="group">
|
|
82
82
|
{menuItems.map((item) => (
|
|
83
83
|
<MenuItem
|
|
84
84
|
key={item.id}
|
|
@@ -136,7 +136,7 @@ describe("MultiSelectMenu", () => {
|
|
|
136
136
|
}
|
|
137
137
|
>
|
|
138
138
|
<MenuContent>
|
|
139
|
-
<MenuGroup>
|
|
139
|
+
<MenuGroup id="group">
|
|
140
140
|
{menuItems.map((item) => (
|
|
141
141
|
<MenuItem
|
|
142
142
|
key={item.id}
|
|
@@ -199,7 +199,7 @@ describe("MultiSelectMenu", () => {
|
|
|
199
199
|
}
|
|
200
200
|
>
|
|
201
201
|
<MenuContent>
|
|
202
|
-
<MenuGroup>
|
|
202
|
+
<MenuGroup id="group">
|
|
203
203
|
{menuItems.map((item) => (
|
|
204
204
|
<MenuItem
|
|
205
205
|
key={item.id}
|
|
@@ -257,7 +257,7 @@ describe("MultiSelectMenu", () => {
|
|
|
257
257
|
selectedItems={[menuItems[1], menuItems[2]]}
|
|
258
258
|
>
|
|
259
259
|
<MenuContent>
|
|
260
|
-
<MenuGroup>
|
|
260
|
+
<MenuGroup id="group">
|
|
261
261
|
{menuItems.map((item) => (
|
|
262
262
|
<MenuItem
|
|
263
263
|
key={item.id}
|
|
@@ -310,7 +310,7 @@ describe("MultiSelectMenu", () => {
|
|
|
310
310
|
onSelectedItemsChange={mockSelectedItemsChange}
|
|
311
311
|
>
|
|
312
312
|
<MenuContent>
|
|
313
|
-
<MenuGroup>
|
|
313
|
+
<MenuGroup id="group">
|
|
314
314
|
{menuItems.map((item) => (
|
|
315
315
|
<MenuItem
|
|
316
316
|
key={item.id}
|
|
@@ -24,11 +24,11 @@ export const SingleSelectMenu = ({
|
|
|
24
24
|
stateReducer: externalStateReducer,
|
|
25
25
|
...useSelectProps
|
|
26
26
|
}: TypeSingleSelectMenuProps) => {
|
|
27
|
-
/**
|
|
27
|
+
/** Get a list of items from the children */
|
|
28
28
|
const { allMenuItems, menuItemsMap } = useItemsFromChildren({ children });
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
|
-
* Destructure Downshift props and call the core useSelect with
|
|
31
|
+
* Destructure Downshift props and call the core useSelect with items and state handler
|
|
32
32
|
*/
|
|
33
33
|
const { isOpen, ...useSelectReturnProps } = useSelect({
|
|
34
34
|
...defaultUseSelectProps,
|
|
@@ -34,7 +34,7 @@ describe("User Behavior", () => {
|
|
|
34
34
|
}
|
|
35
35
|
>
|
|
36
36
|
<MenuContent>
|
|
37
|
-
<MenuGroup title="Menu Title">
|
|
37
|
+
<MenuGroup title="Menu Title" id="menu-title">
|
|
38
38
|
{menuItems.map(({ id, label }) => (
|
|
39
39
|
<MenuItem key={id} id={id}>
|
|
40
40
|
{label}
|
|
@@ -100,7 +100,7 @@ describe("User Behavior", () => {
|
|
|
100
100
|
}
|
|
101
101
|
>
|
|
102
102
|
<MenuContent>
|
|
103
|
-
<MenuGroup title="Menu Title">
|
|
103
|
+
<MenuGroup title="Menu Title" id="menu-title">
|
|
104
104
|
{menuItems.map(({ id, label }) => (
|
|
105
105
|
<MenuItem key={id} id={id}>
|
|
106
106
|
{label}
|
|
@@ -135,7 +135,7 @@ describe("User Behavior", () => {
|
|
|
135
135
|
menuToggleElement={<MenuToggleButton>Click me</MenuToggleButton>}
|
|
136
136
|
>
|
|
137
137
|
<MenuContent>
|
|
138
|
-
<MenuGroup title="Menu Title">
|
|
138
|
+
<MenuGroup title="Menu Title" id="menu-title">
|
|
139
139
|
{menuItems.map(({ id, label }) => (
|
|
140
140
|
<MenuItem key={id} id={id}>
|
|
141
141
|
{label}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
interface TypeBook {
|
|
2
|
+
id: string;
|
|
3
|
+
author: string;
|
|
4
|
+
title: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export const TEST_BOOKS: TypeBook[] = [
|
|
8
|
+
{ id: "book-1", author: "Harper Lee", title: "To Kill a Mockingbird" },
|
|
9
|
+
{ id: "book-2", author: "Leo Tolstoy", title: "War and Peace" },
|
|
10
|
+
{ id: "book-3", author: "Fyodor Dostoyevsy", title: "The Idiot" },
|
|
11
|
+
{ id: "book-4", author: "Oscar Wilde", title: "A Picture of Dorian Gray" },
|
|
12
|
+
{ id: "book-5", author: "George Orwell", title: "Nineteen Eighty Four" },
|
|
13
|
+
{ id: "book-6", author: "Jane Austen", title: "Pride and Prejudice" },
|
|
14
|
+
{ id: "book-7", author: "Marcus Aurelius", title: "Meditations" },
|
|
15
|
+
{
|
|
16
|
+
id: "book-8",
|
|
17
|
+
author: "Fyodor Dostoevsky",
|
|
18
|
+
title: "The Brothers Karamazov",
|
|
19
|
+
},
|
|
20
|
+
{ id: "book-9", author: "Lev Tolstoy", title: "Anna Karenina" },
|
|
21
|
+
{
|
|
22
|
+
id: "book-10",
|
|
23
|
+
author: "Fyodor Dostoevsky",
|
|
24
|
+
title: "Crime and Punishment",
|
|
25
|
+
},
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
export const EXTRA_BOOKS: TypeBook[] = [
|
|
29
|
+
{ id: "book-11", author: "F. Scott Fitzgerald", title: "The Great Gatsby" },
|
|
30
|
+
{ id: "book-12", author: "J. D. Salinger", title: "The Catcher in the Rye" },
|
|
31
|
+
{ id: "book-13", author: "J. R. R. Tolkien", title: "The Lord of the Rings" },
|
|
32
|
+
{ id: "book-14", author: "John Steinbeck", title: "The Grapes of Wrath" },
|
|
33
|
+
{ id: "book-15", author: "George Orwell", title: "Animal Farm" },
|
|
34
|
+
{ id: "book-16", author: "Jack Kerouac", title: "On the Road" },
|
|
35
|
+
{ id: "book-17", author: "Aldous Huxley", title: "Brave New World" },
|
|
36
|
+
];
|
|
@@ -9,6 +9,8 @@ import {
|
|
|
9
9
|
ActionMenu,
|
|
10
10
|
MultiSelectMenu,
|
|
11
11
|
SingleSelectMenu,
|
|
12
|
+
Autocomplete,
|
|
13
|
+
AutocompleteInput,
|
|
12
14
|
MenuToggleButton,
|
|
13
15
|
MenuContent,
|
|
14
16
|
MenuItem,
|
|
@@ -19,19 +21,18 @@ describe("Menu", () => {
|
|
|
19
21
|
{ name: "ActionMenu", Component: ActionMenu },
|
|
20
22
|
{ name: "MultiSelectMenu", Component: MultiSelectMenu },
|
|
21
23
|
{ name: "SingleSelectMenu", Component: SingleSelectMenu },
|
|
22
|
-
|
|
24
|
+
{ name: "Autocomplete", Component: Autocomplete },
|
|
25
|
+
])("$name", ({ name, Component }) => {
|
|
26
|
+
const toggleElement = (
|
|
27
|
+
<>
|
|
28
|
+
{name === "Autocomplete" && <AutocompleteInput />}
|
|
29
|
+
<MenuToggleButton role="button" aria-label="Open Menu">
|
|
30
|
+
Open
|
|
31
|
+
</MenuToggleButton>
|
|
32
|
+
</>
|
|
33
|
+
);
|
|
23
34
|
it("should render the trigger button", () => {
|
|
24
|
-
render(
|
|
25
|
-
<Component
|
|
26
|
-
menuToggleElement={
|
|
27
|
-
<MenuToggleButton role="button" aria-label="Open Menu">
|
|
28
|
-
Open
|
|
29
|
-
</MenuToggleButton>
|
|
30
|
-
}
|
|
31
|
-
>
|
|
32
|
-
Menu
|
|
33
|
-
</Component>
|
|
34
|
-
);
|
|
35
|
+
render(<Component menuToggleElement={toggleElement}>Menu</Component>);
|
|
35
36
|
const menuTrigger = screen.getByRole("button", { name: "Open Menu" });
|
|
36
37
|
expect(menuTrigger).toBeInTheDocument();
|
|
37
38
|
expect(screen.queryByText("Menu")).not.toBeInTheDocument();
|
|
@@ -39,14 +40,7 @@ describe("Menu", () => {
|
|
|
39
40
|
|
|
40
41
|
it("should default to open if initialIsOpen is true", () => {
|
|
41
42
|
render(
|
|
42
|
-
<Component
|
|
43
|
-
menuToggleElement={
|
|
44
|
-
<MenuToggleButton role="button" aria-label="Open Menu">
|
|
45
|
-
Open
|
|
46
|
-
</MenuToggleButton>
|
|
47
|
-
}
|
|
48
|
-
initialIsOpen
|
|
49
|
-
>
|
|
43
|
+
<Component menuToggleElement={toggleElement} initialIsOpen>
|
|
50
44
|
Menu
|
|
51
45
|
</Component>
|
|
52
46
|
);
|
|
@@ -60,15 +54,7 @@ describe("Menu", () => {
|
|
|
60
54
|
|
|
61
55
|
it("should open when trigger button is clicked", async () => {
|
|
62
56
|
const { user } = render(
|
|
63
|
-
<Component
|
|
64
|
-
menuToggleElement={
|
|
65
|
-
<MenuToggleButton role="button" aria-label="Open Menu">
|
|
66
|
-
Open
|
|
67
|
-
</MenuToggleButton>
|
|
68
|
-
}
|
|
69
|
-
>
|
|
70
|
-
Menu
|
|
71
|
-
</Component>
|
|
57
|
+
<Component menuToggleElement={toggleElement}>Menu</Component>
|
|
72
58
|
);
|
|
73
59
|
|
|
74
60
|
// Check that Menu is not open yet
|
|
@@ -85,15 +71,7 @@ describe("Menu", () => {
|
|
|
85
71
|
|
|
86
72
|
it("should open when trigger button is focused and enter key pressed", async () => {
|
|
87
73
|
const { user } = render(
|
|
88
|
-
<Component
|
|
89
|
-
menuToggleElement={
|
|
90
|
-
<MenuToggleButton role="button" aria-label="Open Menu">
|
|
91
|
-
Open
|
|
92
|
-
</MenuToggleButton>
|
|
93
|
-
}
|
|
94
|
-
>
|
|
95
|
-
Menu
|
|
96
|
-
</Component>
|
|
74
|
+
<Component menuToggleElement={toggleElement}>Menu</Component>
|
|
97
75
|
);
|
|
98
76
|
|
|
99
77
|
// Check that Menu is not open yet
|
|
@@ -111,15 +89,7 @@ describe("Menu", () => {
|
|
|
111
89
|
|
|
112
90
|
it("should open when trigger button is focused and space key pressed", async () => {
|
|
113
91
|
const { user } = render(
|
|
114
|
-
<Component
|
|
115
|
-
menuToggleElement={
|
|
116
|
-
<MenuToggleButton role="button" aria-label="Open Menu">
|
|
117
|
-
Open
|
|
118
|
-
</MenuToggleButton>
|
|
119
|
-
}
|
|
120
|
-
>
|
|
121
|
-
Menu
|
|
122
|
-
</Component>
|
|
92
|
+
<Component menuToggleElement={toggleElement}>Menu</Component>
|
|
123
93
|
);
|
|
124
94
|
|
|
125
95
|
// Check that Menu is not open yet
|
|
@@ -137,14 +107,7 @@ describe("Menu", () => {
|
|
|
137
107
|
|
|
138
108
|
it("should close when trigger button is clicked while open", async () => {
|
|
139
109
|
const { user } = render(
|
|
140
|
-
<Component
|
|
141
|
-
menuToggleElement={
|
|
142
|
-
<MenuToggleButton role="button" aria-label="Open Menu">
|
|
143
|
-
Open
|
|
144
|
-
</MenuToggleButton>
|
|
145
|
-
}
|
|
146
|
-
initialIsOpen
|
|
147
|
-
>
|
|
110
|
+
<Component menuToggleElement={toggleElement} initialIsOpen>
|
|
148
111
|
Menu
|
|
149
112
|
</Component>
|
|
150
113
|
);
|
|
@@ -167,11 +130,7 @@ describe("Menu", () => {
|
|
|
167
130
|
const mockOnIsOpenChange = jest.fn();
|
|
168
131
|
const { user } = render(
|
|
169
132
|
<Component
|
|
170
|
-
menuToggleElement={
|
|
171
|
-
<MenuToggleButton role="button" aria-label="Open Menu">
|
|
172
|
-
Open
|
|
173
|
-
</MenuToggleButton>
|
|
174
|
-
}
|
|
133
|
+
menuToggleElement={toggleElement}
|
|
175
134
|
onIsOpenChange={mockOnIsOpenChange}
|
|
176
135
|
initialIsOpen
|
|
177
136
|
>
|
|
@@ -199,11 +158,7 @@ describe("Menu", () => {
|
|
|
199
158
|
const { user } = render(
|
|
200
159
|
<div>
|
|
201
160
|
<Component
|
|
202
|
-
menuToggleElement={
|
|
203
|
-
<MenuToggleButton role="button" aria-label="Open Menu">
|
|
204
|
-
Open
|
|
205
|
-
</MenuToggleButton>
|
|
206
|
-
}
|
|
161
|
+
menuToggleElement={toggleElement}
|
|
207
162
|
onIsOpenChange={mockOnIsOpenChange}
|
|
208
163
|
initialIsOpen
|
|
209
164
|
>
|
|
@@ -230,11 +185,7 @@ describe("Menu", () => {
|
|
|
230
185
|
const mockOnSelectedItemChange = jest.fn();
|
|
231
186
|
const { user } = render(
|
|
232
187
|
<Component
|
|
233
|
-
menuToggleElement={
|
|
234
|
-
<MenuToggleButton role="button" aria-label="Open Menu">
|
|
235
|
-
Open
|
|
236
|
-
</MenuToggleButton>
|
|
237
|
-
}
|
|
188
|
+
menuToggleElement={toggleElement}
|
|
238
189
|
onSelectedItemChange={mockOnSelectedItemChange}
|
|
239
190
|
>
|
|
240
191
|
<MenuContent>
|
|
@@ -266,11 +217,7 @@ describe("Menu", () => {
|
|
|
266
217
|
const mockOnSelectedItemChange = jest.fn();
|
|
267
218
|
const { user } = render(
|
|
268
219
|
<Component
|
|
269
|
-
menuToggleElement={
|
|
270
|
-
<MenuToggleButton role="button" aria-label="Open Menu">
|
|
271
|
-
Open
|
|
272
|
-
</MenuToggleButton>
|
|
273
|
-
}
|
|
220
|
+
menuToggleElement={toggleElement}
|
|
274
221
|
onSelectedItemChange={mockOnSelectedItemChange}
|
|
275
222
|
>
|
|
276
223
|
<MenuContent>
|
|
@@ -295,14 +242,12 @@ describe("Menu", () => {
|
|
|
295
242
|
});
|
|
296
243
|
|
|
297
244
|
it("should call onSelectedItemChange when item selected with space", async () => {
|
|
245
|
+
if (name === "Autocomplete") return;
|
|
246
|
+
|
|
298
247
|
const mockOnSelectedItemChange = jest.fn();
|
|
299
248
|
const { user } = render(
|
|
300
249
|
<Component
|
|
301
|
-
menuToggleElement={
|
|
302
|
-
<MenuToggleButton role="button" aria-label="Open Menu">
|
|
303
|
-
Open
|
|
304
|
-
</MenuToggleButton>
|
|
305
|
-
}
|
|
250
|
+
menuToggleElement={toggleElement}
|
|
306
251
|
onSelectedItemChange={mockOnSelectedItemChange}
|
|
307
252
|
>
|
|
308
253
|
<MenuContent>
|
|
@@ -330,11 +275,7 @@ describe("Menu", () => {
|
|
|
330
275
|
const mockOnIsOpenChange = jest.fn();
|
|
331
276
|
const { user } = render(
|
|
332
277
|
<Component
|
|
333
|
-
menuToggleElement={
|
|
334
|
-
<MenuToggleButton role="button" aria-label="Open Menu">
|
|
335
|
-
Open
|
|
336
|
-
</MenuToggleButton>
|
|
337
|
-
}
|
|
278
|
+
menuToggleElement={toggleElement}
|
|
338
279
|
onIsOpenChange={mockOnIsOpenChange}
|
|
339
280
|
>
|
|
340
281
|
<MenuContent>
|
|
@@ -362,11 +303,7 @@ describe("Menu", () => {
|
|
|
362
303
|
const mockOnHighlightedIndexChange = jest.fn();
|
|
363
304
|
const { user } = render(
|
|
364
305
|
<Component
|
|
365
|
-
menuToggleElement={
|
|
366
|
-
<MenuToggleButton role="button" aria-label="Open Menu">
|
|
367
|
-
Open
|
|
368
|
-
</MenuToggleButton>
|
|
369
|
-
}
|
|
306
|
+
menuToggleElement={toggleElement}
|
|
370
307
|
onHighlightedIndexChange={mockOnHighlightedIndexChange}
|
|
371
308
|
>
|
|
372
309
|
<MenuContent>
|
|
@@ -396,11 +333,7 @@ describe("Menu", () => {
|
|
|
396
333
|
const mockStateReducer = jest.fn((state, { changes }) => changes);
|
|
397
334
|
const { user } = render(
|
|
398
335
|
<Component
|
|
399
|
-
menuToggleElement={
|
|
400
|
-
<MenuToggleButton role="button" aria-label="Open Menu">
|
|
401
|
-
Open
|
|
402
|
-
</MenuToggleButton>
|
|
403
|
-
}
|
|
336
|
+
menuToggleElement={toggleElement}
|
|
404
337
|
stateReducer={mockStateReducer}
|
|
405
338
|
>
|
|
406
339
|
<MenuContent>
|
|
@@ -421,7 +354,7 @@ describe("Menu", () => {
|
|
|
421
354
|
await user.keyboard("{arrowdown}{enter}");
|
|
422
355
|
|
|
423
356
|
// Check that stateReducer is called
|
|
424
|
-
await waitFor(() => expect(mockStateReducer).
|
|
357
|
+
await waitFor(() => expect(mockStateReducer).toHaveBeenCalled());
|
|
425
358
|
});
|
|
426
359
|
});
|
|
427
360
|
});
|
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
import React, { type ReactNode, useMemo } from "react";
|
|
2
|
-
import type
|
|
2
|
+
import { MenuItem, type TypeMenuItemProps } from "../MenuItem";
|
|
3
3
|
import type { TypeMenuContext } from "../MenuContext";
|
|
4
|
+
import type { TypeMenuGroupProps } from "../MenuGroup/index";
|
|
5
|
+
import type { TypeDefaultItemProps } from "../types";
|
|
4
6
|
|
|
5
7
|
export interface TypeUseItemsFromChildrenProps {
|
|
6
8
|
children: ReactNode;
|
|
7
9
|
}
|
|
8
|
-
export
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
export type TypeItemElement = React.ReactElement<TypeMenuItemProps>;
|
|
11
|
+
export type TypeMenuGroupElement = React.ReactElement<TypeMenuGroupProps>;
|
|
12
|
+
|
|
13
|
+
export interface TypeMenuChildren {
|
|
14
|
+
type: "item" | "group";
|
|
15
|
+
element:
|
|
16
|
+
| React.ReactElement<TypeMenuGroupProps>
|
|
17
|
+
| React.ReactElement<TypeMenuItemProps>;
|
|
11
18
|
}
|
|
12
19
|
|
|
13
20
|
// https://stackoverflow.com/questions/69119016/find-specific-children-components-regardless-of-depth-in-react
|
|
@@ -17,7 +24,6 @@ const walkAllChildren = (
|
|
|
17
24
|
) => {
|
|
18
25
|
const walk = (element: ReactNode, parents: readonly ReactNode[]) => {
|
|
19
26
|
if (element === null || element === undefined) return;
|
|
20
|
-
// console.log("element", element);
|
|
21
27
|
callback(element, parents);
|
|
22
28
|
const newParents = [...parents, element];
|
|
23
29
|
const children = (element as any).props?.children;
|
|
@@ -43,37 +49,81 @@ const checkIfIsElement = (element: ReactNode, name: string): boolean => {
|
|
|
43
49
|
};
|
|
44
50
|
|
|
45
51
|
const getAllMenuItems = (children: ReactNode) => {
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
walkAllChildren(children, (element, parents) => {
|
|
52
|
+
const orderedElements: TypeMenuChildren[] = [];
|
|
53
|
+
walkAllChildren(children, (element) => {
|
|
49
54
|
if (checkIfIsElement(element, "MenuItem")) {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
55
|
+
orderedElements.push({
|
|
56
|
+
type: "item",
|
|
57
|
+
element: element as TypeItemElement,
|
|
58
|
+
});
|
|
59
|
+
} else if (checkIfIsElement(element, "MenuGroup")) {
|
|
60
|
+
orderedElements.push({
|
|
61
|
+
type: "group",
|
|
62
|
+
element: element as TypeItemElement,
|
|
53
63
|
});
|
|
54
64
|
}
|
|
55
65
|
});
|
|
56
|
-
return
|
|
66
|
+
return { orderedElements };
|
|
57
67
|
};
|
|
58
68
|
|
|
59
69
|
export const useItemsFromChildren = ({
|
|
60
70
|
children,
|
|
61
71
|
}: TypeUseItemsFromChildrenProps) => {
|
|
62
72
|
const { allMenuItems, menuItemsMap } = useMemo(() => {
|
|
63
|
-
const
|
|
73
|
+
const { orderedElements } = getAllMenuItems(children);
|
|
64
74
|
const menuItemsMap: TypeMenuContext["itemsMap"] = {};
|
|
65
|
-
const allMenuItems:
|
|
66
|
-
|
|
75
|
+
const allMenuItems: TypeDefaultItemProps[] = [];
|
|
76
|
+
let index = 0;
|
|
77
|
+
|
|
78
|
+
orderedElements.forEach(({ type, element }) => {
|
|
79
|
+
if (type === "group") {
|
|
80
|
+
const menuGroup = element as TypeMenuGroupElement;
|
|
81
|
+
const items = menuGroup.props.items;
|
|
82
|
+
if (items) {
|
|
83
|
+
items.forEach((menuItem) => {
|
|
84
|
+
const item = {
|
|
85
|
+
id: menuItem.id,
|
|
86
|
+
index: index++,
|
|
87
|
+
disabled: menuItem.disabled,
|
|
88
|
+
menuItemProps: menuItem,
|
|
89
|
+
menuGroupProps: element.props,
|
|
90
|
+
};
|
|
91
|
+
allMenuItems.push(item);
|
|
92
|
+
menuItemsMap[`${item.id}`] = item;
|
|
93
|
+
});
|
|
94
|
+
} else if (menuGroup.props.children) {
|
|
95
|
+
let children = menuGroup.props.children as TypeItemElement[];
|
|
96
|
+
// If there is only one child, it will be an object. Make it an array.
|
|
97
|
+
if (!Array.isArray(children)) {
|
|
98
|
+
children = [children];
|
|
99
|
+
}
|
|
100
|
+
children.forEach((menuItem) => {
|
|
101
|
+
const item = {
|
|
102
|
+
id: menuItem.props.id,
|
|
103
|
+
index: index++,
|
|
104
|
+
disabled: menuItem.props.disabled,
|
|
105
|
+
menuItemProps: menuItem.props,
|
|
106
|
+
menuGroupProps: element.props,
|
|
107
|
+
};
|
|
108
|
+
allMenuItems.push(item);
|
|
109
|
+
menuItemsMap[`${item.id}`] = item;
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
} else {
|
|
113
|
+
// Items in groups would have already been added to the map.
|
|
114
|
+
if (menuItemsMap[`${element.props.id}`]) return;
|
|
115
|
+
const menuItem = element as TypeItemElement;
|
|
67
116
|
const item = {
|
|
68
|
-
id:
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
menuItemProps:
|
|
117
|
+
id: menuItem.props.id,
|
|
118
|
+
index: index++,
|
|
119
|
+
disabled: menuItem.props.disabled,
|
|
120
|
+
menuItemProps: menuItem.props,
|
|
72
121
|
};
|
|
122
|
+
allMenuItems.push(item);
|
|
73
123
|
menuItemsMap[`${item.id}`] = item;
|
|
74
|
-
return item;
|
|
75
124
|
}
|
|
76
|
-
);
|
|
125
|
+
});
|
|
126
|
+
|
|
77
127
|
return { allMenuItems, menuItemsMap };
|
|
78
128
|
}, [children]);
|
|
79
129
|
|
package/src/index.ts
CHANGED
|
@@ -8,12 +8,12 @@ export const useComboboxStateReducer: Required<
|
|
|
8
8
|
const { type, changes } = actionAndChanges;
|
|
9
9
|
switch (type) {
|
|
10
10
|
/** If downshift is blurred by tabbing within the menu, stay open */
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
11
|
+
case useCombobox.stateChangeTypes.InputBlur:
|
|
12
|
+
return {
|
|
13
|
+
...changes,
|
|
14
|
+
// keep menu open on blur
|
|
15
|
+
isOpen: true,
|
|
16
|
+
};
|
|
17
17
|
|
|
18
18
|
/** Return standard change payload */
|
|
19
19
|
default:
|
|
@@ -21,6 +21,9 @@ export const useComboboxStateReducer: Required<
|
|
|
21
21
|
}
|
|
22
22
|
};
|
|
23
23
|
|
|
24
|
-
export const defaultUseComboboxProps
|
|
25
|
-
|
|
24
|
+
export const defaultUseComboboxProps: Partial<
|
|
25
|
+
UseComboboxProps<TypeDefaultItemProps>
|
|
26
|
+
> = {
|
|
27
|
+
// disabling while item.hidden so that hidden items are ignored by downshift
|
|
28
|
+
isItemDisabled: (item) => Boolean(item.disabled || item.hidden),
|
|
26
29
|
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import styled from "styled-components";
|
|
2
|
+
import { Box } from "@sproutsocial/seeds-react-box";
|
|
3
|
+
import { Icon } from "@sproutsocial/seeds-react-icon";
|
|
4
|
+
import { useMenuContext, MenuToggleButton } from "../index";
|
|
5
|
+
|
|
6
|
+
export interface TypeStyledChevronProps {
|
|
7
|
+
$isOpen?: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const StyledChevron = styled.div<TypeStyledChevronProps>`
|
|
11
|
+
margin-left: ${({ theme }) => theme.space[300]};
|
|
12
|
+
${({ $isOpen }) =>
|
|
13
|
+
$isOpen &&
|
|
14
|
+
`
|
|
15
|
+
transform: rotate(-180deg);
|
|
16
|
+
`}
|
|
17
|
+
transition: transform 0.15s;
|
|
18
|
+
`;
|
|
19
|
+
|
|
20
|
+
interface StorybookMenuToggleButtonProps {
|
|
21
|
+
buttonText?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const StorybookMenuToggleButton = ({
|
|
25
|
+
buttonText,
|
|
26
|
+
}: StorybookMenuToggleButtonProps) => {
|
|
27
|
+
const { isOpen } = useMenuContext();
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<MenuToggleButton appearance="secondary">
|
|
31
|
+
<Box display="flex" justifyContent="space-between">
|
|
32
|
+
{buttonText ? buttonText : <span>Select book</span>}
|
|
33
|
+
<StyledChevron $isOpen={isOpen}>
|
|
34
|
+
<Icon name="chevron-down" />
|
|
35
|
+
</StyledChevron>
|
|
36
|
+
</Box>
|
|
37
|
+
</MenuToggleButton>
|
|
38
|
+
);
|
|
39
|
+
};
|
package/src/types.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import type { TypeMenuItemProps } from "./MenuItem";
|
|
2
|
+
import type { TypeMenuGroupProps } from "./MenuGroup";
|
|
2
3
|
|
|
3
4
|
export interface TypeDefaultItemProps {
|
|
4
5
|
id: string;
|
|
5
6
|
index: number;
|
|
6
7
|
disabled?: boolean;
|
|
8
|
+
hidden?: boolean;
|
|
7
9
|
menuItemProps?: TypeMenuItemProps;
|
|
10
|
+
menuGroupProps?: TypeMenuGroupProps;
|
|
8
11
|
}
|
|
9
12
|
|
|
10
13
|
export type TypeNotEmptyChildren =
|