@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
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,20 @@ 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
|
+
setHiddenItemsMap?: React.Dispatch<
|
|
61
|
+
React.SetStateAction<Record<string, boolean>>
|
|
62
|
+
>;
|
|
63
|
+
subMenuId?: string;
|
|
64
|
+
setSubMenuId?: (id: string) => void;
|
|
65
|
+
contentRef?: React.RefObject<HTMLUListElement>;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface TypeAllDownshiftProps
|
|
56
69
|
extends Omit<
|
|
57
70
|
TypeDownshiftComboboxReturnValue,
|
|
58
71
|
TypeDonwshiftSharedReturnValue
|
|
@@ -62,9 +75,6 @@ export interface TypeMenuContextProps
|
|
|
62
75
|
Omit<TypeDownshiftSelectProps, TypeDonwshiftSharedProps>,
|
|
63
76
|
TypeDownshiftMultiSelectReturnValue,
|
|
64
77
|
TypeDownshiftMultiSelectProps {
|
|
65
|
-
items: TypeDefaultItemProps[];
|
|
66
|
-
itemsMap?: { [key: string]: TypeDefaultItemProps };
|
|
67
|
-
isListbox?: boolean;
|
|
68
78
|
onHighlightedIndexChange?:
|
|
69
79
|
| TypeDownshiftComboboxProps["onHighlightedIndexChange"]
|
|
70
80
|
| TypeDownshiftSelectProps["onHighlightedIndexChange"];
|
|
@@ -90,24 +100,72 @@ export interface TypeMenuContextProps
|
|
|
90
100
|
| TypeDownshiftComboboxReturnValue["getMenuProps"]
|
|
91
101
|
| TypeDownshiftSelectReturnValue["getMenuProps"];
|
|
92
102
|
}
|
|
103
|
+
export interface TypeMenuContextProps
|
|
104
|
+
extends TypeAllDownshiftProps,
|
|
105
|
+
TypeMenuContextBaseProps {
|
|
106
|
+
items: TypeInternalItemProps[];
|
|
107
|
+
itemsMap?: { [key: string]: TypeInternalItemProps };
|
|
108
|
+
selectedItemIds?: { [key: string]: boolean };
|
|
109
|
+
isListbox?: boolean;
|
|
110
|
+
}
|
|
93
111
|
|
|
94
112
|
export interface TypeMenuContext extends TypeMenuContextProps {
|
|
95
|
-
itemsMap: { [key: string]:
|
|
113
|
+
itemsMap: { [key: string]: TypeInternalItemProps };
|
|
114
|
+
isItemSelected: (id: string) => boolean;
|
|
115
|
+
getInputComponentProps: (
|
|
116
|
+
props: Pick<TypeTokenInputProps, "inputProps"> &
|
|
117
|
+
Parameters<Required<TypeMenuContextProps>["getInputProps"]>[0]
|
|
118
|
+
) => Omit<TypeDownshiftMergedInputProps<TypeTokenInputProps>, "onClick">;
|
|
96
119
|
}
|
|
97
120
|
|
|
98
121
|
export interface TypeMenuProviderProps {
|
|
99
122
|
children: TypeNotEmptyChildren;
|
|
100
|
-
|
|
123
|
+
menuContext: TypeMenuContext;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface TypeMenuToggleContext extends TypeMenuContext {
|
|
127
|
+
ref?: (arg0: React.ElementRef<any> | HTMLElement) => void;
|
|
128
|
+
toggle?: () => void;
|
|
129
|
+
show?: () => void;
|
|
130
|
+
hide?: () => void;
|
|
131
|
+
ariaProps?: Record<string, any>;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export interface TypeMenuToggleProviderProps extends TypeMenuProviderProps {
|
|
135
|
+
menuContext: TypeMenuToggleContext;
|
|
101
136
|
}
|
|
102
137
|
|
|
103
138
|
/**
|
|
104
139
|
* Object for Initializing the Menu Context
|
|
105
140
|
*/
|
|
106
|
-
const defaultMenuContext: TypeMenuContext = {
|
|
141
|
+
export const defaultMenuContext: TypeMenuContext = {
|
|
107
142
|
items: [],
|
|
108
143
|
itemsMap: {},
|
|
144
|
+
isItemSelected: () => false,
|
|
145
|
+
getInputComponentProps: () => ({}),
|
|
109
146
|
};
|
|
110
147
|
|
|
148
|
+
export const getSelectedItemIds = <
|
|
149
|
+
T extends TypeMenuContextProps["selectedItems"] | undefined,
|
|
150
|
+
R = T extends undefined ? undefined : Record<string, boolean>
|
|
151
|
+
>(
|
|
152
|
+
selectedItems?: T
|
|
153
|
+
): R =>
|
|
154
|
+
selectedItems?.reduce(
|
|
155
|
+
(selected, item) => ({ ...selected, [item.id]: true }),
|
|
156
|
+
{} as Record<string, boolean>
|
|
157
|
+
) as R;
|
|
158
|
+
|
|
159
|
+
export const checkIfItemSelected = ({
|
|
160
|
+
id,
|
|
161
|
+
selectedItem,
|
|
162
|
+
selectedItemIds,
|
|
163
|
+
}: {
|
|
164
|
+
id: string;
|
|
165
|
+
selectedItem?: TypeInternalItemProps | null;
|
|
166
|
+
selectedItemIds?: TypeMenuContextProps["selectedItemIds"];
|
|
167
|
+
}) => (selectedItemIds ? !!selectedItemIds[id] : selectedItem?.id === id);
|
|
168
|
+
|
|
111
169
|
/**
|
|
112
170
|
* Hook for initializing Menu context
|
|
113
171
|
*/
|
|
@@ -116,10 +174,15 @@ export const useMenu = ({
|
|
|
116
174
|
isListbox = false,
|
|
117
175
|
isOpen = false,
|
|
118
176
|
itemsMap: itemsMapFromProps,
|
|
177
|
+
selectedItemIds: selectedItemIdsFromProps,
|
|
178
|
+
selectedItem,
|
|
179
|
+
selectedItems,
|
|
180
|
+
getInputProps,
|
|
181
|
+
contentRef: contentRefFromProps,
|
|
119
182
|
...rest
|
|
120
183
|
}: Partial<TypeMenuContextProps> = {}): TypeMenuContext => {
|
|
184
|
+
const contentRef = contentRefFromProps || React.createRef<HTMLUListElement>();
|
|
121
185
|
const menuProps = useMemo(() => ({ ...rest }), [rest]);
|
|
122
|
-
|
|
123
186
|
const itemsMap = useMemo(
|
|
124
187
|
() =>
|
|
125
188
|
itemsMapFromProps ||
|
|
@@ -132,45 +195,136 @@ export const useMenu = ({
|
|
|
132
195
|
[items, itemsMapFromProps]
|
|
133
196
|
);
|
|
134
197
|
|
|
198
|
+
const selectedItemIds = useMemo(
|
|
199
|
+
() => selectedItemIdsFromProps || getSelectedItemIds(selectedItems),
|
|
200
|
+
[selectedItems, selectedItemIdsFromProps]
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
const isItemSelected = React.useCallback(
|
|
204
|
+
(id: string) => checkIfItemSelected({ id, selectedItem, selectedItemIds }),
|
|
205
|
+
[selectedItem, selectedItemIds]
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
const getInputComponentProps = React.useCallback<
|
|
209
|
+
TypeMenuContext["getInputComponentProps"]
|
|
210
|
+
>(
|
|
211
|
+
({ inputProps, ...props }) => {
|
|
212
|
+
if (!getInputProps) return {};
|
|
213
|
+
const {
|
|
214
|
+
["aria-activedescendant"]: activedescendant,
|
|
215
|
+
["aria-autocomplete"]: autocomplete,
|
|
216
|
+
["aria-controls"]: controls,
|
|
217
|
+
["aria-expanded"]: expanded,
|
|
218
|
+
["aria-labelledby"]: labelledby,
|
|
219
|
+
role,
|
|
220
|
+
autoComplete,
|
|
221
|
+
onClick,
|
|
222
|
+
value,
|
|
223
|
+
...restProps
|
|
224
|
+
} = getInputProps(props);
|
|
225
|
+
return {
|
|
226
|
+
...restProps,
|
|
227
|
+
inputProps: {
|
|
228
|
+
["aria-activedescendant"]: activedescendant,
|
|
229
|
+
["aria-autocomplete"]: autocomplete,
|
|
230
|
+
["aria-controls"]: controls,
|
|
231
|
+
["aria-expanded"]: expanded,
|
|
232
|
+
["aria-labelledby"]: labelledby,
|
|
233
|
+
role,
|
|
234
|
+
autoComplete,
|
|
235
|
+
onClick,
|
|
236
|
+
value,
|
|
237
|
+
...inputProps,
|
|
238
|
+
},
|
|
239
|
+
};
|
|
240
|
+
},
|
|
241
|
+
[getInputProps]
|
|
242
|
+
);
|
|
243
|
+
|
|
135
244
|
return {
|
|
136
245
|
isOpen,
|
|
137
246
|
isListbox,
|
|
138
247
|
items,
|
|
139
248
|
itemsMap,
|
|
249
|
+
selectedItem,
|
|
250
|
+
selectedItems,
|
|
251
|
+
selectedItemIds,
|
|
252
|
+
isItemSelected,
|
|
253
|
+
getInputProps,
|
|
254
|
+
getInputComponentProps,
|
|
255
|
+
contentRef,
|
|
140
256
|
...menuProps,
|
|
141
257
|
};
|
|
142
258
|
};
|
|
143
259
|
|
|
144
260
|
/**
|
|
145
|
-
*
|
|
261
|
+
* MenuToggleContext factory, sets the type and the initial object
|
|
262
|
+
* @description Context for the Menu Toggle
|
|
146
263
|
*/
|
|
147
|
-
export const
|
|
264
|
+
export const MenuToggleContext =
|
|
265
|
+
createContext<TypeMenuToggleContext>(defaultMenuContext);
|
|
148
266
|
|
|
149
267
|
/**
|
|
150
|
-
*
|
|
268
|
+
* MenuToggleProvider Component sets the context values into the provider
|
|
269
|
+
* @description Context Provider for the Menu Toggle
|
|
151
270
|
*/
|
|
152
|
-
export const
|
|
271
|
+
export const MenuToggleProvider = ({
|
|
153
272
|
children,
|
|
154
|
-
|
|
155
|
-
}:
|
|
156
|
-
|
|
273
|
+
menuContext,
|
|
274
|
+
}: TypeMenuToggleProviderProps) => {
|
|
275
|
+
return (
|
|
276
|
+
<MenuToggleContext.Provider value={{ ...menuContext }}>
|
|
277
|
+
{children}
|
|
278
|
+
</MenuToggleContext.Provider>
|
|
279
|
+
);
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Utility hook for consuming MenuToggleContext
|
|
284
|
+
*/
|
|
285
|
+
export const useMenuToggleContext = () => {
|
|
286
|
+
const menuContextValue = useContext(MenuToggleContext);
|
|
157
287
|
|
|
288
|
+
if (menuContextValue === null) {
|
|
289
|
+
throw new Error(
|
|
290
|
+
"useMenuToggleContext must be used within a <MenuToggleContext.Provider />"
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return menuContextValue;
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* MenuContentContext factory, sets the type and the initial object
|
|
299
|
+
* @description Context for the Menu Content
|
|
300
|
+
*/
|
|
301
|
+
export const MenuContentContext =
|
|
302
|
+
createContext<TypeMenuContext>(defaultMenuContext);
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* MenuContentProvider Component sets the context values into the provider
|
|
306
|
+
* @description Context Provider for the Menu Content
|
|
307
|
+
*/
|
|
308
|
+
export const MenuContentProvider = ({
|
|
309
|
+
children,
|
|
310
|
+
menuContext,
|
|
311
|
+
}: TypeMenuProviderProps) => {
|
|
158
312
|
return (
|
|
159
|
-
<
|
|
313
|
+
<MenuContentContext.Provider value={{ ...menuContext }}>
|
|
160
314
|
{children}
|
|
161
|
-
</
|
|
315
|
+
</MenuContentContext.Provider>
|
|
162
316
|
);
|
|
163
317
|
};
|
|
164
318
|
|
|
165
319
|
/**
|
|
166
|
-
* Utility hook for consuming
|
|
320
|
+
* Utility hook for consuming MenuContentContext
|
|
167
321
|
*/
|
|
168
|
-
export const
|
|
169
|
-
const menuContextValue = useContext(
|
|
322
|
+
export const useMenuContentContext = () => {
|
|
323
|
+
const menuContextValue = useContext(MenuContentContext);
|
|
170
324
|
|
|
171
325
|
if (menuContextValue === null) {
|
|
172
326
|
throw new Error(
|
|
173
|
-
"
|
|
327
|
+
"useMenuContentContext must be used within a <MenuContentContext.Provider />"
|
|
174
328
|
);
|
|
175
329
|
}
|
|
176
330
|
|
|
@@ -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");
|
|
@@ -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> |
|