@sproutsocial/seeds-react-menu 0.2.0 → 0.2.2
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 +13 -0
- package/dist/esm/index.js +35 -20
- package/dist/esm/index.js.map +1 -1
- package/dist/index.d.mts +8 -7
- package/dist/index.d.ts +8 -7
- package/dist/index.js +35 -20
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/ActionMenu/ActionMenu.tsx +2 -2
- package/src/MenuItem/styles.tsx +3 -1
- package/src/MenuRoot/MenuRoot.feature +20 -10
- package/src/MenuRoot/MenuRoot.tsx +1 -0
- package/src/MenuRoot/__tests__/MenuRoot.test.tsx +163 -30
- package/src/MultiSelectMenu/MultiSelectMenu.feature +2 -13
- package/src/MultiSelectMenu/__tests__/MultiSelectMenu.test.tsx +139 -133
- package/src/hooks/useItemsFromChildren.tsx +7 -4
- package/src/index.ts +1 -0
- package/src/types.ts +2 -5
|
@@ -27,7 +27,7 @@ describe("MultiSelectMenu", () => {
|
|
|
27
27
|
cleanup();
|
|
28
28
|
});
|
|
29
29
|
|
|
30
|
-
it("should render the
|
|
30
|
+
it("should render the MenuItems when provided as children", async () => {
|
|
31
31
|
const { user } = render(
|
|
32
32
|
<MultiSelectMenu
|
|
33
33
|
menuToggleElement={
|
|
@@ -70,7 +70,7 @@ describe("MultiSelectMenu", () => {
|
|
|
70
70
|
});
|
|
71
71
|
});
|
|
72
72
|
|
|
73
|
-
|
|
73
|
+
it("should allow for multiple items to be selected", async () => {
|
|
74
74
|
const { user } = render(
|
|
75
75
|
<MultiSelectMenu
|
|
76
76
|
menuToggleElement={
|
|
@@ -113,20 +113,22 @@ describe("MultiSelectMenu", () => {
|
|
|
113
113
|
|
|
114
114
|
await waitFor(() => {
|
|
115
115
|
// Check that the first item is selected
|
|
116
|
-
expect(item1).toHaveAttribute("aria-
|
|
116
|
+
expect(item1).toHaveAttribute("aria-selected", "true");
|
|
117
117
|
});
|
|
118
118
|
|
|
119
119
|
// Select second item
|
|
120
|
-
// TODO: can this be done in the same act as the first item?
|
|
121
120
|
await user.click(item2);
|
|
122
121
|
|
|
123
122
|
await waitFor(() => {
|
|
124
123
|
// Check that the second item is selected
|
|
125
|
-
expect(item2).toHaveAttribute("aria-
|
|
124
|
+
expect(item2).toHaveAttribute("aria-selected", "true");
|
|
126
125
|
});
|
|
126
|
+
|
|
127
|
+
// check that first item is still selected
|
|
128
|
+
expect(item1).toHaveAttribute("aria-selected", "true");
|
|
127
129
|
});
|
|
128
130
|
|
|
129
|
-
|
|
131
|
+
it("Should maintain selected state when closed and reopened", async () => {
|
|
130
132
|
const { user } = render(
|
|
131
133
|
<MultiSelectMenu
|
|
132
134
|
menuToggleElement={
|
|
@@ -167,27 +169,29 @@ describe("MultiSelectMenu", () => {
|
|
|
167
169
|
await user.click(item1);
|
|
168
170
|
|
|
169
171
|
await waitFor(() => {
|
|
170
|
-
expect(item1).toHaveAttribute("aria-
|
|
172
|
+
expect(item1).toHaveAttribute("aria-selected", "true");
|
|
171
173
|
});
|
|
172
174
|
|
|
173
175
|
// Close Menu (MultiSelect does not close automatically)
|
|
174
176
|
await user.click(toggleButton);
|
|
175
177
|
|
|
176
178
|
// Make sure the menu is closed
|
|
177
|
-
|
|
179
|
+
await waitFor(() =>
|
|
180
|
+
expect(screen.queryByRole("listbox")).not.toBeInTheDocument()
|
|
181
|
+
);
|
|
178
182
|
|
|
179
183
|
// Open menu again
|
|
180
184
|
await user.click(toggleButton);
|
|
181
185
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
expect(screen.getByRole("listbox")).toBeInTheDocument()
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
186
|
+
// Make sure the menu is open
|
|
187
|
+
await waitFor(() =>
|
|
188
|
+
expect(screen.getByRole("listbox")).toBeInTheDocument()
|
|
189
|
+
);
|
|
190
|
+
// Make sure the item is still selected
|
|
191
|
+
expect(item1).toHaveAttribute("aria-selected", "true");
|
|
188
192
|
});
|
|
189
193
|
|
|
190
|
-
|
|
194
|
+
it("should be able to navigate using the keyboard", async () => {
|
|
191
195
|
const { user } = render(
|
|
192
196
|
<MultiSelectMenu
|
|
193
197
|
menuToggleElement={
|
|
@@ -213,137 +217,139 @@ describe("MultiSelectMenu", () => {
|
|
|
213
217
|
</MultiSelectMenu>
|
|
214
218
|
);
|
|
215
219
|
|
|
216
|
-
|
|
220
|
+
// Open Menu
|
|
221
|
+
await user.keyboard("{Tab}{Enter}");
|
|
222
|
+
expect(await screen.findByRole("listbox")).toBeInTheDocument();
|
|
217
223
|
|
|
218
|
-
//
|
|
219
|
-
fireEvent.focus(toggleButton);
|
|
220
|
-
await user.keyboard("{Enter}");
|
|
224
|
+
// Check that the first item is highlighted
|
|
221
225
|
await user.keyboard("{ArrowDown}");
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
expect(item1).toHaveFocus();
|
|
227
|
-
});
|
|
226
|
+
expect(screen.getByRole("listbox")).toHaveAttribute(
|
|
227
|
+
"aria-activedescendant",
|
|
228
|
+
screen.getByRole("option", { name: "Item 1" }).getAttribute("id")
|
|
229
|
+
);
|
|
228
230
|
|
|
229
231
|
// Select the first item
|
|
230
232
|
await user.keyboard("{Enter}");
|
|
231
|
-
|
|
233
|
+
|
|
232
234
|
// select the second item
|
|
235
|
+
await user.keyboard("{ArrowDown}");
|
|
233
236
|
await user.keyboard(" ");
|
|
234
237
|
|
|
238
|
+
// Check that the second item is highlighted and selected
|
|
239
|
+
const item2 = screen.getByRole("option", { name: "Item 2" });
|
|
240
|
+
await waitFor(() => expect(item2).toHaveAttribute("aria-selected", "true"));
|
|
241
|
+
expect(screen.getByRole("listbox")).toHaveAttribute(
|
|
242
|
+
"aria-activedescendant",
|
|
243
|
+
item2.getAttribute("id")
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
// Check that the first item is still selected
|
|
247
|
+
const item1 = screen.getByRole("option", { name: "Item 1" });
|
|
248
|
+
expect(item1).toHaveAttribute("aria-selected", "true");
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it("Should work with a initially selected items", async () => {
|
|
252
|
+
const { user } = render(
|
|
253
|
+
<MultiSelectMenu
|
|
254
|
+
menuToggleElement={
|
|
255
|
+
<MenuToggleButton aria-label="testable menu" role="button">
|
|
256
|
+
Testable Menu
|
|
257
|
+
</MenuToggleButton>
|
|
258
|
+
}
|
|
259
|
+
selectedItems={[menuItems[1], menuItems[2]]}
|
|
260
|
+
>
|
|
261
|
+
<MenuContent>
|
|
262
|
+
<MenuGroup>
|
|
263
|
+
{menuItems.map((item) => (
|
|
264
|
+
<MenuItem
|
|
265
|
+
key={item.id}
|
|
266
|
+
id={item.id}
|
|
267
|
+
data-testid={item.id}
|
|
268
|
+
label={item.label}
|
|
269
|
+
>
|
|
270
|
+
{item.label}
|
|
271
|
+
</MenuItem>
|
|
272
|
+
))}
|
|
273
|
+
</MenuGroup>
|
|
274
|
+
</MenuContent>
|
|
275
|
+
</MultiSelectMenu>
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
expect(screen.queryByRole("listbox")).not.toBeInTheDocument();
|
|
279
|
+
|
|
280
|
+
const toggleButton = screen.getByRole("button", {
|
|
281
|
+
name: "testable menu",
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
// Open Menu
|
|
285
|
+
await user.click(toggleButton);
|
|
286
|
+
|
|
287
|
+
const item1 = screen.getByRole("option", { name: "Item 1" });
|
|
288
|
+
const item2 = screen.getByRole("option", { name: "Item 2" });
|
|
289
|
+
const item3 = screen.getByRole("option", { name: "Item 3" });
|
|
290
|
+
|
|
235
291
|
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();
|
|
292
|
+
expect(item1).not.toHaveStyle("pointer-events: none");
|
|
244
293
|
});
|
|
245
294
|
|
|
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
|
-
});
|
|
295
|
+
// Check that the first item is not selected
|
|
296
|
+
expect(item1).not.toHaveAttribute("aria-selected", "true");
|
|
297
|
+
// Check that the second item is selected
|
|
298
|
+
expect(item2).toHaveAttribute("aria-selected", "true");
|
|
299
|
+
// Check that the third item is selected
|
|
300
|
+
expect(item3).toHaveAttribute("aria-selected", "true");
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it("should stay open after menu item selection", async () => {
|
|
304
|
+
const mockSelectedItemsChange = jest.fn();
|
|
305
|
+
const { user } = render(
|
|
306
|
+
<MultiSelectMenu
|
|
307
|
+
menuToggleElement={
|
|
308
|
+
<MenuToggleButton aria-label="testable menu" role="button">
|
|
309
|
+
Testable Menu
|
|
310
|
+
</MenuToggleButton>
|
|
311
|
+
}
|
|
312
|
+
onSelectedItemsChange={mockSelectedItemsChange}
|
|
313
|
+
>
|
|
314
|
+
<MenuContent>
|
|
315
|
+
<MenuGroup>
|
|
316
|
+
{menuItems.map((item) => (
|
|
317
|
+
<MenuItem
|
|
318
|
+
key={item.id}
|
|
319
|
+
id={item.id}
|
|
320
|
+
data-testid={item.id}
|
|
321
|
+
label={item.label}
|
|
322
|
+
>
|
|
323
|
+
{item.label}
|
|
324
|
+
</MenuItem>
|
|
325
|
+
))}
|
|
326
|
+
</MenuGroup>
|
|
327
|
+
</MenuContent>
|
|
328
|
+
</MultiSelectMenu>
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
expect(screen.queryByRole("listbox")).not.toBeInTheDocument();
|
|
332
|
+
|
|
333
|
+
const toggleButton = screen.getByRole("button", {
|
|
334
|
+
name: "testable menu",
|
|
295
335
|
});
|
|
296
336
|
|
|
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
|
-
});
|
|
337
|
+
// Open Menu
|
|
338
|
+
await user.click(toggleButton);
|
|
339
|
+
|
|
340
|
+
const item1 = screen.getByRole("option", { name: "Item 1" });
|
|
341
|
+
|
|
342
|
+
await waitFor(() => {
|
|
343
|
+
expect(item1).not.toHaveStyle("pointer-events: none");
|
|
347
344
|
});
|
|
345
|
+
|
|
346
|
+
// Select Item 1
|
|
347
|
+
await user.click(item1);
|
|
348
|
+
|
|
349
|
+
// Check that the menu is still open
|
|
350
|
+
expect(await screen.findByRole("listbox")).toBeInTheDocument();
|
|
351
|
+
|
|
352
|
+
// Check that callback called
|
|
353
|
+
expect(mockSelectedItemsChange).toHaveBeenCalledTimes(1);
|
|
348
354
|
});
|
|
349
355
|
});
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import React, { type ReactNode, useMemo } from "react";
|
|
2
|
-
import type {
|
|
2
|
+
import type { TypeMenuItemProps } from "../MenuItem";
|
|
3
3
|
import type { TypeMenuContext } from "../MenuContext";
|
|
4
4
|
|
|
5
5
|
export interface TypeUseItemsFromChildrenProps {
|
|
6
6
|
children: ReactNode;
|
|
7
7
|
}
|
|
8
|
+
export interface TypeItemElements {
|
|
9
|
+
element: React.ReactElement<TypeMenuItemProps>;
|
|
10
|
+
parents: readonly React.ReactNode[];
|
|
11
|
+
}
|
|
8
12
|
|
|
9
13
|
// https://stackoverflow.com/questions/69119016/find-specific-children-components-regardless-of-depth-in-react
|
|
10
14
|
const walkAllChildren = (
|
|
@@ -59,13 +63,12 @@ export const useItemsFromChildren = ({
|
|
|
59
63
|
const allMenuItemElements = getAllMenuItems(children);
|
|
60
64
|
const menuItemsMap: TypeMenuContext["itemsMap"] = {};
|
|
61
65
|
const allMenuItems: TypeMenuContext["items"] = allMenuItemElements.map(
|
|
62
|
-
({ element
|
|
66
|
+
({ element }, index) => {
|
|
63
67
|
const item = {
|
|
64
68
|
id: element.props.id,
|
|
65
69
|
disabled: element.props.disabled,
|
|
66
70
|
index,
|
|
67
|
-
element,
|
|
68
|
-
parents,
|
|
71
|
+
menuItemProps: element.props,
|
|
69
72
|
};
|
|
70
73
|
menuItemsMap[`${item.id}`] = item;
|
|
71
74
|
return item;
|
package/src/index.ts
CHANGED
package/src/types.ts
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
import type { TypeMenuItemProps } from "./MenuItem";
|
|
2
2
|
|
|
3
|
-
export interface
|
|
4
|
-
element: React.ReactElement<TypeMenuItemProps>;
|
|
5
|
-
parents: readonly React.ReactNode[];
|
|
6
|
-
}
|
|
7
|
-
export interface TypeDefaultItemProps extends Partial<TypeItemElements> {
|
|
3
|
+
export interface TypeDefaultItemProps {
|
|
8
4
|
id: string;
|
|
9
5
|
index: number;
|
|
10
6
|
disabled?: boolean;
|
|
7
|
+
menuItemProps?: TypeMenuItemProps;
|
|
11
8
|
}
|
|
12
9
|
|
|
13
10
|
export type TypeNotEmptyChildren =
|