@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
|
@@ -1,27 +1,26 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
-
import {
|
|
3
|
-
MultiSelectMenu,
|
|
4
|
-
MenuContent,
|
|
5
|
-
MenuHeader,
|
|
6
|
-
MenuFooter,
|
|
7
|
-
} from "../../index";
|
|
2
|
+
import { Autocomplete, MenuContent, MenuHeader, MenuFooter } from "../../index";
|
|
8
3
|
|
|
9
|
-
export const
|
|
4
|
+
export const AutocompleteTypeTest = () => (
|
|
10
5
|
<>
|
|
11
|
-
{/* Valid
|
|
12
|
-
<
|
|
13
|
-
{/* Valid
|
|
14
|
-
<
|
|
6
|
+
{/* Valid Autocomplete with minimal required props */}
|
|
7
|
+
<Autocomplete menuToggleElement={<>test</>}>Test</Autocomplete>
|
|
8
|
+
{/* Valid Autocomplete allows MenuContent by itself as a child */}
|
|
9
|
+
<Autocomplete menuToggleElement={<>test</>}>
|
|
15
10
|
<MenuContent>Test</MenuContent>
|
|
16
|
-
</
|
|
17
|
-
{/* Valid
|
|
18
|
-
<
|
|
11
|
+
</Autocomplete>
|
|
12
|
+
{/* Valid Autocomplete allows multiple children without fragment */}
|
|
13
|
+
<Autocomplete menuToggleElement={<>test</>}>
|
|
19
14
|
<MenuHeader>Test</MenuHeader>
|
|
20
15
|
<MenuContent>Test</MenuContent>
|
|
21
16
|
<MenuFooter>Test</MenuFooter>
|
|
22
|
-
</
|
|
23
|
-
{/* Valid
|
|
24
|
-
<
|
|
17
|
+
</Autocomplete>
|
|
18
|
+
{/* Valid Autocomplete allows getIsItemVisible function */}
|
|
19
|
+
<Autocomplete menuToggleElement={<>test</>} getIsItemVisible={() => true}>
|
|
20
|
+
Test
|
|
21
|
+
</Autocomplete>
|
|
22
|
+
{/* Valid Autocomplete accepts Downshift props and functions for useCombobox */}
|
|
23
|
+
<Autocomplete
|
|
25
24
|
menuToggleElement={<>test</>}
|
|
26
25
|
onSelectedItemChange={() => {}}
|
|
27
26
|
selectedItem={null}
|
|
@@ -31,31 +30,18 @@ export const MultiSelectMenuTypeTest = () => (
|
|
|
31
30
|
isOpen
|
|
32
31
|
>
|
|
33
32
|
Test
|
|
34
|
-
</
|
|
35
|
-
{/* Valid MultiSelectMenu accepts Downshift props and functions for useMultipleSelection */}
|
|
36
|
-
<MultiSelectMenu
|
|
37
|
-
menuToggleElement={<>test</>}
|
|
38
|
-
onSelectedItemsChange={() => {}}
|
|
39
|
-
selectedItems={[]}
|
|
40
|
-
multiSelectStateReducer={(c) => c}
|
|
41
|
-
onMultiSelectStateChange={() => {}}
|
|
42
|
-
getA11yMultiStatusMessage={() => ""}
|
|
43
|
-
>
|
|
44
|
-
Test
|
|
45
|
-
</MultiSelectMenu>
|
|
46
|
-
|
|
33
|
+
</Autocomplete>
|
|
47
34
|
{/* @ts-expect-error - menuToggleElement is a required prop */}
|
|
48
|
-
<
|
|
49
|
-
<
|
|
35
|
+
<Autocomplete>Test</Autocomplete>
|
|
36
|
+
<Autocomplete
|
|
50
37
|
/* @ts-expect-error - menuToggleElement must be a valid react element */
|
|
51
38
|
menuToggleElement="test"
|
|
52
39
|
>
|
|
53
40
|
Test
|
|
54
|
-
</
|
|
41
|
+
</Autocomplete>
|
|
55
42
|
{/* @ts-expect-error - when no children provided */}
|
|
56
|
-
<
|
|
57
|
-
|
|
58
|
-
<MultiSelectMenu
|
|
43
|
+
<Autocomplete menuToggleElement="test" />
|
|
44
|
+
<Autocomplete
|
|
59
45
|
menuToggleElement={<>test</>}
|
|
60
46
|
/* @ts-expect-error - isOpen can't be a string */
|
|
61
47
|
isOpen="false"
|
|
@@ -65,14 +51,10 @@ export const MultiSelectMenuTypeTest = () => (
|
|
|
65
51
|
selectedItem="test"
|
|
66
52
|
/* @ts-expect-error - stateReducer must have a return value */
|
|
67
53
|
stateReducer={() => {}}
|
|
68
|
-
/* @ts-expect-error -
|
|
69
|
-
|
|
70
|
-
/* @ts-expect-error - selectedItems can't be a string */
|
|
71
|
-
selectedItems="test"
|
|
72
|
-
/* @ts-expect-error - multiSelectStateReducer must have a return value */
|
|
73
|
-
multiSelectStateReducer="test"
|
|
54
|
+
/* @ts-expect-error - getIsItemVisible must be a valid function */
|
|
55
|
+
getIsItemVisible={true}
|
|
74
56
|
>
|
|
75
57
|
Test
|
|
76
|
-
</
|
|
58
|
+
</Autocomplete>
|
|
77
59
|
</>
|
|
78
60
|
);
|
|
@@ -11,10 +11,10 @@ import { useMenuContext } from "../MenuContext";
|
|
|
11
11
|
* <ActionMenu>
|
|
12
12
|
* <MenuHeader>Header</MenuHeader>
|
|
13
13
|
* <MenuContent>
|
|
14
|
-
* <MenuGroup>
|
|
14
|
+
* <MenuGroup id="1">
|
|
15
15
|
* <MenuItem>Item 1</MenuItem>
|
|
16
16
|
* </MenuGroup>
|
|
17
|
-
* <MenuGroup>
|
|
17
|
+
* <MenuGroup id="2">
|
|
18
18
|
* <MenuItem>Item 2</MenuItem>
|
|
19
19
|
* </MenuGroup>
|
|
20
20
|
* </MenuContent>
|
package/src/MenuContext.tsx
CHANGED
|
@@ -63,6 +63,7 @@ export interface TypeMenuContextProps
|
|
|
63
63
|
TypeDownshiftMultiSelectReturnValue,
|
|
64
64
|
TypeDownshiftMultiSelectProps {
|
|
65
65
|
items: TypeDefaultItemProps[];
|
|
66
|
+
itemsMap?: { [key: string]: TypeDefaultItemProps };
|
|
66
67
|
isListbox?: boolean;
|
|
67
68
|
onHighlightedIndexChange?:
|
|
68
69
|
| TypeDownshiftComboboxProps["onHighlightedIndexChange"]
|
|
@@ -94,8 +95,9 @@ export interface TypeMenuContext extends TypeMenuContextProps {
|
|
|
94
95
|
itemsMap: { [key: string]: TypeDefaultItemProps };
|
|
95
96
|
}
|
|
96
97
|
|
|
97
|
-
export interface TypeMenuProviderProps
|
|
98
|
+
export interface TypeMenuProviderProps {
|
|
98
99
|
children: TypeNotEmptyChildren;
|
|
100
|
+
menuProps: Partial<TypeMenuContextProps>;
|
|
99
101
|
}
|
|
100
102
|
|
|
101
103
|
/**
|
|
@@ -113,17 +115,21 @@ export const useMenu = ({
|
|
|
113
115
|
items = [],
|
|
114
116
|
isListbox = false,
|
|
115
117
|
isOpen = false,
|
|
118
|
+
itemsMap: itemsMapFromProps,
|
|
116
119
|
...rest
|
|
117
120
|
}: Partial<TypeMenuContextProps> = {}): TypeMenuContext => {
|
|
118
121
|
const menuProps = useMemo(() => ({ ...rest }), [rest]);
|
|
119
122
|
|
|
120
123
|
const itemsMap = useMemo(
|
|
121
124
|
() =>
|
|
125
|
+
itemsMapFromProps ||
|
|
122
126
|
items.reduce((map, item) => {
|
|
123
|
-
map[`${item.id}`] =
|
|
127
|
+
map[`${item.id}`] = {
|
|
128
|
+
...item,
|
|
129
|
+
};
|
|
124
130
|
return map;
|
|
125
131
|
}, {} as TypeMenuContext["itemsMap"]),
|
|
126
|
-
[items]
|
|
132
|
+
[items, itemsMapFromProps]
|
|
127
133
|
);
|
|
128
134
|
|
|
129
135
|
return {
|
|
@@ -145,9 +151,9 @@ export const MenuContext = createContext<TypeMenuContext>(defaultMenuContext);
|
|
|
145
151
|
*/
|
|
146
152
|
export const MenuProvider = ({
|
|
147
153
|
children,
|
|
148
|
-
|
|
154
|
+
menuProps,
|
|
149
155
|
}: TypeMenuProviderProps) => {
|
|
150
|
-
const filterContext = useMenu(
|
|
156
|
+
const filterContext = useMenu(menuProps);
|
|
151
157
|
|
|
152
158
|
return (
|
|
153
159
|
<MenuContext.Provider value={{ ...filterContext }}>
|
|
@@ -6,20 +6,19 @@ Feature: Menu Group
|
|
|
6
6
|
Background:
|
|
7
7
|
Given an end user is using the component
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
Scenario Outline: MenuGroup can be single select
|
|
10
|
+
When the MenuGroup has isSingleSelect set to <value>
|
|
11
|
+
And MenuContext is set to <menu type>
|
|
12
|
+
Then <selection behavior>
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
Then the group behaves like a multi select
|
|
14
|
+
Examples:
|
|
15
|
+
| value | menu type | selection behavior |
|
|
16
|
+
| true | single | the group behaves like a single select |
|
|
17
|
+
| false | single | the group behaves like a single select |
|
|
18
|
+
| undefined | single | the group behaves like a single select |
|
|
19
|
+
| true | multiple | the group behaves like a single select |
|
|
20
|
+
| false | multiple | the group behaves like a multi select |
|
|
21
|
+
| undefined | multiple | the group behaves like a multi select |
|
|
23
22
|
|
|
24
23
|
Rule: API
|
|
25
24
|
Background:
|
|
@@ -59,26 +58,9 @@ Feature: Menu Group
|
|
|
59
58
|
Given a list of items is not empty
|
|
60
59
|
And the items are a valid object shape
|
|
61
60
|
When that list of items is provided to the items prop on a MenuGroup
|
|
62
|
-
And a valid component is provided to the customMenuItem prop
|
|
61
|
+
And a valid component is provided to the customMenuItem prop
|
|
63
62
|
Then the MenuGroup will render the items using the customMenuItem component
|
|
64
63
|
|
|
65
|
-
Scenario: MenuGroup ignores the menuItemType without items prop
|
|
66
|
-
When a MenuGroup has menuItemType set
|
|
67
|
-
And is not provided with a valid items prop
|
|
68
|
-
Then the menuItemType will be ignored
|
|
69
|
-
|
|
70
|
-
Scenario Outline: MenuGroup can set the type for all MenuItems
|
|
71
|
-
When menuItemType is set to <menu item type>
|
|
72
|
-
And MenuGroup is provided with a valid items prop
|
|
73
|
-
Then each MenuItem rendered by the MenuGroup with use this type
|
|
74
|
-
|
|
75
|
-
Examples: Prop Types
|
|
76
|
-
| menu item type |
|
|
77
|
-
| undefined (default) |
|
|
78
|
-
| checkbox |
|
|
79
|
-
| radio |
|
|
80
|
-
| switch |
|
|
81
|
-
|
|
82
64
|
Scenario Outline: MenuGroup has custom content
|
|
83
65
|
When <custom content> is present at the <location>
|
|
84
66
|
Then <displayed content> will render
|
|
@@ -94,9 +76,9 @@ Feature: Menu Group
|
|
|
94
76
|
Then MenuRoot expects it to be of type <propType>
|
|
95
77
|
|
|
96
78
|
Examples: Prop Types
|
|
97
|
-
| propName
|
|
98
|
-
| children
|
|
99
|
-
| title
|
|
100
|
-
| items
|
|
101
|
-
|
|
|
102
|
-
|
|
|
79
|
+
| propName | propType |
|
|
80
|
+
| children | ReactNode |
|
|
81
|
+
| title | string |
|
|
82
|
+
| items | TypeMenuItemProps[] |
|
|
83
|
+
| isSingleSelect | boolean |
|
|
84
|
+
| MenuItemComponent | tyepof MenuItem |
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import type { TypeMenuGroupProps } from "./MenuGroupTypes";
|
|
3
3
|
import { StyledMenuGroup, StyledTitle, StyledMenuGroupUl } from "./styles";
|
|
4
|
+
import { MenuItem } from "../MenuItem";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* @link https://seeds.sproutsocial.com/components/menu-group/
|
|
@@ -10,10 +11,10 @@ import { StyledMenuGroup, StyledTitle, StyledMenuGroupUl } from "./styles";
|
|
|
10
11
|
* @example
|
|
11
12
|
* <ActionMenu>
|
|
12
13
|
* <MenuContent>
|
|
13
|
-
* <MenuGroup title="Group 1">
|
|
14
|
+
* <MenuGroup title="Group 1" id="1">
|
|
14
15
|
* <MenuItem>Item 1</MenuItem>
|
|
15
16
|
* </MenuGroup>
|
|
16
|
-
* <MenuGroup title="Group
|
|
17
|
+
* <MenuGroup title="Group 2" id="2">
|
|
17
18
|
* <MenuItem>Item 2</MenuItem>
|
|
18
19
|
* </MenuGroup>
|
|
19
20
|
* </MenuContent>
|
|
@@ -24,17 +25,31 @@ import { StyledMenuGroup, StyledTitle, StyledMenuGroupUl } from "./styles";
|
|
|
24
25
|
|
|
25
26
|
export const MenuGroup = ({
|
|
26
27
|
titleAs = "h3",
|
|
27
|
-
children,
|
|
28
|
+
children: childrenProp,
|
|
28
29
|
title,
|
|
29
30
|
color,
|
|
31
|
+
isSingleSelect,
|
|
32
|
+
id,
|
|
33
|
+
items,
|
|
34
|
+
MenuItemComponent = MenuItem,
|
|
30
35
|
...rest
|
|
31
36
|
}: TypeMenuGroupProps) => {
|
|
32
37
|
// Generate a unique ID for the title
|
|
33
38
|
const titleId = `menu-group-title-${Math.random().toString(36).slice(2, 11)}`;
|
|
34
39
|
|
|
40
|
+
const children =
|
|
41
|
+
childrenProp || !items || !items.length
|
|
42
|
+
? childrenProp
|
|
43
|
+
: items.map((item) => <MenuItemComponent key={item.id} {...item} />);
|
|
44
|
+
|
|
45
|
+
if (!children) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
35
48
|
return (
|
|
36
49
|
<StyledMenuGroup
|
|
37
50
|
role="group"
|
|
51
|
+
id={id}
|
|
52
|
+
isSingleSelect={isSingleSelect}
|
|
38
53
|
aria-labelledby={title ? titleId : undefined}
|
|
39
54
|
// TODO: fix this type since `color` should be valid here. TS can't resolve the correct type.
|
|
40
55
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
@@ -9,6 +9,7 @@ import type {
|
|
|
9
9
|
TypeSpaceSystemProps,
|
|
10
10
|
TypeTypographySystemProps,
|
|
11
11
|
} from "@sproutsocial/seeds-react-system-props";
|
|
12
|
+
import { MenuItem, type TypeMenuItemProps } from "../MenuItem/index";
|
|
12
13
|
|
|
13
14
|
export interface TypeMenuGroupStyledProps
|
|
14
15
|
extends TypeBorderSystemProps,
|
|
@@ -23,7 +24,11 @@ export interface TypeMenuGroupStyledProps
|
|
|
23
24
|
export interface TypeMenuGroupProps
|
|
24
25
|
extends TypeMenuGroupStyledProps,
|
|
25
26
|
Omit<React.ComponentPropsWithoutRef<"ul">, "color"> {
|
|
27
|
+
id: string;
|
|
28
|
+
isSingleSelect?: boolean;
|
|
26
29
|
title?: string;
|
|
27
|
-
children
|
|
30
|
+
children?: ReactNode;
|
|
31
|
+
items?: TypeMenuItemProps[];
|
|
32
|
+
MenuItemComponent?: typeof MenuItem;
|
|
28
33
|
titleAs?: string | React.ComponentType<any>;
|
|
29
34
|
}
|
|
@@ -1,6 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
render,
|
|
3
|
+
screen,
|
|
4
|
+
waitFor,
|
|
5
|
+
act,
|
|
6
|
+
} from "@sproutsocial/seeds-react-testing-library";
|
|
7
|
+
import { PointerEventsCheckLevel } from "@testing-library/user-event";
|
|
2
8
|
import {
|
|
3
9
|
SingleSelectMenu,
|
|
10
|
+
MultiSelectMenu,
|
|
4
11
|
MenuContent,
|
|
5
12
|
MenuGroup,
|
|
6
13
|
MenuToggleButton,
|
|
@@ -14,11 +21,162 @@ 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", async () => {
|
|
26
|
+
const { user } = 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
|
+
await user
|
|
48
|
+
.setup({
|
|
49
|
+
pointerEventsCheck: PointerEventsCheckLevel.Never,
|
|
50
|
+
})
|
|
51
|
+
.click(option);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Expect all options to be selected.
|
|
55
|
+
for (const option of options) {
|
|
56
|
+
expect(option).toHaveAttribute("aria-selected", "true");
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
it("allows multiple selections per group when isSingleSelect is false", async () => {
|
|
60
|
+
const { user } = render(
|
|
61
|
+
<MultiSelectMenu
|
|
62
|
+
defaultIsOpen
|
|
63
|
+
menuToggleElement={<MenuToggleButton>Test Menu</MenuToggleButton>}
|
|
64
|
+
>
|
|
65
|
+
<MenuContent>
|
|
66
|
+
<MenuGroup id="group" isSingleSelect={false}>
|
|
67
|
+
{menuItems.map(({ id, label }) => (
|
|
68
|
+
<MenuItem key={id} id={id}>
|
|
69
|
+
{label}
|
|
70
|
+
</MenuItem>
|
|
71
|
+
))}
|
|
72
|
+
</MenuGroup>
|
|
73
|
+
</MenuContent>
|
|
74
|
+
</MultiSelectMenu>
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
const options = screen.getAllByRole("option");
|
|
78
|
+
|
|
79
|
+
// Click on each option
|
|
80
|
+
for (const option of screen.getAllByRole("option")) {
|
|
81
|
+
await user
|
|
82
|
+
.setup({
|
|
83
|
+
pointerEventsCheck: PointerEventsCheckLevel.Never,
|
|
84
|
+
})
|
|
85
|
+
.click(option);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Expect all options to be selected.
|
|
89
|
+
for (const option of options) {
|
|
90
|
+
expect(option).toHaveAttribute("aria-selected", "true");
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
it("only allows one selection per group when isSingleSelect is true", async () => {
|
|
94
|
+
const { user } = render(
|
|
95
|
+
<MultiSelectMenu
|
|
96
|
+
defaultIsOpen
|
|
97
|
+
menuToggleElement={<MenuToggleButton>Test Menu</MenuToggleButton>}
|
|
98
|
+
>
|
|
99
|
+
<MenuContent>
|
|
100
|
+
<MenuGroup id="group" isSingleSelect={true}>
|
|
101
|
+
{menuItems.map(({ id, label }) => (
|
|
102
|
+
<MenuItem key={id} id={id}>
|
|
103
|
+
{label}
|
|
104
|
+
</MenuItem>
|
|
105
|
+
))}
|
|
106
|
+
</MenuGroup>
|
|
107
|
+
</MenuContent>
|
|
108
|
+
</MultiSelectMenu>
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
const options = screen.getAllByRole("option");
|
|
112
|
+
|
|
113
|
+
// Click on the first option
|
|
114
|
+
await user
|
|
115
|
+
.setup({
|
|
116
|
+
pointerEventsCheck: PointerEventsCheckLevel.Never,
|
|
117
|
+
})
|
|
118
|
+
.click(options[0]);
|
|
119
|
+
|
|
120
|
+
// Confirm it is selected
|
|
121
|
+
await waitFor(() =>
|
|
122
|
+
expect(options[0]).toHaveAttribute("aria-selected", "true")
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
// Click on the second option
|
|
126
|
+
await user
|
|
127
|
+
.setup({
|
|
128
|
+
pointerEventsCheck: PointerEventsCheckLevel.Never,
|
|
129
|
+
})
|
|
130
|
+
.click(options[1]);
|
|
131
|
+
|
|
132
|
+
// Expect only the most recently clicked option to be selected.
|
|
133
|
+
await waitFor(() =>
|
|
134
|
+
expect(options[1]).toHaveAttribute("aria-selected", "true")
|
|
135
|
+
);
|
|
136
|
+
await waitFor(() =>
|
|
137
|
+
expect(options[0]).toHaveAttribute("aria-selected", "false")
|
|
138
|
+
);
|
|
139
|
+
});
|
|
140
|
+
it("only allows one selection per group in SingleSelectMenu, even if isSingleSelect is false", async () => {
|
|
141
|
+
const { user } = render(
|
|
142
|
+
<SingleSelectMenu
|
|
143
|
+
defaultIsOpen
|
|
144
|
+
menuToggleElement={<MenuToggleButton>Test Menu</MenuToggleButton>}
|
|
145
|
+
>
|
|
146
|
+
<MenuContent>
|
|
147
|
+
<MenuGroup id="group" isSingleSelect={false}>
|
|
148
|
+
{menuItems.map(({ id, label }) => (
|
|
149
|
+
<MenuItem key={id} id={id}>
|
|
150
|
+
{label}
|
|
151
|
+
</MenuItem>
|
|
152
|
+
))}
|
|
153
|
+
</MenuGroup>
|
|
154
|
+
</MenuContent>
|
|
155
|
+
</SingleSelectMenu>
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
const options = screen.getAllByRole("option");
|
|
159
|
+
|
|
160
|
+
// Click on each option
|
|
161
|
+
for (const option of screen.getAllByRole("option")) {
|
|
162
|
+
await user
|
|
163
|
+
.setup({
|
|
164
|
+
pointerEventsCheck: PointerEventsCheckLevel.Never,
|
|
165
|
+
})
|
|
166
|
+
.click(option);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Expect only the last option to be selected.
|
|
170
|
+
await waitFor(() =>
|
|
171
|
+
expect(options[0]).toHaveAttribute("aria-selected", "false")
|
|
172
|
+
);
|
|
173
|
+
await waitFor(() =>
|
|
174
|
+
expect(options[1]).toHaveAttribute("aria-selected", "false")
|
|
175
|
+
);
|
|
176
|
+
await waitFor(() =>
|
|
177
|
+
expect(options[2]).toHaveAttribute("aria-selected", "true")
|
|
178
|
+
);
|
|
179
|
+
});
|
|
22
180
|
});
|
|
23
181
|
describe("API", () => {
|
|
24
182
|
it("renders a group with a title", () => {
|
|
@@ -29,7 +187,7 @@ describe("MenuGroup", () => {
|
|
|
29
187
|
menuToggleElement={<MenuToggleButton>Test Menu</MenuToggleButton>}
|
|
30
188
|
>
|
|
31
189
|
<MenuContent>
|
|
32
|
-
<MenuGroup title={menuTitle}>
|
|
190
|
+
<MenuGroup title={menuTitle} id={menuTitle}>
|
|
33
191
|
{menuItems.map(({ id, label }) => (
|
|
34
192
|
<MenuItem key={id} id={id}>
|
|
35
193
|
{label}
|
|
@@ -55,7 +213,7 @@ describe("MenuGroup", () => {
|
|
|
55
213
|
menuToggleElement={<MenuToggleButton>Test Menu</MenuToggleButton>}
|
|
56
214
|
>
|
|
57
215
|
<MenuContent>
|
|
58
|
-
<MenuGroup>
|
|
216
|
+
<MenuGroup id="group">
|
|
59
217
|
{menuItems.map(({ id, label }) => (
|
|
60
218
|
<MenuItem key={id} id={id}>
|
|
61
219
|
{label}
|
|
@@ -78,7 +236,7 @@ describe("MenuGroup", () => {
|
|
|
78
236
|
menuToggleElement={<MenuToggleButton>Test Menu</MenuToggleButton>}
|
|
79
237
|
>
|
|
80
238
|
<MenuContent>
|
|
81
|
-
<MenuGroup>
|
|
239
|
+
<MenuGroup id="group">
|
|
82
240
|
{menuItems.map(({ id, label }) => (
|
|
83
241
|
<MenuItem key={id} id={id}>
|
|
84
242
|
{label}
|
|
@@ -4,41 +4,55 @@ 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>
|
|
37
|
+
{/* MenuGroup allows children prop */}
|
|
38
|
+
<MenuGroup id="group">Test</MenuGroup>
|
|
39
|
+
{/* MenuGroup allows items prop */}
|
|
40
|
+
<MenuGroup id="group" items={[{ id: "1", children: "1" }]} />
|
|
29
41
|
|
|
30
|
-
{/* @ts-expect-error - MenuGroup requires children */}
|
|
31
|
-
<MenuGroup />
|
|
32
42
|
{/* @ts-expect-error - MenuGroup title is invalid as number */}
|
|
33
|
-
<MenuGroup title={123}>
|
|
43
|
+
<MenuGroup id="group" title={123}>
|
|
44
|
+
Test
|
|
45
|
+
</MenuGroup>
|
|
34
46
|
{/* @ts-expect-error - MenuGroup title is invalid as boolean */}
|
|
35
|
-
<MenuGroup title={false}>
|
|
47
|
+
<MenuGroup id="group" title={false}>
|
|
48
|
+
Test
|
|
49
|
+
</MenuGroup>
|
|
36
50
|
{/* @ts-expect-error - MenuGroup titleAs is invalid as number */}
|
|
37
|
-
<MenuGroup title="title" titleAs={123}>
|
|
51
|
+
<MenuGroup id="group" title="title" titleAs={123}>
|
|
38
52
|
Test
|
|
39
53
|
</MenuGroup>
|
|
40
54
|
{/* @ts-expect-error - MenuGroup titleAs is invalid as boolean */}
|
|
41
|
-
<MenuGroup title="title" titleAs={false}>
|
|
55
|
+
<MenuGroup id="group" title="title" titleAs={false}>
|
|
42
56
|
Test
|
|
43
57
|
</MenuGroup>
|
|
44
58
|
</>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import { useMenuContext } from "../MenuContext";
|
|
3
3
|
import type { TypeDefaultItemProps } from "../types";
|
|
4
4
|
import type { TypeMenuItemProps } from "./MenuItemTypes";
|
|
5
5
|
import { MenuItemContainer, StyledMenuItem } from "./styles";
|
|
@@ -24,16 +24,19 @@ export const MenuItem = ({
|
|
|
24
24
|
...props
|
|
25
25
|
}: TypeMenuItemProps) => {
|
|
26
26
|
const { getItemProps, itemsMap, highlightedIndex, isListbox } =
|
|
27
|
-
|
|
27
|
+
useMenuContext();
|
|
28
28
|
const { ...optionProps } = useOptionProps({
|
|
29
29
|
id,
|
|
30
30
|
children,
|
|
31
31
|
...props,
|
|
32
32
|
});
|
|
33
33
|
const item = (itemsMap[id] || { index: 0, id }) as TypeDefaultItemProps;
|
|
34
|
+
|
|
34
35
|
const highlighted = highlightedIndex === item?.index;
|
|
35
36
|
|
|
36
|
-
return (
|
|
37
|
+
return item.hidden ? (
|
|
38
|
+
<></>
|
|
39
|
+
) : (
|
|
37
40
|
<MenuItemContainer role="none">
|
|
38
41
|
<StyledMenuItem
|
|
39
42
|
id={id}
|
package/src/MenuLabel.tsx
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import {
|
|
2
|
+
import { Label, type TypeLabelProps } from "@sproutsocial/seeds-react-label";
|
|
3
3
|
import { useMenuContext } from "./MenuContext";
|
|
4
4
|
|
|
5
|
-
export const MenuLabel = ({ children, ...rest }:
|
|
5
|
+
export const MenuLabel = ({ children, ...rest }: Partial<TypeLabelProps>) => {
|
|
6
6
|
const { getLabelProps } = useMenuContext();
|
|
7
7
|
return (
|
|
8
|
-
<
|
|
8
|
+
<Label htmlFor="fallback_menu_label" {...getLabelProps?.()} {...rest}>
|
|
9
9
|
{children}
|
|
10
|
-
</
|
|
10
|
+
</Label>
|
|
11
11
|
);
|
|
12
12
|
};
|