@sproutsocial/seeds-react-menu 0.2.2 → 0.3.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 +18 -0
- package/dist/esm/index.js +18 -4
- package/dist/esm/index.js.map +1 -1
- package/dist/index.d.mts +70 -43
- package/dist/index.d.ts +70 -43
- package/dist/index.js +18 -4
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/ActionMenu/ActionMenu.tsx +2 -16
- package/src/ActionMenu/ActionMenuTypes.ts +12 -0
- package/src/ActionMenu/index.ts +1 -0
- package/src/{ComboBox/ComboBox.feature → Autocomplete/Autocomplete.feature} +16 -16
- package/src/Autocomplete/Autocomplete.tsx +65 -0
- package/src/Autocomplete/AutocompleteTypes.ts +8 -0
- package/src/Autocomplete/__tests__/Autocomplete.test.tsx +113 -0
- package/src/Autocomplete/__tests__/Autocomplete.typetest.tsx +78 -0
- package/src/Autocomplete/index.ts +2 -0
- package/src/MenuContent/MenuContent.tsx +3 -3
- package/src/MenuContext.tsx +65 -9
- package/src/MenuGroup/MenuGroup.feature +15 -15
- package/src/MenuGroup/MenuGroup.tsx +6 -2
- package/src/MenuGroup/MenuGroupTypes.ts +2 -0
- package/src/MenuGroup/__tests__/MenuGroup.test.tsx +144 -9
- package/src/MenuGroup/__tests__/MenuGroup.typetest.tsx +24 -12
- package/src/MenuItem/MenuItem.tsx +1 -1
- package/src/MenuItem/__tests__/MenuItem.test.tsx +4 -1
- package/src/MenuRoot/__tests__/MenuRoot.test.tsx +1 -1
- package/src/MultiSelectMenu/MultiSelectMenu.tsx +8 -0
- package/src/MultiSelectMenu/MultiSelectMenuTypes.ts +5 -1
- package/src/MultiSelectMenu/__tests__/MultiSelectMenu.test.tsx +6 -8
- package/src/MultiSelectMenu/index.ts +1 -0
- package/src/SingleSelectMenu/SingleSelectMenuTypes.ts +2 -3
- package/src/SingleSelectMenu/__tests__/SingleSelectMenu.test.tsx +3 -3
- package/src/hooks/useItemsFromChildren.tsx +12 -5
- package/src/reducers/useComboboxStateReducer.tsx +26 -0
- package/src/types.ts +2 -0
- package/src/ComboBox/TokenInput.feature +0 -84
|
@@ -1,6 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
render,
|
|
3
|
+
screen,
|
|
4
|
+
waitFor,
|
|
5
|
+
fireEvent,
|
|
6
|
+
act,
|
|
7
|
+
} from "@sproutsocial/seeds-react-testing-library";
|
|
2
8
|
import {
|
|
3
9
|
SingleSelectMenu,
|
|
10
|
+
MultiSelectMenu,
|
|
4
11
|
MenuContent,
|
|
5
12
|
MenuGroup,
|
|
6
13
|
MenuToggleButton,
|
|
@@ -14,11 +21,139 @@ const menuItems = [
|
|
|
14
21
|
];
|
|
15
22
|
|
|
16
23
|
describe("MenuGroup", () => {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
24
|
+
describe("User Behavior", () => {
|
|
25
|
+
it("allows multiple selections per group when isSingleSelect is undefined", () => {
|
|
26
|
+
render(
|
|
27
|
+
<MultiSelectMenu
|
|
28
|
+
defaultIsOpen
|
|
29
|
+
menuToggleElement={<MenuToggleButton>Test Menu</MenuToggleButton>}
|
|
30
|
+
>
|
|
31
|
+
<MenuContent>
|
|
32
|
+
<MenuGroup id="group">
|
|
33
|
+
{menuItems.map(({ id, label }) => (
|
|
34
|
+
<MenuItem key={id} id={id}>
|
|
35
|
+
{label}
|
|
36
|
+
</MenuItem>
|
|
37
|
+
))}
|
|
38
|
+
</MenuGroup>
|
|
39
|
+
</MenuContent>
|
|
40
|
+
</MultiSelectMenu>
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
const options = screen.getAllByRole("option");
|
|
44
|
+
|
|
45
|
+
// Click on each option
|
|
46
|
+
for (const option of screen.getAllByRole("option")) {
|
|
47
|
+
fireEvent.click(option);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Expect all options to be selected.
|
|
51
|
+
for (const option of options) {
|
|
52
|
+
expect(option).toHaveAttribute("aria-selected", "true");
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
it("allows multiple selections per group when isSingleSelect is false", () => {
|
|
56
|
+
render(
|
|
57
|
+
<MultiSelectMenu
|
|
58
|
+
defaultIsOpen
|
|
59
|
+
menuToggleElement={<MenuToggleButton>Test Menu</MenuToggleButton>}
|
|
60
|
+
>
|
|
61
|
+
<MenuContent>
|
|
62
|
+
<MenuGroup id="group" isSingleSelect={false}>
|
|
63
|
+
{menuItems.map(({ id, label }) => (
|
|
64
|
+
<MenuItem key={id} id={id}>
|
|
65
|
+
{label}
|
|
66
|
+
</MenuItem>
|
|
67
|
+
))}
|
|
68
|
+
</MenuGroup>
|
|
69
|
+
</MenuContent>
|
|
70
|
+
</MultiSelectMenu>
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
const options = screen.getAllByRole("option");
|
|
74
|
+
|
|
75
|
+
// Click on each option
|
|
76
|
+
for (const option of screen.getAllByRole("option")) {
|
|
77
|
+
fireEvent.click(option);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Expect all options to be selected.
|
|
81
|
+
for (const option of options) {
|
|
82
|
+
expect(option).toHaveAttribute("aria-selected", "true");
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
it("only allows one selection per group when isSingleSelect is true", async () => {
|
|
86
|
+
render(
|
|
87
|
+
<MultiSelectMenu
|
|
88
|
+
defaultIsOpen
|
|
89
|
+
menuToggleElement={<MenuToggleButton>Test Menu</MenuToggleButton>}
|
|
90
|
+
>
|
|
91
|
+
<MenuContent>
|
|
92
|
+
<MenuGroup id="group" isSingleSelect={true}>
|
|
93
|
+
{menuItems.map(({ id, label }) => (
|
|
94
|
+
<MenuItem key={id} id={id}>
|
|
95
|
+
{label}
|
|
96
|
+
</MenuItem>
|
|
97
|
+
))}
|
|
98
|
+
</MenuGroup>
|
|
99
|
+
</MenuContent>
|
|
100
|
+
</MultiSelectMenu>
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
const options = screen.getAllByRole("option");
|
|
104
|
+
|
|
105
|
+
// Click on each option
|
|
106
|
+
for (const option of screen.getAllByRole("option")) {
|
|
107
|
+
fireEvent.click(option);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Expect only the last option to be selected.
|
|
111
|
+
await waitFor(() =>
|
|
112
|
+
expect(options[0]).toHaveAttribute("aria-selected", "false")
|
|
113
|
+
);
|
|
114
|
+
await waitFor(() =>
|
|
115
|
+
expect(options[1]).toHaveAttribute("aria-selected", "false")
|
|
116
|
+
);
|
|
117
|
+
await waitFor(() =>
|
|
118
|
+
expect(options[2]).toHaveAttribute("aria-selected", "true")
|
|
119
|
+
);
|
|
120
|
+
});
|
|
121
|
+
it("only allows one selection per group in SingleSelectMenu, even if isSingleSelect is false", async () => {
|
|
122
|
+
render(
|
|
123
|
+
<SingleSelectMenu
|
|
124
|
+
defaultIsOpen
|
|
125
|
+
menuToggleElement={<MenuToggleButton>Test Menu</MenuToggleButton>}
|
|
126
|
+
>
|
|
127
|
+
<MenuContent>
|
|
128
|
+
<MenuGroup id="group" isSingleSelect={false}>
|
|
129
|
+
{menuItems.map(({ id, label }) => (
|
|
130
|
+
<MenuItem key={id} id={id}>
|
|
131
|
+
{label}
|
|
132
|
+
</MenuItem>
|
|
133
|
+
))}
|
|
134
|
+
</MenuGroup>
|
|
135
|
+
</MenuContent>
|
|
136
|
+
</SingleSelectMenu>
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
const options = screen.getAllByRole("option");
|
|
140
|
+
|
|
141
|
+
// Click on each option
|
|
142
|
+
for (const option of screen.getAllByRole("option")) {
|
|
143
|
+
fireEvent.click(option);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Expect only the last option to be selected.
|
|
147
|
+
await waitFor(() =>
|
|
148
|
+
expect(options[0]).toHaveAttribute("aria-selected", "false")
|
|
149
|
+
);
|
|
150
|
+
await waitFor(() =>
|
|
151
|
+
expect(options[1]).toHaveAttribute("aria-selected", "false")
|
|
152
|
+
);
|
|
153
|
+
await waitFor(() =>
|
|
154
|
+
expect(options[2]).toHaveAttribute("aria-selected", "true")
|
|
155
|
+
);
|
|
156
|
+
});
|
|
22
157
|
});
|
|
23
158
|
describe("API", () => {
|
|
24
159
|
it("renders a group with a title", () => {
|
|
@@ -29,7 +164,7 @@ describe("MenuGroup", () => {
|
|
|
29
164
|
menuToggleElement={<MenuToggleButton>Test Menu</MenuToggleButton>}
|
|
30
165
|
>
|
|
31
166
|
<MenuContent>
|
|
32
|
-
<MenuGroup title={menuTitle}>
|
|
167
|
+
<MenuGroup title={menuTitle} id={menuTitle}>
|
|
33
168
|
{menuItems.map(({ id, label }) => (
|
|
34
169
|
<MenuItem key={id} id={id}>
|
|
35
170
|
{label}
|
|
@@ -55,7 +190,7 @@ describe("MenuGroup", () => {
|
|
|
55
190
|
menuToggleElement={<MenuToggleButton>Test Menu</MenuToggleButton>}
|
|
56
191
|
>
|
|
57
192
|
<MenuContent>
|
|
58
|
-
<MenuGroup>
|
|
193
|
+
<MenuGroup id="group">
|
|
59
194
|
{menuItems.map(({ id, label }) => (
|
|
60
195
|
<MenuItem key={id} id={id}>
|
|
61
196
|
{label}
|
|
@@ -78,7 +213,7 @@ describe("MenuGroup", () => {
|
|
|
78
213
|
menuToggleElement={<MenuToggleButton>Test Menu</MenuToggleButton>}
|
|
79
214
|
>
|
|
80
215
|
<MenuContent>
|
|
81
|
-
<MenuGroup>
|
|
216
|
+
<MenuGroup id="group">
|
|
82
217
|
{menuItems.map(({ id, label }) => (
|
|
83
218
|
<MenuItem key={id} id={id}>
|
|
84
219
|
{label}
|
|
@@ -4,41 +4,53 @@ import { Text } from "@sproutsocial/seeds-react-text";
|
|
|
4
4
|
export const MenuGroupTypeTest = () => (
|
|
5
5
|
<>
|
|
6
6
|
{/* MenuGroup allows children */}
|
|
7
|
-
<MenuGroup>Test</MenuGroup>
|
|
7
|
+
<MenuGroup id="group">Test</MenuGroup>
|
|
8
8
|
{/* MenuGroup allows children with MenuItems */}
|
|
9
|
-
<MenuGroup>
|
|
9
|
+
<MenuGroup id="group">
|
|
10
10
|
<MenuItem id="1">Test 1</MenuItem>
|
|
11
11
|
<MenuItem id="2">Test 2</MenuItem>
|
|
12
12
|
</MenuGroup>
|
|
13
|
+
{/* MenuGroup allows isSingleSelect prop */}
|
|
14
|
+
<MenuGroup id="group-1" isSingleSelect={true}>
|
|
15
|
+
<MenuItem id="1">Test 1</MenuItem>
|
|
16
|
+
</MenuGroup>
|
|
13
17
|
{/* MenuGroup allows title as a string */}
|
|
14
|
-
<MenuGroup title="title">
|
|
18
|
+
<MenuGroup id="group" title="title">
|
|
19
|
+
Test
|
|
20
|
+
</MenuGroup>
|
|
15
21
|
{/* MenuGroup allows titleAs as a string */}
|
|
16
|
-
<MenuGroup title="title" titleAs="h3">
|
|
22
|
+
<MenuGroup id="group" title="title" titleAs="h3">
|
|
17
23
|
Test
|
|
18
24
|
</MenuGroup>
|
|
19
25
|
{/* MenuGroup allows titleAs as a component */}
|
|
20
|
-
<MenuGroup title="title" titleAs={Text}>
|
|
26
|
+
<MenuGroup id="group" title="title" titleAs={Text}>
|
|
21
27
|
Test
|
|
22
28
|
</MenuGroup>
|
|
23
29
|
{/* MenuGroup accepts ul props */}
|
|
24
|
-
<MenuGroup aria-label="test label">
|
|
30
|
+
<MenuGroup id="group" aria-label="test label">
|
|
31
|
+
Test
|
|
32
|
+
</MenuGroup>
|
|
25
33
|
{/* MenuGroup accepts style props */}
|
|
26
|
-
<MenuGroup border="1px solid black" bg="white" width="150px">
|
|
34
|
+
<MenuGroup id="group" border="1px solid black" bg="white" width="150px">
|
|
27
35
|
Test
|
|
28
36
|
</MenuGroup>
|
|
29
37
|
|
|
30
38
|
{/* @ts-expect-error - MenuGroup requires children */}
|
|
31
|
-
<MenuGroup />
|
|
39
|
+
<MenuGroup id="group" />
|
|
32
40
|
{/* @ts-expect-error - MenuGroup title is invalid as number */}
|
|
33
|
-
<MenuGroup title={123}>
|
|
41
|
+
<MenuGroup id="group" title={123}>
|
|
42
|
+
Test
|
|
43
|
+
</MenuGroup>
|
|
34
44
|
{/* @ts-expect-error - MenuGroup title is invalid as boolean */}
|
|
35
|
-
<MenuGroup title={false}>
|
|
45
|
+
<MenuGroup id="group" title={false}>
|
|
46
|
+
Test
|
|
47
|
+
</MenuGroup>
|
|
36
48
|
{/* @ts-expect-error - MenuGroup titleAs is invalid as number */}
|
|
37
|
-
<MenuGroup title="title" titleAs={123}>
|
|
49
|
+
<MenuGroup id="group" title="title" titleAs={123}>
|
|
38
50
|
Test
|
|
39
51
|
</MenuGroup>
|
|
40
52
|
{/* @ts-expect-error - MenuGroup titleAs is invalid as boolean */}
|
|
41
|
-
<MenuGroup title="title" titleAs={false}>
|
|
53
|
+
<MenuGroup id="group" title="title" titleAs={false}>
|
|
42
54
|
Test
|
|
43
55
|
</MenuGroup>
|
|
44
56
|
</>
|
|
@@ -25,12 +25,12 @@ export const MenuItem = ({
|
|
|
25
25
|
}: TypeMenuItemProps) => {
|
|
26
26
|
const { getItemProps, itemsMap, highlightedIndex, isListbox } =
|
|
27
27
|
useContext(MenuContext);
|
|
28
|
+
const item = (itemsMap[id] || { index: 0, id }) as TypeDefaultItemProps;
|
|
28
29
|
const { ...optionProps } = useOptionProps({
|
|
29
30
|
id,
|
|
30
31
|
children,
|
|
31
32
|
...props,
|
|
32
33
|
});
|
|
33
|
-
const item = (itemsMap[id] || { index: 0, id }) as TypeDefaultItemProps;
|
|
34
34
|
const highlighted = highlightedIndex === item?.index;
|
|
35
35
|
|
|
36
36
|
return (
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
screen,
|
|
5
5
|
render,
|
|
6
6
|
waitFor,
|
|
7
|
+
act,
|
|
7
8
|
} from "@sproutsocial/seeds-react-testing-library";
|
|
8
9
|
import {
|
|
9
10
|
PointerEventsCheckLevel,
|
|
@@ -394,6 +395,8 @@ describe("MenuItem when isListbox", () => {
|
|
|
394
395
|
});
|
|
395
396
|
|
|
396
397
|
describe("API", () => {
|
|
398
|
+
// This test has many tests inside it due to the `each` usage, so extend the timeout.
|
|
399
|
+
jest.setTimeout(20 * 1000);
|
|
397
400
|
test.each([INPUT_TYPES.CHECKBOX, INPUT_TYPES.RADIO, INPUT_TYPES.SWITCH])(
|
|
398
401
|
"supports %s input types",
|
|
399
402
|
(inputType) => {
|
|
@@ -491,7 +494,7 @@ describe("MenuItem when isListbox", () => {
|
|
|
491
494
|
const menuItem = screen.getByRole("option", { selected: true });
|
|
492
495
|
testByType[inputType].isSelected();
|
|
493
496
|
|
|
494
|
-
await user.click(menuItem);
|
|
497
|
+
await act(() => user.click(menuItem));
|
|
495
498
|
|
|
496
499
|
expect(
|
|
497
500
|
await screen.findByRole("option", {
|
|
@@ -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;
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import type { TypeMenuRootProps, TypeMenuRootOwnProps } from "../MenuRoot";
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
TypeDownshiftSelectProps,
|
|
4
|
+
TypeDownshiftMultiSelectProps,
|
|
5
|
+
} from "../MenuContext";
|
|
3
6
|
|
|
4
7
|
export interface TypeMultiSelectMenuProps
|
|
5
8
|
extends TypeMenuRootOwnProps,
|
|
9
|
+
TypeDownshiftSelectProps,
|
|
6
10
|
TypeDownshiftMultiSelectProps {
|
|
7
11
|
children: TypeMenuRootProps["children"];
|
|
8
12
|
}
|
|
@@ -9,8 +9,6 @@ import {
|
|
|
9
9
|
import {
|
|
10
10
|
render,
|
|
11
11
|
screen,
|
|
12
|
-
fireEvent,
|
|
13
|
-
act,
|
|
14
12
|
cleanup,
|
|
15
13
|
waitFor,
|
|
16
14
|
} from "@sproutsocial/seeds-react-testing-library";
|
|
@@ -37,7 +35,7 @@ describe("MultiSelectMenu", () => {
|
|
|
37
35
|
}
|
|
38
36
|
>
|
|
39
37
|
<MenuContent>
|
|
40
|
-
<MenuGroup>
|
|
38
|
+
<MenuGroup id="group">
|
|
41
39
|
{menuItems.map((item) => (
|
|
42
40
|
<MenuItem key={item.id} id={item.id} label={item.label}>
|
|
43
41
|
{item.label}
|
|
@@ -80,7 +78,7 @@ describe("MultiSelectMenu", () => {
|
|
|
80
78
|
}
|
|
81
79
|
>
|
|
82
80
|
<MenuContent>
|
|
83
|
-
<MenuGroup>
|
|
81
|
+
<MenuGroup id="group">
|
|
84
82
|
{menuItems.map((item) => (
|
|
85
83
|
<MenuItem
|
|
86
84
|
key={item.id}
|
|
@@ -138,7 +136,7 @@ describe("MultiSelectMenu", () => {
|
|
|
138
136
|
}
|
|
139
137
|
>
|
|
140
138
|
<MenuContent>
|
|
141
|
-
<MenuGroup>
|
|
139
|
+
<MenuGroup id="group">
|
|
142
140
|
{menuItems.map((item) => (
|
|
143
141
|
<MenuItem
|
|
144
142
|
key={item.id}
|
|
@@ -201,7 +199,7 @@ describe("MultiSelectMenu", () => {
|
|
|
201
199
|
}
|
|
202
200
|
>
|
|
203
201
|
<MenuContent>
|
|
204
|
-
<MenuGroup>
|
|
202
|
+
<MenuGroup id="group">
|
|
205
203
|
{menuItems.map((item) => (
|
|
206
204
|
<MenuItem
|
|
207
205
|
key={item.id}
|
|
@@ -259,7 +257,7 @@ describe("MultiSelectMenu", () => {
|
|
|
259
257
|
selectedItems={[menuItems[1], menuItems[2]]}
|
|
260
258
|
>
|
|
261
259
|
<MenuContent>
|
|
262
|
-
<MenuGroup>
|
|
260
|
+
<MenuGroup id="group">
|
|
263
261
|
{menuItems.map((item) => (
|
|
264
262
|
<MenuItem
|
|
265
263
|
key={item.id}
|
|
@@ -312,7 +310,7 @@ describe("MultiSelectMenu", () => {
|
|
|
312
310
|
onSelectedItemsChange={mockSelectedItemsChange}
|
|
313
311
|
>
|
|
314
312
|
<MenuContent>
|
|
315
|
-
<MenuGroup>
|
|
313
|
+
<MenuGroup id="group">
|
|
316
314
|
{menuItems.map((item) => (
|
|
317
315
|
<MenuItem
|
|
318
316
|
key={item.id}
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import type { UseSelectProps } from "downshift";
|
|
2
1
|
import type { TypeMenuRootProps, TypeMenuRootOwnProps } from "../MenuRoot";
|
|
3
|
-
import type {
|
|
2
|
+
import type { TypeDownshiftSelectProps } from "../MenuContext";
|
|
4
3
|
|
|
5
4
|
export interface TypeSingleSelectMenuProps
|
|
6
5
|
extends TypeMenuRootOwnProps,
|
|
7
|
-
|
|
6
|
+
TypeDownshiftSelectProps {
|
|
8
7
|
children: TypeMenuRootProps["children"];
|
|
9
8
|
}
|
|
@@ -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}
|
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
import React, { type ReactNode, useMemo } from "react";
|
|
2
2
|
import type { TypeMenuItemProps } from "../MenuItem";
|
|
3
3
|
import type { TypeMenuContext } from "../MenuContext";
|
|
4
|
+
import type { TypeMenuGroupProps } from "../MenuGroup/index";
|
|
4
5
|
|
|
5
6
|
export interface TypeUseItemsFromChildrenProps {
|
|
6
7
|
children: ReactNode;
|
|
7
8
|
}
|
|
8
|
-
export interface
|
|
9
|
+
export interface TypeItemElement {
|
|
9
10
|
element: React.ReactElement<TypeMenuItemProps>;
|
|
10
11
|
parents: readonly React.ReactNode[];
|
|
11
12
|
}
|
|
13
|
+
export interface TypeMenuGroupElement {
|
|
14
|
+
element: React.ReactElement<TypeMenuGroupProps>;
|
|
15
|
+
}
|
|
12
16
|
|
|
13
17
|
// https://stackoverflow.com/questions/69119016/find-specific-children-components-regardless-of-depth-in-react
|
|
14
18
|
const walkAllChildren = (
|
|
@@ -43,12 +47,11 @@ const checkIfIsElement = (element: ReactNode, name: string): boolean => {
|
|
|
43
47
|
};
|
|
44
48
|
|
|
45
49
|
const getAllMenuItems = (children: ReactNode) => {
|
|
46
|
-
const allMenuItems:
|
|
47
|
-
// console.log("children", children);
|
|
50
|
+
const allMenuItems: TypeItemElement[] = [];
|
|
48
51
|
walkAllChildren(children, (element, parents) => {
|
|
49
52
|
if (checkIfIsElement(element, "MenuItem")) {
|
|
50
53
|
allMenuItems.push({
|
|
51
|
-
element: element as
|
|
54
|
+
element: element as TypeItemElement["element"],
|
|
52
55
|
parents,
|
|
53
56
|
});
|
|
54
57
|
}
|
|
@@ -63,12 +66,16 @@ export const useItemsFromChildren = ({
|
|
|
63
66
|
const allMenuItemElements = getAllMenuItems(children);
|
|
64
67
|
const menuItemsMap: TypeMenuContext["itemsMap"] = {};
|
|
65
68
|
const allMenuItems: TypeMenuContext["items"] = allMenuItemElements.map(
|
|
66
|
-
({ element }, index) => {
|
|
69
|
+
({ element, parents }, index) => {
|
|
70
|
+
const parentMenuGroup = parents.find((parent) =>
|
|
71
|
+
checkIfIsElement(parent, "MenuGroup")
|
|
72
|
+
) as TypeMenuGroupElement["element"];
|
|
67
73
|
const item = {
|
|
68
74
|
id: element.props.id,
|
|
69
75
|
disabled: element.props.disabled,
|
|
70
76
|
index,
|
|
71
77
|
menuItemProps: element.props,
|
|
78
|
+
menuGroupProps: parentMenuGroup ? parentMenuGroup.props : undefined,
|
|
72
79
|
};
|
|
73
80
|
menuItemsMap[`${item.id}`] = item;
|
|
74
81
|
return item;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { useCombobox } from "downshift";
|
|
2
|
+
import type { UseComboboxProps } from "downshift";
|
|
3
|
+
import type { TypeDefaultItemProps } from "../types";
|
|
4
|
+
|
|
5
|
+
export const useComboboxStateReducer: Required<
|
|
6
|
+
UseComboboxProps<TypeDefaultItemProps>
|
|
7
|
+
>["stateReducer"] = (state, actionAndChanges) => {
|
|
8
|
+
const { type, changes } = actionAndChanges;
|
|
9
|
+
switch (type) {
|
|
10
|
+
/** If downshift is blurred by tabbing within the menu, stay open */
|
|
11
|
+
// case useCombobox.stateChangeTypes.InputBlur:
|
|
12
|
+
// return {
|
|
13
|
+
// ...changes,
|
|
14
|
+
// // keep menu open on blur
|
|
15
|
+
// isOpen: true,
|
|
16
|
+
// };
|
|
17
|
+
|
|
18
|
+
/** Return standard change payload */
|
|
19
|
+
default:
|
|
20
|
+
return changes;
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const defaultUseComboboxProps = {
|
|
25
|
+
isItemDisabled: (item) => !!item.disabled,
|
|
26
|
+
};
|
package/src/types.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
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;
|
|
7
8
|
menuItemProps?: TypeMenuItemProps;
|
|
9
|
+
menuGroupProps?: TypeMenuGroupProps;
|
|
8
10
|
}
|
|
9
11
|
|
|
10
12
|
export type TypeNotEmptyChildren =
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
Feature: TokenInput
|
|
2
|
-
|
|
3
|
-
A component that allows users to search, select, and manage tokens with autocomplete functionality.
|
|
4
|
-
|
|
5
|
-
Rule: User behavior
|
|
6
|
-
|
|
7
|
-
Background:
|
|
8
|
-
Given an end user is using the component
|
|
9
|
-
|
|
10
|
-
Scenario: User can add tokens
|
|
11
|
-
Given the ComboBox accepts new values
|
|
12
|
-
When text is typed
|
|
13
|
-
And a delimiter key is pressed
|
|
14
|
-
Then a new token is created from the input text
|
|
15
|
-
|
|
16
|
-
Scenario: User can not add tokens
|
|
17
|
-
Given the ComboBox does not accept new values
|
|
18
|
-
When text is typed
|
|
19
|
-
And a delimiter key is pressed
|
|
20
|
-
Then no new token is added
|
|
21
|
-
|
|
22
|
-
Scenario: User can remove tokens manually
|
|
23
|
-
Given the ComboBox has tokens
|
|
24
|
-
When token's remove button is clicked
|
|
25
|
-
Then the token is removed
|
|
26
|
-
|
|
27
|
-
Scenario: User can remove tokens with backspace
|
|
28
|
-
Given the ComboBox has tokens
|
|
29
|
-
When backspace is hit
|
|
30
|
-
Then the token is removed
|
|
31
|
-
|
|
32
|
-
Scenario: User can navigate tokens via keyboard
|
|
33
|
-
Given the ComboBox has tokens
|
|
34
|
-
And the Combobox Input has focus
|
|
35
|
-
When the tokens are navigated using arrow keys
|
|
36
|
-
Then the activeToken callback fires
|
|
37
|
-
|
|
38
|
-
Scenario: Component handles token addition
|
|
39
|
-
When a new token is added
|
|
40
|
-
Then the onAddToken callback fires
|
|
41
|
-
|
|
42
|
-
Scenario: Component handles token removal
|
|
43
|
-
Given the ComboBox is rendered with tokens
|
|
44
|
-
When a token is removed
|
|
45
|
-
Then the onRemoveToken callback fires
|
|
46
|
-
|
|
47
|
-
Scenario: Component handles token click
|
|
48
|
-
When a token is clicked
|
|
49
|
-
Then the onClickToken callback fires
|
|
50
|
-
|
|
51
|
-
Scenario: Component handles input change
|
|
52
|
-
When the input value changes
|
|
53
|
-
Then the onChange callback fires
|
|
54
|
-
|
|
55
|
-
Rule: API
|
|
56
|
-
Background:
|
|
57
|
-
Given an engineer is developing using the component
|
|
58
|
-
|
|
59
|
-
Scenario: TokenInput extends Combobox
|
|
60
|
-
When Combobox features are used in TokenInput
|
|
61
|
-
Then the TokenInput responds as a Combobox would
|
|
62
|
-
|
|
63
|
-
Scenario: ComboBox accepts tokens
|
|
64
|
-
When a token component is passed to the Combobox
|
|
65
|
-
And an item is selected
|
|
66
|
-
Then the token is rendered in the input
|
|
67
|
-
|
|
68
|
-
Scenario: ComboBox has a maximum token length
|
|
69
|
-
When 'tokenMaxLength' is set to <value>
|
|
70
|
-
Then the token string size is limited to <value>
|
|
71
|
-
|
|
72
|
-
Scenario Outline: ComboBox checks its types
|
|
73
|
-
When <propName> is passed a value
|
|
74
|
-
Then ComboBox expects it to be of type <propType>
|
|
75
|
-
|
|
76
|
-
Examples: Prop Types
|
|
77
|
-
| activeToken | string |
|
|
78
|
-
| tokens | array of { id: string, iconName?: EnumIconNames, value: string, valid?: boolean } |
|
|
79
|
-
| onChangeTokens | function |
|
|
80
|
-
| onAddToken | function |
|
|
81
|
-
| onRemoveToken | function |
|
|
82
|
-
| onClickToken | function |
|
|
83
|
-
| maxLength | number |
|
|
84
|
-
| tokenMaxLength | number |
|