@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
|
@@ -0,0 +1,117 @@
|
|
|
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" | "hiddenItemsCount"
|
|
50
|
+
> {
|
|
51
|
+
defaultDownshiftProps: {
|
|
52
|
+
select: Partial<UseSelectProps<TypeInternalItemProps>>;
|
|
53
|
+
combobox: Partial<UseComboboxProps<TypeInternalItemProps>>;
|
|
54
|
+
multi: Partial<UseMultipleSelectionProps<TypeInternalItemProps>>;
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export const itemToKey = (item: TypeInternalItemProps | null) => item?.id;
|
|
59
|
+
|
|
60
|
+
export const useDownshiftDefaults = ({
|
|
61
|
+
isCombobox = false,
|
|
62
|
+
isMulti = false,
|
|
63
|
+
multiSelectProps = {},
|
|
64
|
+
}: {
|
|
65
|
+
isCombobox?: boolean;
|
|
66
|
+
isMulti?: boolean;
|
|
67
|
+
multiSelectProps?: TypeDownshiftMultiSelectProps;
|
|
68
|
+
} = {}): TypeUseDownshiftDefaultsReturn => {
|
|
69
|
+
const [hiddenItemsMap, setHiddenItemsMap] = React.useState<
|
|
70
|
+
Record<string, boolean>
|
|
71
|
+
>({});
|
|
72
|
+
const hiddenItemsCount = React.useMemo(
|
|
73
|
+
() => Object.keys(hiddenItemsMap).filter(Boolean).length,
|
|
74
|
+
[hiddenItemsMap]
|
|
75
|
+
);
|
|
76
|
+
const isItemDisabled = React.useCallback<
|
|
77
|
+
Required<UseSelectProps<TypeInternalItemProps>>["isItemDisabled"]
|
|
78
|
+
>(
|
|
79
|
+
(item) => Boolean(item.disabled || hiddenItemsMap[item.id]),
|
|
80
|
+
[hiddenItemsMap]
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
hiddenItemsMap,
|
|
85
|
+
hiddenItemsCount,
|
|
86
|
+
setHiddenItemsMap,
|
|
87
|
+
defaultDownshiftProps: {
|
|
88
|
+
select: !isCombobox
|
|
89
|
+
? {
|
|
90
|
+
itemToKey,
|
|
91
|
+
// disabling while item.hidden so that hidden items are ignored by downshift
|
|
92
|
+
isItemDisabled,
|
|
93
|
+
...(isMulti
|
|
94
|
+
? defaultUseSelectPropsForMulti
|
|
95
|
+
: defaultUseSelectProps),
|
|
96
|
+
}
|
|
97
|
+
: {},
|
|
98
|
+
combobox: isCombobox
|
|
99
|
+
? {
|
|
100
|
+
itemToKey,
|
|
101
|
+
// disabling while item.hidden so that hidden items are ignored by downshift
|
|
102
|
+
isItemDisabled,
|
|
103
|
+
...(isMulti
|
|
104
|
+
? defaultUseComboboxPropsForMulti
|
|
105
|
+
: defaultUseComboboxProps),
|
|
106
|
+
}
|
|
107
|
+
: {},
|
|
108
|
+
multi: isMulti
|
|
109
|
+
? {
|
|
110
|
+
itemToKey,
|
|
111
|
+
// handles the prop name mapping for multi select
|
|
112
|
+
...getMultiSelectOverrides(multiSelectProps),
|
|
113
|
+
}
|
|
114
|
+
: {},
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { TypeMenuContext } from "../MenuContext";
|
|
3
|
+
import type { TypeInternalItemProps } from "../types";
|
|
4
|
+
import { itemToString as defaultItemToString } from "../utils/downshiftDefaults";
|
|
5
|
+
|
|
6
|
+
export type TypeGetIsItemVisibleFn = (
|
|
7
|
+
item: TypeMenuContext["items"][number],
|
|
8
|
+
inputValue: string
|
|
9
|
+
) => boolean;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @description A function to get the default getIsItemVisible function
|
|
13
|
+
*
|
|
14
|
+
* @param itemToString - A function to convert an item to a string
|
|
15
|
+
*
|
|
16
|
+
* @returns The default getIsItemVisible function
|
|
17
|
+
*/
|
|
18
|
+
export const getDefaultGetIsItemVisible =
|
|
19
|
+
(
|
|
20
|
+
itemToString: TypeMenuContext["itemToString"] = defaultItemToString
|
|
21
|
+
): TypeGetIsItemVisibleFn =>
|
|
22
|
+
(item, inputValue) =>
|
|
23
|
+
itemToString(item).toLowerCase().includes(inputValue.toLowerCase());
|
|
24
|
+
|
|
25
|
+
export type TypeGetShouldFilterFn = (arg: { inputValue?: string }) => boolean;
|
|
26
|
+
|
|
27
|
+
export const defaultGetShouldFilter: TypeGetShouldFilterFn = ({ inputValue }) =>
|
|
28
|
+
Boolean(inputValue);
|
|
29
|
+
|
|
30
|
+
export interface TypeItemFilteringProps {
|
|
31
|
+
allMenuItems: TypeInternalItemProps[];
|
|
32
|
+
itemToString?: TypeMenuContext["itemToString"];
|
|
33
|
+
getIsItemVisible?: TypeGetIsItemVisibleFn;
|
|
34
|
+
getShouldFilter?: TypeGetShouldFilterFn;
|
|
35
|
+
setHiddenItemsMap?: TypeMenuContext["setHiddenItemsMap"];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const useItemFiltering = ({
|
|
39
|
+
allMenuItems,
|
|
40
|
+
itemToString,
|
|
41
|
+
getIsItemVisible = getDefaultGetIsItemVisible(itemToString),
|
|
42
|
+
getShouldFilter = defaultGetShouldFilter,
|
|
43
|
+
setHiddenItemsMap: setHiddenItemsMapProp,
|
|
44
|
+
}: TypeItemFilteringProps) => {
|
|
45
|
+
const [hiddenItemsMap, setHiddenItemsMap] = React.useState<
|
|
46
|
+
Required<TypeMenuContext>["hiddenItemsMap"]
|
|
47
|
+
>({});
|
|
48
|
+
|
|
49
|
+
const updateFilteredItems = React.useCallback(
|
|
50
|
+
({ inputValue }) => {
|
|
51
|
+
const newHiddenItems = getShouldFilter({ inputValue })
|
|
52
|
+
? allMenuItems.reduce((map, item) => {
|
|
53
|
+
const isItemVisible = getIsItemVisible(item, inputValue);
|
|
54
|
+
return isItemVisible ? map : { ...map, [item.id]: true };
|
|
55
|
+
}, {} as typeof hiddenItemsMap)
|
|
56
|
+
: {};
|
|
57
|
+
|
|
58
|
+
setHiddenItemsMap(newHiddenItems);
|
|
59
|
+
setHiddenItemsMapProp?.(newHiddenItems);
|
|
60
|
+
},
|
|
61
|
+
[allMenuItems, getIsItemVisible, getShouldFilter]
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
getIsItemVisible,
|
|
66
|
+
updateFilteredItems,
|
|
67
|
+
};
|
|
68
|
+
};
|
|
@@ -1,29 +1,40 @@
|
|
|
1
1
|
import React, { type ReactNode, useMemo } from "react";
|
|
2
2
|
import { MenuItem, type TypeMenuItemProps } from "../MenuItem";
|
|
3
|
+
import type {
|
|
4
|
+
TypeSubmenuItemProps,
|
|
5
|
+
TypeSubmenuValidMenuComponentProps,
|
|
6
|
+
} from "../SubmenuItem";
|
|
3
7
|
import type { TypeMenuContext } from "../MenuContext";
|
|
4
|
-
import type { TypeMenuGroupProps } from "../MenuGroup
|
|
5
|
-
import type {
|
|
6
|
-
import
|
|
8
|
+
import type { TypeMenuGroupProps, TypeMenuGroupBaseProps } from "../MenuGroup";
|
|
9
|
+
import type { TypeMenuRootProps } from "../MenuRoot";
|
|
10
|
+
import type {
|
|
11
|
+
TypeNotEmptyChildren,
|
|
12
|
+
TypeChildrenOrItems,
|
|
13
|
+
TypeInternalItemProps,
|
|
14
|
+
} from "../types";
|
|
15
|
+
import { MenuContent, type TypeMenuContentProps } from "../MenuContent";
|
|
7
16
|
|
|
8
|
-
export interface
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
17
|
+
export interface TypeMapMenuItemsProps<
|
|
18
|
+
I extends TypeMenuItemProps = TypeMenuItemProps
|
|
19
|
+
> {
|
|
20
|
+
menuItems: I[];
|
|
21
|
+
MenuItemComponent?: React.ComponentType<I>;
|
|
12
22
|
}
|
|
13
|
-
export
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
export type TypeMenuGroupElement
|
|
19
|
-
|
|
23
|
+
export type TypeItemElement<I extends TypeMenuItemProps = TypeMenuItemProps> =
|
|
24
|
+
React.ReactElement<
|
|
25
|
+
| TypeMenuItemProps
|
|
26
|
+
| TypeSubmenuItemProps<I, TypeSubmenuValidMenuComponentProps<I>>
|
|
27
|
+
>;
|
|
28
|
+
export type TypeMenuGroupElement<
|
|
29
|
+
I extends TypeMenuItemProps = TypeMenuItemProps
|
|
30
|
+
> = React.ReactElement<TypeMenuGroupProps<I>>;
|
|
31
|
+
export type TypeMenuContentElement<
|
|
32
|
+
I extends TypeMenuItemProps = TypeMenuItemProps
|
|
33
|
+
> = React.ReactElement<TypeMenuContentProps<I>>;
|
|
20
34
|
|
|
21
35
|
export interface TypeMenuChildren {
|
|
22
36
|
type: "item" | "group" | "content";
|
|
23
|
-
element:
|
|
24
|
-
| React.ReactElement<TypeMenuGroupProps>
|
|
25
|
-
| React.ReactElement<TypeMenuItemProps>
|
|
26
|
-
| React.ReactElement<TypeMenuContentProps>;
|
|
37
|
+
element: TypeItemElement | TypeMenuGroupElement | TypeMenuContentElement;
|
|
27
38
|
}
|
|
28
39
|
|
|
29
40
|
// https://stackoverflow.com/questions/69119016/find-specific-children-components-regardless-of-depth-in-react
|
|
@@ -46,165 +57,228 @@ const walkAllChildren = (
|
|
|
46
57
|
});
|
|
47
58
|
};
|
|
48
59
|
|
|
60
|
+
// get the name from our custom __MENU_PRIMITIVE_TYPE property
|
|
49
61
|
const getComponentName = (element: ReactNode): string | undefined => {
|
|
50
62
|
if (
|
|
51
63
|
element &&
|
|
52
64
|
typeof element === "object" &&
|
|
53
65
|
"type" in element &&
|
|
54
|
-
typeof element.type
|
|
55
|
-
|
|
66
|
+
(typeof element.type === "function" || typeof element.type === "object") &&
|
|
67
|
+
"__MENU_PRIMITIVE_TYPE" in element.type &&
|
|
68
|
+
typeof element.type.__MENU_PRIMITIVE_TYPE === "string"
|
|
56
69
|
) {
|
|
57
|
-
return element.type.
|
|
70
|
+
return element.type.__MENU_PRIMITIVE_TYPE;
|
|
58
71
|
}
|
|
59
72
|
};
|
|
60
73
|
|
|
61
|
-
|
|
62
|
-
|
|
74
|
+
// Add a menu item to the provided allMenuItems array and menuItemsMap object
|
|
75
|
+
const addMenuItem = ({
|
|
76
|
+
itemProps,
|
|
77
|
+
allMenuItems,
|
|
78
|
+
menuItemsMap,
|
|
79
|
+
}: {
|
|
80
|
+
itemProps: Omit<TypeInternalItemProps, "index">;
|
|
81
|
+
allMenuItems: TypeMenuContext["items"];
|
|
82
|
+
menuItemsMap: TypeMenuContext["itemsMap"];
|
|
83
|
+
}) => {
|
|
84
|
+
const item = {
|
|
85
|
+
...itemProps,
|
|
86
|
+
index: allMenuItems.length,
|
|
87
|
+
};
|
|
88
|
+
allMenuItems.push(item);
|
|
89
|
+
menuItemsMap[`${item.id}`] = item;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// Add items from a MenuGroup element to the allMenuItems array and menuItemsMap object
|
|
93
|
+
const addItemsFromGroup = <I extends TypeMenuItemProps = TypeMenuItemProps>({
|
|
94
|
+
groupElement: menuGroup,
|
|
95
|
+
allMenuItems,
|
|
96
|
+
menuItemsMap,
|
|
97
|
+
}: {
|
|
98
|
+
groupElement: TypeMenuGroupElement<I>;
|
|
99
|
+
allMenuItems: TypeMenuContext["items"];
|
|
100
|
+
menuItemsMap: TypeMenuContext["itemsMap"];
|
|
101
|
+
}) => {
|
|
102
|
+
const menuItems = menuGroup.props.menuItems;
|
|
103
|
+
if (menuItems) {
|
|
104
|
+
menuItems.forEach((menuItem) => {
|
|
105
|
+
addMenuItem({
|
|
106
|
+
itemProps: {
|
|
107
|
+
id: menuItem.id,
|
|
108
|
+
disabled: menuItem.disabled,
|
|
109
|
+
menuItemProps: menuItem,
|
|
110
|
+
menuGroupProps: menuGroup.props,
|
|
111
|
+
},
|
|
112
|
+
allMenuItems,
|
|
113
|
+
menuItemsMap,
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
} else if (menuGroup.props.children) {
|
|
117
|
+
let children = menuGroup.props.children as TypeItemElement[];
|
|
118
|
+
// If there is only one child, it will be an object. Make it an array.
|
|
119
|
+
if (!Array.isArray(children)) {
|
|
120
|
+
children = [children];
|
|
121
|
+
}
|
|
122
|
+
children.forEach((menuItem) => {
|
|
123
|
+
addMenuItem({
|
|
124
|
+
itemProps: {
|
|
125
|
+
id: menuItem.props.id,
|
|
126
|
+
disabled: menuItem.props.disabled,
|
|
127
|
+
menuItemProps: menuItem.props,
|
|
128
|
+
menuGroupProps: menuGroup.props,
|
|
129
|
+
},
|
|
130
|
+
allMenuItems,
|
|
131
|
+
menuItemsMap,
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
// Add items from a MenuContent element to the allMenuItems array and menuItemsMap object
|
|
138
|
+
const addItemsFromContent = <I extends TypeMenuItemProps = TypeMenuItemProps>({
|
|
139
|
+
contentElement: menuContent,
|
|
140
|
+
allMenuItems,
|
|
141
|
+
menuItemsMap,
|
|
142
|
+
}: {
|
|
143
|
+
contentElement: TypeMenuContentElement<I>;
|
|
144
|
+
allMenuItems: TypeMenuContext["items"];
|
|
145
|
+
menuItemsMap: TypeMenuContext["itemsMap"];
|
|
146
|
+
}) => {
|
|
147
|
+
/**
|
|
148
|
+
* No need to add items inside of MenuContent if it does not have menuItems
|
|
149
|
+
* since we already traversed the children and menuContentProps are not needed.
|
|
150
|
+
*/
|
|
151
|
+
if (!("menuItems" in menuContent.props)) return;
|
|
152
|
+
// Add items from menuItems prop if it isn't empty
|
|
153
|
+
if (menuContent.props.menuItems) {
|
|
154
|
+
menuContent.props.menuItems.forEach((menuItem) => {
|
|
155
|
+
addMenuItem({
|
|
156
|
+
itemProps: {
|
|
157
|
+
id: menuItem.id,
|
|
158
|
+
disabled: menuItem.disabled,
|
|
159
|
+
menuItemProps: menuItem,
|
|
160
|
+
},
|
|
161
|
+
allMenuItems,
|
|
162
|
+
menuItemsMap,
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
// Traverse the children of a Menu component and add items to the allMenuItems array and menuItemsMap object
|
|
169
|
+
const setMenuItemsFromChildren = ({
|
|
170
|
+
children,
|
|
171
|
+
allMenuItems,
|
|
172
|
+
menuItemsMap,
|
|
173
|
+
}: {
|
|
174
|
+
children: ReactNode;
|
|
175
|
+
allMenuItems: TypeMenuContext["items"];
|
|
176
|
+
menuItemsMap: TypeMenuContext["itemsMap"];
|
|
177
|
+
}) => {
|
|
63
178
|
walkAllChildren(children, (element) => {
|
|
64
179
|
switch (getComponentName(element)) {
|
|
65
180
|
case "MenuItem":
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
181
|
+
// Cast element to the TypeItemElement since we know it's one of the three.
|
|
182
|
+
// This is needed for the addMenuItem function.
|
|
183
|
+
const menuItem = element as TypeItemElement;
|
|
184
|
+
// Items in groups would have already been added to the map.
|
|
185
|
+
if (menuItemsMap[`${menuItem.props.id}`]) return;
|
|
186
|
+
addMenuItem({
|
|
187
|
+
itemProps: {
|
|
188
|
+
id: menuItem.props.id,
|
|
189
|
+
disabled: menuItem.props.disabled,
|
|
190
|
+
menuItemProps: menuItem.props,
|
|
191
|
+
},
|
|
192
|
+
allMenuItems,
|
|
193
|
+
menuItemsMap,
|
|
69
194
|
});
|
|
70
195
|
break;
|
|
71
196
|
case "MenuGroup":
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
197
|
+
addItemsFromGroup({
|
|
198
|
+
// Cast element to the TypeMenuGroupElement since we know it's a group and is needed for addItemsFromGroup.
|
|
199
|
+
groupElement: element as TypeMenuGroupElement,
|
|
200
|
+
allMenuItems,
|
|
201
|
+
menuItemsMap,
|
|
75
202
|
});
|
|
76
203
|
break;
|
|
77
204
|
case "MenuContent":
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
205
|
+
addItemsFromContent({
|
|
206
|
+
// Cast element to the TypeMenuContentElement since we know it's a content and is needed for addItemsFromContent.
|
|
207
|
+
contentElement: element as TypeMenuContentElement,
|
|
208
|
+
allMenuItems,
|
|
209
|
+
menuItemsMap,
|
|
81
210
|
});
|
|
82
211
|
break;
|
|
83
212
|
default:
|
|
84
213
|
break;
|
|
85
214
|
}
|
|
86
215
|
});
|
|
87
|
-
return { orderedElements };
|
|
88
216
|
};
|
|
89
217
|
|
|
90
|
-
// Returns a fragment containing a `MenuItemComponent` for each item in `
|
|
91
|
-
export const mapMenuItems = ({
|
|
92
|
-
|
|
93
|
-
MenuItemComponent,
|
|
94
|
-
}: TypeMapMenuItemsProps) => {
|
|
218
|
+
// Returns a fragment containing a `MenuItemComponent` for each item in `menuItems`.
|
|
219
|
+
export const mapMenuItems = <I extends TypeMenuItemProps = TypeMenuItemProps>({
|
|
220
|
+
menuItems,
|
|
221
|
+
MenuItemComponent = MenuItem,
|
|
222
|
+
}: TypeMapMenuItemsProps<I>) => {
|
|
95
223
|
return (
|
|
96
224
|
<>
|
|
97
|
-
{
|
|
98
|
-
<MenuItemComponent key={
|
|
225
|
+
{menuItems.map((menuItemProps) => (
|
|
226
|
+
<MenuItemComponent key={menuItemProps.id} {...menuItemProps} />
|
|
99
227
|
))}
|
|
100
228
|
</>
|
|
101
229
|
);
|
|
102
230
|
};
|
|
103
231
|
|
|
104
|
-
|
|
232
|
+
type TypeUseMenuChildrenProps<I extends TypeMenuItemProps = TypeMenuItemProps> =
|
|
233
|
+
{
|
|
234
|
+
children?: TypeMenuRootProps["children"];
|
|
235
|
+
menuItems?: I[];
|
|
236
|
+
MenuItemComponent?: React.ComponentType<I>;
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
// Return a MenuContent using the provided menuItems
|
|
240
|
+
export const getChildrenFromMenuItems = <
|
|
241
|
+
I extends TypeMenuItemProps = TypeMenuItemProps
|
|
242
|
+
>({
|
|
243
|
+
menuItems,
|
|
244
|
+
MenuItemComponent,
|
|
245
|
+
}: {
|
|
246
|
+
menuItems: I[];
|
|
247
|
+
MenuItemComponent?: React.ComponentType<I>;
|
|
248
|
+
}) => (
|
|
249
|
+
<MenuContent menuItems={menuItems} MenuItemComponent={MenuItemComponent} />
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
// Hook to get all menu items and children for a Menu component
|
|
253
|
+
export const useMenuChildren = <
|
|
254
|
+
I extends TypeMenuItemProps = TypeMenuItemProps
|
|
255
|
+
>({
|
|
105
256
|
children: childrenProp,
|
|
106
|
-
|
|
107
|
-
MenuItemComponent
|
|
108
|
-
}: TypeUseMenuChildrenProps) => {
|
|
257
|
+
menuItems,
|
|
258
|
+
MenuItemComponent,
|
|
259
|
+
}: TypeUseMenuChildrenProps<I>) => {
|
|
109
260
|
const { allMenuItems, menuItemsMap, children } = useMemo(() => {
|
|
110
261
|
let children: TypeNotEmptyChildren = <></>;
|
|
111
|
-
let index = 0;
|
|
112
262
|
const menuItemsMap: TypeMenuContext["itemsMap"] = {};
|
|
113
263
|
const allMenuItems: TypeMenuContext["items"] = [];
|
|
114
264
|
|
|
115
|
-
// If an
|
|
116
|
-
if (
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
id: menuItem.id,
|
|
120
|
-
index: index++,
|
|
121
|
-
disabled: menuItem.disabled,
|
|
122
|
-
menuItemProps: menuItem,
|
|
123
|
-
};
|
|
124
|
-
allMenuItems.push(item);
|
|
125
|
-
menuItemsMap[`${item.id}`] = item;
|
|
265
|
+
// If an menuItems prop was passed to the Menu, we can bypass the children traversal.
|
|
266
|
+
if (menuItems && menuItems.length) {
|
|
267
|
+
menuItems.forEach((menuItem) => {
|
|
268
|
+
addMenuItem({ itemProps: menuItem, allMenuItems, menuItemsMap });
|
|
126
269
|
});
|
|
127
|
-
children = (
|
|
128
|
-
<MenuContent>{mapMenuItems({ items, MenuItemComponent })}</MenuContent>
|
|
129
|
-
);
|
|
270
|
+
children = getChildrenFromMenuItems({ menuItems, MenuItemComponent });
|
|
130
271
|
} else if (childrenProp) {
|
|
131
272
|
children = childrenProp as TypeNotEmptyChildren;
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
We do not need to worry about checking its children prop because
|
|
137
|
-
we are already traversing the children, and we do not need to add any
|
|
138
|
-
menuContentProps to add to the item object*/
|
|
139
|
-
if (type === "content") {
|
|
140
|
-
const menuContent = element as TypeMenuContentElement;
|
|
141
|
-
const items = menuContent.props.items;
|
|
142
|
-
if (items) {
|
|
143
|
-
items.forEach((menuItem) => {
|
|
144
|
-
const item = {
|
|
145
|
-
id: menuItem.id,
|
|
146
|
-
index: index++,
|
|
147
|
-
children: menuItem.children,
|
|
148
|
-
disabled: menuItem.disabled,
|
|
149
|
-
menuItemProps: menuItem,
|
|
150
|
-
};
|
|
151
|
-
allMenuItems.push(item);
|
|
152
|
-
menuItemsMap[`${item.id}`] = item;
|
|
153
|
-
});
|
|
154
|
-
}
|
|
155
|
-
} else if (type === "group") {
|
|
156
|
-
const menuGroup = element as TypeMenuGroupElement;
|
|
157
|
-
const items = menuGroup.props.items;
|
|
158
|
-
if (items) {
|
|
159
|
-
items.forEach((menuItem) => {
|
|
160
|
-
const item = {
|
|
161
|
-
id: menuItem.id,
|
|
162
|
-
index: index++,
|
|
163
|
-
children: menuItem.children,
|
|
164
|
-
disabled: menuItem.disabled,
|
|
165
|
-
menuItemProps: menuItem,
|
|
166
|
-
menuGroupProps: element.props as TypeMenuGroupProps,
|
|
167
|
-
};
|
|
168
|
-
allMenuItems.push(item);
|
|
169
|
-
menuItemsMap[`${item.id}`] = item;
|
|
170
|
-
});
|
|
171
|
-
} else if (menuGroup.props.children) {
|
|
172
|
-
let children = menuGroup.props.children as TypeItemElement[];
|
|
173
|
-
// If there is only one child, it will be an object. Make it an array.
|
|
174
|
-
if (!Array.isArray(children)) {
|
|
175
|
-
children = [children];
|
|
176
|
-
}
|
|
177
|
-
children.forEach((menuItem) => {
|
|
178
|
-
const item = {
|
|
179
|
-
id: menuItem.props.id,
|
|
180
|
-
index: index++,
|
|
181
|
-
children: menuItem.props.children,
|
|
182
|
-
disabled: menuItem.props.disabled,
|
|
183
|
-
menuItemProps: menuItem.props,
|
|
184
|
-
menuGroupProps: element.props as TypeMenuGroupProps,
|
|
185
|
-
};
|
|
186
|
-
allMenuItems.push(item);
|
|
187
|
-
menuItemsMap[`${item.id}`] = item;
|
|
188
|
-
});
|
|
189
|
-
}
|
|
190
|
-
} else {
|
|
191
|
-
// Items in groups would have already been added to the map.
|
|
192
|
-
if (menuItemsMap[`${element.props.id}`]) return;
|
|
193
|
-
const menuItem = element as TypeItemElement;
|
|
194
|
-
const item = {
|
|
195
|
-
id: menuItem.props.id,
|
|
196
|
-
index: index++,
|
|
197
|
-
disabled: menuItem.props.disabled,
|
|
198
|
-
menuItemProps: menuItem.props,
|
|
199
|
-
};
|
|
200
|
-
allMenuItems.push(item);
|
|
201
|
-
menuItemsMap[`${item.id}`] = item;
|
|
202
|
-
}
|
|
273
|
+
setMenuItemsFromChildren({
|
|
274
|
+
children: childrenProp,
|
|
275
|
+
allMenuItems,
|
|
276
|
+
menuItemsMap,
|
|
203
277
|
});
|
|
204
278
|
}
|
|
205
279
|
|
|
206
280
|
return { allMenuItems, menuItemsMap, children };
|
|
207
|
-
}, [childrenProp,
|
|
281
|
+
}, [childrenProp, menuItems, MenuItemComponent]);
|
|
208
282
|
|
|
209
283
|
return { allMenuItems, menuItemsMap, children };
|
|
210
284
|
};
|
package/src/index.ts
CHANGED
|
@@ -7,15 +7,21 @@ export * from "./MenuGroup";
|
|
|
7
7
|
export * from "./MenuRoot";
|
|
8
8
|
export * from "./MenuLabel";
|
|
9
9
|
export * from "./MenuToggleButton";
|
|
10
|
+
export * from "./MenuSearchInput";
|
|
11
|
+
export * from "./MenuTokenInput";
|
|
12
|
+
export * from "./MenuSearchTokenInput";
|
|
13
|
+
export * from "./SubmenuItem";
|
|
10
14
|
|
|
11
15
|
// Menus
|
|
12
16
|
export * from "./ActionMenu";
|
|
13
17
|
export * from "./SingleSelectMenu";
|
|
14
18
|
export * from "./MultiSelectMenu";
|
|
15
19
|
export * from "./Autocomplete";
|
|
20
|
+
export * from "./NestedMenu";
|
|
16
21
|
|
|
17
22
|
// Utilities
|
|
18
23
|
export * from "./MenuContext";
|
|
24
|
+
export * from "./NestedMenu/NestedMenuContext";
|
|
19
25
|
export * from "./hooks/useMenuChildren";
|
|
20
26
|
export * from "./reducers/useSelectStateReducer";
|
|
21
27
|
export * from "./types";
|
package/src/menuTestingUtils.tsx
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import {
|
|
3
|
-
|
|
3
|
+
MenuContentContext,
|
|
4
4
|
MenuRoot,
|
|
5
|
+
checkIfItemSelected,
|
|
6
|
+
getSelectedItemIds,
|
|
7
|
+
useMenu,
|
|
5
8
|
type TypeMenuContextProps,
|
|
6
9
|
type TypeMenuContext,
|
|
7
10
|
type TypeMenuRootProps,
|
|
8
11
|
} from "./index";
|
|
9
12
|
|
|
10
|
-
export const testDefaultContext = (
|
|
11
|
-
|
|
12
|
-
|
|
13
|
+
export const testDefaultContext = ({
|
|
14
|
+
getToggleButtonProps,
|
|
15
|
+
...context
|
|
16
|
+
}: Partial<TypeMenuContextProps> = {}): TypeMenuContext => ({
|
|
13
17
|
items: [],
|
|
14
18
|
itemsMap: context.items
|
|
15
19
|
? context.items.reduce((map, item) => {
|
|
@@ -17,10 +21,16 @@ export const testDefaultContext = (
|
|
|
17
21
|
return map;
|
|
18
22
|
}, {} as TypeMenuContext["itemsMap"])
|
|
19
23
|
: {},
|
|
20
|
-
// TODO: Make type on
|
|
24
|
+
// TODO: Make type on MenuContentContext more agnostic for other use cases
|
|
21
25
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
22
26
|
// @ts-expect-error - mocked function does not match real type
|
|
23
27
|
getItemProps: (props) => ({ role: "option", ...props }),
|
|
28
|
+
// @ts-expect-error - mocked function does not match real type
|
|
29
|
+
getToggleButtonProps: ({ ref, refKey, ...props } = {}) => ({
|
|
30
|
+
...getToggleButtonProps?.({ ref, refKey, ...props }),
|
|
31
|
+
...(ref ? { [refKey]: ref } : {}),
|
|
32
|
+
...props,
|
|
33
|
+
}),
|
|
24
34
|
isListbox: true,
|
|
25
35
|
...context,
|
|
26
36
|
});
|
|
@@ -53,12 +63,12 @@ export const TestWithMenuRoot = ({
|
|
|
53
63
|
|
|
54
64
|
/**
|
|
55
65
|
* TestWithMockedMenu
|
|
56
|
-
* Given a children and context, render the children with the raw
|
|
66
|
+
* Given a children and context, render the children with the raw MenuContentContext with a mocked value.
|
|
57
67
|
* Used for unit testing components without the real MenuProvider or MenuRoot.
|
|
58
68
|
*
|
|
59
|
-
* @param children - react children to be loaded inside of the
|
|
69
|
+
* @param children - react children to be loaded inside of the MenuContentContext.Provider
|
|
60
70
|
* @param context - mocked context props to override the testDefaultContext
|
|
61
|
-
* @returns - rendered component with mocked
|
|
71
|
+
* @returns - rendered component with mocked MenuContentContext
|
|
62
72
|
*/
|
|
63
73
|
export const TestWithMockedMenu = ({
|
|
64
74
|
children,
|
|
@@ -67,9 +77,10 @@ export const TestWithMockedMenu = ({
|
|
|
67
77
|
children: React.ReactNode;
|
|
68
78
|
context: Partial<TypeMenuContextProps>;
|
|
69
79
|
}) => {
|
|
80
|
+
const menuContext = useMenu(testDefaultContext(context));
|
|
70
81
|
return (
|
|
71
|
-
<
|
|
82
|
+
<MenuContentContext.Provider value={menuContext}>
|
|
72
83
|
{children}
|
|
73
|
-
</
|
|
84
|
+
</MenuContentContext.Provider>
|
|
74
85
|
);
|
|
75
86
|
};
|