@sproutsocial/seeds-react-menu 0.5.0 → 1.0.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 +72 -0
- package/dist/esm/index.js +1529 -440
- package/dist/esm/index.js.map +1 -1
- package/dist/index.d.mts +275 -92
- package/dist/index.d.ts +275 -92
- package/dist/index.js +1558 -444
- package/dist/index.js.map +1 -1
- package/package.json +15 -12
- package/src/ActionMenu/ActionMenu.stories.tsx +422 -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 +188 -0
- package/src/Autocomplete/SingleAutocomplete.tsx +74 -0
- package/src/Autocomplete/__tests__/Autocomplete.test.tsx +207 -3
- package/src/Autocomplete/__tests__/Autocomplete.typetest.tsx +90 -8
- package/src/MenuContent/MenuContent.stories.tsx +164 -0
- package/src/MenuContent/MenuContent.tsx +28 -12
- package/src/MenuContent/MenuContentTypes.ts +6 -4
- package/src/MenuContent/__tests__/MenuContent.typetest.tsx +8 -8
- package/src/MenuContext.tsx +192 -37
- package/src/MenuFooter/MenuFooter.stories.tsx +202 -0
- package/src/MenuFooter/__tests__/MenuFooter.test.tsx +16 -11
- package/src/MenuFooter/styles.tsx +1 -1
- package/src/MenuGroup/MenuGroup.feature +16 -16
- package/src/MenuGroup/MenuGroup.stories.tsx +248 -0
- package/src/MenuGroup/MenuGroup.tsx +12 -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 +12 -7
- package/src/MenuHeader/__tests__/MenuHeader.test.tsx +23 -13
- package/src/MenuHeader/styles.tsx +22 -14
- package/src/MenuItem/MenuItem.stories.tsx +507 -0
- package/src/MenuItem/MenuItem.tsx +23 -9
- package/src/MenuItem/MenuItemTypes.ts +9 -4
- package/src/MenuItem/__tests__/MenuItem.test.tsx +6 -6
- package/src/MenuItem/styles.tsx +23 -5
- package/src/MenuItem/useOptionProps.tsx +11 -9
- package/src/MenuLabel.tsx +2 -2
- package/src/MenuRoot/MenuRoot.tsx +52 -24
- package/src/MenuRoot/__tests__/MenuRoot.test.tsx +59 -32
- package/src/MenuSearchInput/MenuSearchInput.tsx +85 -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 +108 -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 +222 -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 +75 -0
- package/src/NestedMenu/NestedMenuContext.tsx +56 -0
- package/src/NestedMenu/NestedMenuHeader.tsx +25 -0
- package/src/NestedMenu/NestedMenuItem.tsx +41 -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 +233 -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 +258 -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 +117 -0
- package/src/hooks/useItemFiltering.tsx +68 -0
- package/src/hooks/useMenuChildren.tsx +205 -131
- 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
package/src/MenuContext.tsx
CHANGED
|
@@ -1,9 +1,4 @@
|
|
|
1
|
-
import React, {
|
|
2
|
-
createContext,
|
|
3
|
-
useMemo,
|
|
4
|
-
useContext,
|
|
5
|
-
type RefObject,
|
|
6
|
-
} from "react";
|
|
1
|
+
import React, { createContext, useMemo, useContext } from "react";
|
|
7
2
|
import type {
|
|
8
3
|
UseComboboxReturnValue,
|
|
9
4
|
UseComboboxProps,
|
|
@@ -12,35 +7,40 @@ import type {
|
|
|
12
7
|
UseMultipleSelectionReturnValue,
|
|
13
8
|
UseMultipleSelectionProps,
|
|
14
9
|
} from "downshift";
|
|
15
|
-
import type {
|
|
10
|
+
import type { TypeTokenInputProps } from "@sproutsocial/seeds-react-token-input";
|
|
11
|
+
import type {
|
|
12
|
+
TypeInternalItemProps,
|
|
13
|
+
TypeNotEmptyChildren,
|
|
14
|
+
TypeDownshiftMergedInputProps,
|
|
15
|
+
} from "./types";
|
|
16
16
|
|
|
17
17
|
export interface TypeDownshiftComboboxProps
|
|
18
|
-
extends Partial<UseComboboxProps<
|
|
18
|
+
extends Partial<UseComboboxProps<TypeInternalItemProps>> {}
|
|
19
19
|
|
|
20
20
|
export interface TypeDownshiftSelectProps
|
|
21
|
-
extends Partial<UseSelectProps<
|
|
21
|
+
extends Partial<UseSelectProps<TypeInternalItemProps>> {}
|
|
22
22
|
|
|
23
23
|
export interface TypeDownshiftMultiSelectProps
|
|
24
24
|
extends Partial<
|
|
25
25
|
Omit<
|
|
26
|
-
UseMultipleSelectionProps<
|
|
26
|
+
UseMultipleSelectionProps<TypeInternalItemProps>,
|
|
27
27
|
"stateReducer" | "onStateChange" | "getA11yStatusMessage"
|
|
28
28
|
>
|
|
29
29
|
> {
|
|
30
30
|
// Multi Select Props with conflicting useSelect prop names
|
|
31
|
-
getA11yMultiStatusMessage?: UseMultipleSelectionProps<
|
|
32
|
-
onMultiSelectStateChange?: UseMultipleSelectionProps<
|
|
33
|
-
multiSelectStateReducer?: UseMultipleSelectionProps<
|
|
31
|
+
getA11yMultiStatusMessage?: UseMultipleSelectionProps<TypeInternalItemProps>["getA11yStatusMessage"];
|
|
32
|
+
onMultiSelectStateChange?: UseMultipleSelectionProps<TypeInternalItemProps>["onStateChange"];
|
|
33
|
+
multiSelectStateReducer?: UseMultipleSelectionProps<TypeInternalItemProps>["stateReducer"];
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
export interface TypeDownshiftComboboxReturnValue
|
|
37
|
-
extends Partial<UseComboboxReturnValue<
|
|
37
|
+
extends Partial<UseComboboxReturnValue<TypeInternalItemProps>> {}
|
|
38
38
|
|
|
39
39
|
export interface TypeDownshiftSelectReturnValue
|
|
40
|
-
extends Partial<UseSelectReturnValue<
|
|
40
|
+
extends Partial<UseSelectReturnValue<TypeInternalItemProps>> {}
|
|
41
41
|
|
|
42
42
|
export interface TypeDownshiftMultiSelectReturnValue
|
|
43
|
-
extends Partial<UseMultipleSelectionReturnValue<
|
|
43
|
+
extends Partial<UseMultipleSelectionReturnValue<TypeInternalItemProps>> {}
|
|
44
44
|
|
|
45
45
|
type TypeDonwshiftSharedReturnValue =
|
|
46
46
|
| "getLabelProps"
|
|
@@ -52,7 +52,21 @@ type TypeDonwshiftSharedProps =
|
|
|
52
52
|
| "onSelectedItemChange"
|
|
53
53
|
| "onStateChange"
|
|
54
54
|
| "stateReducer";
|
|
55
|
-
|
|
55
|
+
|
|
56
|
+
export interface TypeMenuContextBaseProps {
|
|
57
|
+
itemsMap?: { [key: string]: TypeInternalItemProps };
|
|
58
|
+
isListbox?: boolean;
|
|
59
|
+
hiddenItemsMap?: Record<string, boolean>;
|
|
60
|
+
hiddenItemsCount?: number;
|
|
61
|
+
setHiddenItemsMap?: React.Dispatch<
|
|
62
|
+
React.SetStateAction<Record<string, boolean>>
|
|
63
|
+
>;
|
|
64
|
+
subMenuId?: string;
|
|
65
|
+
setSubMenuId?: (id: string) => void;
|
|
66
|
+
contentRef?: React.RefObject<HTMLUListElement>;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
interface TypeAllDownshiftProps
|
|
56
70
|
extends Omit<
|
|
57
71
|
TypeDownshiftComboboxReturnValue,
|
|
58
72
|
TypeDonwshiftSharedReturnValue
|
|
@@ -62,9 +76,6 @@ export interface TypeMenuContextProps
|
|
|
62
76
|
Omit<TypeDownshiftSelectProps, TypeDonwshiftSharedProps>,
|
|
63
77
|
TypeDownshiftMultiSelectReturnValue,
|
|
64
78
|
TypeDownshiftMultiSelectProps {
|
|
65
|
-
items: TypeDefaultItemProps[];
|
|
66
|
-
itemsMap?: { [key: string]: TypeDefaultItemProps };
|
|
67
|
-
isListbox?: boolean;
|
|
68
79
|
onHighlightedIndexChange?:
|
|
69
80
|
| TypeDownshiftComboboxProps["onHighlightedIndexChange"]
|
|
70
81
|
| TypeDownshiftSelectProps["onHighlightedIndexChange"];
|
|
@@ -90,24 +101,72 @@ export interface TypeMenuContextProps
|
|
|
90
101
|
| TypeDownshiftComboboxReturnValue["getMenuProps"]
|
|
91
102
|
| TypeDownshiftSelectReturnValue["getMenuProps"];
|
|
92
103
|
}
|
|
104
|
+
export interface TypeMenuContextProps
|
|
105
|
+
extends TypeAllDownshiftProps,
|
|
106
|
+
TypeMenuContextBaseProps {
|
|
107
|
+
items: TypeInternalItemProps[];
|
|
108
|
+
itemsMap?: { [key: string]: TypeInternalItemProps };
|
|
109
|
+
selectedItemIds?: { [key: string]: boolean };
|
|
110
|
+
isListbox?: boolean;
|
|
111
|
+
}
|
|
93
112
|
|
|
94
113
|
export interface TypeMenuContext extends TypeMenuContextProps {
|
|
95
|
-
itemsMap: { [key: string]:
|
|
114
|
+
itemsMap: { [key: string]: TypeInternalItemProps };
|
|
115
|
+
isItemSelected: (id: string) => boolean;
|
|
116
|
+
getInputComponentProps: (
|
|
117
|
+
props: Pick<TypeTokenInputProps, "inputProps"> &
|
|
118
|
+
Parameters<Required<TypeMenuContextProps>["getInputProps"]>[0]
|
|
119
|
+
) => Omit<TypeDownshiftMergedInputProps<TypeTokenInputProps>, "onClick">;
|
|
96
120
|
}
|
|
97
121
|
|
|
98
122
|
export interface TypeMenuProviderProps {
|
|
99
123
|
children: TypeNotEmptyChildren;
|
|
100
|
-
|
|
124
|
+
menuContext: TypeMenuContext;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export interface TypeMenuToggleContext extends TypeMenuContext {
|
|
128
|
+
ref?: (arg0: React.ElementRef<any> | HTMLElement) => void;
|
|
129
|
+
toggle?: () => void;
|
|
130
|
+
show?: () => void;
|
|
131
|
+
hide?: () => void;
|
|
132
|
+
ariaProps?: Record<string, any>;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export interface TypeMenuToggleProviderProps extends TypeMenuProviderProps {
|
|
136
|
+
menuContext: TypeMenuToggleContext;
|
|
101
137
|
}
|
|
102
138
|
|
|
103
139
|
/**
|
|
104
140
|
* Object for Initializing the Menu Context
|
|
105
141
|
*/
|
|
106
|
-
const defaultMenuContext: TypeMenuContext = {
|
|
142
|
+
export const defaultMenuContext: TypeMenuContext = {
|
|
107
143
|
items: [],
|
|
108
144
|
itemsMap: {},
|
|
145
|
+
isItemSelected: () => false,
|
|
146
|
+
getInputComponentProps: () => ({}),
|
|
109
147
|
};
|
|
110
148
|
|
|
149
|
+
export const getSelectedItemIds = <
|
|
150
|
+
T extends TypeMenuContextProps["selectedItems"] | undefined,
|
|
151
|
+
R = T extends undefined ? undefined : Record<string, boolean>
|
|
152
|
+
>(
|
|
153
|
+
selectedItems?: T
|
|
154
|
+
): R =>
|
|
155
|
+
selectedItems?.reduce(
|
|
156
|
+
(selected, item) => ({ ...selected, [item.id]: true }),
|
|
157
|
+
{} as Record<string, boolean>
|
|
158
|
+
) as R;
|
|
159
|
+
|
|
160
|
+
export const checkIfItemSelected = ({
|
|
161
|
+
id,
|
|
162
|
+
selectedItem,
|
|
163
|
+
selectedItemIds,
|
|
164
|
+
}: {
|
|
165
|
+
id: string;
|
|
166
|
+
selectedItem?: TypeInternalItemProps | null;
|
|
167
|
+
selectedItemIds?: TypeMenuContextProps["selectedItemIds"];
|
|
168
|
+
}) => (selectedItemIds ? !!selectedItemIds[id] : selectedItem?.id === id);
|
|
169
|
+
|
|
111
170
|
/**
|
|
112
171
|
* Hook for initializing Menu context
|
|
113
172
|
*/
|
|
@@ -116,10 +175,15 @@ export const useMenu = ({
|
|
|
116
175
|
isListbox = false,
|
|
117
176
|
isOpen = false,
|
|
118
177
|
itemsMap: itemsMapFromProps,
|
|
178
|
+
selectedItemIds: selectedItemIdsFromProps,
|
|
179
|
+
selectedItem,
|
|
180
|
+
selectedItems,
|
|
181
|
+
getInputProps,
|
|
182
|
+
contentRef: contentRefFromProps,
|
|
119
183
|
...rest
|
|
120
184
|
}: Partial<TypeMenuContextProps> = {}): TypeMenuContext => {
|
|
185
|
+
const contentRef = contentRefFromProps || React.createRef<HTMLUListElement>();
|
|
121
186
|
const menuProps = useMemo(() => ({ ...rest }), [rest]);
|
|
122
|
-
|
|
123
187
|
const itemsMap = useMemo(
|
|
124
188
|
() =>
|
|
125
189
|
itemsMapFromProps ||
|
|
@@ -132,45 +196,136 @@ export const useMenu = ({
|
|
|
132
196
|
[items, itemsMapFromProps]
|
|
133
197
|
);
|
|
134
198
|
|
|
199
|
+
const selectedItemIds = useMemo(
|
|
200
|
+
() => selectedItemIdsFromProps || getSelectedItemIds(selectedItems),
|
|
201
|
+
[selectedItems, selectedItemIdsFromProps]
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
const isItemSelected = React.useCallback(
|
|
205
|
+
(id: string) => checkIfItemSelected({ id, selectedItem, selectedItemIds }),
|
|
206
|
+
[selectedItem?.id, selectedItemIds]
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
const getInputComponentProps = React.useCallback<
|
|
210
|
+
TypeMenuContext["getInputComponentProps"]
|
|
211
|
+
>(
|
|
212
|
+
({ inputProps, ...props }) => {
|
|
213
|
+
if (!getInputProps) return {};
|
|
214
|
+
const {
|
|
215
|
+
["aria-activedescendant"]: activedescendant,
|
|
216
|
+
["aria-autocomplete"]: autocomplete,
|
|
217
|
+
["aria-controls"]: controls,
|
|
218
|
+
["aria-expanded"]: expanded,
|
|
219
|
+
["aria-labelledby"]: labelledby,
|
|
220
|
+
role,
|
|
221
|
+
autoComplete,
|
|
222
|
+
onClick,
|
|
223
|
+
value,
|
|
224
|
+
...restProps
|
|
225
|
+
} = getInputProps(props);
|
|
226
|
+
return {
|
|
227
|
+
...restProps,
|
|
228
|
+
inputProps: {
|
|
229
|
+
["aria-activedescendant"]: activedescendant,
|
|
230
|
+
["aria-autocomplete"]: autocomplete,
|
|
231
|
+
["aria-controls"]: controls,
|
|
232
|
+
["aria-expanded"]: expanded,
|
|
233
|
+
["aria-labelledby"]: labelledby,
|
|
234
|
+
role,
|
|
235
|
+
autoComplete,
|
|
236
|
+
onClick,
|
|
237
|
+
value,
|
|
238
|
+
...inputProps,
|
|
239
|
+
},
|
|
240
|
+
};
|
|
241
|
+
},
|
|
242
|
+
[getInputProps]
|
|
243
|
+
);
|
|
244
|
+
|
|
135
245
|
return {
|
|
136
246
|
isOpen,
|
|
137
247
|
isListbox,
|
|
138
248
|
items,
|
|
139
249
|
itemsMap,
|
|
250
|
+
selectedItem,
|
|
251
|
+
selectedItems,
|
|
252
|
+
selectedItemIds,
|
|
253
|
+
isItemSelected,
|
|
254
|
+
getInputProps,
|
|
255
|
+
getInputComponentProps,
|
|
256
|
+
contentRef,
|
|
140
257
|
...menuProps,
|
|
141
258
|
};
|
|
142
259
|
};
|
|
143
260
|
|
|
144
261
|
/**
|
|
145
|
-
*
|
|
262
|
+
* MenuToggleContext factory, sets the type and the initial object
|
|
263
|
+
* @description Context for the Menu Toggle
|
|
146
264
|
*/
|
|
147
|
-
export const
|
|
265
|
+
export const MenuToggleContext =
|
|
266
|
+
createContext<TypeMenuToggleContext>(defaultMenuContext);
|
|
148
267
|
|
|
149
268
|
/**
|
|
150
|
-
*
|
|
269
|
+
* MenuToggleProvider Component sets the context values into the provider
|
|
270
|
+
* @description Context Provider for the Menu Toggle
|
|
151
271
|
*/
|
|
152
|
-
export const
|
|
272
|
+
export const MenuToggleProvider = ({
|
|
153
273
|
children,
|
|
154
|
-
|
|
155
|
-
}:
|
|
156
|
-
|
|
274
|
+
menuContext,
|
|
275
|
+
}: TypeMenuToggleProviderProps) => {
|
|
276
|
+
return (
|
|
277
|
+
<MenuToggleContext.Provider value={{ ...menuContext }}>
|
|
278
|
+
{children}
|
|
279
|
+
</MenuToggleContext.Provider>
|
|
280
|
+
);
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Utility hook for consuming MenuToggleContext
|
|
285
|
+
*/
|
|
286
|
+
export const useMenuToggleContext = () => {
|
|
287
|
+
const menuContextValue = useContext(MenuToggleContext);
|
|
157
288
|
|
|
289
|
+
if (menuContextValue === null) {
|
|
290
|
+
throw new Error(
|
|
291
|
+
"useMenuToggleContext must be used within a <MenuToggleContext.Provider />"
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return menuContextValue;
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* MenuContentContext factory, sets the type and the initial object
|
|
300
|
+
* @description Context for the Menu Content
|
|
301
|
+
*/
|
|
302
|
+
export const MenuContentContext =
|
|
303
|
+
createContext<TypeMenuContext>(defaultMenuContext);
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* MenuContentProvider Component sets the context values into the provider
|
|
307
|
+
* @description Context Provider for the Menu Content
|
|
308
|
+
*/
|
|
309
|
+
export const MenuContentProvider = ({
|
|
310
|
+
children,
|
|
311
|
+
menuContext,
|
|
312
|
+
}: TypeMenuProviderProps) => {
|
|
158
313
|
return (
|
|
159
|
-
<
|
|
314
|
+
<MenuContentContext.Provider value={{ ...menuContext }}>
|
|
160
315
|
{children}
|
|
161
|
-
</
|
|
316
|
+
</MenuContentContext.Provider>
|
|
162
317
|
);
|
|
163
318
|
};
|
|
164
319
|
|
|
165
320
|
/**
|
|
166
|
-
* Utility hook for consuming
|
|
321
|
+
* Utility hook for consuming MenuContentContext
|
|
167
322
|
*/
|
|
168
|
-
export const
|
|
169
|
-
const menuContextValue = useContext(
|
|
323
|
+
export const useMenuContentContext = () => {
|
|
324
|
+
const menuContextValue = useContext(MenuContentContext);
|
|
170
325
|
|
|
171
326
|
if (menuContextValue === null) {
|
|
172
327
|
throw new Error(
|
|
173
|
-
"
|
|
328
|
+
"useMenuContentContext must be used within a <MenuContentContext.Provider />"
|
|
174
329
|
);
|
|
175
330
|
}
|
|
176
331
|
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
3
|
+
import { action } from "@storybook/addon-actions";
|
|
4
|
+
|
|
5
|
+
import { Box } from "@sproutsocial/seeds-react-box";
|
|
6
|
+
import { Button } from "@sproutsocial/seeds-react-button";
|
|
7
|
+
import { Label } from "@sproutsocial/seeds-react-label";
|
|
8
|
+
import { Link } from "@sproutsocial/seeds-react-link";
|
|
9
|
+
import { Switch } from "@sproutsocial/seeds-react-switch";
|
|
10
|
+
import { Text } from "@sproutsocial/seeds-react-text";
|
|
11
|
+
|
|
12
|
+
import { StorybookMenuToggleButton } from "../storybookUtilities/menu-storybook-components";
|
|
13
|
+
import { TEST_BOOKS } from "../__fixtures__/menu-test-data";
|
|
14
|
+
import {
|
|
15
|
+
MenuContent,
|
|
16
|
+
MenuGroup,
|
|
17
|
+
SingleSelectMenu,
|
|
18
|
+
MenuFooter,
|
|
19
|
+
MenuItem,
|
|
20
|
+
type TypeMenuFooterProps,
|
|
21
|
+
} from "../index";
|
|
22
|
+
|
|
23
|
+
interface MenuFooterStoryArgs extends TypeMenuFooterProps {
|
|
24
|
+
"Trigger Text"?: string;
|
|
25
|
+
componentOne?: string;
|
|
26
|
+
componentTwo?: string;
|
|
27
|
+
componentThree?: string;
|
|
28
|
+
}
|
|
29
|
+
type Story = StoryObj<MenuFooterStoryArgs>;
|
|
30
|
+
|
|
31
|
+
const optionsArr = ["Button Example", "Link Example", "Text Example", "none"];
|
|
32
|
+
|
|
33
|
+
const meta: Meta<MenuFooterStoryArgs> = {
|
|
34
|
+
title: "Under Development/Primitives/MenuFooter",
|
|
35
|
+
component: MenuFooter,
|
|
36
|
+
parameters: {
|
|
37
|
+
controls: { expanded: true, sort: "requiredFirst" },
|
|
38
|
+
},
|
|
39
|
+
argTypes: {
|
|
40
|
+
"Trigger Text": {
|
|
41
|
+
table: {
|
|
42
|
+
category: "Button",
|
|
43
|
+
},
|
|
44
|
+
description: "Change text of trigger Button",
|
|
45
|
+
control: "text",
|
|
46
|
+
},
|
|
47
|
+
componentOne: {
|
|
48
|
+
table: {
|
|
49
|
+
category: "MenuFooter",
|
|
50
|
+
},
|
|
51
|
+
description: "Example Child Component",
|
|
52
|
+
options: optionsArr,
|
|
53
|
+
control: { type: "select" },
|
|
54
|
+
},
|
|
55
|
+
componentTwo: {
|
|
56
|
+
table: {
|
|
57
|
+
category: "MenuFooter",
|
|
58
|
+
},
|
|
59
|
+
description: "Example Child Component",
|
|
60
|
+
options: optionsArr,
|
|
61
|
+
control: { type: "select" },
|
|
62
|
+
},
|
|
63
|
+
componentThree: {
|
|
64
|
+
table: {
|
|
65
|
+
category: "MenuFooter",
|
|
66
|
+
},
|
|
67
|
+
description: "Example Child Component",
|
|
68
|
+
options: optionsArr,
|
|
69
|
+
control: { type: "select" },
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
args: {
|
|
73
|
+
"Trigger Text": "Select a Book",
|
|
74
|
+
display: "flex",
|
|
75
|
+
flexDirection: "row",
|
|
76
|
+
alignItems: "center",
|
|
77
|
+
justifyContent: "space-between",
|
|
78
|
+
componentOne: optionsArr[0],
|
|
79
|
+
componentTwo: optionsArr[3],
|
|
80
|
+
componentThree: optionsArr[2],
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export default meta;
|
|
85
|
+
|
|
86
|
+
const handleOnClick = (e: React.MouseEvent, itemName: string) => {
|
|
87
|
+
action(`${itemName}`)(e);
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const StoryFooterExamples = ({ type }) => {
|
|
91
|
+
switch (type) {
|
|
92
|
+
case optionsArr[0]:
|
|
93
|
+
return (
|
|
94
|
+
<Button
|
|
95
|
+
appearance="primary"
|
|
96
|
+
display="flex"
|
|
97
|
+
justifyContent="center"
|
|
98
|
+
alignItems="center"
|
|
99
|
+
>
|
|
100
|
+
Save
|
|
101
|
+
</Button>
|
|
102
|
+
);
|
|
103
|
+
case optionsArr[1]:
|
|
104
|
+
return (
|
|
105
|
+
<Link mt="space.100" href="#">
|
|
106
|
+
Help
|
|
107
|
+
</Link>
|
|
108
|
+
);
|
|
109
|
+
case optionsArr[2]:
|
|
110
|
+
return <Text.SmallByline>Books will update on save.</Text.SmallByline>;
|
|
111
|
+
default:
|
|
112
|
+
case optionsArr[3]:
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export const MenuFooterSandbox: Story = {
|
|
118
|
+
name: "Sandbox",
|
|
119
|
+
render: function MenuWithFooter(args) {
|
|
120
|
+
return (
|
|
121
|
+
<Box display="flex" gap="16px" flexDirection="column" maxWidth={300}>
|
|
122
|
+
<SingleSelectMenu
|
|
123
|
+
defaultIsOpen
|
|
124
|
+
menuToggleElement={<StorybookMenuToggleButton />}
|
|
125
|
+
>
|
|
126
|
+
<MenuContent>
|
|
127
|
+
<MenuGroup id="books">
|
|
128
|
+
{TEST_BOOKS.map((book) => (
|
|
129
|
+
<MenuItem
|
|
130
|
+
key={book.id}
|
|
131
|
+
onClick={(e) => handleOnClick(e, book.title)}
|
|
132
|
+
id={book.id}
|
|
133
|
+
>
|
|
134
|
+
{book.title}
|
|
135
|
+
</MenuItem>
|
|
136
|
+
))}
|
|
137
|
+
</MenuGroup>
|
|
138
|
+
</MenuContent>
|
|
139
|
+
<MenuFooter
|
|
140
|
+
alignItems={args.alignItems}
|
|
141
|
+
justifyContent={args.justifyContent}
|
|
142
|
+
display={args.display}
|
|
143
|
+
flexDirection={args.flexDirection}
|
|
144
|
+
>
|
|
145
|
+
<StoryFooterExamples type={args.componentThree} />
|
|
146
|
+
<StoryFooterExamples type={args.componentOne} />
|
|
147
|
+
</MenuFooter>
|
|
148
|
+
</SingleSelectMenu>
|
|
149
|
+
</Box>
|
|
150
|
+
);
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
export const FooterwithActions: Story = {
|
|
155
|
+
name: "Footer Children",
|
|
156
|
+
parameters: {
|
|
157
|
+
controls: { exclude: ["componentOne", "componentTwo", "componentThree"] },
|
|
158
|
+
},
|
|
159
|
+
render: function FooterWithActions(args) {
|
|
160
|
+
const [checked, setChecked] = useState(false);
|
|
161
|
+
return (
|
|
162
|
+
<Box display="flex" gap="16px" flexDirection="column" maxWidth={300}>
|
|
163
|
+
<SingleSelectMenu
|
|
164
|
+
defaultIsOpen
|
|
165
|
+
menuToggleElement={<StorybookMenuToggleButton />}
|
|
166
|
+
>
|
|
167
|
+
<MenuContent>
|
|
168
|
+
<MenuGroup id="books">
|
|
169
|
+
{TEST_BOOKS.map((book) => (
|
|
170
|
+
<MenuItem
|
|
171
|
+
key={book.id}
|
|
172
|
+
onClick={(e) => handleOnClick(e, book.title)}
|
|
173
|
+
id={book.id}
|
|
174
|
+
>
|
|
175
|
+
{book.title}
|
|
176
|
+
</MenuItem>
|
|
177
|
+
))}
|
|
178
|
+
</MenuGroup>
|
|
179
|
+
</MenuContent>
|
|
180
|
+
<MenuFooter display={args.display} flexDirection="column">
|
|
181
|
+
<Box display="flex" justifyContent="space-between">
|
|
182
|
+
<Box display="flex" justifyContent="flex-start">
|
|
183
|
+
<Switch
|
|
184
|
+
onClick={(event) => {
|
|
185
|
+
event?.preventDefault;
|
|
186
|
+
setChecked((d) => !d);
|
|
187
|
+
}}
|
|
188
|
+
checked={checked}
|
|
189
|
+
id="switch"
|
|
190
|
+
/>
|
|
191
|
+
<Label htmlFor="switch" ml="10px">
|
|
192
|
+
Enable cliff notes
|
|
193
|
+
</Label>
|
|
194
|
+
</Box>
|
|
195
|
+
<Button appearance="primary">Save</Button>
|
|
196
|
+
</Box>
|
|
197
|
+
</MenuFooter>
|
|
198
|
+
</SingleSelectMenu>
|
|
199
|
+
</Box>
|
|
200
|
+
);
|
|
201
|
+
},
|
|
202
|
+
};
|
|
@@ -1,8 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
render,
|
|
3
|
+
screen,
|
|
4
|
+
actRender,
|
|
5
|
+
} from "@sproutsocial/seeds-react-testing-library";
|
|
3
6
|
import { Button } from "@sproutsocial/seeds-react-button";
|
|
4
7
|
// import { Link } from "@sproutsocial/seeds-react-link";
|
|
5
8
|
import { Switch } from "@sproutsocial/seeds-react-switch";
|
|
9
|
+
import { MenuFooter, MenuToggleButton } from "../../index";
|
|
10
|
+
import { TestWithMenuRoot } from "../../menuTestingUtils";
|
|
6
11
|
|
|
7
12
|
describe("MenuFooter API", () => {
|
|
8
13
|
const contextTests = [
|
|
@@ -63,8 +68,8 @@ describe("MenuFooter API", () => {
|
|
|
63
68
|
|
|
64
69
|
it.each(contextTests)(
|
|
65
70
|
"$children rendered when $children provided",
|
|
66
|
-
({ TestComponent, test }) => {
|
|
67
|
-
|
|
71
|
+
async ({ TestComponent, test }) => {
|
|
72
|
+
await actRender(
|
|
68
73
|
<MenuFooter>
|
|
69
74
|
Hello world!
|
|
70
75
|
{TestComponent}
|
|
@@ -77,25 +82,25 @@ describe("MenuFooter API", () => {
|
|
|
77
82
|
|
|
78
83
|
it.each(contextTests)(
|
|
79
84
|
"$children rendered when $children provided in MenuRoot",
|
|
80
|
-
({ TestComponent, test }) => {
|
|
81
|
-
|
|
82
|
-
<
|
|
85
|
+
async ({ TestComponent, test }) => {
|
|
86
|
+
await actRender(
|
|
87
|
+
<TestWithMenuRoot
|
|
83
88
|
isOpen
|
|
84
|
-
menuToggleElement={<
|
|
89
|
+
menuToggleElement={<MenuToggleButton>Open</MenuToggleButton>}
|
|
85
90
|
>
|
|
86
91
|
<MenuFooter>
|
|
87
92
|
Hello world!
|
|
88
93
|
{TestComponent}
|
|
89
94
|
</MenuFooter>
|
|
90
|
-
</
|
|
95
|
+
</TestWithMenuRoot>
|
|
91
96
|
);
|
|
92
97
|
|
|
93
98
|
test();
|
|
94
99
|
}
|
|
95
100
|
);
|
|
96
101
|
|
|
97
|
-
it("extends Box", () => {
|
|
98
|
-
|
|
102
|
+
it("extends Box", async () => {
|
|
103
|
+
await actRender(<MenuFooter bg="#ff0000">Hello world!</MenuFooter>);
|
|
99
104
|
|
|
100
105
|
const menuFooter = screen.getByText("Hello world!");
|
|
101
106
|
expect(menuFooter).toHaveStyle("background: #ff0000");
|
|
@@ -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: ${(
|
|
5
|
+
padding: ${({ theme }) => theme.space[350]} ${({ theme }) => theme.space[400]};
|
|
6
6
|
border-top: 1px solid ${(p) => p.theme.colors.container.border.base};
|
|
7
7
|
`;
|
|
@@ -32,9 +32,9 @@ Feature: Menu Group
|
|
|
32
32
|
When the "title" prop is not set
|
|
33
33
|
Then no title is visible at the top of the group
|
|
34
34
|
|
|
35
|
-
Scenario: MenuGroup has no
|
|
35
|
+
Scenario: MenuGroup has no menuItems provided
|
|
36
36
|
When no children are provided to MenuGroup
|
|
37
|
-
And no
|
|
37
|
+
And no menuItems prop is provided
|
|
38
38
|
Then the MenuGroup will show a warning in the console
|
|
39
39
|
And will not render the empty group
|
|
40
40
|
|
|
@@ -42,24 +42,24 @@ Feature: Menu Group
|
|
|
42
42
|
When MenuItem components are passed as children to a MenuGroup
|
|
43
43
|
Then the MenuGroup will render them inside of the group
|
|
44
44
|
|
|
45
|
-
Scenario: MenuGroup
|
|
46
|
-
Given a list of
|
|
47
|
-
When that list of
|
|
45
|
+
Scenario: MenuGroup menuItems prop might be empty
|
|
46
|
+
Given a list of menuItems is empty
|
|
47
|
+
When that list of menuItems is provided to the menuItems prop on a MenuGroup
|
|
48
48
|
Then the MenuGroup will show a warning in the console
|
|
49
49
|
And will not render the empty group
|
|
50
50
|
|
|
51
|
-
Scenario: MenuGroup can take
|
|
52
|
-
Given a list of
|
|
53
|
-
And the
|
|
54
|
-
When that list of
|
|
55
|
-
Then the MenuGroup will render the
|
|
51
|
+
Scenario: MenuGroup can take menuItems via a prop
|
|
52
|
+
Given a list of menuItems is not empty
|
|
53
|
+
And the menuItems are a valid object shape
|
|
54
|
+
When that list of menuItems is provided to the menuItems prop on a MenuGroup
|
|
55
|
+
Then the MenuGroup will render the menuItems using the MenuItem component
|
|
56
56
|
|
|
57
|
-
Scenario: MenuGroup can use the
|
|
58
|
-
Given a list of
|
|
59
|
-
And the
|
|
60
|
-
When that list of
|
|
57
|
+
Scenario: MenuGroup can use the menuItems prop with a custom MenuItem
|
|
58
|
+
Given a list of menuItems is not empty
|
|
59
|
+
And the menuItems are a valid object shape
|
|
60
|
+
When that list of menuItems is provided to the menuItems prop on a MenuGroup
|
|
61
61
|
And a valid component is provided to the customMenuItem prop
|
|
62
|
-
Then the MenuGroup will render the
|
|
62
|
+
Then the MenuGroup will render the menuItems using the customMenuItem component
|
|
63
63
|
|
|
64
64
|
Scenario Outline: MenuGroup has custom content
|
|
65
65
|
When <custom content> is present at the <location>
|
|
@@ -79,6 +79,6 @@ Feature: Menu Group
|
|
|
79
79
|
| propName | propType |
|
|
80
80
|
| children | ReactNode |
|
|
81
81
|
| title | string |
|
|
82
|
-
|
|
|
82
|
+
| menuItems | TypeMenuItemProps[] |
|
|
83
83
|
| isSingleSelect | boolean |
|
|
84
84
|
| MenuItemComponent | React.ComponentType<TypeMenuItemProps> |
|