@sproutsocial/seeds-react-menu 0.4.1 → 0.6.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/.turbo/turbo-build.log +10 -10
- package/CHANGELOG.md +32 -0
- package/dist/esm/index.js +1553 -423
- package/dist/esm/index.js.map +1 -1
- package/dist/index.d.mts +303 -122
- package/dist/index.d.ts +303 -122
- package/dist/index.js +1561 -405
- package/dist/index.js.map +1 -1
- package/package.json +4 -1
- package/src/ActionMenu/ActionMenu.stories.tsx +426 -0
- package/src/ActionMenu/ActionMenu.tsx +30 -26
- package/src/ActionMenu/ActionMenuTypes.ts +13 -8
- package/src/ActionMenu/__tests__/ActionMenu.test.tsx +17 -30
- package/src/ActionMenu/__tests__/ActionMenu.typetest.tsx +32 -3
- package/src/Autocomplete/Autocomplete.stories.tsx +61 -2
- package/src/Autocomplete/Autocomplete.tsx +15 -74
- package/src/Autocomplete/AutocompleteInput.tsx +18 -27
- package/src/Autocomplete/AutocompleteTokenInput.tsx +50 -0
- package/src/Autocomplete/AutocompleteTypes.ts +27 -11
- package/src/Autocomplete/MultiAutocomplete.tsx +189 -0
- package/src/Autocomplete/SingleAutocomplete.tsx +74 -0
- package/src/Autocomplete/__tests__/Autocomplete.test.tsx +171 -3
- package/src/Autocomplete/__tests__/Autocomplete.typetest.tsx +116 -3
- package/src/MenuContent/MenuContent.stories.tsx +164 -0
- package/src/MenuContent/MenuContent.tsx +35 -8
- package/src/MenuContent/MenuContentTypes.ts +7 -5
- package/src/MenuContent/__tests__/MenuContent.typetest.tsx +18 -2
- package/src/MenuContext.tsx +191 -37
- package/src/MenuFooter/MenuFooter.stories.tsx +202 -0
- package/src/MenuFooter/__tests__/MenuFooter.test.tsx +16 -11
- package/src/MenuGroup/MenuGroup.feature +21 -21
- package/src/MenuGroup/MenuGroup.stories.tsx +248 -0
- package/src/MenuGroup/MenuGroup.tsx +9 -7
- package/src/MenuGroup/MenuGroupTypes.ts +8 -6
- package/src/MenuGroup/__tests__/MenuGroup.test.tsx +6 -6
- package/src/MenuGroup/__tests__/MenuGroup.typetest.tsx +23 -4
- package/src/MenuGroup/styles.tsx +4 -2
- package/src/MenuHeader/MenuHeader.stories.tsx +276 -0
- package/src/MenuHeader/MenuHeader.tsx +3 -1
- package/src/MenuHeader/__tests__/MenuHeader.test.tsx +23 -13
- package/src/MenuHeader/styles.tsx +18 -14
- package/src/MenuItem/MenuItem.stories.tsx +507 -0
- package/src/MenuItem/MenuItem.tsx +18 -9
- package/src/MenuItem/MenuItemTypes.ts +9 -4
- package/src/MenuItem/__tests__/MenuItem.test.tsx +6 -6
- package/src/MenuItem/styles.tsx +21 -5
- package/src/MenuItem/useOptionProps.tsx +10 -8
- package/src/MenuLabel.tsx +2 -2
- package/src/MenuRoot/MenuRoot.tsx +37 -23
- package/src/MenuRoot/__tests__/MenuRoot.test.tsx +59 -32
- package/src/MenuSearchInput/MenuSearchInput.tsx +63 -0
- package/src/MenuSearchInput/__tests__/MenuSearchInput.test.tsx +157 -0
- package/src/MenuSearchInput/__tests__/MenuSearchInput.typetest.tsx +31 -0
- package/src/MenuSearchInput/index.ts +1 -0
- package/src/MenuSearchTokenInput/MenuSearchTokenInput.tsx +98 -0
- package/src/MenuSearchTokenInput/__tests__/MenuSearchTokenInput.test.tsx +247 -0
- package/src/MenuSearchTokenInput/__tests__/MenuSearchTokenInput.typetest.tsx +33 -0
- package/src/MenuSearchTokenInput/index.ts +1 -0
- package/src/MenuToggleButton/MenuToggleButton.stories.tsx +72 -0
- package/src/{MenuToggleButton.tsx → MenuToggleButton/MenuToggleButton.tsx} +6 -5
- package/src/MenuToggleButton/index.ts +1 -0
- package/src/MenuTokenInput.tsx +104 -0
- package/src/MultiSelectMenu/MultiSelectMenu.stories.tsx +214 -0
- package/src/MultiSelectMenu/MultiSelectMenu.tsx +44 -67
- package/src/MultiSelectMenu/MultiSelectMenuTypes.ts +9 -5
- package/src/MultiSelectMenu/__tests__/MultiSelectMenu.test.tsx +46 -60
- package/src/MultiSelectMenu/__tests__/MultiSelectMenu.typetest.tsx +29 -2
- package/src/NestedMenu/NestedMenu.feature +109 -0
- package/src/NestedMenu/NestedMenu.stories.tsx +157 -0
- package/src/NestedMenu/NestedMenu.tsx +102 -0
- package/src/NestedMenu/NestedMenuContent.tsx +71 -0
- package/src/NestedMenu/NestedMenuContext.tsx +56 -0
- package/src/NestedMenu/NestedMenuHeader.tsx +25 -0
- package/src/NestedMenu/NestedMenuItem.tsx +37 -0
- package/src/NestedMenu/NestedMenuTypes.ts +47 -0
- package/src/NestedMenu/__tests__/NestedMenu.test.tsx +31 -0
- package/src/NestedMenu/__tests__/NestedMenu.typetest.tsx +134 -0
- package/src/NestedMenu/index.ts +5 -0
- package/src/SingleSelectMenu/SingleSelectMenu.stories.tsx +227 -0
- package/src/SingleSelectMenu/SingleSelectMenu.tsx +29 -25
- package/src/SingleSelectMenu/SingleSelectMenuTypes.ts +9 -5
- package/src/SingleSelectMenu/__tests__/SingleSelectMenu.typetest.tsx +32 -2
- package/src/SubmenuItem/SubmenuItem.feature +82 -0
- package/src/SubmenuItem/SubmenuItem.stories.tsx +163 -0
- package/src/SubmenuItem/SubmenuItem.tsx +254 -0
- package/src/SubmenuItem/SubmenuItemTypes.ts +24 -0
- package/src/SubmenuItem/__tests__/SubmenuItem.test.tsx +3 -0
- package/src/SubmenuItem/__tests__/SubmenuItem.typetest.tsx +112 -0
- package/src/SubmenuItem/index.ts +2 -0
- package/src/__tests__/Menu.test.tsx +38 -0
- package/src/hooks/useDownshiftDefaults.tsx +107 -0
- package/src/hooks/useItemFiltering.tsx +66 -0
- package/src/hooks/useMenuChildren.tsx +279 -0
- package/src/index.ts +7 -1
- package/src/menuTestingUtils.tsx +21 -10
- package/src/reducers/nestedMenuStateReducer.tsx +23 -0
- package/src/reducers/useComboboxStateReducer.tsx +26 -8
- package/src/reducers/useSelectStateReducer.tsx +24 -6
- package/src/storybookUtilities/menu-storybook-components.tsx +5 -3
- package/src/types.ts +38 -5
- package/src/utils/downshiftDefaults.ts +9 -0
- package/src/utils/getMultiSelectOverrides.ts +21 -0
- package/src/utils/reduceReducers.ts +19 -0
- package/src/hooks/useItemsFromChildren.tsx +0 -131
- package/src/utils/itemToString.ts +0 -5
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
MenuContent,
|
|
4
4
|
MenuHeader,
|
|
5
5
|
MenuFooter,
|
|
6
|
+
MenuItem,
|
|
6
7
|
} from "../../index";
|
|
7
8
|
|
|
8
9
|
export const SingleSelectMenuTypeTest = () => (
|
|
@@ -19,6 +20,17 @@ export const SingleSelectMenuTypeTest = () => (
|
|
|
19
20
|
<MenuContent>Test</MenuContent>
|
|
20
21
|
<MenuFooter>Test</MenuFooter>
|
|
21
22
|
</SingleSelectMenu>
|
|
23
|
+
{/* Valid SingleSelectMenu allows menuItems to be passed using the menuItems prop */}
|
|
24
|
+
<SingleSelectMenu
|
|
25
|
+
menuToggleElement={<>test</>}
|
|
26
|
+
menuItems={[{ children: "Item Name", id: "item-1" }]}
|
|
27
|
+
/>
|
|
28
|
+
{/* Valid SingleSelectMenu allows menuItems prop to be rendered using a MenuItemComponent prop */}
|
|
29
|
+
<SingleSelectMenu
|
|
30
|
+
menuToggleElement={<>test</>}
|
|
31
|
+
menuItems={[{ children: "Item Name", id: "item-1" }]}
|
|
32
|
+
MenuItemComponent={MenuItem}
|
|
33
|
+
/>
|
|
22
34
|
{/* Valid SingleSelectMenu accepts Downshift props and functions for useSelect */}
|
|
23
35
|
<SingleSelectMenu
|
|
24
36
|
menuToggleElement={<>test</>}
|
|
@@ -40,8 +52,26 @@ export const SingleSelectMenuTypeTest = () => (
|
|
|
40
52
|
>
|
|
41
53
|
Test
|
|
42
54
|
</SingleSelectMenu>
|
|
43
|
-
{/* @ts-expect-error - when no children provided */}
|
|
44
|
-
<SingleSelectMenu menuToggleElement=
|
|
55
|
+
{/* @ts-expect-error - when no children or items provided */}
|
|
56
|
+
<SingleSelectMenu menuToggleElement={<>test</>} />
|
|
57
|
+
{/* @ts-expect-error - when both children and MenuItemComponent provided */}
|
|
58
|
+
<SingleSelectMenu
|
|
59
|
+
menuToggleElement={<>test</>}
|
|
60
|
+
MenuItemComponent={MenuItem}
|
|
61
|
+
>
|
|
62
|
+
Children
|
|
63
|
+
</SingleSelectMenu>
|
|
64
|
+
{/* @ts-expect-error - when both children and menuItems provided */}
|
|
65
|
+
<SingleSelectMenu menuToggleElement={<>test</>} menuItems={[]}>
|
|
66
|
+
Children
|
|
67
|
+
</SingleSelectMenu>
|
|
68
|
+
<SingleSelectMenu
|
|
69
|
+
menuToggleElement={<>test</>}
|
|
70
|
+
/* @ts-expect-error - when MenuItemComponent is of the wrong type */
|
|
71
|
+
MenuItemComponent={MenuContent}
|
|
72
|
+
>
|
|
73
|
+
Children
|
|
74
|
+
</SingleSelectMenu>
|
|
45
75
|
|
|
46
76
|
<SingleSelectMenu
|
|
47
77
|
menuToggleElement={<>test</>}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
Scenario: SubmenuItem is expandable via click
|
|
2
|
+
Given a SubmenuItem is not disabled
|
|
3
|
+
When the SubmenuItem is clicked
|
|
4
|
+
Then the submenu should be displayed
|
|
5
|
+
|
|
6
|
+
Scenario Outline: SubmenuItem can be expanded via keyboard
|
|
7
|
+
Given a SubmenuItem is focused
|
|
8
|
+
And the SubmenuItem is not disabled
|
|
9
|
+
When the SubmenuItem is triggered via <interaction>
|
|
10
|
+
Then the submenu should be displayed
|
|
11
|
+
|
|
12
|
+
Examples:
|
|
13
|
+
| interaction |
|
|
14
|
+
| pressing the Enter key |
|
|
15
|
+
| pressing the Space key |
|
|
16
|
+
| pressing the Right Arrow key |
|
|
17
|
+
|
|
18
|
+
Scenario Outline: SubmenuItem is disabled
|
|
19
|
+
Given a SubmenuItem is disabled
|
|
20
|
+
When the SubmenuItem is triggered via <interaction>
|
|
21
|
+
Then no events should fire
|
|
22
|
+
And the submenu should not be displayed
|
|
23
|
+
|
|
24
|
+
Examples:
|
|
25
|
+
| interaction |
|
|
26
|
+
| clicking |
|
|
27
|
+
| focusing and pressing the Enter key |
|
|
28
|
+
| focusing and pressing the Space key |
|
|
29
|
+
| focusing and pressing the Right Arrow key |
|
|
30
|
+
|
|
31
|
+
Scenario: SubmenuItem displays an arrow indicator
|
|
32
|
+
Given a SubmenuItem is rendered
|
|
33
|
+
Then it should display an arrow icon indicating a submenu
|
|
34
|
+
|
|
35
|
+
Scenario: SubmenuItem can be collapsed via keyboard
|
|
36
|
+
Given a SubmenuItem is expanded
|
|
37
|
+
And the submenu is displayed
|
|
38
|
+
When the Left Arrow key is pressed
|
|
39
|
+
Then the submenu should be hidden
|
|
40
|
+
|
|
41
|
+
Rule: API
|
|
42
|
+
Background:
|
|
43
|
+
Given an engineer is developing using the component
|
|
44
|
+
|
|
45
|
+
Scenario Outline: SubmenuItem accepts children
|
|
46
|
+
When <child component(s)> provided
|
|
47
|
+
Then the SubmenuItem should render the <component(s)>
|
|
48
|
+
|
|
49
|
+
Examples: child components
|
|
50
|
+
| child component(s) | component(s) |
|
|
51
|
+
| a Link is | Link |
|
|
52
|
+
| a Text is | Text |
|
|
53
|
+
| an Icon is | Icon |
|
|
54
|
+
| a Text then an Icon are | Text and Icon |
|
|
55
|
+
| an Icon and Text are | Icon and Text |
|
|
56
|
+
|
|
57
|
+
Scenario Outline: A SubmenuItem accepts system props
|
|
58
|
+
When a system prop from the category <systemPropCategory> is passed a value
|
|
59
|
+
Then the SubmenuItem will have <systemPropName> styling
|
|
60
|
+
|
|
61
|
+
Examples: System Props
|
|
62
|
+
| systemPropCategory | systemPropName |
|
|
63
|
+
| color | bg |
|
|
64
|
+
| border | borderColor |
|
|
65
|
+
| layout | display |
|
|
66
|
+
| position | zIndex |
|
|
67
|
+
| flexbox | flex |
|
|
68
|
+
| grid | gridGap |
|
|
69
|
+
| space | p |
|
|
70
|
+
|
|
71
|
+
Scenario Outline: SubmenuItem validates prop types
|
|
72
|
+
When any <propName> is passed to the SubmenuItem
|
|
73
|
+
Then the SubmenuItem should check that each <propName> is of the expected <propType>
|
|
74
|
+
|
|
75
|
+
Examples: Prop Types
|
|
76
|
+
| propName | propType |
|
|
77
|
+
| children | ReactNode |
|
|
78
|
+
| disabled | boolean |
|
|
79
|
+
| elevation | low, medium, or high |
|
|
80
|
+
| href | string |
|
|
81
|
+
| onClick | React.MouseEventHandler |
|
|
82
|
+
| role | string |
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { action } from "@storybook/addon-actions";
|
|
3
|
+
import { Box } from "@sproutsocial/seeds-react-box";
|
|
4
|
+
import {
|
|
5
|
+
MenuItem,
|
|
6
|
+
MenuToggleButton,
|
|
7
|
+
MenuContent,
|
|
8
|
+
ActionMenu,
|
|
9
|
+
MenuGroup,
|
|
10
|
+
SubmenuItem,
|
|
11
|
+
type TypeMenuItemProps,
|
|
12
|
+
} from "../index";
|
|
13
|
+
|
|
14
|
+
type SubmenuItemStoryArgs = TypeMenuItemProps & {
|
|
15
|
+
text?: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
type Story = StoryObj<SubmenuItemStoryArgs>;
|
|
19
|
+
|
|
20
|
+
const meta: Meta<SubmenuItemStoryArgs> = {
|
|
21
|
+
title: "Under Development/Submenu",
|
|
22
|
+
decorators: [(Story) => <Story />],
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export default meta;
|
|
26
|
+
|
|
27
|
+
const StoryMenuToggleButton = ({ appearance, triggerText }) => {
|
|
28
|
+
return (
|
|
29
|
+
<MenuToggleButton appearance={appearance} alignSelf="flex-start">
|
|
30
|
+
{triggerText}
|
|
31
|
+
</MenuToggleButton>
|
|
32
|
+
);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const handleOnClick = (e: React.MouseEvent<HTMLElement>, itemName: string) => {
|
|
36
|
+
action(`${itemName}`)(e);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const submenuItems: TypeMenuItemProps[] = [
|
|
40
|
+
{ id: "item1", children: "Option 1" },
|
|
41
|
+
{ id: "item2", children: "Option 2" },
|
|
42
|
+
{ id: "item3", children: "Option 3" },
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
export const SubmenuItemStory: Story = {
|
|
46
|
+
name: "Simple Action Menu",
|
|
47
|
+
render: (args) => {
|
|
48
|
+
return (
|
|
49
|
+
<ActionMenu
|
|
50
|
+
{...args}
|
|
51
|
+
menuToggleElement={
|
|
52
|
+
<StoryMenuToggleButton
|
|
53
|
+
appearance={args["Trigger Appearance"]}
|
|
54
|
+
triggerText={"Button"}
|
|
55
|
+
/>
|
|
56
|
+
}
|
|
57
|
+
onStateChange={(changes) => action("onStateChange")(changes)}
|
|
58
|
+
>
|
|
59
|
+
<MenuContent>
|
|
60
|
+
<MenuGroup id="actions">
|
|
61
|
+
<MenuItem onClick={(e) => handleOnClick(e, "Compose")} id="Compose">
|
|
62
|
+
Compose
|
|
63
|
+
</MenuItem>
|
|
64
|
+
<SubmenuItem
|
|
65
|
+
id="sj"
|
|
66
|
+
MenuComponent={ActionMenu}
|
|
67
|
+
menuProps={{
|
|
68
|
+
menuItems: submenuItems,
|
|
69
|
+
onStateChange: (changes) =>
|
|
70
|
+
action("submenu onStateChange")(changes),
|
|
71
|
+
}}
|
|
72
|
+
>
|
|
73
|
+
Basic Item
|
|
74
|
+
</SubmenuItem>
|
|
75
|
+
<MenuItem
|
|
76
|
+
onClick={(e) => handleOnClick(e, "View Messages")}
|
|
77
|
+
id="View Messages"
|
|
78
|
+
>
|
|
79
|
+
View Messages
|
|
80
|
+
</MenuItem>
|
|
81
|
+
<MenuItem
|
|
82
|
+
onClick={(e) => handleOnClick(e, "Analyze post")}
|
|
83
|
+
id="Analyze post"
|
|
84
|
+
>
|
|
85
|
+
Analyze post
|
|
86
|
+
</MenuItem>
|
|
87
|
+
<MenuItem
|
|
88
|
+
href="https://sproutsocial.com"
|
|
89
|
+
target="_blank"
|
|
90
|
+
id="settings"
|
|
91
|
+
>
|
|
92
|
+
This Opens a Link
|
|
93
|
+
</MenuItem>
|
|
94
|
+
</MenuGroup>
|
|
95
|
+
</MenuContent>
|
|
96
|
+
</ActionMenu>
|
|
97
|
+
);
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const children = (
|
|
102
|
+
<MenuContent>
|
|
103
|
+
<MenuItem id="sub-menu-item-1">Item 1</MenuItem>
|
|
104
|
+
<SubmenuItem
|
|
105
|
+
id="subsub"
|
|
106
|
+
MenuComponent={ActionMenu}
|
|
107
|
+
menuProps={{ menuItems: submenuItems }}
|
|
108
|
+
>
|
|
109
|
+
Another one
|
|
110
|
+
</SubmenuItem>
|
|
111
|
+
</MenuContent>
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
export const NestedSubmenuItemStory: Story = {
|
|
115
|
+
name: "Nested Submenus",
|
|
116
|
+
render: (args) => {
|
|
117
|
+
return (
|
|
118
|
+
<ActionMenu
|
|
119
|
+
{...args}
|
|
120
|
+
menuToggleElement={
|
|
121
|
+
<StoryMenuToggleButton
|
|
122
|
+
appearance={args["Trigger Appearance"]}
|
|
123
|
+
triggerText={"Button"}
|
|
124
|
+
/>
|
|
125
|
+
}
|
|
126
|
+
>
|
|
127
|
+
<MenuContent>
|
|
128
|
+
<MenuGroup id="actions">
|
|
129
|
+
<MenuItem onClick={(e) => handleOnClick(e, "Compose")} id="Compose">
|
|
130
|
+
Compose
|
|
131
|
+
</MenuItem>
|
|
132
|
+
<SubmenuItem
|
|
133
|
+
id="sj"
|
|
134
|
+
MenuComponent={ActionMenu}
|
|
135
|
+
menuProps={{ children }}
|
|
136
|
+
>
|
|
137
|
+
Basic Item
|
|
138
|
+
</SubmenuItem>
|
|
139
|
+
<MenuItem
|
|
140
|
+
onClick={(e) => handleOnClick(e, "View Messages")}
|
|
141
|
+
id="View Messages"
|
|
142
|
+
>
|
|
143
|
+
View Messages
|
|
144
|
+
</MenuItem>
|
|
145
|
+
<MenuItem
|
|
146
|
+
onClick={(e) => handleOnClick(e, "Analyze post")}
|
|
147
|
+
id="Analyze post"
|
|
148
|
+
>
|
|
149
|
+
Analyze post
|
|
150
|
+
</MenuItem>
|
|
151
|
+
<MenuItem
|
|
152
|
+
href="https://sproutsocial.com"
|
|
153
|
+
target="_blank"
|
|
154
|
+
id="settings"
|
|
155
|
+
>
|
|
156
|
+
This Opens a Link
|
|
157
|
+
</MenuItem>
|
|
158
|
+
</MenuGroup>
|
|
159
|
+
</MenuContent>
|
|
160
|
+
</ActionMenu>
|
|
161
|
+
);
|
|
162
|
+
},
|
|
163
|
+
};
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Icon } from "@sproutsocial/seeds-react-icon";
|
|
3
|
+
import { Box } from "@sproutsocial/seeds-react-box";
|
|
4
|
+
import { mergeRefs } from "@sproutsocial/seeds-react-utilities";
|
|
5
|
+
import type { TypeInternalItemProps } from "../types";
|
|
6
|
+
import { getChildrenFromMenuItems } from "../hooks/useMenuChildren";
|
|
7
|
+
import { useMenuContentContext, useMenuToggleContext } from "../MenuContext";
|
|
8
|
+
import {
|
|
9
|
+
MenuItem,
|
|
10
|
+
MenuItemActionRight,
|
|
11
|
+
type TypeMenuItemProps,
|
|
12
|
+
} from "../MenuItem";
|
|
13
|
+
import type {
|
|
14
|
+
TypeSubmenuItemProps,
|
|
15
|
+
TypeSubmenuValidMenuComponentProps,
|
|
16
|
+
} from "./SubmenuItemTypes";
|
|
17
|
+
|
|
18
|
+
const SubmenuItemTrigger = ({
|
|
19
|
+
id,
|
|
20
|
+
children,
|
|
21
|
+
innerRef,
|
|
22
|
+
...rest
|
|
23
|
+
}: TypeMenuItemProps) => {
|
|
24
|
+
const { getToggleButtonProps, getDropdownProps, ref, ariaProps } =
|
|
25
|
+
useMenuToggleContext();
|
|
26
|
+
const toggleRef =
|
|
27
|
+
ref && innerRef
|
|
28
|
+
? mergeRefs([ref, innerRef].filter(Boolean))
|
|
29
|
+
: ref || innerRef;
|
|
30
|
+
return (
|
|
31
|
+
<MenuItem
|
|
32
|
+
{...rest}
|
|
33
|
+
{...getToggleButtonProps?.(
|
|
34
|
+
{
|
|
35
|
+
...(getDropdownProps?.({
|
|
36
|
+
ref: toggleRef,
|
|
37
|
+
preventKeyAction: true,
|
|
38
|
+
}) || { ref: toggleRef }),
|
|
39
|
+
refKey: "innerRef",
|
|
40
|
+
role: "menuitem",
|
|
41
|
+
...ariaProps,
|
|
42
|
+
},
|
|
43
|
+
{ suppressRefError: true }
|
|
44
|
+
)}
|
|
45
|
+
id={id}
|
|
46
|
+
>
|
|
47
|
+
<Box display="flex" justifyContent="space-between">
|
|
48
|
+
{children}
|
|
49
|
+
<MenuItemActionRight>
|
|
50
|
+
<Icon name="chevron-right-outline" />
|
|
51
|
+
</MenuItemActionRight>
|
|
52
|
+
</Box>
|
|
53
|
+
</MenuItem>
|
|
54
|
+
);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const doesRefContainEventTarget = (
|
|
58
|
+
ref: React.RefObject<HTMLElement | undefined>,
|
|
59
|
+
event: MouseEvent
|
|
60
|
+
) => {
|
|
61
|
+
return (
|
|
62
|
+
ref.current &&
|
|
63
|
+
event.target instanceof Node &&
|
|
64
|
+
ref.current.contains(event.target)
|
|
65
|
+
);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export const SubmenuItem = <
|
|
69
|
+
I extends TypeMenuItemProps,
|
|
70
|
+
P extends TypeSubmenuValidMenuComponentProps<I>
|
|
71
|
+
>({
|
|
72
|
+
MenuComponent,
|
|
73
|
+
menuProps: {
|
|
74
|
+
children: menuChildren,
|
|
75
|
+
menuItems,
|
|
76
|
+
MenuItemComponent,
|
|
77
|
+
popoutProps: { popperProps, ...popoutProps } = {},
|
|
78
|
+
onStateChange,
|
|
79
|
+
...menuProps
|
|
80
|
+
},
|
|
81
|
+
// isOpen,
|
|
82
|
+
// onToggleSubmenu,
|
|
83
|
+
id,
|
|
84
|
+
children,
|
|
85
|
+
...rest
|
|
86
|
+
}: TypeSubmenuItemProps<I, P>) => {
|
|
87
|
+
const {
|
|
88
|
+
highlightedIndex,
|
|
89
|
+
setHighlightedIndex,
|
|
90
|
+
itemsMap,
|
|
91
|
+
isOpen: parentIsOpen,
|
|
92
|
+
} = useMenuContentContext();
|
|
93
|
+
const [submenuIsFocused, setSubmenuIsFocused] = React.useState(false);
|
|
94
|
+
const [submenuIsOpen, setSubmenuIsOpen] = React.useState(false);
|
|
95
|
+
const [submenuIsActive, setSubmenuIsActive] = React.useState(false);
|
|
96
|
+
const submenuItemRef = React.useRef<HTMLElement>(null);
|
|
97
|
+
const submenuRef = React.useRef<HTMLDivElement>(null);
|
|
98
|
+
|
|
99
|
+
// get children for both use cases so it can be wrapped in a submenu div
|
|
100
|
+
const menuContent = menuItems
|
|
101
|
+
? getChildrenFromMenuItems({ menuItems, MenuItemComponent })
|
|
102
|
+
: menuChildren;
|
|
103
|
+
|
|
104
|
+
const item = (itemsMap[id] || {
|
|
105
|
+
index: 0,
|
|
106
|
+
id,
|
|
107
|
+
}) as TypeInternalItemProps;
|
|
108
|
+
|
|
109
|
+
// React.useEffect(() => {
|
|
110
|
+
// debugger;
|
|
111
|
+
// // reset isOpen on render
|
|
112
|
+
// setSubmenuIsOpen(false);
|
|
113
|
+
// setSubmenuIsActive(false);
|
|
114
|
+
// }, []);
|
|
115
|
+
|
|
116
|
+
React.useEffect(() => {
|
|
117
|
+
if (highlightedIndex === item.index) {
|
|
118
|
+
// Open submenu when item becomes highlighted
|
|
119
|
+
!submenuIsOpen && setSubmenuIsOpen(true);
|
|
120
|
+
} else if (submenuIsOpen && highlightedIndex !== -1) {
|
|
121
|
+
// Close submenu when another item is highlighted
|
|
122
|
+
setSubmenuIsOpen(false);
|
|
123
|
+
setSubmenuIsActive(false);
|
|
124
|
+
}
|
|
125
|
+
}, [highlightedIndex, item.index, submenuIsOpen, setSubmenuIsOpen]);
|
|
126
|
+
|
|
127
|
+
const onSubmenuStateChange = React.useCallback(
|
|
128
|
+
(state) => {
|
|
129
|
+
if (state.isOpen !== undefined) {
|
|
130
|
+
// Ensures the menu opens and closes when the state changes
|
|
131
|
+
setSubmenuIsOpen(state.isOpen);
|
|
132
|
+
if (!state.isOpen) {
|
|
133
|
+
// Reset the submenu focus and highlighted index when the submenu closes
|
|
134
|
+
setSubmenuIsFocused(false);
|
|
135
|
+
setHighlightedIndex?.(-1);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
onStateChange?.(state);
|
|
139
|
+
},
|
|
140
|
+
[onStateChange, setSubmenuIsOpen]
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
// Callback for navigating the submenu wit harrow keys
|
|
144
|
+
const onSubmenuKeydown = (e: KeyboardEvent): void => {
|
|
145
|
+
switch (e.key) {
|
|
146
|
+
case "ArrowRight":
|
|
147
|
+
case " ":
|
|
148
|
+
case "Enter":
|
|
149
|
+
// Focus the submenu if item highlighted and right arrow key is pressed
|
|
150
|
+
if (!submenuIsFocused && submenuIsOpen) {
|
|
151
|
+
// stop propagation to avoid interacting with other components
|
|
152
|
+
e.stopPropagation();
|
|
153
|
+
setSubmenuIsFocused(true);
|
|
154
|
+
}
|
|
155
|
+
break;
|
|
156
|
+
case "ArrowLeft":
|
|
157
|
+
// Return focus to the parent menu if submenu is focused and left arrow key is pressed
|
|
158
|
+
if (submenuIsFocused) {
|
|
159
|
+
// stop propagation to avoid interacting with other components
|
|
160
|
+
e.stopPropagation();
|
|
161
|
+
setSubmenuIsFocused(false);
|
|
162
|
+
}
|
|
163
|
+
break;
|
|
164
|
+
default:
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const onMenuItemClick = () => {
|
|
170
|
+
const newIsActive = !submenuIsActive;
|
|
171
|
+
// Lock the submenu in an active open state when clicked
|
|
172
|
+
// or close the menu and unlock if it is open
|
|
173
|
+
setSubmenuIsActive(newIsActive);
|
|
174
|
+
setSubmenuIsOpen(newIsActive);
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const onBodyClick = (e: MouseEvent): void => {
|
|
178
|
+
// Prevent closing the parent menu when clicking on the submenu item
|
|
179
|
+
if (doesRefContainEventTarget(submenuItemRef, e)) {
|
|
180
|
+
e.stopPropagation();
|
|
181
|
+
onMenuItemClick();
|
|
182
|
+
}
|
|
183
|
+
// Prevent closing other popouts when clicking on inside the submenu
|
|
184
|
+
// This doesn't work because it's overridden by the logic in Popout the closes when clicked outside
|
|
185
|
+
// if (doesRefContainEventTarget(submenuRef, e)) {
|
|
186
|
+
// e.stopPropagation();
|
|
187
|
+
// }
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
React.useEffect(() => {
|
|
191
|
+
if (document && document.body) {
|
|
192
|
+
document.body.addEventListener("click", onBodyClick, {
|
|
193
|
+
capture: true,
|
|
194
|
+
});
|
|
195
|
+
document.body.addEventListener("keydown", onSubmenuKeydown, {
|
|
196
|
+
capture: true,
|
|
197
|
+
});
|
|
198
|
+
return () => {
|
|
199
|
+
document.body.removeEventListener("click", onBodyClick, {
|
|
200
|
+
capture: true,
|
|
201
|
+
});
|
|
202
|
+
document.body.removeEventListener("keydown", onSubmenuKeydown, {
|
|
203
|
+
capture: true,
|
|
204
|
+
});
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
}, [document, onSubmenuKeydown]);
|
|
208
|
+
|
|
209
|
+
const subMenuContent = (
|
|
210
|
+
<div
|
|
211
|
+
ref={submenuRef}
|
|
212
|
+
onKeyDown={(e) => {
|
|
213
|
+
// prevent keydown on submenu triggering the parent menu
|
|
214
|
+
e.stopPropagation();
|
|
215
|
+
}}
|
|
216
|
+
tabIndex={-1}
|
|
217
|
+
>
|
|
218
|
+
{menuContent}
|
|
219
|
+
</div>
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
return (
|
|
223
|
+
<MenuComponent
|
|
224
|
+
{...({
|
|
225
|
+
...menuProps,
|
|
226
|
+
children: subMenuContent,
|
|
227
|
+
menuToggleElement: (
|
|
228
|
+
<SubmenuItemTrigger
|
|
229
|
+
innerRef={submenuItemRef}
|
|
230
|
+
id={id}
|
|
231
|
+
children={children}
|
|
232
|
+
active={submenuIsActive}
|
|
233
|
+
{...rest}
|
|
234
|
+
/>
|
|
235
|
+
),
|
|
236
|
+
isOpen: submenuIsOpen,
|
|
237
|
+
onStateChange: onSubmenuStateChange,
|
|
238
|
+
popoutProps: {
|
|
239
|
+
placement: "right",
|
|
240
|
+
popperProps: {
|
|
241
|
+
modifiers: {
|
|
242
|
+
offset: {
|
|
243
|
+
offset: "0,18",
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
...popperProps,
|
|
247
|
+
},
|
|
248
|
+
focusOnContent: submenuIsFocused,
|
|
249
|
+
...popoutProps,
|
|
250
|
+
},
|
|
251
|
+
} as P)}
|
|
252
|
+
/>
|
|
253
|
+
);
|
|
254
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { TypeActionMenuProps } from "../ActionMenu";
|
|
2
|
+
import type { TypeSingleSelectMenuProps } from "../SingleSelectMenu";
|
|
3
|
+
import type { TypeMultiSelectMenuProps } from "../MultiSelectMenu";
|
|
4
|
+
import type { TypeMenuItemProps } from "../MenuItem";
|
|
5
|
+
import type { TypeMenuRootProps } from "../MenuRoot";
|
|
6
|
+
import type { TypeChildrenOrItems } from "../types";
|
|
7
|
+
|
|
8
|
+
export type TypeSubmenuValidMenuComponentProps<I extends TypeMenuItemProps> =
|
|
9
|
+
TypeChildrenOrItems<I> &
|
|
10
|
+
(
|
|
11
|
+
| TypeActionMenuProps<I>
|
|
12
|
+
| TypeSingleSelectMenuProps<I>
|
|
13
|
+
| TypeMultiSelectMenuProps<I>
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
export interface TypeSubmenuItemProps<
|
|
17
|
+
I extends TypeMenuItemProps,
|
|
18
|
+
P extends TypeSubmenuValidMenuComponentProps<I>
|
|
19
|
+
> extends TypeMenuItemProps {
|
|
20
|
+
isOpen?: boolean;
|
|
21
|
+
onToggleSubmenu?: (isOpen: boolean) => void;
|
|
22
|
+
MenuComponent: React.ComponentType<P>;
|
|
23
|
+
menuProps: Omit<P, "menuToggleElement">;
|
|
24
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { SubmenuItem } from "../SubmenuItem";
|
|
2
|
+
import { ActionMenu } from "../../index";
|
|
3
|
+
|
|
4
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
5
|
+
function SubmenuItemTypetest() {
|
|
6
|
+
return (
|
|
7
|
+
<>
|
|
8
|
+
{/* Valid SubmenuItem with minimal required props */}
|
|
9
|
+
<SubmenuItem id="test" MenuComponent={ActionMenu} menuProps={{}}>
|
|
10
|
+
Test
|
|
11
|
+
</SubmenuItem>
|
|
12
|
+
{/* Valid SubmenuItem allows href */}
|
|
13
|
+
<SubmenuItem
|
|
14
|
+
id="test"
|
|
15
|
+
href="test.com"
|
|
16
|
+
MenuComponent={ActionMenu}
|
|
17
|
+
menuProps={{}}
|
|
18
|
+
>
|
|
19
|
+
Test
|
|
20
|
+
</SubmenuItem>
|
|
21
|
+
{/* Valid SubmenuItem allows active prop */}
|
|
22
|
+
<SubmenuItem id="test" active MenuComponent={ActionMenu} menuProps={{}}>
|
|
23
|
+
Test
|
|
24
|
+
</SubmenuItem>
|
|
25
|
+
{/* Valid SubmenuItem allows active prop false */}
|
|
26
|
+
<SubmenuItem
|
|
27
|
+
id="test"
|
|
28
|
+
active={false}
|
|
29
|
+
MenuComponent={ActionMenu}
|
|
30
|
+
menuProps={{}}
|
|
31
|
+
>
|
|
32
|
+
Test
|
|
33
|
+
</SubmenuItem>
|
|
34
|
+
{/* Valid SubmenuItem allows inputType prop to be checkbox */}
|
|
35
|
+
<SubmenuItem
|
|
36
|
+
id="test"
|
|
37
|
+
inputType="checkbox"
|
|
38
|
+
MenuComponent={ActionMenu}
|
|
39
|
+
menuProps={{}}
|
|
40
|
+
>
|
|
41
|
+
Test
|
|
42
|
+
</SubmenuItem>
|
|
43
|
+
{/* Valid SubmenuItem allows inputType prop to be radio */}
|
|
44
|
+
<SubmenuItem
|
|
45
|
+
id="test"
|
|
46
|
+
inputType="radio"
|
|
47
|
+
MenuComponent={ActionMenu}
|
|
48
|
+
menuProps={{}}
|
|
49
|
+
>
|
|
50
|
+
Test
|
|
51
|
+
</SubmenuItem>
|
|
52
|
+
{/* Valid SubmenuItem allows inputType prop to be switch */}
|
|
53
|
+
<SubmenuItem
|
|
54
|
+
id="test"
|
|
55
|
+
inputType="switch"
|
|
56
|
+
MenuComponent={ActionMenu}
|
|
57
|
+
menuProps={{}}
|
|
58
|
+
>
|
|
59
|
+
Test
|
|
60
|
+
</SubmenuItem>
|
|
61
|
+
{/* Valid SubmenuItem allows inputType prop to be icon */}
|
|
62
|
+
<SubmenuItem
|
|
63
|
+
id="test"
|
|
64
|
+
inputType="icon"
|
|
65
|
+
MenuComponent={ActionMenu}
|
|
66
|
+
menuProps={{}}
|
|
67
|
+
>
|
|
68
|
+
Test
|
|
69
|
+
</SubmenuItem>
|
|
70
|
+
{/* Valid SubmenuItem allows inputLabelId prop as a string */}
|
|
71
|
+
<SubmenuItem
|
|
72
|
+
id="test"
|
|
73
|
+
inputLabelId="Test Label"
|
|
74
|
+
MenuComponent={ActionMenu}
|
|
75
|
+
menuProps={{}}
|
|
76
|
+
>
|
|
77
|
+
Test
|
|
78
|
+
</SubmenuItem>
|
|
79
|
+
|
|
80
|
+
{/* @ts-expect-error - no children is invalid */}
|
|
81
|
+
<SubmenuItem id="foo" MenuComponent={ActionMenu} menuProps={{}} />
|
|
82
|
+
{/* @ts-expect-error - missing id is invalid */}
|
|
83
|
+
<SubmenuItem MenuComponent={ActionMenu} menuProps={{}}>
|
|
84
|
+
Test
|
|
85
|
+
</SubmenuItem>
|
|
86
|
+
{/* @ts-expect-error - missing MenuComponent is invalid */}
|
|
87
|
+
<SubmenuItem id="foo" menuProps={{}}>
|
|
88
|
+
Test
|
|
89
|
+
</SubmenuItem>
|
|
90
|
+
{/* @ts-expect-error - missing menuProps is invalid */}
|
|
91
|
+
<SubmenuItem id="foo" MenuComponent={ActionMenu}>
|
|
92
|
+
Test
|
|
93
|
+
</SubmenuItem>
|
|
94
|
+
<SubmenuItem
|
|
95
|
+
/* @ts-expect-error - id may not be a number */
|
|
96
|
+
id={0}
|
|
97
|
+
/* @ts-expect-error - active may not be a string */
|
|
98
|
+
active="true"
|
|
99
|
+
/* @ts-expect-error - inputType must be valid */
|
|
100
|
+
inputType="invalid"
|
|
101
|
+
/* @ts-expect-error - inputLabelId may not be a number */
|
|
102
|
+
inputLabelId={0}
|
|
103
|
+
/* @ts-expect-error - non-Menu component is invalid for MenuComponent */
|
|
104
|
+
MenuComponent={SubmenuItem}
|
|
105
|
+
/* @ts-expect-error - menuProps must be an object */
|
|
106
|
+
menuProps={false}
|
|
107
|
+
>
|
|
108
|
+
Test
|
|
109
|
+
</SubmenuItem>
|
|
110
|
+
</>
|
|
111
|
+
);
|
|
112
|
+
}
|