@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
package/package.json
CHANGED
|
@@ -49,7 +49,7 @@ const handleStateChange = (
|
|
|
49
49
|
if (!selectedItem) return;
|
|
50
50
|
|
|
51
51
|
// if selectedItem is disabled, do nothing
|
|
52
|
-
if (selectedItem.
|
|
52
|
+
if (selectedItem.disabled) {
|
|
53
53
|
return;
|
|
54
54
|
}
|
|
55
55
|
|
|
@@ -58,7 +58,7 @@ const handleStateChange = (
|
|
|
58
58
|
changes.type === useSelect.stateChangeTypes.ToggleButtonKeyDownEnter;
|
|
59
59
|
|
|
60
60
|
// selectedItem props
|
|
61
|
-
const selectedItemProps = selectedItem.
|
|
61
|
+
const selectedItemProps = selectedItem.menuItemProps;
|
|
62
62
|
|
|
63
63
|
if (selectedItemProps?.onClick) {
|
|
64
64
|
// Create a synthetic event to pass into the onClick
|
package/src/MenuItem/styles.tsx
CHANGED
|
@@ -14,6 +14,8 @@ const highlightedStyles = css`
|
|
|
14
14
|
cursor: pointer;
|
|
15
15
|
border: 1px solid ${({ theme }) => theme.colors.listItem.border.base};
|
|
16
16
|
background-color: ${({ theme }) => theme.colors.listItem.background.hover};
|
|
17
|
+
color: ${({ theme }) => theme.colors.text.body};
|
|
18
|
+
text-decoration: none;
|
|
17
19
|
`;
|
|
18
20
|
|
|
19
21
|
export const MenuItemContainer = styled.li`
|
|
@@ -21,7 +23,6 @@ export const MenuItemContainer = styled.li`
|
|
|
21
23
|
list-style-type: none;
|
|
22
24
|
margin-left: ${({ theme }) => theme.space[300]};
|
|
23
25
|
margin-right: ${({ theme }) => theme.space[300]};
|
|
24
|
-
transition: all ${({ theme }) => theme.duration["fast"]};
|
|
25
26
|
|
|
26
27
|
&:first-child {
|
|
27
28
|
margin-top: ${({ theme }) => theme.space[300]};
|
|
@@ -47,6 +48,7 @@ export const StyledMenuItem = styled.div<{ $highlighted: boolean }>`
|
|
|
47
48
|
padding: ${({ theme }) => theme.space[300]};
|
|
48
49
|
outline: 0;
|
|
49
50
|
${({ theme }) => theme.typography[200]};
|
|
51
|
+
transition: all ${({ theme }) => theme.duration["fast"]};
|
|
50
52
|
|
|
51
53
|
/* Highlighted */
|
|
52
54
|
${({ $highlighted }) => $highlighted && highlightedStyles}
|
|
@@ -11,21 +11,31 @@ Feature: Menu Root
|
|
|
11
11
|
When the esc key is pressed
|
|
12
12
|
Then the menu closes
|
|
13
13
|
|
|
14
|
-
Scenario
|
|
15
|
-
Given a
|
|
16
|
-
When the
|
|
17
|
-
Then
|
|
18
|
-
|
|
19
|
-
Examples:
|
|
20
|
-
| use case | the correct element |
|
|
21
|
-
| contains at least one MenuItem | the first MenuItem |
|
|
22
|
-
| has a custom defaultFocusRef | the defaultFocusRef element |
|
|
23
|
-
| has no MenuItems and no defaultFocusRef | an error is logged and the first focusable child |
|
|
14
|
+
Scenario: MenuRoot closes when clicking outside of it
|
|
15
|
+
Given a menu is open
|
|
16
|
+
When the user clicks outside of it
|
|
17
|
+
Then the menu closes
|
|
24
18
|
|
|
25
19
|
Rule: API
|
|
26
20
|
Background:
|
|
27
21
|
Given an engineer is implementing the component
|
|
28
22
|
|
|
23
|
+
Scenario: MenuRoot triggers the Menu to open when isOpen true
|
|
24
|
+
Given the Menu is closed
|
|
25
|
+
And MenuContext.isOpen is false
|
|
26
|
+
When MenuContext.isOpen is changed to true
|
|
27
|
+
Then the Menu will open
|
|
28
|
+
|
|
29
|
+
Scenario: MenuRoot triggers the Menu to close when isOpen false
|
|
30
|
+
Given the Menu is open
|
|
31
|
+
And MenuContext.isOpen is true
|
|
32
|
+
When MenuContext.isOpen is changed to false
|
|
33
|
+
Then the Menu will close
|
|
34
|
+
|
|
35
|
+
Scenario: MenuRoot accepts a custom toggleButtonElement
|
|
36
|
+
When MenuRoot receives a a custom element for the toggleButtonElement prop
|
|
37
|
+
Then the Menu will render the custom element
|
|
38
|
+
|
|
29
39
|
Scenario: MenuRoot is using keyboard instructions
|
|
30
40
|
Given a screen reader is being used
|
|
31
41
|
When a MenuRoot is provided with the keyboardInstructions prop
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
fireEvent,
|
|
4
4
|
render,
|
|
5
5
|
screen,
|
|
6
|
+
waitFor,
|
|
6
7
|
} from "@sproutsocial/seeds-react-testing-library";
|
|
7
8
|
import { Box } from "@sproutsocial/seeds-react-box";
|
|
8
9
|
import { Button } from "@sproutsocial/seeds-react-button";
|
|
@@ -13,6 +14,7 @@ import {
|
|
|
13
14
|
MenuContext,
|
|
14
15
|
MenuFooter,
|
|
15
16
|
MenuHeader,
|
|
17
|
+
MenuGroup,
|
|
16
18
|
type TypeMenuContextProps,
|
|
17
19
|
} from "../../index";
|
|
18
20
|
|
|
@@ -42,28 +44,62 @@ const TestMenuFooter = () => {
|
|
|
42
44
|
// };
|
|
43
45
|
|
|
44
46
|
describe("MenuRoot", () => {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
it("closes when esc is pressed", async () => {
|
|
48
|
+
const mockCloseMenu = jest.fn();
|
|
49
|
+
const { user } = render(
|
|
50
|
+
<MenuRoot
|
|
51
|
+
menuToggleElement={
|
|
52
|
+
<MenuToggleButton role="button" aria-label="Open Menu">
|
|
53
|
+
Open
|
|
54
|
+
</MenuToggleButton>
|
|
55
|
+
}
|
|
56
|
+
closeMenu={mockCloseMenu}
|
|
57
|
+
isOpen
|
|
58
|
+
>
|
|
59
|
+
Menu
|
|
60
|
+
</MenuRoot>
|
|
61
|
+
);
|
|
49
62
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
63
|
+
// Check that Menu is open
|
|
64
|
+
const menuTrigger = screen.getByRole("button", { name: "Open Menu" });
|
|
65
|
+
expect(menuTrigger).toBeInTheDocument();
|
|
66
|
+
expect(await screen.findByText("Menu")).toBeInTheDocument();
|
|
67
|
+
|
|
68
|
+
// Press esc key
|
|
69
|
+
await user.keyboard("{Esc}");
|
|
70
|
+
|
|
71
|
+
// Check that menu closes
|
|
72
|
+
await waitFor(() => expect(mockCloseMenu).toHaveBeenCalled());
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("should close when clicking outside of the menu", async () => {
|
|
76
|
+
const mockCloseMenu = jest.fn();
|
|
77
|
+
const { user } = render(
|
|
78
|
+
<div>
|
|
79
|
+
<MenuRoot
|
|
80
|
+
menuToggleElement={
|
|
81
|
+
<MenuToggleButton role="button" aria-label="Open Menu">
|
|
82
|
+
Open
|
|
83
|
+
</MenuToggleButton>
|
|
84
|
+
}
|
|
85
|
+
closeMenu={mockCloseMenu}
|
|
86
|
+
isOpen
|
|
87
|
+
>
|
|
88
|
+
Menu
|
|
89
|
+
</MenuRoot>
|
|
90
|
+
<button>Outside</button>
|
|
91
|
+
</div>
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
expect(await screen.findByText("Menu")).toBeInTheDocument();
|
|
95
|
+
|
|
96
|
+
// Click outside the menu
|
|
97
|
+
const otherButton = screen.getByRole("button", { name: "Outside" });
|
|
98
|
+
await user.click(otherButton);
|
|
99
|
+
|
|
100
|
+
// Check that menu closes
|
|
101
|
+
await waitFor(() => expect(mockCloseMenu).toHaveBeenCalled());
|
|
102
|
+
});
|
|
67
103
|
|
|
68
104
|
// TODO: write tests for keyboard instructions when feature added
|
|
69
105
|
// it("triggers keyboard instructions", () => {
|
|
@@ -74,7 +110,107 @@ describe("MenuRoot", () => {
|
|
|
74
110
|
// // has no keyboard instructions by default
|
|
75
111
|
// });
|
|
76
112
|
|
|
77
|
-
|
|
113
|
+
it("opens when isOpen changes from false to true", async () => {
|
|
114
|
+
const TestMenu = () => {
|
|
115
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
116
|
+
const toggleMenu = () => setIsOpen(!isOpen);
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
<MenuRoot
|
|
120
|
+
menuToggleElement={
|
|
121
|
+
<MenuToggleButton
|
|
122
|
+
role="button"
|
|
123
|
+
aria-label="Open Menu"
|
|
124
|
+
onClick={toggleMenu}
|
|
125
|
+
>
|
|
126
|
+
Open
|
|
127
|
+
</MenuToggleButton>
|
|
128
|
+
}
|
|
129
|
+
isOpen={isOpen}
|
|
130
|
+
>
|
|
131
|
+
Menu
|
|
132
|
+
</MenuRoot>
|
|
133
|
+
);
|
|
134
|
+
};
|
|
135
|
+
const { user } = render(<TestMenu />);
|
|
136
|
+
|
|
137
|
+
// Check that Menu is open
|
|
138
|
+
const menuTrigger = screen.getByRole("button", { name: "Open Menu" });
|
|
139
|
+
expect(menuTrigger).toBeInTheDocument();
|
|
140
|
+
expect(screen.queryByText("Menu")).not.toBeInTheDocument();
|
|
141
|
+
|
|
142
|
+
// Click the menu trigger
|
|
143
|
+
await user.click(menuTrigger);
|
|
144
|
+
|
|
145
|
+
// Check that menu opens
|
|
146
|
+
expect(await screen.findByText("Menu")).toBeInTheDocument();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("closes when isOpen changes from true to false", async () => {
|
|
150
|
+
const TestMenu = () => {
|
|
151
|
+
const [isOpen, setIsOpen] = useState(true);
|
|
152
|
+
const toggleMenu = () => setIsOpen(!isOpen);
|
|
153
|
+
|
|
154
|
+
return (
|
|
155
|
+
<MenuRoot
|
|
156
|
+
menuToggleElement={
|
|
157
|
+
<MenuToggleButton
|
|
158
|
+
role="button"
|
|
159
|
+
aria-label="Open Menu"
|
|
160
|
+
onClick={toggleMenu}
|
|
161
|
+
>
|
|
162
|
+
Open
|
|
163
|
+
</MenuToggleButton>
|
|
164
|
+
}
|
|
165
|
+
isOpen={isOpen}
|
|
166
|
+
>
|
|
167
|
+
Menu
|
|
168
|
+
</MenuRoot>
|
|
169
|
+
);
|
|
170
|
+
};
|
|
171
|
+
const { user } = render(<TestMenu />);
|
|
172
|
+
|
|
173
|
+
// Check that Menu is open
|
|
174
|
+
const menuTrigger = screen.getByRole("button", { name: "Open Menu" });
|
|
175
|
+
expect(menuTrigger).toBeInTheDocument();
|
|
176
|
+
expect(await screen.findByText("Menu")).toBeInTheDocument();
|
|
177
|
+
|
|
178
|
+
// Click the menu trigger
|
|
179
|
+
await user.click(menuTrigger);
|
|
180
|
+
|
|
181
|
+
// Check that menu opens
|
|
182
|
+
await waitFor(() =>
|
|
183
|
+
expect(screen.queryByText("Menu")).not.toBeInTheDocument()
|
|
184
|
+
);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("renders a custom toggle when passed to menuToggleElement", async () => {
|
|
188
|
+
const mockToggleClick = jest.fn();
|
|
189
|
+
const CustomToggleElement = () => {
|
|
190
|
+
const { getToggleButtonProps } = useContext(MenuContext);
|
|
191
|
+
return (
|
|
192
|
+
<div role="button" {...getToggleButtonProps?.()}>
|
|
193
|
+
Toggle
|
|
194
|
+
</div>
|
|
195
|
+
);
|
|
196
|
+
};
|
|
197
|
+
const { user } = render(
|
|
198
|
+
<MenuRoot
|
|
199
|
+
menuToggleElement={<CustomToggleElement />}
|
|
200
|
+
// @ts-ignore - mock contextProps may be imperfect
|
|
201
|
+
getToggleButtonProps={() => ({ onClick: mockToggleClick })}
|
|
202
|
+
>
|
|
203
|
+
Menu
|
|
204
|
+
</MenuRoot>
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
const menuTrigger = screen.getByRole("button", { name: "Toggle" });
|
|
208
|
+
expect(menuTrigger).toBeInTheDocument();
|
|
209
|
+
await user.click(menuTrigger);
|
|
210
|
+
|
|
211
|
+
expect(mockToggleClick).toHaveBeenCalled();
|
|
212
|
+
});
|
|
213
|
+
|
|
78
214
|
it("renders custom children", () => {
|
|
79
215
|
render(
|
|
80
216
|
<MenuRoot
|
|
@@ -83,9 +219,9 @@ describe("MenuRoot", () => {
|
|
|
83
219
|
>
|
|
84
220
|
<MenuHeader title="Menu Header" />
|
|
85
221
|
<div>Custom Banner</div>
|
|
86
|
-
<
|
|
222
|
+
<MenuGroup title="Menu Group">
|
|
87
223
|
<MenuItem id="1">Item 1</MenuItem>
|
|
88
|
-
</
|
|
224
|
+
</MenuGroup>
|
|
89
225
|
<MenuFooter>Menu Footer</MenuFooter>
|
|
90
226
|
</MenuRoot>
|
|
91
227
|
);
|
|
@@ -94,14 +230,11 @@ describe("MenuRoot", () => {
|
|
|
94
230
|
expect(screen.getByText("Custom Banner")).toBeInTheDocument();
|
|
95
231
|
expect(screen.getByText("Item 1")).toBeInTheDocument();
|
|
96
232
|
expect(screen.getByText("Menu Footer")).toBeInTheDocument();
|
|
233
|
+
expect(
|
|
234
|
+
screen.getByRole("group", { name: "Menu Group" })
|
|
235
|
+
).toBeInTheDocument();
|
|
97
236
|
});
|
|
98
237
|
|
|
99
|
-
// TODO: test this using the SingleSelectMenu, MultiSelectMenu, and ComboBox
|
|
100
|
-
// it("passes downshift props to the MenuContext", () => {
|
|
101
|
-
// // passes downshift props to the MenuContext
|
|
102
|
-
// // Can be tested by the components that use downshift
|
|
103
|
-
// });
|
|
104
|
-
|
|
105
238
|
it("can be implemented without using Downshift", () => {
|
|
106
239
|
const randomText = "Some random text";
|
|
107
240
|
const TestCustomMenu = () => {
|
|
@@ -40,25 +40,14 @@ Feature: MultiSelectMenu
|
|
|
40
40
|
Then the component renders the existing values
|
|
41
41
|
|
|
42
42
|
Scenario: Component handles new selection state
|
|
43
|
-
When the MultiSelectMenu is not passed
|
|
43
|
+
When the MultiSelectMenu is not passed a `selectedItems` prop
|
|
44
44
|
And a new selection is created
|
|
45
|
-
Then the '
|
|
45
|
+
Then the 'onSelectedItemsChange' callback is fired
|
|
46
46
|
|
|
47
47
|
Scenario: Menu does not close on selection
|
|
48
48
|
When an item is selected
|
|
49
49
|
Then the 'onSelectedItemsChange' callback fires without closing the menu
|
|
50
50
|
|
|
51
|
-
Scenario Outline: Actions can be triggered from the MultiSelectMenu
|
|
52
|
-
When actionable elements are provided to the MultiSelectMenu
|
|
53
|
-
And an `<action>` is clicked
|
|
54
|
-
Then the corresponding `onClick` should be executed
|
|
55
|
-
|
|
56
|
-
Examples: Action Types
|
|
57
|
-
| action |
|
|
58
|
-
| ---------------- |
|
|
59
|
-
| select button |
|
|
60
|
-
| deselect button |
|
|
61
|
-
|
|
62
51
|
Scenario Outline: MultiSelectMenu checks its types
|
|
63
52
|
When `<propName>` is passed a value
|
|
64
53
|
Then MultiSelectMenu expects it to be of type `<propType>`
|