@sproutsocial/seeds-react-menu 0.2.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 +6 -0
- package/dist/esm/index.js +34 -19
- package/dist/esm/index.js.map +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +33 -14
- package/dist/index.js.map +1 -1
- package/package.json +5 -3
- package/src/ActionMenu/ActionMenu.tsx +1 -0
- package/src/MenuContent/MenuContentTypes.ts +1 -2
- package/src/MenuContext.tsx +1 -6
- package/src/MenuGroup/MenuGroupTypes.ts +1 -0
- package/src/MenuGroup/__tests__/MenuGroup.test.tsx +1 -0
- package/src/MenuHeader/__tests__/MenuHeader.typetest.tsx +1 -0
- package/src/MenuItem/MenuItem.tsx +4 -0
- package/src/MenuItem/styles.tsx +2 -0
- package/src/MenuItem/useOptionProps.tsx +1 -0
- package/src/MenuRoot/MenuRoot.feature +20 -10
- package/src/MenuRoot/MenuRoot.tsx +2 -1
- package/src/MenuRoot/__tests__/MenuRoot.test.tsx +165 -31
- package/src/MultiSelectMenu/MultiSelectMenu.feature +2 -13
- package/src/MultiSelectMenu/MultiSelectMenu.tsx +1 -0
- package/src/MultiSelectMenu/__tests__/MultiSelectMenu.test.tsx +141 -140
- package/src/SingleSelectMenu/SingleSelectMenu.tsx +1 -0
- package/src/hooks/useItemsFromChildren.tsx +1 -0
- package/tsconfig.json +1 -1
|
@@ -1,3 +1,5 @@
|
|
|
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,
|
|
@@ -9,9 +11,6 @@ import {
|
|
|
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,11 +22,7 @@ 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={
|
|
@@ -70,7 +65,7 @@ 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={
|
|
@@ -113,20 +108,22 @@ 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={
|
|
@@ -167,27 +164,29 @@ 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={
|
|
@@ -213,137 +212,139 @@ describe("MultiSelectMenu", () => {
|
|
|
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
|
-
<MenuItem
|
|
311
|
-
key={item.id}
|
|
312
|
-
id={item.id}
|
|
313
|
-
data-testid={item.id}
|
|
314
|
-
label={item.label}
|
|
315
|
-
>
|
|
316
|
-
{item.label}
|
|
317
|
-
</MenuItem>
|
|
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
|
});
|
|
@@ -36,6 +36,7 @@ export const SingleSelectMenu = ({
|
|
|
36
36
|
itemToString,
|
|
37
37
|
stateReducer: (state, actionAndChanges) => {
|
|
38
38
|
let newState: Partial<UseSelectState<TypeDefaultItemProps>>;
|
|
39
|
+
// eslint-disable-next-line react-hooks/rules-of-hooks, prefer-const
|
|
39
40
|
newState = useSelectStateReducer(state, actionAndChanges);
|
|
40
41
|
return externalStateReducer
|
|
41
42
|
? externalStateReducer(state, {
|
|
@@ -16,6 +16,7 @@ const walkAllChildren = (
|
|
|
16
16
|
// console.log("element", element);
|
|
17
17
|
callback(element, parents);
|
|
18
18
|
const newParents = [...parents, element];
|
|
19
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
19
20
|
const children = (element as any).props?.children;
|
|
20
21
|
React.Children.toArray(children).forEach((child) => {
|
|
21
22
|
walk(child, newParents);
|
package/tsconfig.json
CHANGED