@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.
Files changed (104) hide show
  1. package/.turbo/turbo-build.log +10 -10
  2. package/CHANGELOG.md +72 -0
  3. package/dist/esm/index.js +1529 -440
  4. package/dist/esm/index.js.map +1 -1
  5. package/dist/index.d.mts +275 -92
  6. package/dist/index.d.ts +275 -92
  7. package/dist/index.js +1558 -444
  8. package/dist/index.js.map +1 -1
  9. package/package.json +15 -12
  10. package/src/ActionMenu/ActionMenu.stories.tsx +422 -0
  11. package/src/ActionMenu/ActionMenu.tsx +24 -27
  12. package/src/ActionMenu/ActionMenuTypes.ts +8 -7
  13. package/src/ActionMenu/__tests__/ActionMenu.test.tsx +17 -8
  14. package/src/ActionMenu/__tests__/ActionMenu.typetest.tsx +8 -8
  15. package/src/Autocomplete/Autocomplete.stories.tsx +38 -3
  16. package/src/Autocomplete/Autocomplete.tsx +15 -80
  17. package/src/Autocomplete/AutocompleteInput.tsx +18 -27
  18. package/src/Autocomplete/AutocompleteTokenInput.tsx +50 -0
  19. package/src/Autocomplete/AutocompleteTypes.ts +25 -12
  20. package/src/Autocomplete/MultiAutocomplete.tsx +188 -0
  21. package/src/Autocomplete/SingleAutocomplete.tsx +74 -0
  22. package/src/Autocomplete/__tests__/Autocomplete.test.tsx +207 -3
  23. package/src/Autocomplete/__tests__/Autocomplete.typetest.tsx +90 -8
  24. package/src/MenuContent/MenuContent.stories.tsx +164 -0
  25. package/src/MenuContent/MenuContent.tsx +28 -12
  26. package/src/MenuContent/MenuContentTypes.ts +6 -4
  27. package/src/MenuContent/__tests__/MenuContent.typetest.tsx +8 -8
  28. package/src/MenuContext.tsx +192 -37
  29. package/src/MenuFooter/MenuFooter.stories.tsx +202 -0
  30. package/src/MenuFooter/__tests__/MenuFooter.test.tsx +16 -11
  31. package/src/MenuFooter/styles.tsx +1 -1
  32. package/src/MenuGroup/MenuGroup.feature +16 -16
  33. package/src/MenuGroup/MenuGroup.stories.tsx +248 -0
  34. package/src/MenuGroup/MenuGroup.tsx +12 -7
  35. package/src/MenuGroup/MenuGroupTypes.ts +6 -2
  36. package/src/MenuGroup/__tests__/MenuGroup.test.tsx +6 -6
  37. package/src/MenuGroup/__tests__/MenuGroup.typetest.tsx +7 -7
  38. package/src/MenuGroup/styles.tsx +4 -2
  39. package/src/MenuHeader/MenuHeader.stories.tsx +276 -0
  40. package/src/MenuHeader/MenuHeader.tsx +12 -7
  41. package/src/MenuHeader/__tests__/MenuHeader.test.tsx +23 -13
  42. package/src/MenuHeader/styles.tsx +22 -14
  43. package/src/MenuItem/MenuItem.stories.tsx +507 -0
  44. package/src/MenuItem/MenuItem.tsx +23 -9
  45. package/src/MenuItem/MenuItemTypes.ts +9 -4
  46. package/src/MenuItem/__tests__/MenuItem.test.tsx +6 -6
  47. package/src/MenuItem/styles.tsx +23 -5
  48. package/src/MenuItem/useOptionProps.tsx +11 -9
  49. package/src/MenuLabel.tsx +2 -2
  50. package/src/MenuRoot/MenuRoot.tsx +52 -24
  51. package/src/MenuRoot/__tests__/MenuRoot.test.tsx +59 -32
  52. package/src/MenuSearchInput/MenuSearchInput.tsx +85 -0
  53. package/src/MenuSearchInput/__tests__/MenuSearchInput.test.tsx +157 -0
  54. package/src/MenuSearchInput/__tests__/MenuSearchInput.typetest.tsx +31 -0
  55. package/src/MenuSearchInput/index.ts +1 -0
  56. package/src/MenuSearchTokenInput/MenuSearchTokenInput.tsx +108 -0
  57. package/src/MenuSearchTokenInput/__tests__/MenuSearchTokenInput.test.tsx +247 -0
  58. package/src/MenuSearchTokenInput/__tests__/MenuSearchTokenInput.typetest.tsx +33 -0
  59. package/src/MenuSearchTokenInput/index.ts +1 -0
  60. package/src/MenuToggleButton/MenuToggleButton.stories.tsx +72 -0
  61. package/src/{MenuToggleButton.tsx → MenuToggleButton/MenuToggleButton.tsx} +6 -5
  62. package/src/MenuToggleButton/index.ts +1 -0
  63. package/src/MenuTokenInput.tsx +104 -0
  64. package/src/MultiSelectMenu/MultiSelectMenu.stories.tsx +222 -0
  65. package/src/MultiSelectMenu/MultiSelectMenu.tsx +38 -68
  66. package/src/MultiSelectMenu/MultiSelectMenuTypes.ts +8 -6
  67. package/src/MultiSelectMenu/__tests__/MultiSelectMenu.test.tsx +46 -17
  68. package/src/MultiSelectMenu/__tests__/MultiSelectMenu.typetest.tsx +6 -6
  69. package/src/NestedMenu/NestedMenu.feature +109 -0
  70. package/src/NestedMenu/NestedMenu.stories.tsx +157 -0
  71. package/src/NestedMenu/NestedMenu.tsx +102 -0
  72. package/src/NestedMenu/NestedMenuContent.tsx +75 -0
  73. package/src/NestedMenu/NestedMenuContext.tsx +56 -0
  74. package/src/NestedMenu/NestedMenuHeader.tsx +25 -0
  75. package/src/NestedMenu/NestedMenuItem.tsx +41 -0
  76. package/src/NestedMenu/NestedMenuTypes.ts +47 -0
  77. package/src/NestedMenu/__tests__/NestedMenu.test.tsx +31 -0
  78. package/src/NestedMenu/__tests__/NestedMenu.typetest.tsx +134 -0
  79. package/src/NestedMenu/index.ts +5 -0
  80. package/src/SingleSelectMenu/SingleSelectMenu.stories.tsx +233 -0
  81. package/src/SingleSelectMenu/SingleSelectMenu.tsx +23 -26
  82. package/src/SingleSelectMenu/SingleSelectMenuTypes.ts +7 -5
  83. package/src/SingleSelectMenu/__tests__/SingleSelectMenu.typetest.tsx +6 -6
  84. package/src/SubmenuItem/SubmenuItem.feature +82 -0
  85. package/src/SubmenuItem/SubmenuItem.stories.tsx +163 -0
  86. package/src/SubmenuItem/SubmenuItem.tsx +258 -0
  87. package/src/SubmenuItem/SubmenuItemTypes.ts +24 -0
  88. package/src/SubmenuItem/__tests__/SubmenuItem.test.tsx +3 -0
  89. package/src/SubmenuItem/__tests__/SubmenuItem.typetest.tsx +112 -0
  90. package/src/SubmenuItem/index.ts +2 -0
  91. package/src/hooks/useDownshiftDefaults.tsx +117 -0
  92. package/src/hooks/useItemFiltering.tsx +68 -0
  93. package/src/hooks/useMenuChildren.tsx +205 -131
  94. package/src/index.ts +6 -0
  95. package/src/menuTestingUtils.tsx +21 -10
  96. package/src/reducers/nestedMenuStateReducer.tsx +23 -0
  97. package/src/reducers/useComboboxStateReducer.tsx +26 -8
  98. package/src/reducers/useSelectStateReducer.tsx +24 -6
  99. package/src/storybookUtilities/menu-storybook-components.tsx +5 -3
  100. package/src/types.ts +38 -30
  101. package/src/utils/downshiftDefaults.ts +9 -0
  102. package/src/utils/getMultiSelectOverrides.ts +21 -0
  103. package/src/utils/reduceReducers.ts +19 -0
  104. 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/index";
5
- import type { TypeNotEmptyChildren } from "../types";
6
- import { MenuContent, type TypeMenuContentProps } from "../MenuContent/index";
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 TypeUseMenuChildrenProps {
9
- children?: ReactNode;
10
- items?: TypeMenuItemProps[];
11
- MenuItemComponent?: React.ComponentType<TypeMenuItemProps>;
17
+ export interface TypeMapMenuItemsProps<
18
+ I extends TypeMenuItemProps = TypeMenuItemProps
19
+ > {
20
+ menuItems: I[];
21
+ MenuItemComponent?: React.ComponentType<I>;
12
22
  }
13
- export interface TypeMapMenuItemsProps {
14
- items: TypeMenuItemProps[];
15
- MenuItemComponent: React.ComponentType<TypeMenuItemProps>;
16
- }
17
- export type TypeItemElement = React.ReactElement<TypeMenuItemProps>;
18
- export type TypeMenuGroupElement = React.ReactElement<TypeMenuGroupProps>;
19
- export type TypeMenuContentElement = React.ReactElement<TypeMenuContentProps>;
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 !== "string" &&
55
- typeof element.type.name === "string"
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.name;
70
+ return element.type.__MENU_PRIMITIVE_TYPE;
58
71
  }
59
72
  };
60
73
 
61
- const getAllMenuItems = (children: ReactNode) => {
62
- const orderedElements: TypeMenuChildren[] = [];
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
- orderedElements.push({
67
- type: "item",
68
- element: element as TypeItemElement,
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
- orderedElements.push({
73
- type: "group",
74
- element: element as TypeMenuGroupElement,
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
- orderedElements.push({
79
- type: "content",
80
- element: element as TypeMenuContentElement,
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 `items`.
91
- export const mapMenuItems = ({
92
- items,
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
- {items.map((menuItem) => (
98
- <MenuItemComponent key={menuItem.id} {...menuItem} />
225
+ {menuItems.map((menuItemProps) => (
226
+ <MenuItemComponent key={menuItemProps.id} {...menuItemProps} />
99
227
  ))}
100
228
  </>
101
229
  );
102
230
  };
103
231
 
104
- export const useMenuChildren = ({
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
- items,
107
- MenuItemComponent = MenuItem,
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 items prop was passed to the Menu, we can bypass the children traversal.
116
- if (items && items.length) {
117
- items.forEach((menuItem) => {
118
- const item = {
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
- const { orderedElements } = getAllMenuItems(childrenProp);
133
-
134
- orderedElements.forEach(({ type, element }) => {
135
- /* If MenuContent has items, we need to add them to the menuItemsMap.
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, items, MenuItemComponent]);
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";
@@ -1,15 +1,19 @@
1
1
  import React from "react";
2
2
  import {
3
- MenuContext,
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
- context: Partial<TypeMenuContextProps> = {}
12
- ): TypeMenuContext => ({
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 MenuContext more agnostic for other use cases
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 MenuContext with a mocked value.
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 MenuContext.Provider
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 MenuContext
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
- <MenuContext.Provider value={testDefaultContext(context)}>
82
+ <MenuContentContext.Provider value={menuContext}>
72
83
  {children}
73
- </MenuContext.Provider>
84
+ </MenuContentContext.Provider>
74
85
  );
75
86
  };