@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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sproutsocial/seeds-react-menu",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Seeds React Menu",
|
|
5
5
|
"author": "Sprout Social, Inc.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -14,22 +14,25 @@
|
|
|
14
14
|
"clean:modules": "rm -rf node_modules",
|
|
15
15
|
"typecheck": "tsc --noEmit",
|
|
16
16
|
"test": "jest",
|
|
17
|
-
"test:watch": "jest --watch --coverage=false"
|
|
17
|
+
"test:watch": "jest --watch --coverage=false",
|
|
18
|
+
"lint": "eslint . --ext .js,.jsx,.ts,.tsx",
|
|
19
|
+
"lint:fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix"
|
|
18
20
|
},
|
|
19
21
|
"dependencies": {
|
|
20
22
|
"@sproutsocial/seeds-react-box": "*",
|
|
21
23
|
"@sproutsocial/seeds-react-button": "*",
|
|
22
|
-
"@sproutsocial/seeds-react-icon": "*",
|
|
23
24
|
"@sproutsocial/seeds-react-checkbox": "*",
|
|
25
|
+
"@sproutsocial/seeds-react-icon": "*",
|
|
24
26
|
"@sproutsocial/seeds-react-popout": "*",
|
|
25
27
|
"@sproutsocial/seeds-react-radio": "*",
|
|
26
28
|
"@sproutsocial/seeds-react-switch": "*",
|
|
27
29
|
"@sproutsocial/seeds-react-system-props": "*",
|
|
28
30
|
"@sproutsocial/seeds-react-theme": "*",
|
|
29
|
-
"downshift": "9.0.4"
|
|
31
|
+
"downshift": "9.0.4",
|
|
32
|
+
"motion": "^11.11.17"
|
|
30
33
|
},
|
|
31
34
|
"devDependencies": {
|
|
32
|
-
"
|
|
35
|
+
"eslint-config-seeds": "*",
|
|
33
36
|
"@sproutsocial/seeds-react-testing-library": "*",
|
|
34
37
|
"@sproutsocial/seeds-testing": "*",
|
|
35
38
|
"@types/react": "^18.0.0",
|
|
@@ -37,8 +40,8 @@
|
|
|
37
40
|
"@types/styled-components": "^5.1.26",
|
|
38
41
|
"react": "^18.0.0",
|
|
39
42
|
"react-dom": "^18.0.0",
|
|
40
|
-
"
|
|
41
|
-
"
|
|
43
|
+
"tsup": "^8.0.2",
|
|
44
|
+
"typescript": "^5.2.2"
|
|
42
45
|
},
|
|
43
46
|
"peerDependencies": {
|
|
44
47
|
"react": "^17.0.0 || ^18.0.0",
|
|
@@ -14,7 +14,13 @@ Feature: Action Menu
|
|
|
14
14
|
When an array of MenuItems is passed to the ActionMenu as children
|
|
15
15
|
Then the ActionMenu should render the MenuItems
|
|
16
16
|
|
|
17
|
-
Scenario: ActionMenu automatically closes after an item is
|
|
18
|
-
When a MenuItem is
|
|
17
|
+
Scenario: ActionMenu automatically closes after an item is selected
|
|
18
|
+
When a MenuItem is <selection action>
|
|
19
19
|
And the corresponding onClick executes
|
|
20
|
-
Then the ActionMenu should no longer render
|
|
20
|
+
Then the ActionMenu should no longer render
|
|
21
|
+
|
|
22
|
+
Examples: child components
|
|
23
|
+
| selection action |
|
|
24
|
+
| clicked |
|
|
25
|
+
| activated with space |
|
|
26
|
+
| activated with enter |
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import React from "react";
|
|
1
|
+
import React, { useEffect } from "react";
|
|
2
2
|
import {
|
|
3
3
|
useSelect,
|
|
4
4
|
type UseSelectProps,
|
|
5
5
|
type UseSelectState,
|
|
6
|
-
type
|
|
6
|
+
type UseSelectStateChange,
|
|
7
7
|
} from "downshift";
|
|
8
8
|
import type { TypeDefaultItemProps } from "../types";
|
|
9
9
|
import { useItemsFromChildren } from "../hooks/useItemsFromChildren";
|
|
@@ -28,60 +28,90 @@ export interface TypeActionMenuProps
|
|
|
28
28
|
children: TypeMenuRootProps["children"];
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
31
|
+
// Manually fire the onClick handler when Enter or Space is pressed
|
|
32
|
+
const handleStateChange = (
|
|
33
|
+
changes: UseSelectStateChange<TypeDefaultItemProps>,
|
|
34
|
+
currentSelectedItem: UseSelectState<TypeDefaultItemProps>["selectedItem"]
|
|
35
|
+
) => {
|
|
36
|
+
// if not Enter or Space key, do nothing
|
|
37
|
+
if (
|
|
38
|
+
!(
|
|
39
|
+
changes.type === useSelect.stateChangeTypes.ToggleButtonKeyDownEnter ||
|
|
40
|
+
changes.type === useSelect.stateChangeTypes.ToggleButtonKeyDownSpaceButton
|
|
41
|
+
)
|
|
42
|
+
) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
37
45
|
|
|
38
|
-
|
|
39
|
-
const handleStateChange = (
|
|
40
|
-
changes: UseSelectSelectedItemChange<TypeDefaultItemProps>
|
|
41
|
-
) => {
|
|
42
|
-
const { selectedItem } = changes;
|
|
46
|
+
const selectedItem = changes.selectedItem || currentSelectedItem;
|
|
43
47
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
return;
|
|
47
|
-
}
|
|
48
|
+
// if selectedItem empty, do nothing
|
|
49
|
+
if (!selectedItem) return;
|
|
48
50
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
) {
|
|
54
|
-
// Determine if Enter key was pressed
|
|
55
|
-
const isEnterKeyDown =
|
|
56
|
-
changes.type === useSelect.stateChangeTypes.ToggleButtonKeyDownEnter;
|
|
51
|
+
// if selectedItem is disabled, do nothing
|
|
52
|
+
if (selectedItem.element?.props?.disabled) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
57
55
|
|
|
58
|
-
|
|
59
|
-
|
|
56
|
+
// Determine if Enter key was pressed
|
|
57
|
+
const isEnterKeyDown =
|
|
58
|
+
changes.type === useSelect.stateChangeTypes.ToggleButtonKeyDownEnter;
|
|
60
59
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
key: isEnterKeyDown ? "Enter" : " ",
|
|
64
|
-
code: isEnterKeyDown ? "Enter" : "Space",
|
|
65
|
-
bubbles: true,
|
|
66
|
-
cancelable: true,
|
|
67
|
-
view: window,
|
|
68
|
-
});
|
|
60
|
+
// selectedItem props
|
|
61
|
+
const selectedItemProps = selectedItem.element?.props;
|
|
69
62
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
63
|
+
if (selectedItemProps?.onClick) {
|
|
64
|
+
// Create a synthetic event to pass into the onClick
|
|
65
|
+
const syntheticKeyboardEvent = new KeyboardEvent("keydown", {
|
|
66
|
+
key: isEnterKeyDown ? "Enter" : " ",
|
|
67
|
+
code: isEnterKeyDown ? "Enter" : "Space",
|
|
68
|
+
bubbles: true,
|
|
69
|
+
cancelable: true,
|
|
70
|
+
view: window,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Manually call the onClick handler with a simulated event
|
|
74
|
+
// Cast it to React.MouseEvent to match onClick expectations
|
|
75
|
+
selectedItemProps?.onClick?.(
|
|
76
|
+
syntheticKeyboardEvent as unknown as React.MouseEvent<HTMLElement>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (selectedItemProps?.href) {
|
|
81
|
+
if (selectedItemProps.target) {
|
|
82
|
+
window.open(selectedItemProps.href, selectedItemProps.target);
|
|
83
|
+
} else {
|
|
84
|
+
window.location.href = selectedItemProps?.href;
|
|
75
85
|
}
|
|
76
|
-
}
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* @link https://seeds.sproutsocial.com/components/action-menu/
|
|
91
|
+
*
|
|
92
|
+
* @description An ActionMenu contains a list of clickable items like buttons or links. ActionMenus do not hold a selection.
|
|
93
|
+
*
|
|
94
|
+
* **To select menu items:**
|
|
95
|
+
* @see {@link https://github.com/sproutsocial/seeds/tree/dev/seeds-react/seeds-react-menu/src/SingleSelectMenu | SingleSelectMenu}
|
|
96
|
+
* @see {@link https://github.com/sproutsocial/seeds/tree/dev/seeds-react/seeds-react-menu/src/MultiSelectMenu | MultiSelectMenu}
|
|
97
|
+
*/
|
|
98
|
+
export const ActionMenu = ({
|
|
99
|
+
children,
|
|
100
|
+
stateReducer: externalStateReducer,
|
|
101
|
+
...useSelectProps
|
|
102
|
+
}: TypeActionMenuProps) => {
|
|
103
|
+
const { allMenuItems, menuItemsMap } = useItemsFromChildren({ children });
|
|
104
|
+
const [currentSelectedItem, setCurrentSelectedItem] =
|
|
105
|
+
React.useState<UseSelectState<TypeDefaultItemProps>["selectedItem"]>(null);
|
|
77
106
|
|
|
78
107
|
const { isOpen, ...useSelectReturnProps } = useSelect({
|
|
79
108
|
...defaultUseSelectProps,
|
|
80
109
|
items: allMenuItems,
|
|
81
110
|
itemToString,
|
|
82
|
-
|
|
111
|
+
onStateChange: (changes) => handleStateChange(changes, currentSelectedItem),
|
|
83
112
|
stateReducer: (state, actionAndChanges) => {
|
|
84
113
|
let newState: Partial<UseSelectState<TypeDefaultItemProps>>;
|
|
114
|
+
// eslint-disable-next-line prefer-const, react-hooks/rules-of-hooks
|
|
85
115
|
newState = useSelectStateReducer(state, actionAndChanges);
|
|
86
116
|
return externalStateReducer
|
|
87
117
|
? externalStateReducer(state, {
|
|
@@ -93,6 +123,10 @@ export const ActionMenu = ({
|
|
|
93
123
|
...useSelectProps,
|
|
94
124
|
});
|
|
95
125
|
|
|
126
|
+
useEffect(() => {
|
|
127
|
+
setCurrentSelectedItem(useSelectReturnProps.selectedItem);
|
|
128
|
+
}, [useSelectReturnProps.selectedItem]);
|
|
129
|
+
|
|
96
130
|
return (
|
|
97
131
|
<MenuRoot
|
|
98
132
|
{...{
|
|
@@ -5,18 +5,16 @@ import {
|
|
|
5
5
|
screen,
|
|
6
6
|
waitFor,
|
|
7
7
|
} from "@sproutsocial/seeds-react-testing-library";
|
|
8
|
+
import { PointerEventsCheckLevel } from "@testing-library/user-event";
|
|
9
|
+
import { type UserEvent } from "@testing-library/user-event";
|
|
8
10
|
import { ActionMenu } from "../ActionMenu";
|
|
9
11
|
import { MenuContent, MenuItem, MenuToggleButton } from "../../index";
|
|
10
12
|
|
|
11
13
|
describe("ActionMenu", () => {
|
|
12
|
-
it.skip("extends Menu", () => {
|
|
13
|
-
// run shared tests for all menus
|
|
14
|
-
});
|
|
15
|
-
|
|
16
14
|
it("should render the MenuItems when provided as children", async () => {
|
|
17
15
|
render(
|
|
18
16
|
<ActionMenu
|
|
19
|
-
|
|
17
|
+
menuToggleElement={<MenuToggleButton>Test Menu</MenuToggleButton>}
|
|
20
18
|
>
|
|
21
19
|
<MenuContent>
|
|
22
20
|
<MenuItem id="test-item-1">Item 1</MenuItem>
|
|
@@ -35,11 +33,37 @@ describe("ActionMenu", () => {
|
|
|
35
33
|
expect(screen.getByRole("menuitem", { name: "Item 2" })).toBeVisible();
|
|
36
34
|
});
|
|
37
35
|
|
|
38
|
-
|
|
36
|
+
test.each([
|
|
37
|
+
{
|
|
38
|
+
action: "click",
|
|
39
|
+
activate: async (user: UserEvent) => {
|
|
40
|
+
const firstItem = screen.getByRole("menuitem", {
|
|
41
|
+
name: "Item 1",
|
|
42
|
+
});
|
|
43
|
+
await user
|
|
44
|
+
.setup({
|
|
45
|
+
pointerEventsCheck: PointerEventsCheckLevel.Never,
|
|
46
|
+
})
|
|
47
|
+
.click(firstItem);
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
action: "pressed the Enter key",
|
|
52
|
+
activate: async (user: UserEvent) => {
|
|
53
|
+
await user.keyboard("{ArrowDown}{Enter}");
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
action: "pressed the Space key",
|
|
58
|
+
activate: async (user: UserEvent) => {
|
|
59
|
+
await user.keyboard("{ArrowDown} ");
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
])("should close after a MenuItem is $action", async ({ activate }) => {
|
|
39
63
|
const item1MockClick = jest.fn();
|
|
40
|
-
render(
|
|
64
|
+
const { user } = render(
|
|
41
65
|
<ActionMenu
|
|
42
|
-
|
|
66
|
+
menuToggleElement={<MenuToggleButton>Test Menu</MenuToggleButton>}
|
|
43
67
|
>
|
|
44
68
|
<MenuContent>
|
|
45
69
|
<MenuItem id="test-item-1" onClick={item1MockClick}>
|
|
@@ -51,49 +75,19 @@ describe("ActionMenu", () => {
|
|
|
51
75
|
);
|
|
52
76
|
|
|
53
77
|
const toggleBtn = screen.getByRole("button", { name: "Test Menu" });
|
|
54
|
-
|
|
78
|
+
await user.click(toggleBtn);
|
|
55
79
|
|
|
56
80
|
await waitFor(() => expect(screen.getByRole("menu")).toBeVisible());
|
|
57
81
|
|
|
58
82
|
const item1 = screen.getByRole("menuitem", { name: "Item 1" });
|
|
59
83
|
expect(item1).toBeVisible();
|
|
60
|
-
fireEvent.click(item1);
|
|
61
|
-
|
|
62
|
-
await waitFor(() => {
|
|
63
|
-
expect(item1MockClick).toHaveBeenCalled();
|
|
64
|
-
expect(screen.queryByRole("menu")).not.toBeInTheDocument();
|
|
65
|
-
});
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
// TODO: Should be fixed after onClick bug fixed on menu items
|
|
69
|
-
it.skip("should close after a MenuItem is accessed by the keyboard", async () => {
|
|
70
|
-
const item1MockClick = jest.fn();
|
|
71
|
-
const { user } = render(
|
|
72
|
-
<ActionMenu
|
|
73
|
-
triggerElement={<MenuToggleButton>Test Menu</MenuToggleButton>}
|
|
74
|
-
>
|
|
75
|
-
<MenuContent>
|
|
76
|
-
<MenuItem id="test-item-1" onClick={item1MockClick}>
|
|
77
|
-
Item 1
|
|
78
|
-
</MenuItem>
|
|
79
|
-
<MenuItem id="test-item-2">Item 2</MenuItem>
|
|
80
|
-
</MenuContent>
|
|
81
|
-
</ActionMenu>
|
|
82
|
-
);
|
|
83
|
-
|
|
84
|
-
expect(screen.queryByRole("menu")).not.toBeInTheDocument();
|
|
85
|
-
const toggleBtn = screen.getByRole("button", { name: "Test Menu" });
|
|
86
|
-
fireEvent.click(toggleBtn);
|
|
87
|
-
|
|
88
|
-
await waitFor(() => expect(screen.getByRole("menu")).toBeVisible());
|
|
89
84
|
|
|
90
|
-
await user
|
|
85
|
+
await activate(user);
|
|
91
86
|
|
|
92
|
-
await waitFor(() => {
|
|
93
|
-
expect(item1MockClick).toHaveBeenCalled();
|
|
94
|
-
});
|
|
95
87
|
await waitFor(() => {
|
|
96
88
|
expect(screen.queryByRole("menu")).not.toBeInTheDocument();
|
|
97
89
|
});
|
|
90
|
+
|
|
91
|
+
expect(item1MockClick).toHaveBeenCalled();
|
|
98
92
|
});
|
|
99
93
|
});
|
|
@@ -3,20 +3,20 @@ import { ActionMenu, MenuContent, MenuHeader, MenuFooter } from "../../index";
|
|
|
3
3
|
export const ActionMenuTypeTest = () => (
|
|
4
4
|
<>
|
|
5
5
|
{/* Valid ActionMenu with minimal required props */}
|
|
6
|
-
<ActionMenu
|
|
6
|
+
<ActionMenu menuToggleElement={<>test</>}>Test</ActionMenu>
|
|
7
7
|
{/* Valid ActionMenu allows MenuContent by itself as a child */}
|
|
8
|
-
<ActionMenu
|
|
8
|
+
<ActionMenu menuToggleElement={<>test</>}>
|
|
9
9
|
<MenuContent>Test</MenuContent>
|
|
10
10
|
</ActionMenu>
|
|
11
11
|
{/* Valid ActionMenu allows multiple children without fragment */}
|
|
12
|
-
<ActionMenu
|
|
12
|
+
<ActionMenu menuToggleElement={<>test</>}>
|
|
13
13
|
<MenuHeader>Test</MenuHeader>
|
|
14
14
|
<MenuContent>Test</MenuContent>
|
|
15
15
|
<MenuFooter>Test</MenuFooter>
|
|
16
16
|
</ActionMenu>
|
|
17
17
|
{/* Valid ActionMenu accepts Downshift props and functions for useSelect other than selectedItem */}
|
|
18
18
|
<ActionMenu
|
|
19
|
-
|
|
19
|
+
menuToggleElement={<>test</>}
|
|
20
20
|
onSelectedItemChange={() => {}}
|
|
21
21
|
stateReducer={(c) => c}
|
|
22
22
|
isOpen
|
|
@@ -24,21 +24,21 @@ export const ActionMenuTypeTest = () => (
|
|
|
24
24
|
Test
|
|
25
25
|
</ActionMenu>
|
|
26
26
|
{/* Valid ActionMenu accepts Downshift props and functions for useMultipleSelection */}
|
|
27
|
-
<ActionMenu
|
|
27
|
+
<ActionMenu menuToggleElement={<>test</>}>Test</ActionMenu>
|
|
28
28
|
|
|
29
|
-
{/* @ts-expect-error -
|
|
29
|
+
{/* @ts-expect-error - menuToggleElement is a required prop */}
|
|
30
30
|
<ActionMenu>Test</ActionMenu>
|
|
31
31
|
<ActionMenu
|
|
32
|
-
/* @ts-expect-error -
|
|
33
|
-
|
|
32
|
+
/* @ts-expect-error - menuToggleElement must be a valid react element */
|
|
33
|
+
menuToggleElement="test"
|
|
34
34
|
>
|
|
35
35
|
Test
|
|
36
36
|
</ActionMenu>
|
|
37
37
|
{/* @ts-expect-error - when no children provided */}
|
|
38
|
-
<ActionMenu
|
|
38
|
+
<ActionMenu menuToggleElement="test" />
|
|
39
39
|
|
|
40
40
|
<ActionMenu
|
|
41
|
-
|
|
41
|
+
menuToggleElement={<>test</>}
|
|
42
42
|
/* @ts-expect-error - isOpen can't be a string */
|
|
43
43
|
isOpen="false"
|
|
44
44
|
/* @ts-expect-error - onSelectedItemChange can't be a string */
|
|
@@ -49,21 +49,21 @@ export const ActionMenuTypeTest = () => (
|
|
|
49
49
|
Test
|
|
50
50
|
</ActionMenu>
|
|
51
51
|
<ActionMenu
|
|
52
|
-
|
|
52
|
+
menuToggleElement={<>test</>}
|
|
53
53
|
/* @ts-expect-error - selectedItem is never allowed for ActionMenu */
|
|
54
54
|
selectedItem={null}
|
|
55
55
|
>
|
|
56
56
|
Test
|
|
57
57
|
</ActionMenu>
|
|
58
58
|
<ActionMenu
|
|
59
|
-
|
|
59
|
+
menuToggleElement={<>test</>}
|
|
60
60
|
/* @ts-expect-error - initialSelectedItem is never allowed for ActionMenu */
|
|
61
61
|
initialSelectedItem={null}
|
|
62
62
|
>
|
|
63
63
|
Test
|
|
64
64
|
</ActionMenu>
|
|
65
65
|
<ActionMenu
|
|
66
|
-
|
|
66
|
+
menuToggleElement={<>test</>}
|
|
67
67
|
/* @ts-expect-error - defaultSelectedItem is never allowed for ActionMenu */
|
|
68
68
|
defaultSelectedItem={null}
|
|
69
69
|
>
|
|
@@ -2,12 +2,35 @@ import type { TypeMenuContentProps } from "./MenuContentTypes";
|
|
|
2
2
|
import { StyledMenuContent } from "./styles";
|
|
3
3
|
import { useMenuContext } from "../MenuContext";
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* @link https://seeds.sproutsocial.com/components/menu-content/
|
|
7
|
+
*
|
|
8
|
+
* @description MenuContent is used to contain lists or grouped lists of {@link https://github.com/sproutsocial/seeds/tree/dev/seeds-react/seeds-react-menu/src/MenuItem | MenuItems}.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* <ActionMenu>
|
|
12
|
+
* <MenuHeader>Header</MenuHeader>
|
|
13
|
+
* <MenuContent>
|
|
14
|
+
* <MenuGroup>
|
|
15
|
+
* <MenuItem>Item 1</MenuItem>
|
|
16
|
+
* </MenuGroup>
|
|
17
|
+
* <MenuGroup>
|
|
18
|
+
* <MenuItem>Item 2</MenuItem>
|
|
19
|
+
* </MenuGroup>
|
|
20
|
+
* </MenuContent>
|
|
21
|
+
* <MenuFooter>Footer</MenuFooter>
|
|
22
|
+
* </ActionMenu>
|
|
23
|
+
*
|
|
24
|
+
* @see {@link https://seeds.sproutsocial.com/components/menu-item/ | MenuItem}
|
|
25
|
+
* @see {@link https://seeds.sproutsocial.com/components/menu-group/ | MenuGroup}
|
|
26
|
+
*/
|
|
27
|
+
|
|
5
28
|
const MenuContent = ({
|
|
6
29
|
id = "menu_content",
|
|
7
30
|
children,
|
|
8
31
|
...rest
|
|
9
32
|
}: TypeMenuContentProps) => {
|
|
10
|
-
const { getToggleButtonProps } = useMenuContext();
|
|
33
|
+
const { getToggleButtonProps, getMenuProps, isListbox } = useMenuContext();
|
|
11
34
|
|
|
12
35
|
return (
|
|
13
36
|
<StyledMenuContent
|
|
@@ -18,6 +41,13 @@ const MenuContent = ({
|
|
|
18
41
|
onKeyDown={getToggleButtonProps?.().onKeyDown}
|
|
19
42
|
// apply active descendent updates to container for proper accessibility
|
|
20
43
|
aria-activedescendant={getToggleButtonProps?.()["aria-activedescendant"]}
|
|
44
|
+
{...getMenuProps?.(
|
|
45
|
+
{
|
|
46
|
+
...(isListbox ? {} : { role: "menu" }),
|
|
47
|
+
},
|
|
48
|
+
// Suppress ref error for now
|
|
49
|
+
{ suppressRefError: true }
|
|
50
|
+
)}
|
|
21
51
|
{...rest}
|
|
22
52
|
>
|
|
23
53
|
{children}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import
|
|
2
|
-
import type { ReactNode
|
|
1
|
+
import { type HTMLMotionProps } from "motion/react";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
3
|
|
|
4
|
-
export interface TypeMenuContentProps
|
|
5
|
-
extends React.ButtonHTMLAttributes<HTMLUListElement> {
|
|
4
|
+
export interface TypeMenuContentProps extends HTMLMotionProps<"ul"> {
|
|
6
5
|
children: ReactNode;
|
|
7
6
|
}
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import styled from "styled-components";
|
|
2
|
-
import {
|
|
2
|
+
import { motion } from "motion/react";
|
|
3
|
+
//import { focusRing } from "@sproutsocial/seeds-react-mixins";
|
|
3
4
|
|
|
4
|
-
export const StyledMenuContent = styled.ul`
|
|
5
|
+
export const StyledMenuContent = styled(motion.ul)`
|
|
5
6
|
margin: 0;
|
|
6
|
-
padding:
|
|
7
|
-
&:focus {
|
|
8
|
-
${focusRing}
|
|
9
|
-
}
|
|
7
|
+
padding: 0;
|
|
10
8
|
`;
|
|
9
|
+
|
|
10
|
+
// Removing this for now because it's insanely intrusive and distracting to the user... interestingly the out of the box browser focus ring works better only appears when the content is actually focused.
|
|
11
|
+
|
|
12
|
+
// &:focus {
|
|
13
|
+
// ${focusRing}
|
|
14
|
+
// }
|
package/src/MenuContext.tsx
CHANGED
|
@@ -11,7 +11,7 @@ Feature: Menu Footer
|
|
|
11
11
|
Then the MenuFooter should render the <component(s)>
|
|
12
12
|
|
|
13
13
|
Examples: child components
|
|
14
|
-
| child component(s) | component(s)
|
|
14
|
+
| child component(s) | component(s) |
|
|
15
15
|
| a Link is | Link |
|
|
16
16
|
| a Switch is | Switch |
|
|
17
17
|
| a Button is | Button |
|
|
@@ -2,6 +2,14 @@ import React from "react";
|
|
|
2
2
|
import type { TypeMenuFooterProps } from "./MenuFooterTypes";
|
|
3
3
|
import { StyledMenuFooterBox } from "./styles";
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* @link https://seeds.sproutsocial.com/components/menu-footer/
|
|
7
|
+
*
|
|
8
|
+
* @description MenuFooter is a container used to hold footer content within a Menu.
|
|
9
|
+
*
|
|
10
|
+
* @see {@link https://seeds.sproutsocial.com/components/menu-header/ | MenuHeader}
|
|
11
|
+
*/
|
|
12
|
+
|
|
5
13
|
export const MenuFooter = ({ children, ...rest }: TypeMenuFooterProps) => {
|
|
6
14
|
return <StyledMenuFooterBox {...rest}>{children}</StyledMenuFooterBox>;
|
|
7
15
|
};
|
|
@@ -1,35 +1,69 @@
|
|
|
1
1
|
import { render, screen } from "@sproutsocial/seeds-react-testing-library";
|
|
2
2
|
import { MenuFooter, MenuRoot } from "../../index";
|
|
3
3
|
import { Button } from "@sproutsocial/seeds-react-button";
|
|
4
|
+
// import { Link } from "@sproutsocial/seeds-react-link";
|
|
4
5
|
import { Switch } from "@sproutsocial/seeds-react-switch";
|
|
5
6
|
|
|
6
7
|
describe("MenuFooter API", () => {
|
|
7
8
|
const contextTests = [
|
|
8
9
|
{
|
|
9
|
-
|
|
10
|
-
TestComponent: <
|
|
10
|
+
children: "Link",
|
|
11
|
+
// TestComponent: <Link>Find me!</Link>,
|
|
12
|
+
TestComponent: (
|
|
13
|
+
<Button as="a" href="https://sproutsocial.com">
|
|
14
|
+
Find me!
|
|
15
|
+
</Button>
|
|
16
|
+
),
|
|
17
|
+
test: () => {
|
|
18
|
+
expect(screen.getByText("Hello world!")).toContainElement(
|
|
19
|
+
screen.getByRole("link", { name: "Find me!" })
|
|
20
|
+
);
|
|
21
|
+
},
|
|
11
22
|
},
|
|
12
23
|
{
|
|
13
|
-
|
|
24
|
+
children: "Switch",
|
|
14
25
|
TestComponent: (
|
|
15
|
-
<Switch
|
|
16
|
-
role="button"
|
|
17
|
-
checked={false}
|
|
18
|
-
onClick={() => {}}
|
|
19
|
-
aria-label="Find me!"
|
|
20
|
-
/>
|
|
26
|
+
<Switch checked={false} onClick={() => {}} aria-label="Find me!" />
|
|
21
27
|
),
|
|
28
|
+
test: () => {
|
|
29
|
+
expect(screen.getByText("Hello world!")).toContainElement(
|
|
30
|
+
screen.getByRole("switch", { name: "Find me!" })
|
|
31
|
+
);
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
children: "Button",
|
|
36
|
+
TestComponent: <Button>Find me!</Button>,
|
|
37
|
+
test: () => {
|
|
38
|
+
expect(screen.getByText("Hello world!")).toContainElement(
|
|
39
|
+
screen.getByRole("button", { name: "Find me!" })
|
|
40
|
+
);
|
|
41
|
+
},
|
|
22
42
|
},
|
|
23
43
|
{
|
|
24
|
-
|
|
25
|
-
TestComponent:
|
|
44
|
+
children: "Link and Button",
|
|
45
|
+
TestComponent: (
|
|
46
|
+
<>
|
|
47
|
+
<Button>Find me!</Button>
|
|
48
|
+
<Button as="a" href="https://sproutsocial.com">
|
|
49
|
+
Find me too!
|
|
50
|
+
</Button>
|
|
51
|
+
</>
|
|
52
|
+
),
|
|
53
|
+
test: () => {
|
|
54
|
+
expect(screen.getByText("Hello world!")).toContainElement(
|
|
55
|
+
screen.getByRole("button", { name: "Find me!" })
|
|
56
|
+
);
|
|
57
|
+
expect(screen.getByText("Hello world!")).toContainElement(
|
|
58
|
+
screen.getByRole("link", { name: "Find me too!" })
|
|
59
|
+
);
|
|
60
|
+
},
|
|
26
61
|
},
|
|
27
62
|
];
|
|
28
63
|
|
|
29
64
|
it.each(contextTests)(
|
|
30
|
-
"$
|
|
31
|
-
|
|
32
|
-
({ child, TestComponent }) => {
|
|
65
|
+
"$children rendered when $children provided",
|
|
66
|
+
({ TestComponent, test }) => {
|
|
33
67
|
render(
|
|
34
68
|
<MenuFooter>
|
|
35
69
|
Hello world!
|
|
@@ -37,21 +71,17 @@ describe("MenuFooter API", () => {
|
|
|
37
71
|
</MenuFooter>
|
|
38
72
|
);
|
|
39
73
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const parentComponent = screen.getByText("Hello world!");
|
|
43
|
-
expect(parentComponent).toContainElement(testBtn);
|
|
74
|
+
test();
|
|
44
75
|
}
|
|
45
76
|
);
|
|
46
77
|
|
|
47
78
|
it.each(contextTests)(
|
|
48
|
-
"$
|
|
49
|
-
|
|
50
|
-
({ child, TestComponent }) => {
|
|
79
|
+
"$children rendered when $children provided in MenuRoot",
|
|
80
|
+
({ TestComponent, test }) => {
|
|
51
81
|
render(
|
|
52
82
|
<MenuRoot
|
|
53
83
|
isOpen
|
|
54
|
-
|
|
84
|
+
menuToggleElement={<Button onClick={jest.fn()}>Open</Button>}
|
|
55
85
|
>
|
|
56
86
|
<MenuFooter>
|
|
57
87
|
Hello world!
|
|
@@ -60,10 +90,7 @@ describe("MenuFooter API", () => {
|
|
|
60
90
|
</MenuRoot>
|
|
61
91
|
);
|
|
62
92
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const parentComponent = screen.getByText("Hello world!");
|
|
66
|
-
expect(parentComponent).toContainElement(testBtn);
|
|
93
|
+
test();
|
|
67
94
|
}
|
|
68
95
|
);
|
|
69
96
|
|
|
@@ -2,6 +2,6 @@ import styled from "styled-components";
|
|
|
2
2
|
import { Box } from "@sproutsocial/seeds-react-box";
|
|
3
3
|
|
|
4
4
|
export const StyledMenuFooterBox = styled(Box)`
|
|
5
|
-
padding: ${(p) => p.theme.space[
|
|
5
|
+
padding: ${(p) => p.theme.space[400]};
|
|
6
6
|
border-top: 1px solid ${(p) => p.theme.colors.container.border.base};
|
|
7
7
|
`;
|