@sproutsocial/seeds-react-menu 0.1.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/.eslintrc.js +4 -0
- package/.turbo/turbo-build.log +21 -0
- package/CHANGELOG.md +46 -0
- package/dist/esm/index.js +2651 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/index.d.mts +197 -0
- package/dist/index.d.ts +197 -0
- package/dist/index.js +2670 -0
- package/dist/index.js.map +1 -0
- package/jest.config.js +12 -0
- package/package.json +50 -0
- package/src/ActionMenu/ActionMenu.feature +20 -0
- package/src/ActionMenu/ActionMenu.tsx +113 -0
- package/src/ActionMenu/__tests__/ActionMenu.test.tsx +99 -0
- package/src/ActionMenu/__tests__/ActionMenu.typetest.tsx +73 -0
- package/src/ActionMenu/index.ts +1 -0
- package/src/ComboBox/ComboBox.feature +96 -0
- package/src/ComboBox/TokenInput.feature +84 -0
- package/src/MenuContent/MenuContent.tsx +28 -0
- package/src/MenuContent/MenuContentTypes.ts +7 -0
- package/src/MenuContent/__tests__/MenuContent.test.tsx +59 -0
- package/src/MenuContent/__tests__/MenuContent.typetest.tsx +18 -0
- package/src/MenuContent/index.tsx +2 -0
- package/src/MenuContent/styles.tsx +10 -0
- package/src/MenuContext.tsx +116 -0
- package/src/MenuFooter/MenuFooter.feature +30 -0
- package/src/MenuFooter/MenuFooter.tsx +7 -0
- package/src/MenuFooter/MenuFooterTypes.ts +1 -0
- package/src/MenuFooter/__tests__/MenuFooter.test.tsx +76 -0
- package/src/MenuFooter/__tests__/MenuFooter.typetest.tsx +20 -0
- package/src/MenuFooter/index.ts +2 -0
- package/src/MenuFooter/styles.tsx +7 -0
- package/src/MenuGroup/MenuGroup.feature +114 -0
- package/src/MenuGroup/MenuGroup.tsx +33 -0
- package/src/MenuGroup/MenuGroupTypes.ts +29 -0
- package/src/MenuGroup/__tests__/MenuGroup.test.tsx +95 -0
- package/src/MenuGroup/__tests__/MenuGroup.typetest.tsx +45 -0
- package/src/MenuGroup/index.tsx +8 -0
- package/src/MenuGroup/styles.tsx +37 -0
- package/src/MenuHeader/MenuHeader.feature +59 -0
- package/src/MenuHeader/MenuHeader.tsx +30 -0
- package/src/MenuHeader/MenuHeaderTypes.ts +26 -0
- package/src/MenuHeader/__tests__/MenuHeader.test.tsx +67 -0
- package/src/MenuHeader/__tests__/MenuHeader.typetest.tsx +27 -0
- package/src/MenuHeader/index.ts +2 -0
- package/src/MenuHeader/styles.tsx +75 -0
- package/src/MenuItem/MenuItem.feature +88 -0
- package/src/MenuItem/MenuItem.tsx +44 -0
- package/src/MenuItem/MenuItemTypes.ts +37 -0
- package/src/MenuItem/__tests__/MenuItem.test.tsx +228 -0
- package/src/MenuItem/__tests__/MenuItem.typetest.tsx +41 -0
- package/src/MenuItem/index.ts +5 -0
- package/src/MenuItem/styles.tsx +71 -0
- package/src/MenuItemSelectable/MenuItemSelectable.feature +99 -0
- package/src/MenuItemSelectable/MenuItemSelectable.tsx +136 -0
- package/src/MenuItemSelectable/MenuItemSelectableTypes.ts +15 -0
- package/src/MenuItemSelectable/__tests__/MenuItemSelectable.test.tsx +303 -0
- package/src/MenuItemSelectable/__tests__/MenuItemSelectable.typetest.tsx +56 -0
- package/src/MenuItemSelectable/index.ts +5 -0
- package/src/MenuItemSelectable/styles.tsx +21 -0
- package/src/MenuLabel.tsx +12 -0
- package/src/MenuRoot/MenuRoot.feature +93 -0
- package/src/MenuRoot/MenuRoot.tsx +70 -0
- package/src/MenuRoot/__tests__/MenuRoot.test.tsx +197 -0
- package/src/MenuRoot/__tests__/MenuRoot.typetest.tsx +76 -0
- package/src/MenuRoot/index.ts +1 -0
- package/src/MenuToggleButton.tsx +29 -0
- package/src/MultiSelectMenu/MultiSelectMenu.feature +70 -0
- package/src/MultiSelectMenu/MultiSelectMenu.tsx +133 -0
- package/src/MultiSelectMenu/MultiSelectMenuTypes.ts +8 -0
- package/src/MultiSelectMenu/__tests__/MultiSelectMenu.test.tsx +349 -0
- package/src/MultiSelectMenu/__tests__/MultiSelectMenu.typetest.tsx +78 -0
- package/src/MultiSelectMenu/index.ts +1 -0
- package/src/SingleSelectMenu/SingleSelectMenu.feature +23 -0
- package/src/SingleSelectMenu/SingleSelectMenu.tsx +56 -0
- package/src/SingleSelectMenu/SingleSelectMenuTypes.ts +9 -0
- package/src/SingleSelectMenu/__tests__/SingleSelectMenu.test.tsx +167 -0
- package/src/SingleSelectMenu/__tests__/SingleSelectMenu.typetest.tsx +95 -0
- package/src/SingleSelectMenu/index.ts +3 -0
- package/src/__tests__/Menu.test.tsx +423 -0
- package/src/constants.ts +17 -0
- package/src/hooks/useItemsFromChildren.tsx +81 -0
- package/src/index.ts +20 -0
- package/src/menuTestingUtils.tsx +72 -0
- package/src/reducers/__tests__/useSelectStateReducer.test.tsx +37 -0
- package/src/reducers/useSelectStateReducer.tsx +27 -0
- package/src/styled.d.ts +7 -0
- package/src/types.ts +16 -0
- package/src/utils/itemToString.ts +5 -0
- package/tsconfig.json +12 -0
- package/tsup.config.ts +11 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/* eslint-disable testing-library/no-unnecessary-act */
|
|
2
|
+
import React from "react";
|
|
3
|
+
import {
|
|
4
|
+
render,
|
|
5
|
+
screen,
|
|
6
|
+
waitFor,
|
|
7
|
+
act,
|
|
8
|
+
} from "@sproutsocial/seeds-react-testing-library";
|
|
9
|
+
import {
|
|
10
|
+
SingleSelectMenu,
|
|
11
|
+
MenuToggleButton,
|
|
12
|
+
MenuContent,
|
|
13
|
+
MenuGroup,
|
|
14
|
+
MenuItemSelectable,
|
|
15
|
+
MenuFooter,
|
|
16
|
+
} from "../../index";
|
|
17
|
+
import { Switch } from "@sproutsocial/seeds-react-switch";
|
|
18
|
+
|
|
19
|
+
const menuItems = [
|
|
20
|
+
{ id: "Select an option", index: 0, label: "Select an option" },
|
|
21
|
+
{ id: "item-1", index: 1, label: "Item 1" },
|
|
22
|
+
{ id: "item-2", index: 2, label: "Item 2" },
|
|
23
|
+
{ id: "item-3", index: 3, label: "Item 3" },
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
describe("User Behavior", () => {
|
|
27
|
+
it("should set a selected item", async () => {
|
|
28
|
+
const checkOnChange = jest.fn();
|
|
29
|
+
|
|
30
|
+
const { user } = render(
|
|
31
|
+
<SingleSelectMenu
|
|
32
|
+
onSelectedItemChange={checkOnChange}
|
|
33
|
+
triggerElement={
|
|
34
|
+
<MenuToggleButton role="button">Click me</MenuToggleButton>
|
|
35
|
+
}
|
|
36
|
+
>
|
|
37
|
+
<MenuContent>
|
|
38
|
+
<MenuGroup title="Menu Title">
|
|
39
|
+
{menuItems.map(({ id, label }) => (
|
|
40
|
+
<MenuItemSelectable key={id} id={id}>
|
|
41
|
+
{label}
|
|
42
|
+
</MenuItemSelectable>
|
|
43
|
+
))}
|
|
44
|
+
</MenuGroup>
|
|
45
|
+
</MenuContent>
|
|
46
|
+
</SingleSelectMenu>
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
// Open the Menu
|
|
50
|
+
const toggleBtn = screen.getByRole("button", { name: "Click me" });
|
|
51
|
+
await user.click(toggleBtn);
|
|
52
|
+
|
|
53
|
+
await waitFor(() => expect(screen.getByRole("listbox")).toBeVisible());
|
|
54
|
+
|
|
55
|
+
// Select the first option
|
|
56
|
+
const item1 = screen.getByRole("option", { name: menuItems[0].label });
|
|
57
|
+
expect(item1).toBeVisible();
|
|
58
|
+
await user.keyboard("{arrowdown}{enter}");
|
|
59
|
+
|
|
60
|
+
// Wait for menu to close
|
|
61
|
+
await waitFor(() =>
|
|
62
|
+
expect(screen.queryByRole("listbox")).not.toBeInTheDocument()
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
expect(checkOnChange).toHaveBeenCalledTimes(1);
|
|
66
|
+
|
|
67
|
+
// Reopen the menu
|
|
68
|
+
await user.click(toggleBtn);
|
|
69
|
+
|
|
70
|
+
await waitFor(() => expect(screen.getByRole("listbox")).toBeVisible());
|
|
71
|
+
|
|
72
|
+
// Check that the selected option is selected
|
|
73
|
+
expect(
|
|
74
|
+
screen.getByRole("option", { name: menuItems[0].label, selected: true })
|
|
75
|
+
).toBeInTheDocument();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("should keep menu open when a switch is selected in the menu footer", async () => {
|
|
79
|
+
const MenuFooterContent = () => {
|
|
80
|
+
const [isChecked, setIsChecked] = React.useState(false);
|
|
81
|
+
return (
|
|
82
|
+
<MenuFooter>
|
|
83
|
+
<ul>
|
|
84
|
+
<li>
|
|
85
|
+
<label htmlFor="switch-1">dark mode</label>
|
|
86
|
+
<Switch
|
|
87
|
+
id="switch-1"
|
|
88
|
+
checked={isChecked}
|
|
89
|
+
onClick={() => setIsChecked(!isChecked)}
|
|
90
|
+
/>
|
|
91
|
+
</li>
|
|
92
|
+
</ul>
|
|
93
|
+
</MenuFooter>
|
|
94
|
+
);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const { user } = render(
|
|
98
|
+
<SingleSelectMenu
|
|
99
|
+
triggerElement={
|
|
100
|
+
<MenuToggleButton role="button">Click me</MenuToggleButton>
|
|
101
|
+
}
|
|
102
|
+
>
|
|
103
|
+
<MenuContent>
|
|
104
|
+
<MenuGroup title="Menu Title">
|
|
105
|
+
{menuItems.map(({ id, label }) => (
|
|
106
|
+
<MenuItemSelectable key={id} id={id}>
|
|
107
|
+
{label}
|
|
108
|
+
</MenuItemSelectable>
|
|
109
|
+
))}
|
|
110
|
+
</MenuGroup>
|
|
111
|
+
</MenuContent>
|
|
112
|
+
<MenuFooterContent />
|
|
113
|
+
</SingleSelectMenu>
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
// Open the Menu
|
|
117
|
+
const toggleBtn = screen.getByRole("button", { name: "Click me" });
|
|
118
|
+
await user.click(toggleBtn);
|
|
119
|
+
|
|
120
|
+
await waitFor(() => expect(screen.getByRole("listbox")).toBeVisible());
|
|
121
|
+
|
|
122
|
+
// Toggle the switch
|
|
123
|
+
const switchElement = screen.getByRole("switch", { name: "dark mode" });
|
|
124
|
+
await user.click(switchElement);
|
|
125
|
+
|
|
126
|
+
// Check that the switch is checked
|
|
127
|
+
expect(switchElement).toBeChecked();
|
|
128
|
+
|
|
129
|
+
// Check that the menu is still open
|
|
130
|
+
expect(screen.getByRole("listbox")).toBeVisible();
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe("API", () => {
|
|
135
|
+
it("should close automatically on selection", async () => {
|
|
136
|
+
const { user } = render(
|
|
137
|
+
<SingleSelectMenu
|
|
138
|
+
triggerElement={<MenuToggleButton>Click me</MenuToggleButton>}
|
|
139
|
+
>
|
|
140
|
+
<MenuContent>
|
|
141
|
+
<MenuGroup title="Menu Title">
|
|
142
|
+
{menuItems.map(({ id, label }) => (
|
|
143
|
+
<MenuItemSelectable key={id} id={id}>
|
|
144
|
+
{label}
|
|
145
|
+
</MenuItemSelectable>
|
|
146
|
+
))}
|
|
147
|
+
</MenuGroup>
|
|
148
|
+
</MenuContent>
|
|
149
|
+
</SingleSelectMenu>
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
// Open the Menu
|
|
153
|
+
const menuButton = screen.getByText("Click me");
|
|
154
|
+
await user.click(menuButton);
|
|
155
|
+
expect(await screen.findByText("Menu Title")).toBeInTheDocument();
|
|
156
|
+
|
|
157
|
+
// Click one of the options
|
|
158
|
+
const menuItem = screen.getAllByRole("option")[0];
|
|
159
|
+
expect(menuItem).toBeInTheDocument();
|
|
160
|
+
await user.click(menuItem);
|
|
161
|
+
|
|
162
|
+
// Check that the menu closed
|
|
163
|
+
await waitFor(() =>
|
|
164
|
+
expect(screen.queryByText("Menu Title")).not.toBeInTheDocument()
|
|
165
|
+
);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SingleSelectMenu,
|
|
3
|
+
MenuContent,
|
|
4
|
+
MenuHeader,
|
|
5
|
+
MenuFooter,
|
|
6
|
+
} from "../../index";
|
|
7
|
+
|
|
8
|
+
export const SingleSelectMenuTypeTest = () => (
|
|
9
|
+
<>
|
|
10
|
+
{/* Valid SingleSelectMenu with minimal required props */}
|
|
11
|
+
<SingleSelectMenu triggerElement={<>test</>}>Test</SingleSelectMenu>
|
|
12
|
+
{/* Valid SingleSelectMenu allows MenuContent by itself as a child */}
|
|
13
|
+
<SingleSelectMenu triggerElement={<>test</>}>
|
|
14
|
+
<MenuContent>Test</MenuContent>
|
|
15
|
+
</SingleSelectMenu>
|
|
16
|
+
{/* Valid SingleSelectMenu allows multiple children without fragment */}
|
|
17
|
+
<SingleSelectMenu triggerElement={<>test</>}>
|
|
18
|
+
<MenuHeader>Test</MenuHeader>
|
|
19
|
+
<MenuContent>Test</MenuContent>
|
|
20
|
+
<MenuFooter>Test</MenuFooter>
|
|
21
|
+
</SingleSelectMenu>
|
|
22
|
+
{/* Valid SingleSelectMenu accepts Downshift props and functions for useSelect */}
|
|
23
|
+
<SingleSelectMenu
|
|
24
|
+
triggerElement={<>test</>}
|
|
25
|
+
onSelectedItemChange={() => {}}
|
|
26
|
+
selectedItem={null}
|
|
27
|
+
stateReducer={(c) => c}
|
|
28
|
+
onStateChange={() => {}}
|
|
29
|
+
getA11yStatusMessage={() => ""}
|
|
30
|
+
isOpen
|
|
31
|
+
>
|
|
32
|
+
Test
|
|
33
|
+
</SingleSelectMenu>
|
|
34
|
+
|
|
35
|
+
{/* @ts-expect-error - triggerElement is a required prop */}
|
|
36
|
+
<SingleSelectMenu>Test</SingleSelectMenu>
|
|
37
|
+
<SingleSelectMenu
|
|
38
|
+
/* @ts-expect-error - triggerElement must be a valid react element */
|
|
39
|
+
triggerElement="test"
|
|
40
|
+
>
|
|
41
|
+
Test
|
|
42
|
+
</SingleSelectMenu>
|
|
43
|
+
{/* @ts-expect-error - when no children provided */}
|
|
44
|
+
<SingleSelectMenu triggerElement="test" />
|
|
45
|
+
|
|
46
|
+
<SingleSelectMenu
|
|
47
|
+
triggerElement={<>test</>}
|
|
48
|
+
/* @ts-expect-error - isOpen can't be a string */
|
|
49
|
+
isOpen="false"
|
|
50
|
+
/* @ts-expect-error - onSelectedItemChange can't be a string */
|
|
51
|
+
onSelectedItemChange="test"
|
|
52
|
+
/* @ts-expect-error - selectedItem can't be a string */
|
|
53
|
+
selectedItem="test"
|
|
54
|
+
/* @ts-expect-error - stateReducer must have a return value */
|
|
55
|
+
stateReducer={() => {}}
|
|
56
|
+
>
|
|
57
|
+
Test
|
|
58
|
+
</SingleSelectMenu>
|
|
59
|
+
<SingleSelectMenu
|
|
60
|
+
triggerElement={<>test</>}
|
|
61
|
+
/* @ts-expect-error - onMultiSelectStateChange can't be used on SingleSelectMenu */
|
|
62
|
+
onMultiSelectStateChange={() => {}}
|
|
63
|
+
>
|
|
64
|
+
Test
|
|
65
|
+
</SingleSelectMenu>
|
|
66
|
+
<SingleSelectMenu
|
|
67
|
+
triggerElement={<>test</>}
|
|
68
|
+
/* @ts-expect-error - selectedItems can't be used on SingleSelectMenu */
|
|
69
|
+
selectedItems={[]}
|
|
70
|
+
>
|
|
71
|
+
Test
|
|
72
|
+
</SingleSelectMenu>
|
|
73
|
+
<SingleSelectMenu
|
|
74
|
+
triggerElement={<>test</>}
|
|
75
|
+
/* @ts-expect-error - multiSelectStateReducer can't be used on SingleSelectMenu */
|
|
76
|
+
multiSelectStateReducer={(c) => c}
|
|
77
|
+
>
|
|
78
|
+
Test
|
|
79
|
+
</SingleSelectMenu>
|
|
80
|
+
<SingleSelectMenu
|
|
81
|
+
triggerElement={<>test</>}
|
|
82
|
+
/* @ts-expect-error - onMultiSelectStateChange can't be used on SingleSelectMenu */
|
|
83
|
+
onMultiSelectStateChange={() => {}}
|
|
84
|
+
>
|
|
85
|
+
Test
|
|
86
|
+
</SingleSelectMenu>
|
|
87
|
+
<SingleSelectMenu
|
|
88
|
+
triggerElement={<>test</>}
|
|
89
|
+
/* @ts-expect-error - getA11yMultiStatusMessage can't be used on SingleSelectMenu */
|
|
90
|
+
getA11yMultiStatusMessage={() => ""}
|
|
91
|
+
>
|
|
92
|
+
Test
|
|
93
|
+
</SingleSelectMenu>
|
|
94
|
+
</>
|
|
95
|
+
);
|
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
/* eslint-disable testing-library/no-unnecessary-act */
|
|
2
|
+
import {
|
|
3
|
+
render,
|
|
4
|
+
screen,
|
|
5
|
+
waitFor,
|
|
6
|
+
act,
|
|
7
|
+
} from "@sproutsocial/seeds-react-testing-library";
|
|
8
|
+
import {
|
|
9
|
+
ActionMenu,
|
|
10
|
+
MultiSelectMenu,
|
|
11
|
+
SingleSelectMenu,
|
|
12
|
+
MenuToggleButton,
|
|
13
|
+
MenuContent,
|
|
14
|
+
MenuItem,
|
|
15
|
+
} from "../index";
|
|
16
|
+
|
|
17
|
+
describe("Menu", () => {
|
|
18
|
+
describe.each([
|
|
19
|
+
{ name: "ActionMenu", Component: ActionMenu },
|
|
20
|
+
{ name: "MultiSelectMenu", Component: MultiSelectMenu },
|
|
21
|
+
{ name: "SingleSelectMenu", Component: SingleSelectMenu },
|
|
22
|
+
])("$name", ({ Component }) => {
|
|
23
|
+
it("should render the trigger button", () => {
|
|
24
|
+
render(
|
|
25
|
+
<Component
|
|
26
|
+
triggerElement={
|
|
27
|
+
<MenuToggleButton role="button" aria-label="Open Menu">
|
|
28
|
+
Open
|
|
29
|
+
</MenuToggleButton>
|
|
30
|
+
}
|
|
31
|
+
>
|
|
32
|
+
Menu
|
|
33
|
+
</Component>
|
|
34
|
+
);
|
|
35
|
+
const menuTrigger = screen.getByRole("button", { name: "Open Menu" });
|
|
36
|
+
expect(menuTrigger).toBeInTheDocument();
|
|
37
|
+
expect(screen.queryByText("Menu")).not.toBeInTheDocument();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("should default to open if initialIsOpen is true", () => {
|
|
41
|
+
render(
|
|
42
|
+
<Component
|
|
43
|
+
triggerElement={
|
|
44
|
+
<MenuToggleButton role="button" aria-label="Open Menu">
|
|
45
|
+
Open
|
|
46
|
+
</MenuToggleButton>
|
|
47
|
+
}
|
|
48
|
+
initialIsOpen
|
|
49
|
+
>
|
|
50
|
+
Menu
|
|
51
|
+
</Component>
|
|
52
|
+
);
|
|
53
|
+
const menuTrigger = screen.getByRole("button", {
|
|
54
|
+
name: "Open Menu",
|
|
55
|
+
expanded: true,
|
|
56
|
+
});
|
|
57
|
+
expect(menuTrigger).toBeInTheDocument();
|
|
58
|
+
expect(screen.getByText("Menu")).toBeInTheDocument();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("should open when trigger button is clicked", async () => {
|
|
62
|
+
const { user } = render(
|
|
63
|
+
<Component
|
|
64
|
+
triggerElement={
|
|
65
|
+
<MenuToggleButton role="button" aria-label="Open Menu">
|
|
66
|
+
Open
|
|
67
|
+
</MenuToggleButton>
|
|
68
|
+
}
|
|
69
|
+
>
|
|
70
|
+
Menu
|
|
71
|
+
</Component>
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
// Check that Menu is not open yet
|
|
75
|
+
const menuTrigger = screen.getByRole("button", { name: "Open Menu" });
|
|
76
|
+
expect(menuTrigger).toBeInTheDocument();
|
|
77
|
+
expect(screen.queryByText("Menu")).not.toBeInTheDocument();
|
|
78
|
+
|
|
79
|
+
// Click the menu trigger
|
|
80
|
+
await user.click(menuTrigger);
|
|
81
|
+
|
|
82
|
+
// Check that menu opens
|
|
83
|
+
expect(await screen.findByText("Menu")).toBeInTheDocument();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("should open when trigger button is focused and enter key pressed", async () => {
|
|
87
|
+
const { user } = render(
|
|
88
|
+
<Component
|
|
89
|
+
triggerElement={
|
|
90
|
+
<MenuToggleButton role="button" aria-label="Open Menu">
|
|
91
|
+
Open
|
|
92
|
+
</MenuToggleButton>
|
|
93
|
+
}
|
|
94
|
+
>
|
|
95
|
+
Menu
|
|
96
|
+
</Component>
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
// Check that Menu is not open yet
|
|
100
|
+
const menuTrigger = screen.getByRole("button", { name: "Open Menu" });
|
|
101
|
+
expect(menuTrigger).toBeInTheDocument();
|
|
102
|
+
expect(screen.queryByText("Menu")).not.toBeInTheDocument();
|
|
103
|
+
|
|
104
|
+
// Focus on trigger and activate with enter
|
|
105
|
+
menuTrigger.focus();
|
|
106
|
+
await user.keyboard("{enter}");
|
|
107
|
+
|
|
108
|
+
// Check that menu opens
|
|
109
|
+
expect(await screen.findByText("Menu")).toBeInTheDocument();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("should open when trigger button is focused and space key pressed", async () => {
|
|
113
|
+
const { user } = render(
|
|
114
|
+
<Component
|
|
115
|
+
triggerElement={
|
|
116
|
+
<MenuToggleButton role="button" aria-label="Open Menu">
|
|
117
|
+
Open
|
|
118
|
+
</MenuToggleButton>
|
|
119
|
+
}
|
|
120
|
+
>
|
|
121
|
+
Menu
|
|
122
|
+
</Component>
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
// Check that Menu is not open yet
|
|
126
|
+
const menuTrigger = screen.getByRole("button", { name: "Open Menu" });
|
|
127
|
+
expect(menuTrigger).toBeInTheDocument();
|
|
128
|
+
expect(screen.queryByText("Menu")).not.toBeInTheDocument();
|
|
129
|
+
|
|
130
|
+
// Focus on trigger and activate with space
|
|
131
|
+
menuTrigger.focus();
|
|
132
|
+
await user.keyboard(" ");
|
|
133
|
+
|
|
134
|
+
// Check that menu opens
|
|
135
|
+
expect(await screen.findByText("Menu")).toBeInTheDocument();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("should close when trigger button is clicked while open", async () => {
|
|
139
|
+
const { user } = render(
|
|
140
|
+
<Component
|
|
141
|
+
triggerElement={
|
|
142
|
+
<MenuToggleButton role="button" aria-label="Open Menu">
|
|
143
|
+
Open
|
|
144
|
+
</MenuToggleButton>
|
|
145
|
+
}
|
|
146
|
+
initialIsOpen
|
|
147
|
+
>
|
|
148
|
+
Menu
|
|
149
|
+
</Component>
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
// Check that Menu is open
|
|
153
|
+
const menuTrigger = screen.getByRole("button", { name: "Open Menu" });
|
|
154
|
+
expect(menuTrigger).toBeInTheDocument();
|
|
155
|
+
expect(await screen.findByText("Menu")).toBeInTheDocument();
|
|
156
|
+
|
|
157
|
+
// Click the menu trigger
|
|
158
|
+
await user.click(menuTrigger);
|
|
159
|
+
|
|
160
|
+
// Check that menu closes
|
|
161
|
+
await waitFor(() =>
|
|
162
|
+
expect(screen.queryByText("Menu")).not.toBeInTheDocument()
|
|
163
|
+
);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("should close when the ESC key is pressed", async () => {
|
|
167
|
+
const mockOnIsOpenChange = jest.fn();
|
|
168
|
+
const { user } = render(
|
|
169
|
+
<Component
|
|
170
|
+
triggerElement={
|
|
171
|
+
<MenuToggleButton role="button" aria-label="Open Menu">
|
|
172
|
+
Open
|
|
173
|
+
</MenuToggleButton>
|
|
174
|
+
}
|
|
175
|
+
onIsOpenChange={mockOnIsOpenChange}
|
|
176
|
+
initialIsOpen
|
|
177
|
+
>
|
|
178
|
+
Menu
|
|
179
|
+
</Component>
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
// Check that Menu is open
|
|
183
|
+
const menuTrigger = screen.getByRole("button", { name: "Open Menu" });
|
|
184
|
+
expect(menuTrigger).toBeInTheDocument();
|
|
185
|
+
expect(await screen.findByText("Menu")).toBeInTheDocument();
|
|
186
|
+
|
|
187
|
+
// Press esc key
|
|
188
|
+
await user.keyboard("{Esc}");
|
|
189
|
+
|
|
190
|
+
// Check that menu closes
|
|
191
|
+
await waitFor(() =>
|
|
192
|
+
expect(screen.queryByText("Menu")).not.toBeInTheDocument()
|
|
193
|
+
);
|
|
194
|
+
expect(mockOnIsOpenChange).toHaveBeenCalled();
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("should close when clicking outside of the menu", async () => {
|
|
198
|
+
const mockOnIsOpenChange = jest.fn();
|
|
199
|
+
const { user } = render(
|
|
200
|
+
<div>
|
|
201
|
+
<Component
|
|
202
|
+
triggerElement={
|
|
203
|
+
<MenuToggleButton role="button" aria-label="Open Menu">
|
|
204
|
+
Open
|
|
205
|
+
</MenuToggleButton>
|
|
206
|
+
}
|
|
207
|
+
onIsOpenChange={mockOnIsOpenChange}
|
|
208
|
+
initialIsOpen
|
|
209
|
+
>
|
|
210
|
+
Menu
|
|
211
|
+
</Component>
|
|
212
|
+
<button>Outside</button>
|
|
213
|
+
</div>
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
expect(await screen.findByText("Menu")).toBeInTheDocument();
|
|
217
|
+
|
|
218
|
+
// Click outside the menu
|
|
219
|
+
const otherButton = screen.getByRole("button", { name: "Outside" });
|
|
220
|
+
await user.click(otherButton);
|
|
221
|
+
|
|
222
|
+
// Check that menu closes
|
|
223
|
+
await waitFor(() =>
|
|
224
|
+
expect(screen.queryByText("Menu")).not.toBeInTheDocument()
|
|
225
|
+
);
|
|
226
|
+
expect(mockOnIsOpenChange).toHaveBeenCalled();
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it("should call onSelectedItemChange when item clicked", async () => {
|
|
230
|
+
const mockOnSelectedItemChange = jest.fn();
|
|
231
|
+
const { user } = render(
|
|
232
|
+
<Component
|
|
233
|
+
triggerElement={
|
|
234
|
+
<MenuToggleButton role="button" aria-label="Open Menu">
|
|
235
|
+
Open
|
|
236
|
+
</MenuToggleButton>
|
|
237
|
+
}
|
|
238
|
+
onSelectedItemChange={mockOnSelectedItemChange}
|
|
239
|
+
>
|
|
240
|
+
<MenuContent>
|
|
241
|
+
<MenuItem id="item-1">Item 1</MenuItem>
|
|
242
|
+
<MenuItem id="item-2">Item 2</MenuItem>
|
|
243
|
+
</MenuContent>
|
|
244
|
+
</Component>
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
// Open Menu
|
|
248
|
+
const menuTrigger = screen.getByRole("button", { name: "Open Menu" });
|
|
249
|
+
await user.click(menuTrigger);
|
|
250
|
+
|
|
251
|
+
// Wait till Menu is fully open
|
|
252
|
+
const menuItem = await screen.findByText("Item 1");
|
|
253
|
+
|
|
254
|
+
// Use click to activate menu item
|
|
255
|
+
await user.click(menuItem);
|
|
256
|
+
|
|
257
|
+
// Check that onSelectedItemChange is called
|
|
258
|
+
await waitFor(() => expect(mockOnSelectedItemChange).toHaveBeenCalled());
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it("should call onSelectedItemChange when item selected with enter", async () => {
|
|
262
|
+
const mockOnSelectedItemChange = jest.fn();
|
|
263
|
+
const { user } = render(
|
|
264
|
+
<Component
|
|
265
|
+
triggerElement={
|
|
266
|
+
<MenuToggleButton role="button" aria-label="Open Menu">
|
|
267
|
+
Open
|
|
268
|
+
</MenuToggleButton>
|
|
269
|
+
}
|
|
270
|
+
onSelectedItemChange={mockOnSelectedItemChange}
|
|
271
|
+
>
|
|
272
|
+
<MenuContent>
|
|
273
|
+
<MenuItem id="item-1">Item 1</MenuItem>
|
|
274
|
+
<MenuItem id="item-2">Item 2</MenuItem>
|
|
275
|
+
</MenuContent>
|
|
276
|
+
</Component>
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
// Open Menu
|
|
280
|
+
const menuTrigger = screen.getByRole("button", { name: "Open Menu" });
|
|
281
|
+
await user.click(menuTrigger);
|
|
282
|
+
|
|
283
|
+
// Wait till Menu is fully open
|
|
284
|
+
await screen.findByText("Item 1");
|
|
285
|
+
|
|
286
|
+
// Use enter to activate menu item
|
|
287
|
+
await user.keyboard("{arrowdown}{enter}");
|
|
288
|
+
|
|
289
|
+
// Check that onSelectedItemChange is called
|
|
290
|
+
await waitFor(() => expect(mockOnSelectedItemChange).toHaveBeenCalled());
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it("should call onSelectedItemChange when item selected with space", async () => {
|
|
294
|
+
const mockOnSelectedItemChange = jest.fn();
|
|
295
|
+
const { user } = render(
|
|
296
|
+
<Component
|
|
297
|
+
triggerElement={
|
|
298
|
+
<MenuToggleButton role="button" aria-label="Open Menu">
|
|
299
|
+
Open
|
|
300
|
+
</MenuToggleButton>
|
|
301
|
+
}
|
|
302
|
+
onSelectedItemChange={mockOnSelectedItemChange}
|
|
303
|
+
>
|
|
304
|
+
<MenuContent>
|
|
305
|
+
<MenuItem id="item-1">Item 1</MenuItem>
|
|
306
|
+
<MenuItem id="item-2">Item 2</MenuItem>
|
|
307
|
+
</MenuContent>
|
|
308
|
+
</Component>
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
// Open Menu
|
|
312
|
+
const menuTrigger = screen.getByRole("button", { name: "Open Menu" });
|
|
313
|
+
await user.click(menuTrigger);
|
|
314
|
+
|
|
315
|
+
// Wait till Menu is fully open
|
|
316
|
+
await screen.findByText("Item 1");
|
|
317
|
+
|
|
318
|
+
// Use space to activate menu item
|
|
319
|
+
await user.keyboard("{arrowdown} ");
|
|
320
|
+
|
|
321
|
+
// Check that onSelectedItemChange is called
|
|
322
|
+
await waitFor(() => expect(mockOnSelectedItemChange).toHaveBeenCalled());
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it("should call onIsOpenChange when menu is opened or closed", async () => {
|
|
326
|
+
const mockOnIsOpenChange = jest.fn();
|
|
327
|
+
const { user } = render(
|
|
328
|
+
<Component
|
|
329
|
+
triggerElement={
|
|
330
|
+
<MenuToggleButton role="button" aria-label="Open Menu">
|
|
331
|
+
Open
|
|
332
|
+
</MenuToggleButton>
|
|
333
|
+
}
|
|
334
|
+
onIsOpenChange={mockOnIsOpenChange}
|
|
335
|
+
>
|
|
336
|
+
<MenuContent>
|
|
337
|
+
<MenuItem id="item-1">Item 1</MenuItem>
|
|
338
|
+
<MenuItem id="item-2">Item 2</MenuItem>
|
|
339
|
+
</MenuContent>
|
|
340
|
+
</Component>
|
|
341
|
+
);
|
|
342
|
+
|
|
343
|
+
// Open menu by clicking trigger
|
|
344
|
+
const menuTrigger = screen.getByRole("button", { name: "Open Menu" });
|
|
345
|
+
await user.click(menuTrigger);
|
|
346
|
+
|
|
347
|
+
// Wait till menu is open
|
|
348
|
+
await screen.findByText("Item 1");
|
|
349
|
+
|
|
350
|
+
// Close menu by clicking trigger
|
|
351
|
+
await user.click(menuTrigger);
|
|
352
|
+
|
|
353
|
+
// Check that onIsOpenChange is called
|
|
354
|
+
await waitFor(() => expect(mockOnIsOpenChange).toHaveBeenCalledTimes(2));
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
it("should call onHighlightedIndexChange when navigating menu with the keyboard", async () => {
|
|
358
|
+
const mockOnHighlightedIndexChange = jest.fn();
|
|
359
|
+
const { user } = render(
|
|
360
|
+
<Component
|
|
361
|
+
triggerElement={
|
|
362
|
+
<MenuToggleButton role="button" aria-label="Open Menu">
|
|
363
|
+
Open
|
|
364
|
+
</MenuToggleButton>
|
|
365
|
+
}
|
|
366
|
+
onHighlightedIndexChange={mockOnHighlightedIndexChange}
|
|
367
|
+
>
|
|
368
|
+
<MenuContent>
|
|
369
|
+
<MenuItem id="item-1">Item 1</MenuItem>
|
|
370
|
+
<MenuItem id="item-2">Item 2</MenuItem>
|
|
371
|
+
</MenuContent>
|
|
372
|
+
</Component>
|
|
373
|
+
);
|
|
374
|
+
|
|
375
|
+
// Open Menu
|
|
376
|
+
const menuTrigger = screen.getByRole("button", { name: "Open Menu" });
|
|
377
|
+
await user.click(menuTrigger);
|
|
378
|
+
|
|
379
|
+
// Wait till Menu is fully open
|
|
380
|
+
await screen.findByText("Item 1");
|
|
381
|
+
|
|
382
|
+
// Use arrowdown to navigate the menu
|
|
383
|
+
await user.keyboard("{arrowdown}");
|
|
384
|
+
|
|
385
|
+
// Check that onHighlightedIndexChange is called
|
|
386
|
+
await waitFor(() =>
|
|
387
|
+
expect(mockOnHighlightedIndexChange).toHaveBeenCalled()
|
|
388
|
+
);
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
it("should accept a custom stateReducer", async () => {
|
|
392
|
+
const mockStateReducer = jest.fn((state, { changes }) => changes);
|
|
393
|
+
const { user } = render(
|
|
394
|
+
<Component
|
|
395
|
+
triggerElement={
|
|
396
|
+
<MenuToggleButton role="button" aria-label="Open Menu">
|
|
397
|
+
Open
|
|
398
|
+
</MenuToggleButton>
|
|
399
|
+
}
|
|
400
|
+
stateReducer={mockStateReducer}
|
|
401
|
+
>
|
|
402
|
+
<MenuContent>
|
|
403
|
+
<MenuItem id="item-1">Item 1</MenuItem>
|
|
404
|
+
<MenuItem id="item-2">Item 2</MenuItem>
|
|
405
|
+
</MenuContent>
|
|
406
|
+
</Component>
|
|
407
|
+
);
|
|
408
|
+
|
|
409
|
+
// Open Menu
|
|
410
|
+
const menuTrigger = screen.getByRole("button", { name: "Open Menu" });
|
|
411
|
+
await user.click(menuTrigger);
|
|
412
|
+
|
|
413
|
+
// Wait till Menu is fully open
|
|
414
|
+
await screen.findByText("Item 1");
|
|
415
|
+
|
|
416
|
+
// Use arrowdown and enter to select item
|
|
417
|
+
await user.keyboard("{arrowdown}{enter}");
|
|
418
|
+
|
|
419
|
+
// Check that stateReducer is called
|
|
420
|
+
await waitFor(() => expect(mockStateReducer).toHaveBeenCalledTimes(4));
|
|
421
|
+
});
|
|
422
|
+
});
|
|
423
|
+
});
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export const MENU_ROLES = {
|
|
2
|
+
MENU: "menu",
|
|
3
|
+
LIST: "list",
|
|
4
|
+
LISTBOX: "listbox",
|
|
5
|
+
} as const;
|
|
6
|
+
|
|
7
|
+
export const MENU_ITEM_ROLES = {
|
|
8
|
+
MENUITEM: "menuitem",
|
|
9
|
+
LISTITEM: "listitem",
|
|
10
|
+
OPTION: "option",
|
|
11
|
+
RADIO: "menuitemradio",
|
|
12
|
+
CHECKBOX: "menuitemcheckbox",
|
|
13
|
+
} as const;
|
|
14
|
+
|
|
15
|
+
export type TypeMenuRoles = (typeof MENU_ROLES)[keyof typeof MENU_ROLES];
|
|
16
|
+
export type TypeMenuItemRoles =
|
|
17
|
+
(typeof MENU_ITEM_ROLES)[keyof typeof MENU_ITEM_ROLES];
|