@sproutsocial/seeds-react-menu 0.4.0 → 0.5.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 +35 -0
- package/dist/esm/index.js +221 -137
- package/dist/esm/index.js.map +1 -1
- package/dist/index.d.mts +126 -101
- package/dist/index.d.ts +126 -101
- package/dist/index.js +236 -151
- package/dist/index.js.map +1 -1
- package/package.json +12 -12
- package/src/ActionMenu/ActionMenu.tsx +10 -3
- package/src/ActionMenu/ActionMenuTypes.ts +10 -6
- package/src/ActionMenu/__tests__/ActionMenu.test.tsx +0 -22
- package/src/ActionMenu/__tests__/ActionMenu.typetest.tsx +32 -3
- package/src/Autocomplete/Autocomplete.stories.tsx +24 -0
- package/src/Autocomplete/Autocomplete.tsx +10 -4
- package/src/Autocomplete/AutocompleteTypes.ts +6 -3
- package/src/Autocomplete/__tests__/Autocomplete.typetest.tsx +34 -3
- package/src/MenuContent/MenuContent.tsx +14 -1
- package/src/MenuContent/MenuContentTypes.ts +5 -5
- package/src/MenuContent/__tests__/MenuContent.typetest.tsx +17 -1
- package/src/MenuGroup/MenuGroup.feature +6 -6
- package/src/MenuGroup/MenuGroup.tsx +2 -1
- package/src/MenuGroup/MenuGroupTypes.ts +4 -6
- package/src/MenuGroup/__tests__/MenuGroup.typetest.tsx +23 -4
- package/src/MenuItem/MenuItem.tsx +0 -1
- package/src/MultiSelectMenu/MultiSelectMenu.tsx +10 -3
- package/src/MultiSelectMenu/MultiSelectMenuTypes.ts +8 -6
- package/src/MultiSelectMenu/__tests__/MultiSelectMenu.test.tsx +0 -43
- package/src/MultiSelectMenu/__tests__/MultiSelectMenu.typetest.tsx +29 -2
- package/src/SingleSelectMenu/SingleSelectMenu.tsx +10 -3
- package/src/SingleSelectMenu/SingleSelectMenuTypes.ts +7 -5
- package/src/SingleSelectMenu/__tests__/SingleSelectMenu.typetest.tsx +32 -2
- package/src/__tests__/Menu.test.tsx +38 -0
- package/src/hooks/useMenuChildren.tsx +210 -0
- package/src/index.ts +1 -1
- package/src/types.ts +26 -1
- package/src/hooks/useItemsFromChildren.tsx +0 -131
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
MultiSelectMenu,
|
|
4
4
|
MenuContent,
|
|
5
5
|
MenuHeader,
|
|
6
|
+
MenuItem,
|
|
6
7
|
MenuFooter,
|
|
7
8
|
} from "../../index";
|
|
8
9
|
|
|
@@ -20,6 +21,17 @@ export const MultiSelectMenuTypeTest = () => (
|
|
|
20
21
|
<MenuContent>Test</MenuContent>
|
|
21
22
|
<MenuFooter>Test</MenuFooter>
|
|
22
23
|
</MultiSelectMenu>
|
|
24
|
+
{/* Valid MultiSelectMenu allows items to be passed using the items prop */}
|
|
25
|
+
<MultiSelectMenu
|
|
26
|
+
menuToggleElement={<>test</>}
|
|
27
|
+
items={[{ children: "Item Name", id: "item-1" }]}
|
|
28
|
+
/>
|
|
29
|
+
{/* Valid MultiSelectMenu allows items prop to be rendered using a MenuItemComponent prop */}
|
|
30
|
+
<MultiSelectMenu
|
|
31
|
+
menuToggleElement={<>test</>}
|
|
32
|
+
items={[{ children: "Item Name", id: "item-1" }]}
|
|
33
|
+
MenuItemComponent={MenuItem}
|
|
34
|
+
/>
|
|
23
35
|
{/* Valid MultiSelectMenu accepts Downshift props and functions for useSelect */}
|
|
24
36
|
<MultiSelectMenu
|
|
25
37
|
menuToggleElement={<>test</>}
|
|
@@ -52,8 +64,23 @@ export const MultiSelectMenuTypeTest = () => (
|
|
|
52
64
|
>
|
|
53
65
|
Test
|
|
54
66
|
</MultiSelectMenu>
|
|
55
|
-
{/* @ts-expect-error - when no children provided */}
|
|
56
|
-
<MultiSelectMenu menuToggleElement=
|
|
67
|
+
{/* @ts-expect-error - when no children or items provided */}
|
|
68
|
+
<MultiSelectMenu menuToggleElement={<>test</>} />
|
|
69
|
+
{/* @ts-expect-error - when both children and MenuItemComponent provided */}
|
|
70
|
+
<MultiSelectMenu menuToggleElement={<>test</>} MenuItemComponent={MenuItem}>
|
|
71
|
+
Children
|
|
72
|
+
</MultiSelectMenu>
|
|
73
|
+
{/* @ts-expect-error - when both children and items provided */}
|
|
74
|
+
<MultiSelectMenu menuToggleElement={<>test</>} items={[]}>
|
|
75
|
+
Children
|
|
76
|
+
</MultiSelectMenu>
|
|
77
|
+
<MultiSelectMenu
|
|
78
|
+
menuToggleElement={<>test</>}
|
|
79
|
+
/* @ts-expect-error - when MenuItemComponent is of the wrong type */
|
|
80
|
+
MenuItemComponent={MenuContent}
|
|
81
|
+
>
|
|
82
|
+
Children
|
|
83
|
+
</MultiSelectMenu>
|
|
57
84
|
|
|
58
85
|
<MultiSelectMenu
|
|
59
86
|
menuToggleElement={<>test</>}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { useSelect, type UseSelectState } from "downshift";
|
|
3
|
-
import {
|
|
3
|
+
import { useMenuChildren } from "../hooks/useMenuChildren";
|
|
4
4
|
import {
|
|
5
5
|
defaultUseSelectProps,
|
|
6
6
|
useSelectStateReducer,
|
|
@@ -9,6 +9,7 @@ import { itemToString } from "../utils/itemToString";
|
|
|
9
9
|
import { MenuRoot } from "../MenuRoot";
|
|
10
10
|
import type { TypeDefaultItemProps } from "../types";
|
|
11
11
|
import type { TypeSingleSelectMenuProps } from "./SingleSelectMenuTypes";
|
|
12
|
+
import { MenuItem } from "../MenuItem/index";
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* @link https://seeds.sproutsocial.com/components/singleselect-menu/
|
|
@@ -20,12 +21,18 @@ import type { TypeSingleSelectMenuProps } from "./SingleSelectMenuTypes";
|
|
|
20
21
|
*/
|
|
21
22
|
|
|
22
23
|
export const SingleSelectMenu = ({
|
|
23
|
-
children,
|
|
24
|
+
children: childrenProp,
|
|
24
25
|
stateReducer: externalStateReducer,
|
|
26
|
+
items,
|
|
27
|
+
MenuItemComponent = MenuItem,
|
|
25
28
|
...useSelectProps
|
|
26
29
|
}: TypeSingleSelectMenuProps) => {
|
|
27
30
|
/** Get a list of items from the children */
|
|
28
|
-
const { allMenuItems, menuItemsMap } =
|
|
31
|
+
const { allMenuItems, menuItemsMap, children } = useMenuChildren({
|
|
32
|
+
children: childrenProp,
|
|
33
|
+
items,
|
|
34
|
+
MenuItemComponent,
|
|
35
|
+
});
|
|
29
36
|
|
|
30
37
|
/**
|
|
31
38
|
* Destructure Downshift props and call the core useSelect with items and state handler
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { TypeMenuRootOwnProps } from "../MenuRoot";
|
|
2
2
|
import type { TypeDownshiftSelectProps } from "../MenuContext";
|
|
3
|
+
import type { TypeChildrenOrItems } from "../types";
|
|
3
4
|
|
|
4
|
-
|
|
5
|
+
interface TypeSingleSelectMenuBaseProps
|
|
5
6
|
extends TypeMenuRootOwnProps,
|
|
6
|
-
TypeDownshiftSelectProps {
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
Omit<TypeDownshiftSelectProps, "items"> {}
|
|
8
|
+
|
|
9
|
+
export type TypeSingleSelectMenuProps = TypeSingleSelectMenuBaseProps &
|
|
10
|
+
TypeChildrenOrItems;
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
MenuContent,
|
|
4
4
|
MenuHeader,
|
|
5
5
|
MenuFooter,
|
|
6
|
+
MenuItem,
|
|
6
7
|
} from "../../index";
|
|
7
8
|
|
|
8
9
|
export const SingleSelectMenuTypeTest = () => (
|
|
@@ -19,6 +20,17 @@ export const SingleSelectMenuTypeTest = () => (
|
|
|
19
20
|
<MenuContent>Test</MenuContent>
|
|
20
21
|
<MenuFooter>Test</MenuFooter>
|
|
21
22
|
</SingleSelectMenu>
|
|
23
|
+
{/* Valid SingleSelectMenu allows items to be passed using the items prop */}
|
|
24
|
+
<SingleSelectMenu
|
|
25
|
+
menuToggleElement={<>test</>}
|
|
26
|
+
items={[{ children: "Item Name", id: "item-1" }]}
|
|
27
|
+
/>
|
|
28
|
+
{/* Valid SingleSelectMenu allows items prop to be rendered using a MenuItemComponent prop */}
|
|
29
|
+
<SingleSelectMenu
|
|
30
|
+
menuToggleElement={<>test</>}
|
|
31
|
+
items={[{ children: "Item Name", id: "item-1" }]}
|
|
32
|
+
MenuItemComponent={MenuItem}
|
|
33
|
+
/>
|
|
22
34
|
{/* Valid SingleSelectMenu accepts Downshift props and functions for useSelect */}
|
|
23
35
|
<SingleSelectMenu
|
|
24
36
|
menuToggleElement={<>test</>}
|
|
@@ -40,8 +52,26 @@ export const SingleSelectMenuTypeTest = () => (
|
|
|
40
52
|
>
|
|
41
53
|
Test
|
|
42
54
|
</SingleSelectMenu>
|
|
43
|
-
{/* @ts-expect-error - when no children provided */}
|
|
44
|
-
<SingleSelectMenu menuToggleElement=
|
|
55
|
+
{/* @ts-expect-error - when no children or items provided */}
|
|
56
|
+
<SingleSelectMenu menuToggleElement={<>test</>} />
|
|
57
|
+
{/* @ts-expect-error - when both children and MenuItemComponent provided */}
|
|
58
|
+
<SingleSelectMenu
|
|
59
|
+
menuToggleElement={<>test</>}
|
|
60
|
+
MenuItemComponent={MenuItem}
|
|
61
|
+
>
|
|
62
|
+
Children
|
|
63
|
+
</SingleSelectMenu>
|
|
64
|
+
{/* @ts-expect-error - when both children and items provided */}
|
|
65
|
+
<SingleSelectMenu menuToggleElement={<>test</>} items={[]}>
|
|
66
|
+
Children
|
|
67
|
+
</SingleSelectMenu>
|
|
68
|
+
<SingleSelectMenu
|
|
69
|
+
menuToggleElement={<>test</>}
|
|
70
|
+
/* @ts-expect-error - when MenuItemComponent is of the wrong type */
|
|
71
|
+
MenuItemComponent={MenuContent}
|
|
72
|
+
>
|
|
73
|
+
Children
|
|
74
|
+
</SingleSelectMenu>
|
|
45
75
|
|
|
46
76
|
<SingleSelectMenu
|
|
47
77
|
menuToggleElement={<>test</>}
|
|
@@ -356,5 +356,43 @@ describe("Menu", () => {
|
|
|
356
356
|
// Check that stateReducer is called
|
|
357
357
|
await waitFor(() => expect(mockStateReducer).toHaveBeenCalled());
|
|
358
358
|
});
|
|
359
|
+
|
|
360
|
+
it("should render the MenuItems when provided as children", async () => {
|
|
361
|
+
const { user } = render(
|
|
362
|
+
<Component menuToggleElement={toggleElement}>
|
|
363
|
+
<MenuContent>
|
|
364
|
+
<MenuItem id="item-1">Item 1</MenuItem>
|
|
365
|
+
<MenuItem id="item-2">Item 2</MenuItem>
|
|
366
|
+
</MenuContent>
|
|
367
|
+
</Component>
|
|
368
|
+
);
|
|
369
|
+
|
|
370
|
+
// Open Menu
|
|
371
|
+
const menuTrigger = screen.getByRole("button", { name: "Open Menu" });
|
|
372
|
+
await user.click(menuTrigger);
|
|
373
|
+
|
|
374
|
+
// Check if both items are rendered
|
|
375
|
+
await screen.findByText("Item 1");
|
|
376
|
+
await screen.findByText("Item 2");
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
it("should render the MenuItems when provided as items", async () => {
|
|
380
|
+
const { user } = render(
|
|
381
|
+
<Component menuToggleElement={toggleElement}>
|
|
382
|
+
<MenuContent>
|
|
383
|
+
<MenuItem id="item-1">Item 1</MenuItem>
|
|
384
|
+
<MenuItem id="item-2">Item 2</MenuItem>
|
|
385
|
+
</MenuContent>
|
|
386
|
+
</Component>
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
// Open Menu
|
|
390
|
+
const menuTrigger = screen.getByRole("button", { name: "Open Menu" });
|
|
391
|
+
await user.click(menuTrigger);
|
|
392
|
+
|
|
393
|
+
// Check if both items are rendered
|
|
394
|
+
await screen.findByText("Item 1");
|
|
395
|
+
await screen.findByText("Item 2");
|
|
396
|
+
});
|
|
359
397
|
});
|
|
360
398
|
});
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import React, { type ReactNode, useMemo } from "react";
|
|
2
|
+
import { MenuItem, type TypeMenuItemProps } from "../MenuItem";
|
|
3
|
+
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";
|
|
7
|
+
|
|
8
|
+
export interface TypeUseMenuChildrenProps {
|
|
9
|
+
children?: ReactNode;
|
|
10
|
+
items?: TypeMenuItemProps[];
|
|
11
|
+
MenuItemComponent?: React.ComponentType<TypeMenuItemProps>;
|
|
12
|
+
}
|
|
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>;
|
|
20
|
+
|
|
21
|
+
export interface TypeMenuChildren {
|
|
22
|
+
type: "item" | "group" | "content";
|
|
23
|
+
element:
|
|
24
|
+
| React.ReactElement<TypeMenuGroupProps>
|
|
25
|
+
| React.ReactElement<TypeMenuItemProps>
|
|
26
|
+
| React.ReactElement<TypeMenuContentProps>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// https://stackoverflow.com/questions/69119016/find-specific-children-components-regardless-of-depth-in-react
|
|
30
|
+
const walkAllChildren = (
|
|
31
|
+
children: ReactNode,
|
|
32
|
+
callback: (element: ReactNode, parents: readonly ReactNode[]) => void
|
|
33
|
+
) => {
|
|
34
|
+
const walk = (element: ReactNode, parents: readonly ReactNode[]) => {
|
|
35
|
+
if (element === null || element === undefined) return;
|
|
36
|
+
callback(element, parents);
|
|
37
|
+
const newParents = [...parents, element];
|
|
38
|
+
const children = (element as any).props?.children;
|
|
39
|
+
React.Children.toArray(children).forEach((child) => {
|
|
40
|
+
walk(child, newParents);
|
|
41
|
+
});
|
|
42
|
+
};
|
|
43
|
+
const parents = [];
|
|
44
|
+
React.Children.toArray(children).forEach((child) => {
|
|
45
|
+
walk(child, parents);
|
|
46
|
+
});
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const getComponentName = (element: ReactNode): string | undefined => {
|
|
50
|
+
if (
|
|
51
|
+
element &&
|
|
52
|
+
typeof element === "object" &&
|
|
53
|
+
"type" in element &&
|
|
54
|
+
typeof element.type !== "string" &&
|
|
55
|
+
typeof element.type.name === "string"
|
|
56
|
+
) {
|
|
57
|
+
return element.type.name;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const getAllMenuItems = (children: ReactNode) => {
|
|
62
|
+
const orderedElements: TypeMenuChildren[] = [];
|
|
63
|
+
walkAllChildren(children, (element) => {
|
|
64
|
+
switch (getComponentName(element)) {
|
|
65
|
+
case "MenuItem":
|
|
66
|
+
orderedElements.push({
|
|
67
|
+
type: "item",
|
|
68
|
+
element: element as TypeItemElement,
|
|
69
|
+
});
|
|
70
|
+
break;
|
|
71
|
+
case "MenuGroup":
|
|
72
|
+
orderedElements.push({
|
|
73
|
+
type: "group",
|
|
74
|
+
element: element as TypeMenuGroupElement,
|
|
75
|
+
});
|
|
76
|
+
break;
|
|
77
|
+
case "MenuContent":
|
|
78
|
+
orderedElements.push({
|
|
79
|
+
type: "content",
|
|
80
|
+
element: element as TypeMenuContentElement,
|
|
81
|
+
});
|
|
82
|
+
break;
|
|
83
|
+
default:
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
return { orderedElements };
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// Returns a fragment containing a `MenuItemComponent` for each item in `items`.
|
|
91
|
+
export const mapMenuItems = ({
|
|
92
|
+
items,
|
|
93
|
+
MenuItemComponent,
|
|
94
|
+
}: TypeMapMenuItemsProps) => {
|
|
95
|
+
return (
|
|
96
|
+
<>
|
|
97
|
+
{items.map((menuItem) => (
|
|
98
|
+
<MenuItemComponent key={menuItem.id} {...menuItem} />
|
|
99
|
+
))}
|
|
100
|
+
</>
|
|
101
|
+
);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
export const useMenuChildren = ({
|
|
105
|
+
children: childrenProp,
|
|
106
|
+
items,
|
|
107
|
+
MenuItemComponent = MenuItem,
|
|
108
|
+
}: TypeUseMenuChildrenProps) => {
|
|
109
|
+
const { allMenuItems, menuItemsMap, children } = useMemo(() => {
|
|
110
|
+
let children: TypeNotEmptyChildren = <></>;
|
|
111
|
+
let index = 0;
|
|
112
|
+
const menuItemsMap: TypeMenuContext["itemsMap"] = {};
|
|
113
|
+
const allMenuItems: TypeMenuContext["items"] = [];
|
|
114
|
+
|
|
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;
|
|
126
|
+
});
|
|
127
|
+
children = (
|
|
128
|
+
<MenuContent>{mapMenuItems({ items, MenuItemComponent })}</MenuContent>
|
|
129
|
+
);
|
|
130
|
+
} else if (childrenProp) {
|
|
131
|
+
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
|
+
}
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return { allMenuItems, menuItemsMap, children };
|
|
207
|
+
}, [childrenProp, items, MenuItemComponent]);
|
|
208
|
+
|
|
209
|
+
return { allMenuItems, menuItemsMap, children };
|
|
210
|
+
};
|
package/src/index.ts
CHANGED
package/src/types.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type TypeMenuItemProps } from "./MenuItem";
|
|
2
2
|
import type { TypeMenuGroupProps } from "./MenuGroup";
|
|
3
|
+
import type { TypeMenuRootProps } from "./MenuRoot/index";
|
|
3
4
|
|
|
4
5
|
export interface TypeDefaultItemProps {
|
|
5
6
|
id: string;
|
|
@@ -10,6 +11,30 @@ export interface TypeDefaultItemProps {
|
|
|
10
11
|
menuGroupProps?: TypeMenuGroupProps;
|
|
11
12
|
}
|
|
12
13
|
|
|
14
|
+
/* The base type, which is narrowed to specific configurations.*/
|
|
15
|
+
type TypeBaseChildrenOrItems = {
|
|
16
|
+
/** Allows menu items to be passed in as children. Cannot be used with the items prop.*/
|
|
17
|
+
children?: TypeMenuRootProps["children"];
|
|
18
|
+
/** Allows menu items to be passed in as objects. Cannot be used with the children prop.*/
|
|
19
|
+
items?: TypeMenuItemProps[];
|
|
20
|
+
/** Allows a custom MenuItem component to be used when rendering using the items prop.*/
|
|
21
|
+
MenuItemComponent?: React.ComponentType<TypeMenuItemProps>;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type TypePropsWithChildren = {
|
|
25
|
+
children: TypeMenuRootProps["children"];
|
|
26
|
+
items?: never;
|
|
27
|
+
MenuItemComponent?: never;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
type TypePropsWithItems = {
|
|
31
|
+
children?: never;
|
|
32
|
+
items: TypeMenuItemProps[];
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export type TypeChildrenOrItems = TypeBaseChildrenOrItems &
|
|
36
|
+
(TypePropsWithChildren | TypePropsWithItems);
|
|
37
|
+
|
|
13
38
|
export type TypeNotEmptyChildren =
|
|
14
39
|
| React.ReactChild
|
|
15
40
|
| React.ReactFragment
|
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
import React, { type ReactNode, useMemo } from "react";
|
|
2
|
-
import { MenuItem, type TypeMenuItemProps } from "../MenuItem";
|
|
3
|
-
import type { TypeMenuContext } from "../MenuContext";
|
|
4
|
-
import type { TypeMenuGroupProps } from "../MenuGroup/index";
|
|
5
|
-
import type { TypeDefaultItemProps } from "../types";
|
|
6
|
-
|
|
7
|
-
export interface TypeUseItemsFromChildrenProps {
|
|
8
|
-
children: ReactNode;
|
|
9
|
-
}
|
|
10
|
-
export type TypeItemElement = React.ReactElement<TypeMenuItemProps>;
|
|
11
|
-
export type TypeMenuGroupElement = React.ReactElement<TypeMenuGroupProps>;
|
|
12
|
-
|
|
13
|
-
export interface TypeMenuChildren {
|
|
14
|
-
type: "item" | "group";
|
|
15
|
-
element:
|
|
16
|
-
| React.ReactElement<TypeMenuGroupProps>
|
|
17
|
-
| React.ReactElement<TypeMenuItemProps>;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
// https://stackoverflow.com/questions/69119016/find-specific-children-components-regardless-of-depth-in-react
|
|
21
|
-
const walkAllChildren = (
|
|
22
|
-
children: ReactNode,
|
|
23
|
-
callback: (element: ReactNode, parents: readonly ReactNode[]) => void
|
|
24
|
-
) => {
|
|
25
|
-
const walk = (element: ReactNode, parents: readonly ReactNode[]) => {
|
|
26
|
-
if (element === null || element === undefined) return;
|
|
27
|
-
callback(element, parents);
|
|
28
|
-
const newParents = [...parents, element];
|
|
29
|
-
const children = (element as any).props?.children;
|
|
30
|
-
React.Children.toArray(children).forEach((child) => {
|
|
31
|
-
walk(child, newParents);
|
|
32
|
-
});
|
|
33
|
-
};
|
|
34
|
-
const parents = [];
|
|
35
|
-
React.Children.toArray(children).forEach((child) => {
|
|
36
|
-
walk(child, parents);
|
|
37
|
-
});
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
const checkIfIsElement = (element: ReactNode, name: string): boolean => {
|
|
41
|
-
return !!(
|
|
42
|
-
element &&
|
|
43
|
-
typeof element === "object" &&
|
|
44
|
-
"type" in element &&
|
|
45
|
-
typeof element.type !== "string" &&
|
|
46
|
-
typeof element.type.name === "string" &&
|
|
47
|
-
element.type.name === name
|
|
48
|
-
);
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
const getAllMenuItems = (children: ReactNode) => {
|
|
52
|
-
const orderedElements: TypeMenuChildren[] = [];
|
|
53
|
-
walkAllChildren(children, (element) => {
|
|
54
|
-
if (checkIfIsElement(element, "MenuItem")) {
|
|
55
|
-
orderedElements.push({
|
|
56
|
-
type: "item",
|
|
57
|
-
element: element as TypeItemElement,
|
|
58
|
-
});
|
|
59
|
-
} else if (checkIfIsElement(element, "MenuGroup")) {
|
|
60
|
-
orderedElements.push({
|
|
61
|
-
type: "group",
|
|
62
|
-
element: element as TypeItemElement,
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
});
|
|
66
|
-
return { orderedElements };
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
export const useItemsFromChildren = ({
|
|
70
|
-
children,
|
|
71
|
-
}: TypeUseItemsFromChildrenProps) => {
|
|
72
|
-
const { allMenuItems, menuItemsMap } = useMemo(() => {
|
|
73
|
-
const { orderedElements } = getAllMenuItems(children);
|
|
74
|
-
const menuItemsMap: TypeMenuContext["itemsMap"] = {};
|
|
75
|
-
const allMenuItems: TypeDefaultItemProps[] = [];
|
|
76
|
-
let index = 0;
|
|
77
|
-
|
|
78
|
-
orderedElements.forEach(({ type, element }) => {
|
|
79
|
-
if (type === "group") {
|
|
80
|
-
const menuGroup = element as TypeMenuGroupElement;
|
|
81
|
-
const items = menuGroup.props.items;
|
|
82
|
-
if (items) {
|
|
83
|
-
items.forEach((menuItem) => {
|
|
84
|
-
const item = {
|
|
85
|
-
id: menuItem.id,
|
|
86
|
-
index: index++,
|
|
87
|
-
disabled: menuItem.disabled,
|
|
88
|
-
menuItemProps: menuItem,
|
|
89
|
-
menuGroupProps: element.props,
|
|
90
|
-
};
|
|
91
|
-
allMenuItems.push(item);
|
|
92
|
-
menuItemsMap[`${item.id}`] = item;
|
|
93
|
-
});
|
|
94
|
-
} else if (menuGroup.props.children) {
|
|
95
|
-
let children = menuGroup.props.children as TypeItemElement[];
|
|
96
|
-
// If there is only one child, it will be an object. Make it an array.
|
|
97
|
-
if (!Array.isArray(children)) {
|
|
98
|
-
children = [children];
|
|
99
|
-
}
|
|
100
|
-
children.forEach((menuItem) => {
|
|
101
|
-
const item = {
|
|
102
|
-
id: menuItem.props.id,
|
|
103
|
-
index: index++,
|
|
104
|
-
disabled: menuItem.props.disabled,
|
|
105
|
-
menuItemProps: menuItem.props,
|
|
106
|
-
menuGroupProps: element.props,
|
|
107
|
-
};
|
|
108
|
-
allMenuItems.push(item);
|
|
109
|
-
menuItemsMap[`${item.id}`] = item;
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
} else {
|
|
113
|
-
// Items in groups would have already been added to the map.
|
|
114
|
-
if (menuItemsMap[`${element.props.id}`]) return;
|
|
115
|
-
const menuItem = element as TypeItemElement;
|
|
116
|
-
const item = {
|
|
117
|
-
id: menuItem.props.id,
|
|
118
|
-
index: index++,
|
|
119
|
-
disabled: menuItem.props.disabled,
|
|
120
|
-
menuItemProps: menuItem.props,
|
|
121
|
-
};
|
|
122
|
-
allMenuItems.push(item);
|
|
123
|
-
menuItemsMap[`${item.id}`] = item;
|
|
124
|
-
}
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
return { allMenuItems, menuItemsMap };
|
|
128
|
-
}, [children]);
|
|
129
|
-
|
|
130
|
-
return { allMenuItems, menuItemsMap };
|
|
131
|
-
};
|