@sproutsocial/seeds-react-menu 0.1.0 → 0.2.1
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/.eslintignore +6 -0
- package/.eslintrc.js +6 -2
- package/.turbo/turbo-build.log +10 -10
- package/CHANGELOG.md +23 -1
- package/dist/esm/index.js +243 -217
- package/dist/esm/index.js.map +1 -1
- package/dist/index.d.mts +104 -38
- package/dist/index.d.ts +104 -38
- package/dist/index.js +272 -243
- package/dist/index.js.map +1 -1
- package/package.json +10 -7
- package/src/ActionMenu/ActionMenu.feature +9 -3
- package/src/ActionMenu/ActionMenu.tsx +76 -42
- package/src/ActionMenu/__tests__/ActionMenu.test.tsx +36 -42
- package/src/ActionMenu/__tests__/ActionMenu.typetest.tsx +13 -13
- package/src/MenuContent/MenuContent.tsx +31 -1
- package/src/MenuContent/MenuContentTypes.ts +3 -4
- package/src/MenuContent/styles.tsx +10 -6
- package/src/MenuContext.tsx +1 -6
- package/src/MenuFooter/MenuFooter.feature +1 -1
- package/src/MenuFooter/MenuFooter.tsx +8 -0
- package/src/MenuFooter/__tests__/MenuFooter.test.tsx +53 -26
- package/src/MenuFooter/styles.tsx +1 -1
- package/src/MenuGroup/MenuGroup.feature +14 -26
- package/src/MenuGroup/MenuGroup.tsx +21 -1
- package/src/MenuGroup/MenuGroupTypes.ts +1 -0
- package/src/MenuGroup/__tests__/MenuGroup.test.tsx +41 -6
- package/src/MenuGroup/styles.tsx +4 -3
- package/src/MenuHeader/MenuHeader.tsx +16 -5
- package/src/MenuHeader/__tests__/MenuHeader.test.tsx +50 -6
- package/src/MenuHeader/__tests__/MenuHeader.typetest.tsx +1 -0
- package/src/MenuHeader/styles.tsx +21 -22
- package/src/MenuItem/MenuItem.feature +66 -26
- package/src/MenuItem/MenuItem.tsx +39 -20
- package/src/MenuItem/MenuItemTypes.ts +13 -1
- package/src/MenuItem/__tests__/MenuItem.test.tsx +431 -154
- package/src/MenuItem/__tests__/MenuItem.typetest.tsx +57 -38
- package/src/MenuItem/styles.tsx +44 -3
- package/src/{MenuItemSelectable/MenuItemSelectable.tsx → MenuItem/useOptionProps.tsx} +32 -54
- package/src/MenuRoot/MenuRoot.feature +20 -10
- package/src/MenuRoot/MenuRoot.tsx +14 -18
- package/src/MenuRoot/__tests__/MenuRoot.test.tsx +168 -34
- package/src/MenuRoot/__tests__/MenuRoot.typetest.tsx +10 -10
- package/src/MultiSelectMenu/MultiSelectMenu.feature +2 -13
- package/src/MultiSelectMenu/MultiSelectMenu.tsx +9 -1
- package/src/MultiSelectMenu/__tests__/MultiSelectMenu.test.tsx +154 -153
- package/src/MultiSelectMenu/__tests__/MultiSelectMenu.typetest.tsx +10 -10
- package/src/SingleSelectMenu/SingleSelectMenu.feature +11 -0
- package/src/SingleSelectMenu/SingleSelectMenu.tsx +10 -0
- package/src/SingleSelectMenu/__tests__/SingleSelectMenu.test.tsx +11 -14
- package/src/SingleSelectMenu/__tests__/SingleSelectMenu.typetest.tsx +14 -14
- package/src/__tests__/Menu.test.tsx +20 -16
- package/src/hooks/useItemsFromChildren.tsx +2 -4
- package/src/index.ts +0 -1
- package/src/menuTestingUtils.tsx +7 -4
- package/tsconfig.json +1 -1
- package/src/MenuItemSelectable/MenuItemSelectable.feature +0 -99
- package/src/MenuItemSelectable/MenuItemSelectableTypes.ts +0 -15
- package/src/MenuItemSelectable/__tests__/MenuItemSelectable.test.tsx +0 -303
- package/src/MenuItemSelectable/__tests__/MenuItemSelectable.typetest.tsx +0 -56
- package/src/MenuItemSelectable/index.ts +0 -5
- package/src/MenuItemSelectable/styles.tsx +0 -21
|
@@ -4,20 +4,20 @@ import { MenuRoot, MenuContent, MenuHeader, MenuFooter } from "../../index";
|
|
|
4
4
|
export const MenuRootTypeTest = () => (
|
|
5
5
|
<>
|
|
6
6
|
{/* Valid MenuRoot with minimal required props */}
|
|
7
|
-
<MenuRoot
|
|
7
|
+
<MenuRoot menuToggleElement={<>test</>}>Test</MenuRoot>
|
|
8
8
|
{/* Valid MenuRoot allows MenuContent by itself as a child */}
|
|
9
|
-
<MenuRoot
|
|
9
|
+
<MenuRoot menuToggleElement={<>test</>}>
|
|
10
10
|
<MenuContent>Test</MenuContent>
|
|
11
11
|
</MenuRoot>
|
|
12
12
|
{/* Valid MenuRoot allows multiple children without fragment */}
|
|
13
|
-
<MenuRoot
|
|
13
|
+
<MenuRoot menuToggleElement={<>test</>}>
|
|
14
14
|
<MenuHeader>Test</MenuHeader>
|
|
15
15
|
<MenuContent>Test</MenuContent>
|
|
16
16
|
<MenuFooter>Test</MenuFooter>
|
|
17
17
|
</MenuRoot>
|
|
18
18
|
{/* Valid MenuRoot accepts Downshift props and functions for useSelect */}
|
|
19
19
|
<MenuRoot
|
|
20
|
-
|
|
20
|
+
menuToggleElement={<>test</>}
|
|
21
21
|
onSelectedItemChange={() => {}}
|
|
22
22
|
selectItem={() => {}}
|
|
23
23
|
selectedItem={null}
|
|
@@ -30,7 +30,7 @@ export const MenuRootTypeTest = () => (
|
|
|
30
30
|
</MenuRoot>
|
|
31
31
|
{/* Valid MenuRoot accepts Downshift props and functions for useMultipleSelection */}
|
|
32
32
|
<MenuRoot
|
|
33
|
-
|
|
33
|
+
menuToggleElement={<>test</>}
|
|
34
34
|
onSelectedItemsChange={() => {}}
|
|
35
35
|
selectedItems={[]}
|
|
36
36
|
multiSelectStateReducer={(c) => c}
|
|
@@ -40,19 +40,19 @@ export const MenuRootTypeTest = () => (
|
|
|
40
40
|
Test
|
|
41
41
|
</MenuRoot>
|
|
42
42
|
|
|
43
|
-
{/* @ts-expect-error -
|
|
43
|
+
{/* @ts-expect-error - menuToggleElement is a required prop */}
|
|
44
44
|
<MenuRoot>Test</MenuRoot>
|
|
45
45
|
<MenuRoot
|
|
46
|
-
/* @ts-expect-error -
|
|
47
|
-
|
|
46
|
+
/* @ts-expect-error - menuToggleElement must be a valid react element */
|
|
47
|
+
menuToggleElement="test"
|
|
48
48
|
>
|
|
49
49
|
Test
|
|
50
50
|
</MenuRoot>
|
|
51
51
|
{/* @ts-expect-error - when no children provided */}
|
|
52
|
-
<MenuRoot
|
|
52
|
+
<MenuRoot menuToggleElement="test" />
|
|
53
53
|
|
|
54
54
|
<MenuRoot
|
|
55
|
-
|
|
55
|
+
menuToggleElement={<>test</>}
|
|
56
56
|
/* @ts-expect-error - isOpen can't be a string */
|
|
57
57
|
isOpen="false"
|
|
58
58
|
/* @ts-expect-error - onSelectedItemChange can't be a string */
|
|
@@ -40,25 +40,14 @@ Feature: MultiSelectMenu
|
|
|
40
40
|
Then the component renders the existing values
|
|
41
41
|
|
|
42
42
|
Scenario: Component handles new selection state
|
|
43
|
-
When the MultiSelectMenu is not passed
|
|
43
|
+
When the MultiSelectMenu is not passed a `selectedItems` prop
|
|
44
44
|
And a new selection is created
|
|
45
|
-
Then the '
|
|
45
|
+
Then the 'onSelectedItemsChange' callback is fired
|
|
46
46
|
|
|
47
47
|
Scenario: Menu does not close on selection
|
|
48
48
|
When an item is selected
|
|
49
49
|
Then the 'onSelectedItemsChange' callback fires without closing the menu
|
|
50
50
|
|
|
51
|
-
Scenario Outline: Actions can be triggered from the MultiSelectMenu
|
|
52
|
-
When actionable elements are provided to the MultiSelectMenu
|
|
53
|
-
And an `<action>` is clicked
|
|
54
|
-
Then the corresponding `onClick` should be executed
|
|
55
|
-
|
|
56
|
-
Examples: Action Types
|
|
57
|
-
| action |
|
|
58
|
-
| ---------------- |
|
|
59
|
-
| select button |
|
|
60
|
-
| deselect button |
|
|
61
|
-
|
|
62
51
|
Scenario Outline: MultiSelectMenu checks its types
|
|
63
52
|
When `<propName>` is passed a value
|
|
64
53
|
Then MultiSelectMenu expects it to be of type `<propType>`
|
|
@@ -34,6 +34,14 @@ const selectReducerForMultiSelect: TypeMultiSelectMenuProps["stateReducer"] = (
|
|
|
34
34
|
return changes;
|
|
35
35
|
};
|
|
36
36
|
|
|
37
|
+
/**
|
|
38
|
+
* @link https://seeds.sproutsocial.com/components/multiselect-menu/
|
|
39
|
+
*
|
|
40
|
+
* @description A MultiSelectMenu allows multiple {@link https://github.com/sproutsocial/seeds/tree/dev/seeds-react/seeds-react-menu/src/MenuItem | MenuItems} to be selected from a list or groups of lists.
|
|
41
|
+
*
|
|
42
|
+
* @see {@link https://seeds.sproutsocial.com/components/menu-group/ | MenuGroup}
|
|
43
|
+
*/
|
|
44
|
+
|
|
37
45
|
export const MultiSelectMenu = ({
|
|
38
46
|
children,
|
|
39
47
|
stateReducer: externalStateReducer,
|
|
@@ -74,6 +82,7 @@ export const MultiSelectMenu = ({
|
|
|
74
82
|
defaultHighlightedIndex: 0,
|
|
75
83
|
stateReducer: (state, actionAndChanges) => {
|
|
76
84
|
let newState: Partial<UseSelectState<TypeDefaultItemProps>>;
|
|
85
|
+
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
77
86
|
newState = useSelectStateReducer(state, actionAndChanges);
|
|
78
87
|
newState = selectReducerForMultiSelect(state, {
|
|
79
88
|
...actionAndChanges,
|
|
@@ -91,7 +100,6 @@ export const MultiSelectMenu = ({
|
|
|
91
100
|
case useSelect.stateChangeTypes.ToggleButtonKeyDownEnter:
|
|
92
101
|
case useSelect.stateChangeTypes.ToggleButtonKeyDownSpaceButton:
|
|
93
102
|
case useSelect.stateChangeTypes.ItemClick:
|
|
94
|
-
case useSelect.stateChangeTypes.ToggleButtonBlur:
|
|
95
103
|
if (!newSelectedItem) break;
|
|
96
104
|
if (selectedItems?.find((item) => item?.id === newSelectedItem.id)) {
|
|
97
105
|
removeSelectedItem(newSelectedItem);
|
|
@@ -1,17 +1,16 @@
|
|
|
1
|
+
/* eslint-disable testing-library/prefer-find-by */
|
|
2
|
+
/* eslint-disable testing-library/no-wait-for-multiple-assertions */
|
|
1
3
|
/* eslint-disable testing-library/no-unnecessary-act */
|
|
2
4
|
import {
|
|
3
5
|
MultiSelectMenu,
|
|
4
6
|
MenuContent,
|
|
5
7
|
MenuToggleButton,
|
|
6
|
-
|
|
8
|
+
MenuItem,
|
|
7
9
|
MenuGroup,
|
|
8
10
|
} from "../../index";
|
|
9
11
|
import {
|
|
10
12
|
render,
|
|
11
13
|
screen,
|
|
12
|
-
fireEvent,
|
|
13
|
-
act,
|
|
14
|
-
cleanup,
|
|
15
14
|
waitFor,
|
|
16
15
|
} from "@sproutsocial/seeds-react-testing-library";
|
|
17
16
|
|
|
@@ -23,14 +22,10 @@ const menuItems = [
|
|
|
23
22
|
];
|
|
24
23
|
|
|
25
24
|
describe("MultiSelectMenu", () => {
|
|
26
|
-
|
|
27
|
-
cleanup();
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it("should render the menuItemSelectables when provided as children", async () => {
|
|
25
|
+
it("should render the MenuItems when provided as children", async () => {
|
|
31
26
|
const { user } = render(
|
|
32
27
|
<MultiSelectMenu
|
|
33
|
-
|
|
28
|
+
menuToggleElement={
|
|
34
29
|
<MenuToggleButton aria-label="testable menu" role="button">
|
|
35
30
|
Testable Menu
|
|
36
31
|
</MenuToggleButton>
|
|
@@ -39,9 +34,9 @@ describe("MultiSelectMenu", () => {
|
|
|
39
34
|
<MenuContent>
|
|
40
35
|
<MenuGroup>
|
|
41
36
|
{menuItems.map((item) => (
|
|
42
|
-
<
|
|
37
|
+
<MenuItem key={item.id} id={item.id} label={item.label}>
|
|
43
38
|
{item.label}
|
|
44
|
-
</
|
|
39
|
+
</MenuItem>
|
|
45
40
|
))}
|
|
46
41
|
</MenuGroup>
|
|
47
42
|
</MenuContent>
|
|
@@ -70,10 +65,10 @@ describe("MultiSelectMenu", () => {
|
|
|
70
65
|
});
|
|
71
66
|
});
|
|
72
67
|
|
|
73
|
-
|
|
68
|
+
it("should allow for multiple items to be selected", async () => {
|
|
74
69
|
const { user } = render(
|
|
75
70
|
<MultiSelectMenu
|
|
76
|
-
|
|
71
|
+
menuToggleElement={
|
|
77
72
|
<MenuToggleButton aria-label="testable menu" role="button">
|
|
78
73
|
Testable Menu
|
|
79
74
|
</MenuToggleButton>
|
|
@@ -82,14 +77,14 @@ describe("MultiSelectMenu", () => {
|
|
|
82
77
|
<MenuContent>
|
|
83
78
|
<MenuGroup>
|
|
84
79
|
{menuItems.map((item) => (
|
|
85
|
-
<
|
|
80
|
+
<MenuItem
|
|
86
81
|
key={item.id}
|
|
87
82
|
id={item.id}
|
|
88
83
|
data-testid={item.id}
|
|
89
84
|
label={item.label}
|
|
90
85
|
>
|
|
91
86
|
{item.label}
|
|
92
|
-
</
|
|
87
|
+
</MenuItem>
|
|
93
88
|
))}
|
|
94
89
|
</MenuGroup>
|
|
95
90
|
</MenuContent>
|
|
@@ -113,23 +108,25 @@ describe("MultiSelectMenu", () => {
|
|
|
113
108
|
|
|
114
109
|
await waitFor(() => {
|
|
115
110
|
// Check that the first item is selected
|
|
116
|
-
expect(item1).toHaveAttribute("aria-
|
|
111
|
+
expect(item1).toHaveAttribute("aria-selected", "true");
|
|
117
112
|
});
|
|
118
113
|
|
|
119
114
|
// Select second item
|
|
120
|
-
// TODO: can this be done in the same act as the first item?
|
|
121
115
|
await user.click(item2);
|
|
122
116
|
|
|
123
117
|
await waitFor(() => {
|
|
124
118
|
// Check that the second item is selected
|
|
125
|
-
expect(item2).toHaveAttribute("aria-
|
|
119
|
+
expect(item2).toHaveAttribute("aria-selected", "true");
|
|
126
120
|
});
|
|
121
|
+
|
|
122
|
+
// check that first item is still selected
|
|
123
|
+
expect(item1).toHaveAttribute("aria-selected", "true");
|
|
127
124
|
});
|
|
128
125
|
|
|
129
|
-
|
|
126
|
+
it("Should maintain selected state when closed and reopened", async () => {
|
|
130
127
|
const { user } = render(
|
|
131
128
|
<MultiSelectMenu
|
|
132
|
-
|
|
129
|
+
menuToggleElement={
|
|
133
130
|
<MenuToggleButton aria-label="testable menu" role="button">
|
|
134
131
|
Testable Menu
|
|
135
132
|
</MenuToggleButton>
|
|
@@ -138,14 +135,14 @@ describe("MultiSelectMenu", () => {
|
|
|
138
135
|
<MenuContent>
|
|
139
136
|
<MenuGroup>
|
|
140
137
|
{menuItems.map((item) => (
|
|
141
|
-
<
|
|
138
|
+
<MenuItem
|
|
142
139
|
key={item.id}
|
|
143
140
|
id={item.id}
|
|
144
141
|
data-testid={item.id}
|
|
145
142
|
label={item.label}
|
|
146
143
|
>
|
|
147
144
|
{item.label}
|
|
148
|
-
</
|
|
145
|
+
</MenuItem>
|
|
149
146
|
))}
|
|
150
147
|
</MenuGroup>
|
|
151
148
|
</MenuContent>
|
|
@@ -167,30 +164,32 @@ describe("MultiSelectMenu", () => {
|
|
|
167
164
|
await user.click(item1);
|
|
168
165
|
|
|
169
166
|
await waitFor(() => {
|
|
170
|
-
expect(item1).toHaveAttribute("aria-
|
|
167
|
+
expect(item1).toHaveAttribute("aria-selected", "true");
|
|
171
168
|
});
|
|
172
169
|
|
|
173
170
|
// Close Menu (MultiSelect does not close automatically)
|
|
174
171
|
await user.click(toggleButton);
|
|
175
172
|
|
|
176
173
|
// Make sure the menu is closed
|
|
177
|
-
|
|
174
|
+
await waitFor(() =>
|
|
175
|
+
expect(screen.queryByRole("listbox")).not.toBeInTheDocument()
|
|
176
|
+
);
|
|
178
177
|
|
|
179
178
|
// Open menu again
|
|
180
179
|
await user.click(toggleButton);
|
|
181
180
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
expect(screen.getByRole("listbox")).toBeInTheDocument()
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
181
|
+
// Make sure the menu is open
|
|
182
|
+
await waitFor(() =>
|
|
183
|
+
expect(screen.getByRole("listbox")).toBeInTheDocument()
|
|
184
|
+
);
|
|
185
|
+
// Make sure the item is still selected
|
|
186
|
+
expect(item1).toHaveAttribute("aria-selected", "true");
|
|
188
187
|
});
|
|
189
188
|
|
|
190
|
-
|
|
189
|
+
it("should be able to navigate using the keyboard", async () => {
|
|
191
190
|
const { user } = render(
|
|
192
191
|
<MultiSelectMenu
|
|
193
|
-
|
|
192
|
+
menuToggleElement={
|
|
194
193
|
<MenuToggleButton aria-label="testable menu" role="button">
|
|
195
194
|
Testable Menu
|
|
196
195
|
</MenuToggleButton>
|
|
@@ -199,151 +198,153 @@ describe("MultiSelectMenu", () => {
|
|
|
199
198
|
<MenuContent>
|
|
200
199
|
<MenuGroup>
|
|
201
200
|
{menuItems.map((item) => (
|
|
202
|
-
<
|
|
201
|
+
<MenuItem
|
|
203
202
|
key={item.id}
|
|
204
203
|
id={item.id}
|
|
205
204
|
data-testid={item.id}
|
|
206
205
|
label={item.label}
|
|
207
206
|
>
|
|
208
207
|
{item.label}
|
|
209
|
-
</
|
|
208
|
+
</MenuItem>
|
|
210
209
|
))}
|
|
211
210
|
</MenuGroup>
|
|
212
211
|
</MenuContent>
|
|
213
212
|
</MultiSelectMenu>
|
|
214
213
|
);
|
|
215
214
|
|
|
216
|
-
|
|
215
|
+
// Open Menu
|
|
216
|
+
await user.keyboard("{Tab}{Enter}");
|
|
217
|
+
expect(await screen.findByRole("listbox")).toBeInTheDocument();
|
|
217
218
|
|
|
218
|
-
//
|
|
219
|
-
fireEvent.focus(toggleButton);
|
|
220
|
-
await user.keyboard("{Enter}");
|
|
219
|
+
// Check that the first item is highlighted
|
|
221
220
|
await user.keyboard("{ArrowDown}");
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
expect(item1).toHaveFocus();
|
|
227
|
-
});
|
|
221
|
+
expect(screen.getByRole("listbox")).toHaveAttribute(
|
|
222
|
+
"aria-activedescendant",
|
|
223
|
+
screen.getByRole("option", { name: "Item 1" }).getAttribute("id")
|
|
224
|
+
);
|
|
228
225
|
|
|
229
226
|
// Select the first item
|
|
230
227
|
await user.keyboard("{Enter}");
|
|
231
|
-
|
|
228
|
+
|
|
232
229
|
// select the second item
|
|
230
|
+
await user.keyboard("{ArrowDown}");
|
|
233
231
|
await user.keyboard(" ");
|
|
234
232
|
|
|
233
|
+
// Check that the second item is highlighted and selected
|
|
234
|
+
const item2 = screen.getByRole("option", { name: "Item 2" });
|
|
235
|
+
await waitFor(() => expect(item2).toHaveAttribute("aria-selected", "true"));
|
|
236
|
+
expect(screen.getByRole("listbox")).toHaveAttribute(
|
|
237
|
+
"aria-activedescendant",
|
|
238
|
+
item2.getAttribute("id")
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
// Check that the first item is still selected
|
|
242
|
+
const item1 = screen.getByRole("option", { name: "Item 1" });
|
|
243
|
+
expect(item1).toHaveAttribute("aria-selected", "true");
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it("Should work with a initially selected items", async () => {
|
|
247
|
+
const { user } = render(
|
|
248
|
+
<MultiSelectMenu
|
|
249
|
+
menuToggleElement={
|
|
250
|
+
<MenuToggleButton aria-label="testable menu" role="button">
|
|
251
|
+
Testable Menu
|
|
252
|
+
</MenuToggleButton>
|
|
253
|
+
}
|
|
254
|
+
selectedItems={[menuItems[1], menuItems[2]]}
|
|
255
|
+
>
|
|
256
|
+
<MenuContent>
|
|
257
|
+
<MenuGroup>
|
|
258
|
+
{menuItems.map((item) => (
|
|
259
|
+
<MenuItem
|
|
260
|
+
key={item.id}
|
|
261
|
+
id={item.id}
|
|
262
|
+
data-testid={item.id}
|
|
263
|
+
label={item.label}
|
|
264
|
+
>
|
|
265
|
+
{item.label}
|
|
266
|
+
</MenuItem>
|
|
267
|
+
))}
|
|
268
|
+
</MenuGroup>
|
|
269
|
+
</MenuContent>
|
|
270
|
+
</MultiSelectMenu>
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
expect(screen.queryByRole("listbox")).not.toBeInTheDocument();
|
|
274
|
+
|
|
275
|
+
const toggleButton = screen.getByRole("button", {
|
|
276
|
+
name: "testable menu",
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
// Open Menu
|
|
280
|
+
await user.click(toggleButton);
|
|
281
|
+
|
|
282
|
+
const item1 = screen.getByRole("option", { name: "Item 1" });
|
|
283
|
+
const item2 = screen.getByRole("option", { name: "Item 2" });
|
|
284
|
+
const item3 = screen.getByRole("option", { name: "Item 3" });
|
|
285
|
+
|
|
235
286
|
await waitFor(() => {
|
|
236
|
-
|
|
237
|
-
const item2 = screen.getByRole("option", { name: "Item 2" });
|
|
238
|
-
expect(item2).toHaveFocus();
|
|
239
|
-
expect(item2).toBeChecked();
|
|
240
|
-
|
|
241
|
-
// Check that the first item is still selected
|
|
242
|
-
const item1 = screen.getByRole("menuitem", { name: "Item 1" });
|
|
243
|
-
expect(item1).toBeChecked();
|
|
287
|
+
expect(item1).not.toHaveStyle("pointer-events: none");
|
|
244
288
|
});
|
|
245
289
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
expect(item1).not.toHaveStyle("pointer-events: none");
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
// Select Item 1
|
|
290
|
-
await user.click(item1);
|
|
291
|
-
|
|
292
|
-
await waitFor(() => {
|
|
293
|
-
expect(menuContentContainer).toBeInTheDocument();
|
|
294
|
-
});
|
|
290
|
+
// Check that the first item is not selected
|
|
291
|
+
expect(item1).not.toHaveAttribute("aria-selected", "true");
|
|
292
|
+
// Check that the second item is selected
|
|
293
|
+
expect(item2).toHaveAttribute("aria-selected", "true");
|
|
294
|
+
// Check that the third item is selected
|
|
295
|
+
expect(item3).toHaveAttribute("aria-selected", "true");
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it("should stay open after menu item selection", async () => {
|
|
299
|
+
const mockSelectedItemsChange = jest.fn();
|
|
300
|
+
const { user } = render(
|
|
301
|
+
<MultiSelectMenu
|
|
302
|
+
menuToggleElement={
|
|
303
|
+
<MenuToggleButton aria-label="testable menu" role="button">
|
|
304
|
+
Testable Menu
|
|
305
|
+
</MenuToggleButton>
|
|
306
|
+
}
|
|
307
|
+
onSelectedItemsChange={mockSelectedItemsChange}
|
|
308
|
+
>
|
|
309
|
+
<MenuContent>
|
|
310
|
+
<MenuGroup>
|
|
311
|
+
{menuItems.map((item) => (
|
|
312
|
+
<MenuItem
|
|
313
|
+
key={item.id}
|
|
314
|
+
id={item.id}
|
|
315
|
+
data-testid={item.id}
|
|
316
|
+
label={item.label}
|
|
317
|
+
>
|
|
318
|
+
{item.label}
|
|
319
|
+
</MenuItem>
|
|
320
|
+
))}
|
|
321
|
+
</MenuGroup>
|
|
322
|
+
</MenuContent>
|
|
323
|
+
</MultiSelectMenu>
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
expect(screen.queryByRole("listbox")).not.toBeInTheDocument();
|
|
327
|
+
|
|
328
|
+
const toggleButton = screen.getByRole("button", {
|
|
329
|
+
name: "testable menu",
|
|
295
330
|
});
|
|
296
331
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
}
|
|
305
|
-
defaultSelectedItems={[menuItems[1], menuItems[2]]}
|
|
306
|
-
>
|
|
307
|
-
<MenuContent>
|
|
308
|
-
<MenuGroup>
|
|
309
|
-
{menuItems.map((item) => (
|
|
310
|
-
<MenuItemSelectable
|
|
311
|
-
key={item.id}
|
|
312
|
-
id={item.id}
|
|
313
|
-
data-testid={item.id}
|
|
314
|
-
label={item.label}
|
|
315
|
-
>
|
|
316
|
-
{item.label}
|
|
317
|
-
</MenuItemSelectable>
|
|
318
|
-
))}
|
|
319
|
-
</MenuGroup>
|
|
320
|
-
</MenuContent>
|
|
321
|
-
</MultiSelectMenu>
|
|
322
|
-
);
|
|
323
|
-
|
|
324
|
-
expect(screen.queryByRole("listbox")).not.toBeInTheDocument();
|
|
325
|
-
|
|
326
|
-
const toggleButton = screen.getByRole("button", {
|
|
327
|
-
name: "testable menu",
|
|
328
|
-
});
|
|
329
|
-
|
|
330
|
-
// Open Menu
|
|
331
|
-
await user.click(toggleButton);
|
|
332
|
-
|
|
333
|
-
const item1 = screen.getByRole("option", { name: "Item 1" });
|
|
334
|
-
const item2 = screen.getByRole("option", { name: "Item 2" });
|
|
335
|
-
const item3 = screen.getByRole("option", { name: "Item 3" });
|
|
336
|
-
|
|
337
|
-
await waitFor(() => {
|
|
338
|
-
expect(item1).not.toHaveStyle("pointer-events: none");
|
|
339
|
-
});
|
|
340
|
-
|
|
341
|
-
act(() => {
|
|
342
|
-
// Check that the second item is selected
|
|
343
|
-
expect(item2).toHaveAttribute("aria-checked", "true");
|
|
344
|
-
// Check that the third item is selected
|
|
345
|
-
expect(item3).toHaveAttribute("aria-checked", "true");
|
|
346
|
-
});
|
|
332
|
+
// Open Menu
|
|
333
|
+
await user.click(toggleButton);
|
|
334
|
+
|
|
335
|
+
const item1 = screen.getByRole("option", { name: "Item 1" });
|
|
336
|
+
|
|
337
|
+
await waitFor(() => {
|
|
338
|
+
expect(item1).not.toHaveStyle("pointer-events: none");
|
|
347
339
|
});
|
|
340
|
+
|
|
341
|
+
// Select Item 1
|
|
342
|
+
await user.click(item1);
|
|
343
|
+
|
|
344
|
+
// Check that the menu is still open
|
|
345
|
+
expect(await screen.findByRole("listbox")).toBeInTheDocument();
|
|
346
|
+
|
|
347
|
+
// Check that callback called
|
|
348
|
+
expect(mockSelectedItemsChange).toHaveBeenCalledTimes(1);
|
|
348
349
|
});
|
|
349
350
|
});
|
|
@@ -9,20 +9,20 @@ import {
|
|
|
9
9
|
export const MultiSelectMenuTypeTest = () => (
|
|
10
10
|
<>
|
|
11
11
|
{/* Valid MultiSelectMenu with minimal required props */}
|
|
12
|
-
<MultiSelectMenu
|
|
12
|
+
<MultiSelectMenu menuToggleElement={<>test</>}>Test</MultiSelectMenu>
|
|
13
13
|
{/* Valid MultiSelectMenu allows MenuContent by itself as a child */}
|
|
14
|
-
<MultiSelectMenu
|
|
14
|
+
<MultiSelectMenu menuToggleElement={<>test</>}>
|
|
15
15
|
<MenuContent>Test</MenuContent>
|
|
16
16
|
</MultiSelectMenu>
|
|
17
17
|
{/* Valid MultiSelectMenu allows multiple children without fragment */}
|
|
18
|
-
<MultiSelectMenu
|
|
18
|
+
<MultiSelectMenu menuToggleElement={<>test</>}>
|
|
19
19
|
<MenuHeader>Test</MenuHeader>
|
|
20
20
|
<MenuContent>Test</MenuContent>
|
|
21
21
|
<MenuFooter>Test</MenuFooter>
|
|
22
22
|
</MultiSelectMenu>
|
|
23
23
|
{/* Valid MultiSelectMenu accepts Downshift props and functions for useSelect */}
|
|
24
24
|
<MultiSelectMenu
|
|
25
|
-
|
|
25
|
+
menuToggleElement={<>test</>}
|
|
26
26
|
onSelectedItemChange={() => {}}
|
|
27
27
|
selectedItem={null}
|
|
28
28
|
stateReducer={(c) => c}
|
|
@@ -34,7 +34,7 @@ export const MultiSelectMenuTypeTest = () => (
|
|
|
34
34
|
</MultiSelectMenu>
|
|
35
35
|
{/* Valid MultiSelectMenu accepts Downshift props and functions for useMultipleSelection */}
|
|
36
36
|
<MultiSelectMenu
|
|
37
|
-
|
|
37
|
+
menuToggleElement={<>test</>}
|
|
38
38
|
onSelectedItemsChange={() => {}}
|
|
39
39
|
selectedItems={[]}
|
|
40
40
|
multiSelectStateReducer={(c) => c}
|
|
@@ -44,19 +44,19 @@ export const MultiSelectMenuTypeTest = () => (
|
|
|
44
44
|
Test
|
|
45
45
|
</MultiSelectMenu>
|
|
46
46
|
|
|
47
|
-
{/* @ts-expect-error -
|
|
47
|
+
{/* @ts-expect-error - menuToggleElement is a required prop */}
|
|
48
48
|
<MultiSelectMenu>Test</MultiSelectMenu>
|
|
49
49
|
<MultiSelectMenu
|
|
50
|
-
/* @ts-expect-error -
|
|
51
|
-
|
|
50
|
+
/* @ts-expect-error - menuToggleElement must be a valid react element */
|
|
51
|
+
menuToggleElement="test"
|
|
52
52
|
>
|
|
53
53
|
Test
|
|
54
54
|
</MultiSelectMenu>
|
|
55
55
|
{/* @ts-expect-error - when no children provided */}
|
|
56
|
-
<MultiSelectMenu
|
|
56
|
+
<MultiSelectMenu menuToggleElement="test" />
|
|
57
57
|
|
|
58
58
|
<MultiSelectMenu
|
|
59
|
-
|
|
59
|
+
menuToggleElement={<>test</>}
|
|
60
60
|
/* @ts-expect-error - isOpen can't be a string */
|
|
61
61
|
isOpen="false"
|
|
62
62
|
/* @ts-expect-error - onSelectedItemChange can't be a string */
|
|
@@ -10,6 +10,17 @@ Feature: Single Select Menu
|
|
|
10
10
|
When an item is selected
|
|
11
11
|
Then a selectedItem is set
|
|
12
12
|
|
|
13
|
+
Scenario: SingleSelectMenu remains open when interacting with the footer
|
|
14
|
+
Given the menu has a footer
|
|
15
|
+
And the footer contains an interactive element
|
|
16
|
+
When an interactive element in the footer is clicked
|
|
17
|
+
Then the menu will remain open
|
|
18
|
+
|
|
19
|
+
Scenario: SingleSelectMenu closes when a selection is made
|
|
20
|
+
Given the menu is using the built in behavior
|
|
21
|
+
When a menu item is selected
|
|
22
|
+
Then the menu will close
|
|
23
|
+
|
|
13
24
|
Rule: API
|
|
14
25
|
Background:
|
|
15
26
|
Given an engineer is implementing a SingleSelectMenu
|
|
@@ -10,6 +10,15 @@ import { MenuRoot } from "../MenuRoot";
|
|
|
10
10
|
import type { TypeDefaultItemProps } from "../types";
|
|
11
11
|
import type { TypeSingleSelectMenuProps } from "./SingleSelectMenuTypes";
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* @link https://seeds.sproutsocial.com/components/singleselect-menu/
|
|
15
|
+
*
|
|
16
|
+
* @description A SingleSelectMenu allows a single selection of a {@link https://github.com/sproutsocial/seeds/tree/dev/seeds-react/seeds-react-menu/src/MenuItem | MenuItem} from a list or groups or lists.
|
|
17
|
+
*
|
|
18
|
+
* **To trigger actions from a menu:**
|
|
19
|
+
* @see {@link https://github.com/sproutsocial/seeds/tree/dev/seeds-react/seeds-react-menu/src/ActionMenu | ActionMenu}
|
|
20
|
+
*/
|
|
21
|
+
|
|
13
22
|
export const SingleSelectMenu = ({
|
|
14
23
|
children,
|
|
15
24
|
stateReducer: externalStateReducer,
|
|
@@ -27,6 +36,7 @@ export const SingleSelectMenu = ({
|
|
|
27
36
|
itemToString,
|
|
28
37
|
stateReducer: (state, actionAndChanges) => {
|
|
29
38
|
let newState: Partial<UseSelectState<TypeDefaultItemProps>>;
|
|
39
|
+
// eslint-disable-next-line react-hooks/rules-of-hooks, prefer-const
|
|
30
40
|
newState = useSelectStateReducer(state, actionAndChanges);
|
|
31
41
|
return externalStateReducer
|
|
32
42
|
? externalStateReducer(state, {
|