@sproutsocial/seeds-react-menu 0.1.0 → 0.2.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/.eslintignore +6 -0
- package/.eslintrc.js +6 -2
- package/.turbo/turbo-build.log +10 -10
- package/CHANGELOG.md +23 -1
- package/dist/esm/index.js +243 -217
- package/dist/esm/index.js.map +1 -1
- package/dist/index.d.mts +104 -38
- package/dist/index.d.ts +104 -38
- package/dist/index.js +272 -243
- package/dist/index.js.map +1 -1
- package/package.json +10 -7
- package/src/ActionMenu/ActionMenu.feature +9 -3
- package/src/ActionMenu/ActionMenu.tsx +76 -42
- package/src/ActionMenu/__tests__/ActionMenu.test.tsx +36 -42
- package/src/ActionMenu/__tests__/ActionMenu.typetest.tsx +13 -13
- package/src/MenuContent/MenuContent.tsx +31 -1
- package/src/MenuContent/MenuContentTypes.ts +3 -4
- package/src/MenuContent/styles.tsx +10 -6
- package/src/MenuContext.tsx +1 -6
- package/src/MenuFooter/MenuFooter.feature +1 -1
- package/src/MenuFooter/MenuFooter.tsx +8 -0
- package/src/MenuFooter/__tests__/MenuFooter.test.tsx +53 -26
- package/src/MenuFooter/styles.tsx +1 -1
- package/src/MenuGroup/MenuGroup.feature +14 -26
- package/src/MenuGroup/MenuGroup.tsx +21 -1
- package/src/MenuGroup/MenuGroupTypes.ts +1 -0
- package/src/MenuGroup/__tests__/MenuGroup.test.tsx +41 -6
- package/src/MenuGroup/styles.tsx +4 -3
- package/src/MenuHeader/MenuHeader.tsx +16 -5
- package/src/MenuHeader/__tests__/MenuHeader.test.tsx +50 -6
- package/src/MenuHeader/__tests__/MenuHeader.typetest.tsx +1 -0
- package/src/MenuHeader/styles.tsx +21 -22
- package/src/MenuItem/MenuItem.feature +66 -26
- package/src/MenuItem/MenuItem.tsx +39 -20
- package/src/MenuItem/MenuItemTypes.ts +13 -1
- package/src/MenuItem/__tests__/MenuItem.test.tsx +431 -154
- package/src/MenuItem/__tests__/MenuItem.typetest.tsx +57 -38
- package/src/MenuItem/styles.tsx +44 -3
- package/src/{MenuItemSelectable/MenuItemSelectable.tsx → MenuItem/useOptionProps.tsx} +32 -54
- package/src/MenuRoot/MenuRoot.feature +20 -10
- package/src/MenuRoot/MenuRoot.tsx +14 -18
- package/src/MenuRoot/__tests__/MenuRoot.test.tsx +168 -34
- package/src/MenuRoot/__tests__/MenuRoot.typetest.tsx +10 -10
- package/src/MultiSelectMenu/MultiSelectMenu.feature +2 -13
- package/src/MultiSelectMenu/MultiSelectMenu.tsx +9 -1
- package/src/MultiSelectMenu/__tests__/MultiSelectMenu.test.tsx +154 -153
- package/src/MultiSelectMenu/__tests__/MultiSelectMenu.typetest.tsx +10 -10
- package/src/SingleSelectMenu/SingleSelectMenu.feature +11 -0
- package/src/SingleSelectMenu/SingleSelectMenu.tsx +10 -0
- package/src/SingleSelectMenu/__tests__/SingleSelectMenu.test.tsx +11 -14
- package/src/SingleSelectMenu/__tests__/SingleSelectMenu.typetest.tsx +14 -14
- package/src/__tests__/Menu.test.tsx +20 -16
- package/src/hooks/useItemsFromChildren.tsx +2 -4
- package/src/index.ts +0 -1
- package/src/menuTestingUtils.tsx +7 -4
- package/tsconfig.json +1 -1
- package/src/MenuItemSelectable/MenuItemSelectable.feature +0 -99
- package/src/MenuItemSelectable/MenuItemSelectableTypes.ts +0 -15
- package/src/MenuItemSelectable/__tests__/MenuItemSelectable.test.tsx +0 -303
- package/src/MenuItemSelectable/__tests__/MenuItemSelectable.typetest.tsx +0 -56
- package/src/MenuItemSelectable/index.ts +0 -5
- package/src/MenuItemSelectable/styles.tsx +0 -21
|
@@ -1,41 +1,60 @@
|
|
|
1
1
|
import { MenuItem } from "../MenuItem";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
3
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
4
|
+
function MenuItemTypeTest() {
|
|
5
|
+
return (
|
|
6
|
+
<>
|
|
7
|
+
{/* Valid MenuItem with minimal required props */}
|
|
8
|
+
<MenuItem id="test">Test</MenuItem>
|
|
9
|
+
{/* Valid MenuItem allows href */}
|
|
10
|
+
<MenuItem id="test" href="test.com">
|
|
11
|
+
Test
|
|
12
|
+
</MenuItem>
|
|
13
|
+
{/* Valid MenuItem allows active prop */}
|
|
14
|
+
<MenuItem id="test" active>
|
|
15
|
+
Test
|
|
16
|
+
</MenuItem>
|
|
17
|
+
{/* Valid MenuItem allows active prop false */}
|
|
18
|
+
<MenuItem id="test" active={false}>
|
|
19
|
+
Test
|
|
20
|
+
</MenuItem>
|
|
21
|
+
{/* Valid MenuItem allows inputType prop to be checkbox */}
|
|
22
|
+
<MenuItem id="test" inputType="checkbox">
|
|
23
|
+
Test
|
|
24
|
+
</MenuItem>
|
|
25
|
+
{/* Valid MenuItem allows inputType prop to be radio */}
|
|
26
|
+
<MenuItem id="test" inputType="radio">
|
|
27
|
+
Test
|
|
28
|
+
</MenuItem>
|
|
29
|
+
{/* Valid MenuItem allows inputType prop to be switch */}
|
|
30
|
+
<MenuItem id="test" inputType="switch">
|
|
31
|
+
Test
|
|
32
|
+
</MenuItem>
|
|
33
|
+
{/* Valid MenuItem allows inputType prop to be icon */}
|
|
34
|
+
<MenuItem id="test" inputType="icon">
|
|
35
|
+
Test
|
|
36
|
+
</MenuItem>
|
|
37
|
+
{/* Valid MenuItem allows inputLabelId prop as a string */}
|
|
38
|
+
<MenuItem id="test" inputLabelId="Test Label">
|
|
39
|
+
Test
|
|
40
|
+
</MenuItem>
|
|
15
41
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
/* @ts-expect-error - disabled may not be a string */
|
|
36
|
-
disabled="true"
|
|
37
|
-
>
|
|
38
|
-
Test
|
|
39
|
-
</MenuItem>
|
|
40
|
-
</>
|
|
41
|
-
);
|
|
42
|
+
{/* @ts-expect-error - no children is invalid */}
|
|
43
|
+
<MenuItem id="foo" />
|
|
44
|
+
{/* @ts-expect-error - missing id is invalid */}
|
|
45
|
+
<MenuItem>Test</MenuItem>
|
|
46
|
+
<MenuItem
|
|
47
|
+
/* @ts-expect-error - id may not be a number */
|
|
48
|
+
id={0}
|
|
49
|
+
/* @ts-expect-error - active may not be a string */
|
|
50
|
+
active="true"
|
|
51
|
+
/* @ts-expect-error - inputType must be valid */
|
|
52
|
+
inputType="invalid"
|
|
53
|
+
/* @ts-expect-error - inputLabelId may not be a number */
|
|
54
|
+
inputLabelId={0}
|
|
55
|
+
>
|
|
56
|
+
Test
|
|
57
|
+
</MenuItem>
|
|
58
|
+
</>
|
|
59
|
+
);
|
|
60
|
+
}
|
package/src/MenuItem/styles.tsx
CHANGED
|
@@ -14,20 +14,41 @@ 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;
|
|
19
|
+
`;
|
|
20
|
+
|
|
21
|
+
export const MenuItemContainer = styled.li`
|
|
22
|
+
box-sizing: border-box;
|
|
23
|
+
list-style-type: none;
|
|
24
|
+
margin-left: ${({ theme }) => theme.space[300]};
|
|
25
|
+
margin-right: ${({ theme }) => theme.space[300]};
|
|
26
|
+
transition: all ${({ theme }) => theme.duration["fast"]};
|
|
27
|
+
|
|
28
|
+
&:first-child {
|
|
29
|
+
margin-top: ${({ theme }) => theme.space[300]};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
&:last-child {
|
|
33
|
+
margin-bottom: ${({ theme }) => theme.space[300]};
|
|
34
|
+
}
|
|
17
35
|
`;
|
|
18
36
|
|
|
19
|
-
export const
|
|
37
|
+
export const StyledMenuItem = styled.div<{ $highlighted: boolean }>`
|
|
38
|
+
display: block;
|
|
39
|
+
box-sizing: border-box;
|
|
40
|
+
width: 100%;
|
|
20
41
|
border-radius: ${({ theme }) => theme.radii[500]};
|
|
21
42
|
background-color: ${({ theme }) => theme.colors.listItem.background.base};
|
|
22
43
|
border: 1px solid ${({ theme }) => theme.colors.listItem.background.base};
|
|
23
44
|
color: ${({ theme }) => theme.colors.text.body};
|
|
24
45
|
font-family: ${({ theme }) => theme.fontFamily};
|
|
25
46
|
font-weight: ${({ theme }) => theme.fontWeights.normal};
|
|
47
|
+
text-align: left;
|
|
48
|
+
text-decoration: none;
|
|
26
49
|
padding: ${({ theme }) => theme.space[300]};
|
|
27
|
-
list-style-type: none;
|
|
28
50
|
outline: 0;
|
|
29
51
|
${({ theme }) => theme.typography[200]};
|
|
30
|
-
transition: all ${({ theme }) => theme.duration["fast"]};
|
|
31
52
|
|
|
32
53
|
/* Highlighted */
|
|
33
54
|
${({ $highlighted }) => $highlighted && highlightedStyles}
|
|
@@ -69,3 +90,23 @@ export const MenuItemContainer = styled.li<{ $highlighted: boolean }>`
|
|
|
69
90
|
${layout}
|
|
70
91
|
${space}
|
|
71
92
|
`;
|
|
93
|
+
|
|
94
|
+
export const MenuItemRow = styled.div`
|
|
95
|
+
display: flex;
|
|
96
|
+
align-items: center;
|
|
97
|
+
pointer-events: none;
|
|
98
|
+
`;
|
|
99
|
+
|
|
100
|
+
export const MenuItemAction = styled.div`
|
|
101
|
+
margin-right: ${({ theme }) => theme.space[300]};
|
|
102
|
+
display: flex;
|
|
103
|
+
align-items: center;
|
|
104
|
+
justify-content: center;
|
|
105
|
+
flex-shrink: 0;
|
|
106
|
+
`;
|
|
107
|
+
|
|
108
|
+
export const MenuItemText = styled.div`
|
|
109
|
+
word-break: break-word;
|
|
110
|
+
flex-grow: 1;
|
|
111
|
+
min-width: 0;
|
|
112
|
+
`;
|
|
@@ -6,18 +6,9 @@ import { Radio } from "@sproutsocial/seeds-react-radio";
|
|
|
6
6
|
import { useTheme } from "styled-components";
|
|
7
7
|
import { Box } from "@sproutsocial/seeds-react-box";
|
|
8
8
|
import { MenuContext } from "../MenuContext";
|
|
9
|
-
import { MenuItem } from "../MenuItem";
|
|
10
9
|
|
|
11
|
-
|
|
12
|
-
import {
|
|
13
|
-
MenuItemSelectableAction,
|
|
14
|
-
MenuItemSelectableRow,
|
|
15
|
-
MenuItemSelectableText,
|
|
16
|
-
} from "./styles";
|
|
17
|
-
import type {
|
|
18
|
-
TypeMenuItemSelectableProps,
|
|
19
|
-
InputType,
|
|
20
|
-
} from "./MenuItemSelectableTypes";
|
|
10
|
+
import type { TypeMenuItemProps, InputType } from "./MenuItemTypes";
|
|
11
|
+
import { MenuItemAction, MenuItemRow, MenuItemText } from "./styles";
|
|
21
12
|
|
|
22
13
|
/**
|
|
23
14
|
* SelectionIndicator - handles logic for appropriate rendering of inputs
|
|
@@ -47,7 +38,7 @@ export const SelectionIndicator = ({
|
|
|
47
38
|
onChange={() => {}}
|
|
48
39
|
checked={selected}
|
|
49
40
|
aria-labelledby={labeledBy}
|
|
50
|
-
id={`
|
|
41
|
+
id={`MenuItem-${inputType}-${id}`}
|
|
51
42
|
/>
|
|
52
43
|
);
|
|
53
44
|
case "switch":
|
|
@@ -56,15 +47,15 @@ export const SelectionIndicator = ({
|
|
|
56
47
|
onClick={inputStub}
|
|
57
48
|
checked={!!selected}
|
|
58
49
|
aria-labelledby={labeledBy}
|
|
59
|
-
id={`
|
|
50
|
+
id={`MenuItem-${inputType}-${id}`}
|
|
60
51
|
/>
|
|
61
52
|
);
|
|
62
53
|
case "radio":
|
|
63
54
|
return (
|
|
64
55
|
<Radio
|
|
65
56
|
checked={selected}
|
|
66
|
-
id={`
|
|
67
|
-
name={`
|
|
57
|
+
id={`MenuItem-${inputType}-${id}`}
|
|
58
|
+
name={`MenuItem-${inputType}`}
|
|
68
59
|
aria-labelledby={labeledBy}
|
|
69
60
|
/>
|
|
70
61
|
);
|
|
@@ -80,57 +71,44 @@ export const SelectionIndicator = ({
|
|
|
80
71
|
};
|
|
81
72
|
|
|
82
73
|
/**
|
|
83
|
-
*
|
|
74
|
+
* useOptionProps - props needed for the option menu items
|
|
84
75
|
*/
|
|
85
|
-
export const
|
|
76
|
+
export const useOptionProps = ({
|
|
86
77
|
id,
|
|
87
78
|
children,
|
|
88
79
|
inputType = "icon",
|
|
89
|
-
|
|
80
|
+
inputLabelId,
|
|
90
81
|
...props
|
|
91
|
-
}:
|
|
82
|
+
}: TypeMenuItemProps) => {
|
|
92
83
|
const { selectedItem, selectedItems, isListbox } = useContext(MenuContext);
|
|
93
84
|
const selected = useMemo(
|
|
94
85
|
() =>
|
|
95
86
|
selectedItem?.id === id ||
|
|
96
87
|
!!selectedItems?.find((item) => item?.id === id),
|
|
88
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
97
89
|
[selectedItem, selectedItems]
|
|
98
90
|
);
|
|
99
91
|
|
|
100
|
-
|
|
101
|
-
console.warn(
|
|
102
|
-
"MenuItemSelectable is invalid when isListbox is false (Menu)"
|
|
103
|
-
);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// const roles: Record<InputType, TypeMenuItemRoles> = {
|
|
107
|
-
// checkbox: "menuitemcheckbox",
|
|
108
|
-
// switch: "menuitemcheckbox",
|
|
109
|
-
// radio: "menuitemradio",
|
|
110
|
-
// icon: "option",
|
|
111
|
-
// };
|
|
112
|
-
|
|
113
|
-
const label = inputLabel || `menu-item-child-${id}`;
|
|
92
|
+
const label = inputLabelId || `menu-item-child-${id}`;
|
|
114
93
|
|
|
115
|
-
return
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
);
|
|
94
|
+
return isListbox
|
|
95
|
+
? {
|
|
96
|
+
active: selected,
|
|
97
|
+
["aria-selected"]: selected,
|
|
98
|
+
children: (
|
|
99
|
+
<MenuItemRow>
|
|
100
|
+
<MenuItemAction>
|
|
101
|
+
<SelectionIndicator
|
|
102
|
+
id={id}
|
|
103
|
+
aria-labelledby={label}
|
|
104
|
+
inputType={inputType}
|
|
105
|
+
selected={!!selected}
|
|
106
|
+
/>
|
|
107
|
+
</MenuItemAction>
|
|
108
|
+
<MenuItemText id={label}>{children}</MenuItemText>
|
|
109
|
+
</MenuItemRow>
|
|
110
|
+
),
|
|
111
|
+
...props,
|
|
112
|
+
}
|
|
113
|
+
: {};
|
|
136
114
|
};
|
|
@@ -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
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import React, { useCallback
|
|
1
|
+
import React, { useCallback } from "react";
|
|
2
2
|
import styled from "styled-components";
|
|
3
3
|
import { Popout, type TypePopoutProps } from "@sproutsocial/seeds-react-popout";
|
|
4
4
|
|
|
5
5
|
import { MenuProvider, type TypeMenuProviderProps } from "../MenuContext";
|
|
6
6
|
|
|
7
7
|
export interface TypeMenuRootOwnProps {
|
|
8
|
-
|
|
8
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
9
|
+
menuToggleElement: React.ReactElement<any>;
|
|
9
10
|
popoutProps?: Partial<
|
|
10
11
|
Omit<TypePopoutProps, "isOpen" | "setIsOpen" | "content" | "children">
|
|
11
12
|
>;
|
|
@@ -20,14 +21,21 @@ const CustomPopoutContent = styled(Popout.Content)`
|
|
|
20
21
|
margin-left: 0;
|
|
21
22
|
margin-right: 0;
|
|
22
23
|
`;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @link https://seeds.sproutsocial.com/components/menu-root/
|
|
27
|
+
*
|
|
28
|
+
* @description MenuRoot is a utility component used to handle {@link https://github.com/sproutsocial/seeds/blob/dev/seeds-react/seeds-react-menu/src/MenuContext.tsx | MenuContext}. This component should rarely, if ever, be used directly unless building a custom menu type.
|
|
29
|
+
*/
|
|
30
|
+
|
|
23
31
|
export const MenuRoot = ({
|
|
24
32
|
children,
|
|
25
|
-
|
|
33
|
+
menuToggleElement,
|
|
26
34
|
popoutProps,
|
|
27
35
|
width = "300px",
|
|
28
36
|
...contextProps
|
|
29
37
|
}: TypeMenuRootProps) => {
|
|
30
|
-
const {
|
|
38
|
+
const { isOpen, openMenu, closeMenu } = contextProps;
|
|
31
39
|
const setIsOpen = useCallback(
|
|
32
40
|
(newIsOpen: boolean) => {
|
|
33
41
|
newIsOpen ? openMenu?.() : closeMenu?.();
|
|
@@ -41,19 +49,7 @@ export const MenuRoot = ({
|
|
|
41
49
|
isOpen={!!isOpen}
|
|
42
50
|
setIsOpen={setIsOpen}
|
|
43
51
|
content={
|
|
44
|
-
<CustomPopoutContent width={width}>
|
|
45
|
-
<div
|
|
46
|
-
{...getMenuProps?.(
|
|
47
|
-
{
|
|
48
|
-
...(isListbox ? {} : { role: "menu" }),
|
|
49
|
-
},
|
|
50
|
-
// Suppress ref error for now
|
|
51
|
-
{ suppressRefError: true }
|
|
52
|
-
)}
|
|
53
|
-
>
|
|
54
|
-
{children}
|
|
55
|
-
</div>
|
|
56
|
-
</CustomPopoutContent>
|
|
52
|
+
<CustomPopoutContent width={width}>{children}</CustomPopoutContent>
|
|
57
53
|
}
|
|
58
54
|
focusLockProps={{
|
|
59
55
|
// correct jerking motion when scrolling while the Popout is open
|
|
@@ -63,7 +59,7 @@ export const MenuRoot = ({
|
|
|
63
59
|
disableWrapperAria
|
|
64
60
|
{...popoutProps}
|
|
65
61
|
>
|
|
66
|
-
{
|
|
62
|
+
{menuToggleElement}
|
|
67
63
|
</Popout>
|
|
68
64
|
</MenuProvider>
|
|
69
65
|
);
|
|
@@ -3,8 +3,8 @@ import {
|
|
|
3
3
|
fireEvent,
|
|
4
4
|
render,
|
|
5
5
|
screen,
|
|
6
|
+
waitFor,
|
|
6
7
|
} from "@sproutsocial/seeds-react-testing-library";
|
|
7
|
-
import { Box } from "@sproutsocial/seeds-react-box";
|
|
8
8
|
import { Button } from "@sproutsocial/seeds-react-button";
|
|
9
9
|
import {
|
|
10
10
|
MenuRoot,
|
|
@@ -13,9 +13,11 @@ import {
|
|
|
13
13
|
MenuContext,
|
|
14
14
|
MenuFooter,
|
|
15
15
|
MenuHeader,
|
|
16
|
+
MenuGroup,
|
|
16
17
|
type TypeMenuContextProps,
|
|
17
18
|
} from "../../index";
|
|
18
19
|
|
|
20
|
+
// eslint-disable-next-line react/display-name
|
|
19
21
|
jest.mock("@sproutsocial/seeds-react-portal", () => ({ children }) => (
|
|
20
22
|
<div>{children}</div>
|
|
21
23
|
));
|
|
@@ -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,18 +110,119 @@ 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
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
201
|
+
// @ts-ignore - mock contextProps may be imperfect
|
|
202
|
+
getToggleButtonProps={() => ({ onClick: mockToggleClick })}
|
|
203
|
+
>
|
|
204
|
+
Menu
|
|
205
|
+
</MenuRoot>
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
const menuTrigger = screen.getByRole("button", { name: "Toggle" });
|
|
209
|
+
expect(menuTrigger).toBeInTheDocument();
|
|
210
|
+
await user.click(menuTrigger);
|
|
211
|
+
|
|
212
|
+
expect(mockToggleClick).toHaveBeenCalled();
|
|
213
|
+
});
|
|
214
|
+
|
|
78
215
|
it("renders custom children", () => {
|
|
79
216
|
render(
|
|
80
217
|
<MenuRoot
|
|
81
|
-
|
|
218
|
+
menuToggleElement={<MenuToggleButton>Open Menu</MenuToggleButton>}
|
|
82
219
|
isOpen
|
|
83
220
|
>
|
|
84
221
|
<MenuHeader title="Menu Header" />
|
|
85
222
|
<div>Custom Banner</div>
|
|
86
|
-
<
|
|
223
|
+
<MenuGroup title="Menu Group">
|
|
87
224
|
<MenuItem id="1">Item 1</MenuItem>
|
|
88
|
-
</
|
|
225
|
+
</MenuGroup>
|
|
89
226
|
<MenuFooter>Menu Footer</MenuFooter>
|
|
90
227
|
</MenuRoot>
|
|
91
228
|
);
|
|
@@ -94,14 +231,11 @@ describe("MenuRoot", () => {
|
|
|
94
231
|
expect(screen.getByText("Custom Banner")).toBeInTheDocument();
|
|
95
232
|
expect(screen.getByText("Item 1")).toBeInTheDocument();
|
|
96
233
|
expect(screen.getByText("Menu Footer")).toBeInTheDocument();
|
|
234
|
+
expect(
|
|
235
|
+
screen.getByRole("group", { name: "Menu Group" })
|
|
236
|
+
).toBeInTheDocument();
|
|
97
237
|
});
|
|
98
238
|
|
|
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
239
|
it("can be implemented without using Downshift", () => {
|
|
106
240
|
const randomText = "Some random text";
|
|
107
241
|
const TestCustomMenu = () => {
|
|
@@ -120,7 +254,7 @@ describe("MenuRoot", () => {
|
|
|
120
254
|
|
|
121
255
|
return (
|
|
122
256
|
<MenuRoot
|
|
123
|
-
|
|
257
|
+
menuToggleElement={
|
|
124
258
|
<MenuToggleButton appearance="secondary">
|
|
125
259
|
Open Menu (<TestComponent />)
|
|
126
260
|
</MenuToggleButton>
|
|
@@ -181,7 +315,7 @@ describe("MenuRoot", () => {
|
|
|
181
315
|
({ TestComponent, contextProps, testBtnText, testFn }) => {
|
|
182
316
|
render(
|
|
183
317
|
<MenuRoot
|
|
184
|
-
|
|
318
|
+
menuToggleElement={<MenuToggleButton>Open Menu</MenuToggleButton>}
|
|
185
319
|
isOpen
|
|
186
320
|
{...contextProps}
|
|
187
321
|
>
|