@sproutsocial/seeds-react-menu 0.5.0 → 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 +15 -0
- package/dist/esm/index.js +1486 -440
- package/dist/esm/index.js.map +1 -1
- package/dist/index.d.mts +248 -92
- package/dist/index.d.ts +248 -92
- package/dist/index.js +1500 -429
- 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 +24 -27
- package/src/ActionMenu/ActionMenuTypes.ts +8 -7
- package/src/ActionMenu/__tests__/ActionMenu.test.tsx +17 -8
- package/src/ActionMenu/__tests__/ActionMenu.typetest.tsx +8 -8
- package/src/Autocomplete/Autocomplete.stories.tsx +38 -3
- package/src/Autocomplete/Autocomplete.tsx +15 -80
- package/src/Autocomplete/AutocompleteInput.tsx +18 -27
- package/src/Autocomplete/AutocompleteTokenInput.tsx +50 -0
- package/src/Autocomplete/AutocompleteTypes.ts +25 -12
- 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 +90 -8
- package/src/MenuContent/MenuContent.stories.tsx +164 -0
- package/src/MenuContent/MenuContent.tsx +26 -12
- package/src/MenuContent/MenuContentTypes.ts +6 -4
- package/src/MenuContent/__tests__/MenuContent.typetest.tsx +8 -8
- 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 +16 -16
- package/src/MenuGroup/MenuGroup.stories.tsx +248 -0
- package/src/MenuGroup/MenuGroup.tsx +8 -7
- package/src/MenuGroup/MenuGroupTypes.ts +6 -2
- package/src/MenuGroup/__tests__/MenuGroup.test.tsx +6 -6
- package/src/MenuGroup/__tests__/MenuGroup.typetest.tsx +7 -7
- 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 -8
- 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 +38 -68
- package/src/MultiSelectMenu/MultiSelectMenuTypes.ts +8 -6
- package/src/MultiSelectMenu/__tests__/MultiSelectMenu.test.tsx +46 -17
- package/src/MultiSelectMenu/__tests__/MultiSelectMenu.typetest.tsx +6 -6
- 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 +23 -26
- package/src/SingleSelectMenu/SingleSelectMenuTypes.ts +7 -5
- package/src/SingleSelectMenu/__tests__/SingleSelectMenu.typetest.tsx +6 -6
- 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/hooks/useDownshiftDefaults.tsx +107 -0
- package/src/hooks/useItemFiltering.tsx +66 -0
- package/src/hooks/useMenuChildren.tsx +197 -128
- package/src/index.ts +6 -0
- 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 -30
- package/src/utils/downshiftDefaults.ts +9 -0
- package/src/utils/getMultiSelectOverrides.ts +21 -0
- package/src/utils/reduceReducers.ts +19 -0
- package/src/utils/itemToString.ts +0 -5
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type {
|
|
3
|
+
UseComboboxProps,
|
|
4
|
+
UseSelectProps,
|
|
5
|
+
UseMultipleSelectionProps,
|
|
6
|
+
} from "downshift";
|
|
7
|
+
import type { TypeInternalItemProps } from "../types";
|
|
8
|
+
import type {
|
|
9
|
+
TypeMenuContextBaseProps,
|
|
10
|
+
TypeDownshiftMultiSelectProps,
|
|
11
|
+
} from "../MenuContext";
|
|
12
|
+
import {
|
|
13
|
+
useComboboxStateReducer,
|
|
14
|
+
useComboboxStateReducerForMulti,
|
|
15
|
+
} from "../reducers/useComboboxStateReducer";
|
|
16
|
+
import {
|
|
17
|
+
useSelectStateReducer,
|
|
18
|
+
useSelectStateReducerForMulti,
|
|
19
|
+
} from "../reducers/useSelectStateReducer";
|
|
20
|
+
import { getMultiSelectOverrides } from "../utils/getMultiSelectOverrides";
|
|
21
|
+
|
|
22
|
+
export const defaultUseSelectProps: Partial<
|
|
23
|
+
UseSelectProps<TypeInternalItemProps>
|
|
24
|
+
> = {
|
|
25
|
+
stateReducer: useSelectStateReducer,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const defaultUseComboboxProps: Partial<
|
|
29
|
+
UseComboboxProps<TypeInternalItemProps>
|
|
30
|
+
> = {
|
|
31
|
+
stateReducer: useComboboxStateReducer,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const defaultUseSelectPropsForMulti: Partial<
|
|
35
|
+
UseSelectProps<TypeInternalItemProps>
|
|
36
|
+
> = {
|
|
37
|
+
stateReducer: useSelectStateReducerForMulti,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const defaultUseComboboxPropsForMulti: Partial<
|
|
41
|
+
UseComboboxProps<TypeInternalItemProps>
|
|
42
|
+
> = {
|
|
43
|
+
stateReducer: useComboboxStateReducerForMulti,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export interface TypeUseDownshiftDefaultsReturn
|
|
47
|
+
extends Pick<
|
|
48
|
+
Required<TypeMenuContextBaseProps>,
|
|
49
|
+
"hiddenItemsMap" | "setHiddenItemsMap"
|
|
50
|
+
> {
|
|
51
|
+
defaultDownshiftProps: {
|
|
52
|
+
select: Partial<UseSelectProps<TypeInternalItemProps>>;
|
|
53
|
+
combobox: Partial<UseComboboxProps<TypeInternalItemProps>>;
|
|
54
|
+
multi: Partial<UseMultipleSelectionProps<TypeInternalItemProps>>;
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export const useDownshiftDefaults = ({
|
|
59
|
+
isCombobox = false,
|
|
60
|
+
isMulti = false,
|
|
61
|
+
multiSelectProps = {},
|
|
62
|
+
}: {
|
|
63
|
+
isCombobox?: boolean;
|
|
64
|
+
isMulti?: boolean;
|
|
65
|
+
multiSelectProps?: TypeDownshiftMultiSelectProps;
|
|
66
|
+
} = {}): TypeUseDownshiftDefaultsReturn => {
|
|
67
|
+
const [hiddenItemsMap, setHiddenItemsMap] = React.useState<
|
|
68
|
+
Record<string, boolean>
|
|
69
|
+
>({});
|
|
70
|
+
const isItemDisabled = React.useCallback<
|
|
71
|
+
Required<UseSelectProps<TypeInternalItemProps>>["isItemDisabled"]
|
|
72
|
+
>(
|
|
73
|
+
(item) => Boolean(item.disabled || hiddenItemsMap[item.id]),
|
|
74
|
+
[hiddenItemsMap]
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
hiddenItemsMap,
|
|
79
|
+
setHiddenItemsMap,
|
|
80
|
+
defaultDownshiftProps: {
|
|
81
|
+
select: !isCombobox
|
|
82
|
+
? {
|
|
83
|
+
// disabling while item.hidden so that hidden items are ignored by downshift
|
|
84
|
+
isItemDisabled,
|
|
85
|
+
...(isMulti
|
|
86
|
+
? defaultUseSelectPropsForMulti
|
|
87
|
+
: defaultUseSelectProps),
|
|
88
|
+
}
|
|
89
|
+
: {},
|
|
90
|
+
combobox: isCombobox
|
|
91
|
+
? {
|
|
92
|
+
// disabling while item.hidden so that hidden items are ignored by downshift
|
|
93
|
+
isItemDisabled,
|
|
94
|
+
...(isMulti
|
|
95
|
+
? defaultUseComboboxPropsForMulti
|
|
96
|
+
: defaultUseComboboxProps),
|
|
97
|
+
}
|
|
98
|
+
: {},
|
|
99
|
+
multi: isMulti
|
|
100
|
+
? {
|
|
101
|
+
// handles the prop name mapping for multi select
|
|
102
|
+
...getMultiSelectOverrides(multiSelectProps),
|
|
103
|
+
}
|
|
104
|
+
: {},
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
};
|