@sproutsocial/seeds-react-menu 1.0.1 → 1.1.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/.turbo/turbo-build.log +85 -9
- package/CHANGELOG.md +32 -0
- package/dist/esm/index.js +505 -122
- package/dist/esm/index.js.map +1 -1
- package/dist/index.d.mts +45 -16
- package/dist/index.d.ts +45 -16
- package/dist/index.js +568 -179
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/src/ActionMenu/ActionMenu.tsx +4 -3
- package/src/ActionMenu/ActionMenuTypes.ts +5 -4
- package/src/ActionMenu/__tests__/ActionMenu.test.tsx +1 -1
- package/src/Autocomplete/Autocomplete.stories.tsx +95 -32
- package/src/Autocomplete/AutocompleteTypes.ts +5 -2
- package/src/Autocomplete/MultiAutocomplete.tsx +3 -2
- package/src/Autocomplete/SingleAutocomplete.tsx +3 -3
- package/src/Autocomplete/index.ts +1 -0
- package/src/ListboxToggleButton.tsx +65 -0
- package/src/MenuContent/MenuContentTypes.ts +1 -2
- package/src/MenuContent/styles.tsx +2 -9
- package/src/MenuGroup/__tests__/MenuGroup.test.tsx +1 -1
- package/src/MenuItem/__tests__/MenuItem.test.tsx +1 -3
- package/src/MenuRoot/MenuRoot.tsx +95 -15
- package/src/MultiSelectMenu/MultiSelectMenu.stories.tsx +0 -1
- package/src/MultiSelectMenu/MultiSelectMenu.tsx +3 -2
- package/src/MultiSelectMenu/MultiSelectMenuTypes.ts +5 -3
- package/src/NestedMenu/NestedMenu.feature +1 -1
- package/src/NestedMenu/NestedMenu.stories.tsx +17 -7
- package/src/NestedMenu/NestedMenu.tsx +217 -43
- package/src/NestedMenu/NestedMenuContext.tsx +16 -0
- package/src/NestedMenu/NestedMenuHeader.tsx +19 -6
- package/src/NestedMenu/NestedMenuPopout.tsx +55 -0
- package/src/NestedMenu/NestedMenuRoot.tsx +27 -0
- package/src/NestedMenu/NestedMenuTypes.ts +3 -2
- package/src/NestedMenu/__tests__/NestedMenu.test.tsx +210 -19
- package/src/SingleSelectMenu/SingleSelectMenu.stories.tsx +0 -1
- package/src/SingleSelectMenu/SingleSelectMenu.tsx +3 -2
- package/src/SingleSelectMenu/SingleSelectMenuTypes.ts +5 -3
- package/src/__tests__/Menu.test.tsx +1 -1
- package/src/index.ts +1 -0
- package/src/storybookUtilities/menu-storybook-components.tsx +11 -31
- package/src/types.ts +5 -1
|
@@ -1,31 +1,222 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
3
|
-
NestedMenu,
|
|
4
|
-
MenuContent,
|
|
5
|
-
MenuToggleButton,
|
|
6
|
-
MenuItem,
|
|
7
|
-
MenuGroup,
|
|
8
|
-
} from "../../index";
|
|
1
|
+
import React from "react";
|
|
9
2
|
import {
|
|
10
3
|
render,
|
|
11
4
|
screen,
|
|
12
|
-
|
|
5
|
+
act,
|
|
13
6
|
waitFor,
|
|
7
|
+
PointerEventsCheckLevel,
|
|
14
8
|
} from "@sproutsocial/seeds-react-testing-library";
|
|
9
|
+
import {
|
|
10
|
+
NestedMenu,
|
|
11
|
+
MenuToggleButton,
|
|
12
|
+
ActionMenu,
|
|
13
|
+
SingleSelectMenu,
|
|
14
|
+
MultiSelectMenu,
|
|
15
|
+
} from "../../index";
|
|
16
|
+
|
|
17
|
+
const menus = {
|
|
18
|
+
root: {
|
|
19
|
+
MenuComponent: ActionMenu,
|
|
20
|
+
menuProps: {
|
|
21
|
+
menuItems: [
|
|
22
|
+
{ id: "child1", children: "Go to Child Menu 1", childMenuId: "child1" },
|
|
23
|
+
{ id: "child2", children: "Go to Child Menu 2", childMenuId: "child2" },
|
|
24
|
+
{ id: "child3", children: "No Child Menu" },
|
|
25
|
+
],
|
|
26
|
+
},
|
|
27
|
+
MenuHeaderComponent: null,
|
|
28
|
+
},
|
|
29
|
+
child1: {
|
|
30
|
+
MenuComponent: SingleSelectMenu,
|
|
31
|
+
menuProps: {
|
|
32
|
+
menuItems: [
|
|
33
|
+
{ id: "item1", children: "Child Item 1.1" },
|
|
34
|
+
{ id: "item2", children: "Child Item 1.2" },
|
|
35
|
+
],
|
|
36
|
+
},
|
|
37
|
+
menuHeaderProps: {
|
|
38
|
+
title: "Child Menu 1",
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
child2: {
|
|
42
|
+
MenuComponent: MultiSelectMenu,
|
|
43
|
+
menuProps: {
|
|
44
|
+
menuItems: [
|
|
45
|
+
{ id: "item1", children: "Child Item 2.1" },
|
|
46
|
+
{ id: "item2", children: "Child Item 2.2" },
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
menuHeaderProps: {
|
|
50
|
+
title: "Child Menu 2",
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
};
|
|
15
54
|
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
55
|
+
const testProps = {
|
|
56
|
+
initialMenuId: "root",
|
|
57
|
+
backArrowLabel: "Back",
|
|
58
|
+
menus,
|
|
59
|
+
menuToggleElement: <MenuToggleButton>Open Menu</MenuToggleButton>,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const openNestedMenu = async (props = {}) => {
|
|
63
|
+
const utils = render(<NestedMenu {...testProps} {...props} />);
|
|
64
|
+
const toggleButton = screen.getByRole("button", { name: "Open Menu" });
|
|
65
|
+
await act(async () => {
|
|
66
|
+
await utils.user.click(toggleButton);
|
|
67
|
+
});
|
|
68
|
+
return utils;
|
|
69
|
+
};
|
|
22
70
|
|
|
23
71
|
describe("NestedMenu", () => {
|
|
24
|
-
|
|
25
|
-
|
|
72
|
+
it("opens the initial menu when the menu button is clicked", async () => {
|
|
73
|
+
await openNestedMenu();
|
|
74
|
+
|
|
75
|
+
expect(await screen.findByText("Go to Child Menu 1")).toBeInTheDocument();
|
|
76
|
+
expect(await screen.findByText("Go to Child Menu 2")).toBeInTheDocument();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("opens a child menu when an item with a childMenuId is clicked", async () => {
|
|
80
|
+
const { user } = await openNestedMenu();
|
|
81
|
+
|
|
82
|
+
const child1Item = screen.getByText("Go to Child Menu 1");
|
|
83
|
+
await act(async () => {
|
|
84
|
+
await user.click(child1Item);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
await waitFor(() =>
|
|
88
|
+
expect(screen.queryByText("Go to Child Menu 1")).not.toBeInTheDocument()
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
expect(await screen.findByText("Child Item 1.1")).toBeInTheDocument();
|
|
92
|
+
expect(await screen.findByText("Child Item 1.2")).toBeInTheDocument();
|
|
93
|
+
expect(await screen.findByText("Child Menu 1")).toBeInTheDocument();
|
|
26
94
|
});
|
|
27
95
|
|
|
28
|
-
it("
|
|
29
|
-
|
|
96
|
+
it("returns to the previous menu when the back button is clicked", async () => {
|
|
97
|
+
const { user } = await openNestedMenu();
|
|
98
|
+
|
|
99
|
+
const child1Item = screen.getByText("Go to Child Menu 1");
|
|
100
|
+
await act(async () => {
|
|
101
|
+
await user.click(child1Item);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
await waitFor(() =>
|
|
105
|
+
expect(screen.queryByText("Go to Child Menu 1")).not.toBeInTheDocument()
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
const backButton = screen.getByRole("button", { name: "Back" });
|
|
109
|
+
await act(async () => {
|
|
110
|
+
await user.click(backButton);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
expect(await screen.findByText("Go to Child Menu 1")).toBeInTheDocument();
|
|
114
|
+
expect(screen.getByText("Go to Child Menu 2")).toBeInTheDocument();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("closes the menu when a menu item without a childMenuId is clicked in an ActionMenu", async () => {
|
|
118
|
+
const { user } = await openNestedMenu();
|
|
119
|
+
|
|
120
|
+
const item = screen.getByText("No Child Menu");
|
|
121
|
+
await act(async () => {
|
|
122
|
+
await user.click(item);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
await waitFor(() =>
|
|
126
|
+
expect(screen.queryByText("No Child Menu")).not.toBeInTheDocument()
|
|
127
|
+
);
|
|
128
|
+
await waitFor(() =>
|
|
129
|
+
expect(screen.queryByText("Child Menu 1")).not.toBeInTheDocument()
|
|
130
|
+
);
|
|
131
|
+
await waitFor(() =>
|
|
132
|
+
expect(screen.queryByText("Child Menu 2")).not.toBeInTheDocument()
|
|
133
|
+
);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("closes the menu when a menu item without a childMenuId is clicked in a SingleSelectMenu", async () => {
|
|
137
|
+
const { user } = await openNestedMenu();
|
|
138
|
+
|
|
139
|
+
const child1Item = screen.getByText("Go to Child Menu 1");
|
|
140
|
+
await act(async () => {
|
|
141
|
+
await user.click(child1Item);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
await waitFor(() =>
|
|
145
|
+
expect(screen.queryByText("Go to Child Menu 1")).not.toBeInTheDocument()
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
const child1Item1 = screen.getByText("Child Item 1.1");
|
|
149
|
+
await act(async () => {
|
|
150
|
+
await user
|
|
151
|
+
.setup({
|
|
152
|
+
pointerEventsCheck: PointerEventsCheckLevel.Never,
|
|
153
|
+
})
|
|
154
|
+
.click(child1Item1);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
await waitFor(() =>
|
|
158
|
+
expect(screen.queryByText("No Child Menu")).not.toBeInTheDocument()
|
|
159
|
+
);
|
|
160
|
+
await waitFor(() =>
|
|
161
|
+
expect(screen.queryByText("Child Menu 1")).not.toBeInTheDocument()
|
|
162
|
+
);
|
|
163
|
+
await waitFor(() =>
|
|
164
|
+
expect(screen.queryByText("Child Menu 2")).not.toBeInTheDocument()
|
|
165
|
+
);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("does not close the menu when a menu item without a childMenuId is clicked in a MultiSelectMenu", async () => {
|
|
169
|
+
const { user } = await openNestedMenu();
|
|
170
|
+
|
|
171
|
+
const child2Item = screen.getByText("Go to Child Menu 2");
|
|
172
|
+
await act(async () => {
|
|
173
|
+
await user.click(child2Item);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
await waitFor(() =>
|
|
177
|
+
expect(screen.queryByText("Go to Child Menu 2")).not.toBeInTheDocument()
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
const child2Item1 = screen.getByText("Child Item 2.1");
|
|
181
|
+
await act(async () => {
|
|
182
|
+
await user
|
|
183
|
+
.setup({
|
|
184
|
+
pointerEventsCheck: PointerEventsCheckLevel.Never,
|
|
185
|
+
})
|
|
186
|
+
.click(child2Item1);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
await waitFor(() =>
|
|
190
|
+
expect(screen.queryByText("Child Menu 2")).toBeInTheDocument()
|
|
191
|
+
);
|
|
192
|
+
await waitFor(() =>
|
|
193
|
+
expect(screen.queryByText("Child Item 2.1")).toBeInTheDocument()
|
|
194
|
+
);
|
|
195
|
+
await waitFor(() =>
|
|
196
|
+
expect(screen.queryByText("Child Item 2.2")).toBeInTheDocument()
|
|
197
|
+
);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("calls onBackButtonClick when the back button is clicked", async () => {
|
|
201
|
+
const mockOnBackButtonClick = jest.fn();
|
|
202
|
+
const { user } = await openNestedMenu({
|
|
203
|
+
onBackButtonClick: mockOnBackButtonClick,
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
const child1Item = screen.getByText("Go to Child Menu 1");
|
|
207
|
+
await act(async () => {
|
|
208
|
+
await user.click(child1Item);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
await waitFor(() =>
|
|
212
|
+
expect(screen.queryByText("Go to Child Menu 1")).not.toBeInTheDocument()
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
const backButton = screen.getByRole("button", { name: "Back" });
|
|
216
|
+
await act(async () => {
|
|
217
|
+
await user.click(backButton);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
expect(mockOnBackButtonClick).toHaveBeenCalled();
|
|
30
221
|
});
|
|
31
222
|
});
|
|
@@ -24,6 +24,7 @@ export const SingleSelectMenu = <
|
|
|
24
24
|
stateReducer: externalStateReducer,
|
|
25
25
|
menuItems,
|
|
26
26
|
MenuItemComponent,
|
|
27
|
+
MenuRootComponent = MenuRoot,
|
|
27
28
|
...useSelectProps
|
|
28
29
|
}: TypeSingleSelectMenuProps<I>) => {
|
|
29
30
|
/** Get a list of menuItems from the children */
|
|
@@ -51,7 +52,7 @@ export const SingleSelectMenu = <
|
|
|
51
52
|
});
|
|
52
53
|
|
|
53
54
|
return (
|
|
54
|
-
<
|
|
55
|
+
<MenuRootComponent
|
|
55
56
|
{...{
|
|
56
57
|
isListbox: true,
|
|
57
58
|
items: allMenuItems,
|
|
@@ -64,6 +65,6 @@ export const SingleSelectMenu = <
|
|
|
64
65
|
}}
|
|
65
66
|
>
|
|
66
67
|
{children}
|
|
67
|
-
</
|
|
68
|
+
</MenuRootComponent>
|
|
68
69
|
);
|
|
69
70
|
};
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import type { TypeMenuRootOwnProps, TypeMenuRootProps } from "../MenuRoot";
|
|
2
1
|
import type { TypeDownshiftSelectProps } from "../MenuContext";
|
|
3
2
|
import type { TypeMenuItemProps } from "../MenuItem";
|
|
4
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
TypeChildrenOrItems,
|
|
5
|
+
TypeHigherOrderComponentProps,
|
|
6
|
+
} from "../types";
|
|
5
7
|
|
|
6
8
|
export interface TypeSingleSelectMenuBaseProps
|
|
7
|
-
extends
|
|
9
|
+
extends TypeHigherOrderComponentProps,
|
|
8
10
|
TypeDownshiftSelectProps {}
|
|
9
11
|
|
|
10
12
|
export type TypeSingleSelectMenuProps<
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/* eslint-disable testing-library/no-unnecessary-act */
|
|
2
|
-
import { PointerEventsCheckLevel } from "@testing-library/user-event";
|
|
3
2
|
import {
|
|
4
3
|
render,
|
|
5
4
|
screen,
|
|
6
5
|
waitFor,
|
|
6
|
+
PointerEventsCheckLevel,
|
|
7
7
|
} from "@sproutsocial/seeds-react-testing-library";
|
|
8
8
|
import {
|
|
9
9
|
ActionMenu,
|
package/src/index.ts
CHANGED
|
@@ -1,41 +1,21 @@
|
|
|
1
1
|
import styled from "styled-components";
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
$isOpen?: boolean;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
const StyledChevron = styled.div<TypeStyledChevronProps>`
|
|
11
|
-
margin-left: ${({ theme }) => theme.space[300]};
|
|
12
|
-
${({ $isOpen }) =>
|
|
13
|
-
$isOpen &&
|
|
14
|
-
`
|
|
15
|
-
transform: rotate(-180deg);
|
|
16
|
-
`}
|
|
17
|
-
transition: transform 0.15s;
|
|
18
|
-
`;
|
|
2
|
+
import {
|
|
3
|
+
useMenuToggleContext,
|
|
4
|
+
MenuToggleButton,
|
|
5
|
+
ListboxToggleButton,
|
|
6
|
+
} from "../index";
|
|
19
7
|
|
|
20
8
|
interface StorybookMenuToggleButtonProps {
|
|
21
9
|
buttonText?: string;
|
|
22
10
|
buttonProps?: Partial<React.ComponentProps<typeof MenuToggleButton>>;
|
|
11
|
+
useListboxButton?: boolean;
|
|
23
12
|
}
|
|
24
13
|
|
|
25
14
|
export const StorybookMenuToggleButton = ({
|
|
26
15
|
buttonText,
|
|
27
16
|
buttonProps,
|
|
28
|
-
}: StorybookMenuToggleButtonProps) =>
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
<Box display="flex" justifyContent="space-between">
|
|
34
|
-
{buttonText ? buttonText : <span>Select book</span>}
|
|
35
|
-
<StyledChevron $isOpen={isOpen}>
|
|
36
|
-
<Icon name="chevron-down" />
|
|
37
|
-
</StyledChevron>
|
|
38
|
-
</Box>
|
|
39
|
-
</MenuToggleButton>
|
|
40
|
-
);
|
|
41
|
-
};
|
|
17
|
+
}: StorybookMenuToggleButtonProps) => (
|
|
18
|
+
<ListboxToggleButton {...buttonProps}>
|
|
19
|
+
{buttonText ? buttonText : <span>Select book</span>}
|
|
20
|
+
</ListboxToggleButton>
|
|
21
|
+
);
|
package/src/types.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { UseComboboxGetInputPropsOptions } from "downshift";
|
|
2
2
|
import type { TypeMenuItemProps } from "./MenuItem";
|
|
3
3
|
import type { TypeMenuGroupBaseProps } from "./MenuGroup";
|
|
4
|
-
import type { TypeMenuRootProps } from "./MenuRoot";
|
|
4
|
+
import type { TypeMenuRootOwnProps, TypeMenuRootProps } from "./MenuRoot";
|
|
5
5
|
|
|
6
6
|
// The items object used internally for Menu functionality
|
|
7
7
|
export interface TypeInternalItemProps<Item = TypeMenuItemProps> {
|
|
@@ -12,6 +12,10 @@ export interface TypeInternalItemProps<Item = TypeMenuItemProps> {
|
|
|
12
12
|
menuGroupProps?: TypeMenuGroupBaseProps;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
export interface TypeHigherOrderComponentProps extends TypeMenuRootOwnProps {
|
|
16
|
+
MenuRootComponent?: React.ComponentType<TypeMenuRootProps>;
|
|
17
|
+
}
|
|
18
|
+
|
|
15
19
|
export type TypeChildrenOrItems<
|
|
16
20
|
I extends TypeMenuItemProps = TypeMenuItemProps
|
|
17
21
|
> =
|