@sproutsocial/seeds-react-menu 1.0.1 → 1.1.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/.turbo/turbo-build.log +85 -9
- package/CHANGELOG.md +32 -0
- package/dist/esm/index.js +505 -122
- package/dist/esm/index.js.map +1 -1
- package/dist/index.d.mts +45 -16
- package/dist/index.d.ts +45 -16
- package/dist/index.js +568 -179
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/src/ActionMenu/ActionMenu.tsx +4 -3
- package/src/ActionMenu/ActionMenuTypes.ts +5 -4
- package/src/ActionMenu/__tests__/ActionMenu.test.tsx +1 -1
- package/src/Autocomplete/Autocomplete.stories.tsx +95 -32
- package/src/Autocomplete/AutocompleteTypes.ts +5 -2
- package/src/Autocomplete/MultiAutocomplete.tsx +3 -2
- package/src/Autocomplete/SingleAutocomplete.tsx +3 -3
- package/src/Autocomplete/index.ts +1 -0
- package/src/ListboxToggleButton.tsx +65 -0
- package/src/MenuContent/MenuContentTypes.ts +1 -2
- package/src/MenuContent/styles.tsx +2 -9
- package/src/MenuGroup/__tests__/MenuGroup.test.tsx +1 -1
- package/src/MenuItem/__tests__/MenuItem.test.tsx +1 -3
- package/src/MenuRoot/MenuRoot.tsx +95 -15
- package/src/MultiSelectMenu/MultiSelectMenu.stories.tsx +0 -1
- package/src/MultiSelectMenu/MultiSelectMenu.tsx +3 -2
- package/src/MultiSelectMenu/MultiSelectMenuTypes.ts +5 -3
- package/src/NestedMenu/NestedMenu.feature +1 -1
- package/src/NestedMenu/NestedMenu.stories.tsx +17 -7
- package/src/NestedMenu/NestedMenu.tsx +217 -43
- package/src/NestedMenu/NestedMenuContext.tsx +16 -0
- package/src/NestedMenu/NestedMenuHeader.tsx +19 -6
- package/src/NestedMenu/NestedMenuPopout.tsx +55 -0
- package/src/NestedMenu/NestedMenuRoot.tsx +27 -0
- package/src/NestedMenu/NestedMenuTypes.ts +3 -2
- package/src/NestedMenu/__tests__/NestedMenu.test.tsx +210 -19
- package/src/SingleSelectMenu/SingleSelectMenu.stories.tsx +0 -1
- package/src/SingleSelectMenu/SingleSelectMenu.tsx +3 -2
- package/src/SingleSelectMenu/SingleSelectMenuTypes.ts +5 -3
- package/src/__tests__/Menu.test.tsx +1 -1
- package/src/index.ts +1 -0
- package/src/storybookUtilities/menu-storybook-components.tsx +11 -31
- package/src/types.ts +5 -1
|
@@ -12,9 +12,11 @@ import {
|
|
|
12
12
|
MenuToggleButton,
|
|
13
13
|
MultiSelectMenu,
|
|
14
14
|
NestedMenu,
|
|
15
|
+
MenuSearchInput,
|
|
15
16
|
} from "../index";
|
|
16
17
|
import { NestedMenuItem } from "./NestedMenuItem";
|
|
17
18
|
import { NestedMenuContent } from "./NestedMenuContent";
|
|
19
|
+
import { MotionConfig } from "motion/react";
|
|
18
20
|
|
|
19
21
|
type CustomNestedMenuArgs = React.ComponentProps<typeof NestedMenu>;
|
|
20
22
|
|
|
@@ -61,7 +63,7 @@ const menus = {
|
|
|
61
63
|
MenuComponent: ActionMenu,
|
|
62
64
|
menuProps: {
|
|
63
65
|
children: (
|
|
64
|
-
<NestedMenuContent>
|
|
66
|
+
<NestedMenuContent id="MenuContentWithChildren">
|
|
65
67
|
<NestedMenuItem
|
|
66
68
|
id="item-2"
|
|
67
69
|
children="Books as ActionMenu"
|
|
@@ -100,6 +102,7 @@ const menus = {
|
|
|
100
102
|
"books-multiselect": {
|
|
101
103
|
MenuComponent: MultiSelectMenu,
|
|
102
104
|
menuProps: {
|
|
105
|
+
itemToString: (item) => item.menuItemProps.children,
|
|
103
106
|
menuItems: TEST_BOOKS.map((book) => ({
|
|
104
107
|
id: book.id,
|
|
105
108
|
children: book.title,
|
|
@@ -107,11 +110,13 @@ const menus = {
|
|
|
107
110
|
},
|
|
108
111
|
menuHeaderProps: {
|
|
109
112
|
title: "Select Books",
|
|
113
|
+
children: <MenuSearchInput id="nested-search" name="nested-search" />,
|
|
110
114
|
},
|
|
111
115
|
},
|
|
112
116
|
"books-select": {
|
|
113
117
|
MenuComponent: SingleSelectMenu,
|
|
114
118
|
menuProps: {
|
|
119
|
+
itemToString: (item) => item.menuItemProps.children,
|
|
115
120
|
menuItems: TEST_BOOKS.map((book) => ({
|
|
116
121
|
id: book.id,
|
|
117
122
|
children: book.title,
|
|
@@ -119,6 +124,7 @@ const menus = {
|
|
|
119
124
|
},
|
|
120
125
|
menuHeaderProps: {
|
|
121
126
|
title: "Select One Book",
|
|
127
|
+
children: <MenuSearchInput id="nested-search" name="nested-search" />,
|
|
122
128
|
},
|
|
123
129
|
},
|
|
124
130
|
"edit-book": {
|
|
@@ -146,12 +152,16 @@ export const NestedMenuMultiLevelStory: Story = {
|
|
|
146
152
|
name: "Multi Level NestedMenu",
|
|
147
153
|
render: (args) => {
|
|
148
154
|
return (
|
|
149
|
-
<
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
+
<MotionConfig reducedMotion="user">
|
|
156
|
+
<NestedMenu
|
|
157
|
+
initialMenuId="root"
|
|
158
|
+
menus={menus}
|
|
159
|
+
backArrowLabel="Go Back"
|
|
160
|
+
menuToggleElement={
|
|
161
|
+
<MenuToggleButton width="200px">Open Menu</MenuToggleButton>
|
|
162
|
+
}
|
|
163
|
+
/>
|
|
164
|
+
</MotionConfig>
|
|
155
165
|
);
|
|
156
166
|
},
|
|
157
167
|
};
|
|
@@ -1,15 +1,27 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { useId, useEffect, useState } from "react";
|
|
2
|
+
import { AnimatePresence, LayoutGroup, motion } from "motion/react";
|
|
3
|
+
import { useSelect } from "downshift";
|
|
4
|
+
|
|
5
|
+
import { useDownshiftDefaults } from "../hooks/useDownshiftDefaults";
|
|
6
|
+
import { reduceReducers } from "../utils/reduceReducers";
|
|
7
|
+
import { nestedMenuStateReducer } from "../reducers/nestedMenuStateReducer";
|
|
8
|
+
import {
|
|
9
|
+
defaultMenuContext,
|
|
10
|
+
type TypeMenuContext,
|
|
11
|
+
type TypeDownshiftSelectProps,
|
|
12
|
+
} from "../MenuContext";
|
|
2
13
|
import type { TypeMenuItemProps } from "../MenuItem";
|
|
3
14
|
import type {
|
|
4
15
|
TypeNestedMenuItemProps,
|
|
5
16
|
TypeNestedMenuProps,
|
|
6
17
|
} from "./NestedMenuTypes";
|
|
7
|
-
import { reduceReducers } from "../utils/reduceReducers";
|
|
8
|
-
import { NestedMenuHeader } from "./NestedMenuHeader";
|
|
9
|
-
import { nestedMenuStateReducer } from "../reducers/nestedMenuStateReducer";
|
|
10
18
|
import { NestedMenuContent } from "./NestedMenuContent";
|
|
11
19
|
import { NestedMenuProvider } from "./NestedMenuContext";
|
|
20
|
+
import { NestedMenuHeader } from "./NestedMenuHeader";
|
|
12
21
|
import { NestedMenuItem } from "./NestedMenuItem";
|
|
22
|
+
import { MOTION_DURATION_FAST } from "@sproutsocial/seeds-motion/unitless";
|
|
23
|
+
import { NestedMenuRoot } from "./NestedMenuRoot";
|
|
24
|
+
import { NestedMenuPopout } from "./NestedMenuPopout";
|
|
13
25
|
|
|
14
26
|
export const NestedMenu = <
|
|
15
27
|
I extends TypeMenuItemProps = TypeNestedMenuItemProps
|
|
@@ -19,16 +31,93 @@ export const NestedMenu = <
|
|
|
19
31
|
onBackButtonClick,
|
|
20
32
|
backArrowLabel,
|
|
21
33
|
menuToggleElement,
|
|
34
|
+
popoutProps = {},
|
|
35
|
+
width = "300px",
|
|
36
|
+
stateReducer: rootExternalStateReducer,
|
|
37
|
+
id: externalNestedMenuId,
|
|
38
|
+
...useSelectProps
|
|
22
39
|
}: TypeNestedMenuProps<I>) => {
|
|
40
|
+
/**
|
|
41
|
+
* Handling the Open/Close and accessibility of the NestedMenu itself
|
|
42
|
+
*/
|
|
43
|
+
const { defaultDownshiftProps, ...restDefaults } = useDownshiftDefaults({
|
|
44
|
+
isCombobox: false,
|
|
45
|
+
});
|
|
46
|
+
const uniqueId = useId();
|
|
47
|
+
const nestedMenuId = externalNestedMenuId ?? `nested-menu-${uniqueId}`;
|
|
48
|
+
const [hasLoadedOnce, setHasLoadedOnce] = useState(false);
|
|
49
|
+
const useSelectReturnProps = useSelect({
|
|
50
|
+
...defaultDownshiftProps.select,
|
|
51
|
+
id: nestedMenuId,
|
|
52
|
+
items: [],
|
|
53
|
+
stateReducer: reduceReducers(
|
|
54
|
+
defaultDownshiftProps.select.stateReducer,
|
|
55
|
+
rootExternalStateReducer
|
|
56
|
+
),
|
|
57
|
+
...useSelectProps,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const rootMenuContextProps = {
|
|
61
|
+
...useSelectProps,
|
|
62
|
+
...useSelectReturnProps,
|
|
63
|
+
...restDefaults,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
if (!useSelectReturnProps.isOpen) {
|
|
68
|
+
setHasLoadedOnce(false);
|
|
69
|
+
} else {
|
|
70
|
+
!hasLoadedOnce && setHasLoadedOnce(true);
|
|
71
|
+
}
|
|
72
|
+
}, [useSelectReturnProps.isOpen]);
|
|
73
|
+
|
|
74
|
+
// onNestedIsOpenChange handles closing the popout based on the selected menu's built in open/close logic.
|
|
75
|
+
const onNestedIsOpenChange: TypeDownshiftSelectProps["onIsOpenChange"] = ({
|
|
76
|
+
isOpen: newIsOpen,
|
|
77
|
+
}) => {
|
|
78
|
+
// Close the popout when a child menu's close logic is triggered
|
|
79
|
+
!newIsOpen && useSelectReturnProps.closeMenu?.();
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const nestedMenuPropsFromRoot: Partial<TypeDownshiftSelectProps> = {
|
|
83
|
+
// The selected menu needs to receive this id so that the toggle element can be properly associated with it.
|
|
84
|
+
id: nestedMenuId,
|
|
85
|
+
onIsOpenChange: onNestedIsOpenChange,
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Handling the state of the NestedMenu content
|
|
90
|
+
*/
|
|
23
91
|
const [selectedMenuPath, setSelectedMenuPath] = useState([initialMenuId]);
|
|
24
|
-
const
|
|
92
|
+
const [activeMenuContext, setActiveMenuContext] =
|
|
93
|
+
useState<TypeMenuContext>(defaultMenuContext);
|
|
94
|
+
const [currentMenuId, setCurrentMenuId] = useState(initialMenuId);
|
|
95
|
+
const [isAdding, setIsAdding] = useState(false);
|
|
25
96
|
|
|
26
97
|
const setSelectedMenuId = (id: string) => {
|
|
98
|
+
setIsAdding(true);
|
|
27
99
|
setSelectedMenuPath((currentPath) => [...currentPath, id]);
|
|
28
100
|
};
|
|
101
|
+
|
|
102
|
+
// Set the currentMenuId reactively after the selectedMenuPath changes, so that
|
|
103
|
+
// isAdding has time to be set before the animation starts.
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
if (selectedMenuPath.length > 0) {
|
|
106
|
+
const newMenuId = selectedMenuPath[selectedMenuPath.length - 1];
|
|
107
|
+
setCurrentMenuId(newMenuId);
|
|
108
|
+
}
|
|
109
|
+
}, [selectedMenuPath.join("-")]);
|
|
110
|
+
|
|
111
|
+
const resetSelectedMenuPath = () => {
|
|
112
|
+
setSelectedMenuPath([initialMenuId]);
|
|
113
|
+
setCurrentMenuId(initialMenuId);
|
|
114
|
+
};
|
|
115
|
+
|
|
29
116
|
const goBack = () => {
|
|
117
|
+
setIsAdding(false);
|
|
30
118
|
setSelectedMenuPath((currentPath) => currentPath.slice(0, -1));
|
|
31
|
-
|
|
119
|
+
// user-defined callback
|
|
120
|
+
onBackButtonClick?.(currentMenuId);
|
|
32
121
|
};
|
|
33
122
|
|
|
34
123
|
const {
|
|
@@ -36,14 +125,14 @@ export const NestedMenu = <
|
|
|
36
125
|
menuProps,
|
|
37
126
|
MenuHeaderComponent = NestedMenuHeader,
|
|
38
127
|
menuHeaderProps = {},
|
|
39
|
-
} = menus[
|
|
128
|
+
} = menus[currentMenuId] || {};
|
|
129
|
+
|
|
40
130
|
const {
|
|
41
131
|
children,
|
|
42
132
|
menuItems,
|
|
43
133
|
MenuItemComponent,
|
|
44
134
|
stateReducer: externalStateReducer,
|
|
45
|
-
|
|
46
|
-
...props
|
|
135
|
+
...restMenuProps
|
|
47
136
|
} = menuProps || {};
|
|
48
137
|
|
|
49
138
|
const stateReducer = reduceReducers(
|
|
@@ -51,52 +140,137 @@ export const NestedMenu = <
|
|
|
51
140
|
externalStateReducer
|
|
52
141
|
);
|
|
53
142
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
143
|
+
const animationSpeed = MOTION_DURATION_FAST;
|
|
144
|
+
|
|
145
|
+
const contentVariants = {
|
|
146
|
+
arrive: (custom = { hasLoadedOnce: false, isAdding: false }) => ({
|
|
147
|
+
opacity: 0,
|
|
148
|
+
// Start from left if is first render of root, else use default
|
|
149
|
+
x: !custom?.hasLoadedOnce ? "200%" : custom?.isAdding ? "200%" : "-200%",
|
|
150
|
+
}),
|
|
151
|
+
enter: (custom = { hasLoadedOnce: false }) => ({
|
|
152
|
+
opacity: 1,
|
|
153
|
+
x: 0,
|
|
154
|
+
transition: {
|
|
155
|
+
type: "tween",
|
|
156
|
+
duration: animationSpeed,
|
|
157
|
+
ease: "circOut",
|
|
158
|
+
delay: !custom.hasLoadedOnce ? animationSpeed : 0,
|
|
159
|
+
},
|
|
160
|
+
}),
|
|
161
|
+
leave: (custom = { hasLoadedOnce: false, isAdding: false }) => ({
|
|
162
|
+
opacity: 0,
|
|
163
|
+
x: custom?.isAdding ? "-200%" : "200%",
|
|
164
|
+
transition: {
|
|
165
|
+
type: "tween",
|
|
166
|
+
duration: animationSpeed,
|
|
167
|
+
ease: "circIn",
|
|
168
|
+
},
|
|
169
|
+
}),
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
// This is suggested even though they are exactly the same
|
|
173
|
+
const menuVariants = {
|
|
174
|
+
arrive: (custom = { hasLoadedOnce: false, isAdding: false }) => ({
|
|
175
|
+
opacity: 0,
|
|
176
|
+
// Start from left if is first render of root, else use default
|
|
177
|
+
x: !custom?.hasLoadedOnce ? "200%" : custom?.isAdding ? "200%" : "-200%",
|
|
178
|
+
}),
|
|
179
|
+
enter: (custom = { hasLoadedOnce: false }) => ({
|
|
180
|
+
opacity: 1,
|
|
181
|
+
x: 0,
|
|
182
|
+
transition: {
|
|
183
|
+
type: "tween",
|
|
184
|
+
duration: animationSpeed,
|
|
185
|
+
ease: "circOut",
|
|
186
|
+
delay: !custom.hasLoadedOnce ? animationSpeed : 0,
|
|
187
|
+
},
|
|
188
|
+
}),
|
|
189
|
+
leave: (custom = { hasLoadedOnce: false, isAdding: false }) => ({
|
|
190
|
+
opacity: 0,
|
|
191
|
+
x: custom?.isAdding ? "-200%" : "200%",
|
|
192
|
+
transition: {
|
|
193
|
+
type: "tween",
|
|
194
|
+
duration: animationSpeed,
|
|
195
|
+
ease: "circIn",
|
|
196
|
+
},
|
|
197
|
+
}),
|
|
63
198
|
};
|
|
64
199
|
|
|
65
200
|
const nestedMenuProps = {
|
|
201
|
+
...nestedMenuPropsFromRoot,
|
|
66
202
|
menuToggleElement,
|
|
67
203
|
stateReducer,
|
|
68
|
-
|
|
69
|
-
|
|
204
|
+
MenuRootComponent: NestedMenuRoot,
|
|
205
|
+
// This ensures that onNestedIsOpenChange will be triggered if the selected menu attempts to close itself.
|
|
206
|
+
initialIsOpen: true,
|
|
70
207
|
children: (
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
208
|
+
// Handles the smooth animating of the content
|
|
209
|
+
<AnimatePresence mode="wait">
|
|
210
|
+
<motion.div
|
|
211
|
+
key={currentMenuId}
|
|
212
|
+
variants={contentVariants}
|
|
213
|
+
initial="arrive"
|
|
214
|
+
animate="enter"
|
|
215
|
+
exit="leave"
|
|
216
|
+
custom={{ isAdding, hasLoadedOnce }}
|
|
217
|
+
>
|
|
218
|
+
{MenuHeaderComponent && (
|
|
219
|
+
<MenuHeaderComponent
|
|
220
|
+
backArrowLabel={backArrowLabel}
|
|
221
|
+
{...menuHeaderProps}
|
|
222
|
+
/>
|
|
223
|
+
)}
|
|
224
|
+
{/*children takes precedence over menuItems if both are passed.*/}
|
|
225
|
+
{children && children}
|
|
226
|
+
{menuItems && !children && (
|
|
227
|
+
<NestedMenuContent
|
|
228
|
+
menuItems={menuItems}
|
|
229
|
+
MenuItemComponent={MenuItemComponent ?? NestedMenuItem}
|
|
230
|
+
/>
|
|
231
|
+
)}
|
|
232
|
+
</motion.div>
|
|
233
|
+
</AnimatePresence>
|
|
88
234
|
),
|
|
89
|
-
...
|
|
235
|
+
...restMenuProps,
|
|
90
236
|
};
|
|
91
237
|
|
|
92
238
|
return (
|
|
93
239
|
<NestedMenuProvider
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
240
|
+
{...{
|
|
241
|
+
selectedMenuPath,
|
|
242
|
+
setSelectedMenuPath,
|
|
243
|
+
setSelectedMenuId,
|
|
244
|
+
resetSelectedMenuPath,
|
|
245
|
+
goBack,
|
|
246
|
+
rootMenuContextProps,
|
|
247
|
+
activeMenuContext,
|
|
248
|
+
setActiveMenuContext,
|
|
249
|
+
animationSpeed,
|
|
250
|
+
}}
|
|
98
251
|
>
|
|
99
|
-
|
|
252
|
+
<NestedMenuPopout
|
|
253
|
+
content={
|
|
254
|
+
MenuComponent && (
|
|
255
|
+
// Handles the rerender of the component
|
|
256
|
+
<AnimatePresence mode="wait">
|
|
257
|
+
<motion.div
|
|
258
|
+
key={`${currentMenuId}-content`}
|
|
259
|
+
variants={menuVariants}
|
|
260
|
+
initial="arrive"
|
|
261
|
+
animate="enter"
|
|
262
|
+
exit="leave"
|
|
263
|
+
custom={{ isAdding, hasLoadedOnce }}
|
|
264
|
+
>
|
|
265
|
+
<MenuComponent {...nestedMenuProps} />
|
|
266
|
+
</motion.div>
|
|
267
|
+
</AnimatePresence>
|
|
268
|
+
)
|
|
269
|
+
}
|
|
270
|
+
menuToggleElement={menuToggleElement}
|
|
271
|
+
width={width}
|
|
272
|
+
{...popoutProps}
|
|
273
|
+
/>
|
|
100
274
|
</NestedMenuProvider>
|
|
101
275
|
);
|
|
102
276
|
};
|
|
@@ -1,18 +1,34 @@
|
|
|
1
1
|
import React, { createContext, useContext } from "react";
|
|
2
2
|
import type { TypeNotEmptyChildren } from "../types";
|
|
3
|
+
import {
|
|
4
|
+
defaultMenuContext,
|
|
5
|
+
type TypeMenuContext,
|
|
6
|
+
type TypeMenuContextProps,
|
|
7
|
+
} from "../MenuContext";
|
|
8
|
+
import { MOTION_DURATION_FAST } from "@sproutsocial/seeds-motion/unitless";
|
|
3
9
|
|
|
4
10
|
export interface TypeNestedMenuContext {
|
|
5
11
|
selectedMenuPath: string[];
|
|
6
12
|
setSelectedMenuId?: (id: string) => void;
|
|
7
13
|
setSelectedMenuPath?: (path: string[]) => void;
|
|
14
|
+
resetSelectedMenuPath?: () => void;
|
|
8
15
|
goBack?: () => void;
|
|
16
|
+
rootMenuContextProps: Omit<TypeMenuContextProps, "items">;
|
|
17
|
+
activeMenuContext: TypeMenuContext;
|
|
18
|
+
setActiveMenuContext: (context: TypeMenuContext) => void;
|
|
19
|
+
animationSpeed: number;
|
|
9
20
|
}
|
|
10
21
|
|
|
11
22
|
const defaultNestedMenuContext: TypeNestedMenuContext = {
|
|
12
23
|
selectedMenuPath: [],
|
|
13
24
|
setSelectedMenuId: () => {},
|
|
14
25
|
setSelectedMenuPath: () => {},
|
|
26
|
+
resetSelectedMenuPath: () => {},
|
|
15
27
|
goBack: () => {},
|
|
28
|
+
rootMenuContextProps: {},
|
|
29
|
+
activeMenuContext: defaultMenuContext,
|
|
30
|
+
setActiveMenuContext: () => {},
|
|
31
|
+
animationSpeed: MOTION_DURATION_FAST,
|
|
16
32
|
};
|
|
17
33
|
|
|
18
34
|
export interface TypeNestedMenuProviderProps extends TypeNestedMenuContext {
|
|
@@ -3,21 +3,34 @@ import { useNestedMenuContext } from "./NestedMenuContext";
|
|
|
3
3
|
import { Button } from "@sproutsocial/seeds-react-button";
|
|
4
4
|
import { Icon } from "@sproutsocial/seeds-react-icon";
|
|
5
5
|
import type { TypeNestedMenuHeaderProps } from "./NestedMenuTypes";
|
|
6
|
+
import { AnimatePresence, motion } from "motion/react";
|
|
6
7
|
|
|
7
8
|
export const NestedMenuHeader = ({
|
|
8
9
|
backArrowLabel,
|
|
9
10
|
...props
|
|
10
11
|
}: TypeNestedMenuHeaderProps) => {
|
|
11
|
-
const { goBack, selectedMenuPath } = useNestedMenuContext();
|
|
12
|
+
const { goBack, selectedMenuPath, animationSpeed } = useNestedMenuContext();
|
|
12
13
|
const showBackArrow = selectedMenuPath.length > 1;
|
|
13
14
|
return (
|
|
14
15
|
<MenuHeader
|
|
15
16
|
leftAction={
|
|
16
|
-
|
|
17
|
-
<
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
<AnimatePresence mode="wait">
|
|
18
|
+
<motion.div
|
|
19
|
+
key={`${selectedMenuPath}-header--button`}
|
|
20
|
+
initial={{ opacity: 0 }}
|
|
21
|
+
animate={{ opacity: 1 }}
|
|
22
|
+
exit={{
|
|
23
|
+
opacity: 0,
|
|
24
|
+
transition: { duration: animationSpeed },
|
|
25
|
+
}}
|
|
26
|
+
>
|
|
27
|
+
{showBackArrow ? (
|
|
28
|
+
<Button onClick={goBack} ariaLabel={backArrowLabel}>
|
|
29
|
+
<Icon name="arrow-left" />
|
|
30
|
+
</Button>
|
|
31
|
+
) : undefined}
|
|
32
|
+
</motion.div>
|
|
33
|
+
</AnimatePresence>
|
|
21
34
|
}
|
|
22
35
|
{...props}
|
|
23
36
|
/>
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { useNestedMenuContext } from "./NestedMenuContext";
|
|
2
|
+
import {
|
|
3
|
+
MenuPopout,
|
|
4
|
+
type TypeMenuPopoutProps,
|
|
5
|
+
type TypeMenuRootProps,
|
|
6
|
+
} from "../MenuRoot";
|
|
7
|
+
import { MenuToggleProvider } from "../MenuContext";
|
|
8
|
+
import { motion } from "motion/react";
|
|
9
|
+
import styled from "styled-components";
|
|
10
|
+
|
|
11
|
+
const StyledDiv = styled(motion.div)`
|
|
12
|
+
overflow: hidden;
|
|
13
|
+
`;
|
|
14
|
+
|
|
15
|
+
export const NestedMenuPopout = ({
|
|
16
|
+
content,
|
|
17
|
+
menuToggleElement,
|
|
18
|
+
width,
|
|
19
|
+
...popoutProps
|
|
20
|
+
}: Omit<TypeMenuPopoutProps, "children"> & {
|
|
21
|
+
menuToggleElement: TypeMenuRootProps["menuToggleElement"];
|
|
22
|
+
}) => {
|
|
23
|
+
const { activeMenuContext, rootMenuContextProps, resetSelectedMenuPath } =
|
|
24
|
+
useNestedMenuContext();
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<MenuPopout
|
|
28
|
+
{...popoutProps}
|
|
29
|
+
// Reset the selected menu path after the close animation completes
|
|
30
|
+
onClose={() => setTimeout(() => resetSelectedMenuPath?.(), 100)}
|
|
31
|
+
isOpen={!!rootMenuContextProps.isOpen}
|
|
32
|
+
openMenu={rootMenuContextProps.openMenu}
|
|
33
|
+
closeMenu={rootMenuContextProps.closeMenu}
|
|
34
|
+
width={width}
|
|
35
|
+
content={(props) => (
|
|
36
|
+
<StyledDiv layout="position">
|
|
37
|
+
{typeof content === "function" ? content(props) : content}
|
|
38
|
+
</StyledDiv>
|
|
39
|
+
)}
|
|
40
|
+
>
|
|
41
|
+
{(toggleProps) => (
|
|
42
|
+
<MenuToggleProvider
|
|
43
|
+
// rootMenuContextProps overrides activeMenuContext since it handles open/close logic
|
|
44
|
+
menuContext={{
|
|
45
|
+
...activeMenuContext,
|
|
46
|
+
...rootMenuContextProps,
|
|
47
|
+
...toggleProps,
|
|
48
|
+
}}
|
|
49
|
+
>
|
|
50
|
+
{menuToggleElement}
|
|
51
|
+
</MenuToggleProvider>
|
|
52
|
+
)}
|
|
53
|
+
</MenuPopout>
|
|
54
|
+
);
|
|
55
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { type TypeMenuRootProps } from "../MenuRoot";
|
|
3
|
+
import { MenuContentProvider, useMenu } from "../MenuContext";
|
|
4
|
+
import { useNestedMenuContext } from "./NestedMenuContext";
|
|
5
|
+
|
|
6
|
+
export const NestedMenuRoot = ({
|
|
7
|
+
children,
|
|
8
|
+
menuToggleElement,
|
|
9
|
+
...contextProps
|
|
10
|
+
}: TypeMenuRootProps) => {
|
|
11
|
+
const menuContext = useMenu(contextProps);
|
|
12
|
+
const { setActiveMenuContext, selectedMenuPath } = useNestedMenuContext();
|
|
13
|
+
|
|
14
|
+
React.useEffect(() => {
|
|
15
|
+
setActiveMenuContext(menuContext);
|
|
16
|
+
}, [
|
|
17
|
+
selectedMenuPath[selectedMenuPath.length - 1],
|
|
18
|
+
menuContext.selectedItem,
|
|
19
|
+
menuContext.selectedItems?.length,
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<MenuContentProvider menuContext={menuContext}>
|
|
24
|
+
{children}
|
|
25
|
+
</MenuContentProvider>
|
|
26
|
+
);
|
|
27
|
+
};
|
|
@@ -4,7 +4,9 @@ import type { TypeSingleSelectMenuProps } from "../SingleSelectMenu";
|
|
|
4
4
|
import type { TypeMenuItemProps } from "../MenuItem";
|
|
5
5
|
import type { TypeMenuRootProps } from "../MenuRoot";
|
|
6
6
|
import type { TypeMenuHeaderProps } from "../MenuHeader";
|
|
7
|
+
import type { TypeActionMenuBaseProps } from "../ActionMenu";
|
|
7
8
|
import type { TypeChildrenOrItems } from "../types";
|
|
9
|
+
import type { TypePopoutProps } from "@sproutsocial/seeds-react-popout";
|
|
8
10
|
|
|
9
11
|
export interface TypeNestedMenuMapProps<
|
|
10
12
|
P,
|
|
@@ -30,9 +32,8 @@ export interface TypeNestedMenuItemProps extends TypeMenuItemProps {
|
|
|
30
32
|
|
|
31
33
|
export interface TypeNestedMenuProps<
|
|
32
34
|
I extends TypeMenuItemProps = TypeNestedMenuItemProps
|
|
33
|
-
> {
|
|
35
|
+
> extends Omit<TypeActionMenuBaseProps, "MenuRootComponent"> {
|
|
34
36
|
initialMenuId: string;
|
|
35
|
-
menuToggleElement: TypeMenuRootProps["menuToggleElement"];
|
|
36
37
|
onBackButtonClick?: (menuId: string) => void;
|
|
37
38
|
/** Sets the default localized label for the back arrow in all child menus.
|
|
38
39
|
* This label can be overridden by including backArrowLabel in a menu's menuHeaderProps. */
|