@sproutsocial/seeds-react-menu 0.1.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/.eslintrc.js +4 -0
- package/.turbo/turbo-build.log +21 -0
- package/CHANGELOG.md +46 -0
- package/dist/esm/index.js +2651 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/index.d.mts +197 -0
- package/dist/index.d.ts +197 -0
- package/dist/index.js +2670 -0
- package/dist/index.js.map +1 -0
- package/jest.config.js +12 -0
- package/package.json +50 -0
- package/src/ActionMenu/ActionMenu.feature +20 -0
- package/src/ActionMenu/ActionMenu.tsx +113 -0
- package/src/ActionMenu/__tests__/ActionMenu.test.tsx +99 -0
- package/src/ActionMenu/__tests__/ActionMenu.typetest.tsx +73 -0
- package/src/ActionMenu/index.ts +1 -0
- package/src/ComboBox/ComboBox.feature +96 -0
- package/src/ComboBox/TokenInput.feature +84 -0
- package/src/MenuContent/MenuContent.tsx +28 -0
- package/src/MenuContent/MenuContentTypes.ts +7 -0
- package/src/MenuContent/__tests__/MenuContent.test.tsx +59 -0
- package/src/MenuContent/__tests__/MenuContent.typetest.tsx +18 -0
- package/src/MenuContent/index.tsx +2 -0
- package/src/MenuContent/styles.tsx +10 -0
- package/src/MenuContext.tsx +116 -0
- package/src/MenuFooter/MenuFooter.feature +30 -0
- package/src/MenuFooter/MenuFooter.tsx +7 -0
- package/src/MenuFooter/MenuFooterTypes.ts +1 -0
- package/src/MenuFooter/__tests__/MenuFooter.test.tsx +76 -0
- package/src/MenuFooter/__tests__/MenuFooter.typetest.tsx +20 -0
- package/src/MenuFooter/index.ts +2 -0
- package/src/MenuFooter/styles.tsx +7 -0
- package/src/MenuGroup/MenuGroup.feature +114 -0
- package/src/MenuGroup/MenuGroup.tsx +33 -0
- package/src/MenuGroup/MenuGroupTypes.ts +29 -0
- package/src/MenuGroup/__tests__/MenuGroup.test.tsx +95 -0
- package/src/MenuGroup/__tests__/MenuGroup.typetest.tsx +45 -0
- package/src/MenuGroup/index.tsx +8 -0
- package/src/MenuGroup/styles.tsx +37 -0
- package/src/MenuHeader/MenuHeader.feature +59 -0
- package/src/MenuHeader/MenuHeader.tsx +30 -0
- package/src/MenuHeader/MenuHeaderTypes.ts +26 -0
- package/src/MenuHeader/__tests__/MenuHeader.test.tsx +67 -0
- package/src/MenuHeader/__tests__/MenuHeader.typetest.tsx +27 -0
- package/src/MenuHeader/index.ts +2 -0
- package/src/MenuHeader/styles.tsx +75 -0
- package/src/MenuItem/MenuItem.feature +88 -0
- package/src/MenuItem/MenuItem.tsx +44 -0
- package/src/MenuItem/MenuItemTypes.ts +37 -0
- package/src/MenuItem/__tests__/MenuItem.test.tsx +228 -0
- package/src/MenuItem/__tests__/MenuItem.typetest.tsx +41 -0
- package/src/MenuItem/index.ts +5 -0
- package/src/MenuItem/styles.tsx +71 -0
- package/src/MenuItemSelectable/MenuItemSelectable.feature +99 -0
- package/src/MenuItemSelectable/MenuItemSelectable.tsx +136 -0
- package/src/MenuItemSelectable/MenuItemSelectableTypes.ts +15 -0
- package/src/MenuItemSelectable/__tests__/MenuItemSelectable.test.tsx +303 -0
- package/src/MenuItemSelectable/__tests__/MenuItemSelectable.typetest.tsx +56 -0
- package/src/MenuItemSelectable/index.ts +5 -0
- package/src/MenuItemSelectable/styles.tsx +21 -0
- package/src/MenuLabel.tsx +12 -0
- package/src/MenuRoot/MenuRoot.feature +93 -0
- package/src/MenuRoot/MenuRoot.tsx +70 -0
- package/src/MenuRoot/__tests__/MenuRoot.test.tsx +197 -0
- package/src/MenuRoot/__tests__/MenuRoot.typetest.tsx +76 -0
- package/src/MenuRoot/index.ts +1 -0
- package/src/MenuToggleButton.tsx +29 -0
- package/src/MultiSelectMenu/MultiSelectMenu.feature +70 -0
- package/src/MultiSelectMenu/MultiSelectMenu.tsx +133 -0
- package/src/MultiSelectMenu/MultiSelectMenuTypes.ts +8 -0
- package/src/MultiSelectMenu/__tests__/MultiSelectMenu.test.tsx +349 -0
- package/src/MultiSelectMenu/__tests__/MultiSelectMenu.typetest.tsx +78 -0
- package/src/MultiSelectMenu/index.ts +1 -0
- package/src/SingleSelectMenu/SingleSelectMenu.feature +23 -0
- package/src/SingleSelectMenu/SingleSelectMenu.tsx +56 -0
- package/src/SingleSelectMenu/SingleSelectMenuTypes.ts +9 -0
- package/src/SingleSelectMenu/__tests__/SingleSelectMenu.test.tsx +167 -0
- package/src/SingleSelectMenu/__tests__/SingleSelectMenu.typetest.tsx +95 -0
- package/src/SingleSelectMenu/index.ts +3 -0
- package/src/__tests__/Menu.test.tsx +423 -0
- package/src/constants.ts +17 -0
- package/src/hooks/useItemsFromChildren.tsx +81 -0
- package/src/index.ts +20 -0
- package/src/menuTestingUtils.tsx +72 -0
- package/src/reducers/__tests__/useSelectStateReducer.test.tsx +37 -0
- package/src/reducers/useSelectStateReducer.tsx +27 -0
- package/src/styled.d.ts +7 -0
- package/src/types.ts +16 -0
- package/src/utils/itemToString.ts +5 -0
- package/tsconfig.json +12 -0
- package/tsup.config.ts +11 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import React, { type ReactNode, useMemo } from "react";
|
|
2
|
+
import type { TypeItemElements } from "../types";
|
|
3
|
+
import type { TypeMenuContext } from "../MenuContext";
|
|
4
|
+
|
|
5
|
+
export interface TypeUseItemsFromChildrenProps {
|
|
6
|
+
children: ReactNode;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// https://stackoverflow.com/questions/69119016/find-specific-children-components-regardless-of-depth-in-react
|
|
10
|
+
const walkAllChildren = (
|
|
11
|
+
children: ReactNode,
|
|
12
|
+
callback: (element: ReactNode, parents: readonly ReactNode[]) => void
|
|
13
|
+
) => {
|
|
14
|
+
const walk = (element: ReactNode, parents: readonly ReactNode[]) => {
|
|
15
|
+
if (element === null || element === undefined) return;
|
|
16
|
+
// console.log("element", element);
|
|
17
|
+
callback(element, parents);
|
|
18
|
+
const newParents = [...parents, element];
|
|
19
|
+
const children = (element as any).props?.children;
|
|
20
|
+
React.Children.toArray(children).forEach((child) => {
|
|
21
|
+
walk(child, newParents);
|
|
22
|
+
});
|
|
23
|
+
};
|
|
24
|
+
const parents = [];
|
|
25
|
+
React.Children.toArray(children).forEach((child) => {
|
|
26
|
+
walk(child, parents);
|
|
27
|
+
});
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const checkIfIsElement = (element: ReactNode, name: string): boolean => {
|
|
31
|
+
return !!(
|
|
32
|
+
element &&
|
|
33
|
+
typeof element === "object" &&
|
|
34
|
+
"type" in element &&
|
|
35
|
+
typeof element.type !== "string" &&
|
|
36
|
+
typeof element.type.name === "string" &&
|
|
37
|
+
element.type.name === name
|
|
38
|
+
);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const getAllMenuItems = (children: ReactNode) => {
|
|
42
|
+
const allMenuItems: TypeItemElements[] = [];
|
|
43
|
+
// console.log("children", children);
|
|
44
|
+
walkAllChildren(children, (element, parents) => {
|
|
45
|
+
if (
|
|
46
|
+
checkIfIsElement(element, "MenuItem") ||
|
|
47
|
+
checkIfIsElement(element, "MenuItemSelectable")
|
|
48
|
+
) {
|
|
49
|
+
allMenuItems.push({
|
|
50
|
+
element: element as TypeItemElements["element"],
|
|
51
|
+
parents,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
return allMenuItems;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export const useItemsFromChildren = ({
|
|
59
|
+
children,
|
|
60
|
+
}: TypeUseItemsFromChildrenProps) => {
|
|
61
|
+
const { allMenuItems, menuItemsMap } = useMemo(() => {
|
|
62
|
+
const allMenuItemElements = getAllMenuItems(children);
|
|
63
|
+
const menuItemsMap: TypeMenuContext["itemsMap"] = {};
|
|
64
|
+
const allMenuItems: TypeMenuContext["items"] = allMenuItemElements.map(
|
|
65
|
+
({ element, parents }, index) => {
|
|
66
|
+
const item = {
|
|
67
|
+
id: element.props.id,
|
|
68
|
+
disabled: element.props.disabled,
|
|
69
|
+
index,
|
|
70
|
+
element,
|
|
71
|
+
parents,
|
|
72
|
+
};
|
|
73
|
+
menuItemsMap[`${item.id}`] = item;
|
|
74
|
+
return item;
|
|
75
|
+
}
|
|
76
|
+
);
|
|
77
|
+
return { allMenuItems, menuItemsMap };
|
|
78
|
+
}, [children]);
|
|
79
|
+
|
|
80
|
+
return { allMenuItems, menuItemsMap };
|
|
81
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Primitives
|
|
2
|
+
export * from "./MenuItem";
|
|
3
|
+
export * from "./MenuItemSelectable";
|
|
4
|
+
export * from "./MenuHeader";
|
|
5
|
+
export * from "./MenuContent";
|
|
6
|
+
export * from "./MenuFooter";
|
|
7
|
+
export * from "./MenuGroup";
|
|
8
|
+
export * from "./MenuRoot";
|
|
9
|
+
export * from "./MenuLabel";
|
|
10
|
+
export * from "./MenuToggleButton";
|
|
11
|
+
|
|
12
|
+
// Menus
|
|
13
|
+
export * from "./ActionMenu";
|
|
14
|
+
export * from "./SingleSelectMenu";
|
|
15
|
+
export * from "./MultiSelectMenu";
|
|
16
|
+
|
|
17
|
+
// Utilities
|
|
18
|
+
export * from "./MenuContext";
|
|
19
|
+
export * from "./hooks/useItemsFromChildren";
|
|
20
|
+
export * from "./reducers/useSelectStateReducer";
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import {
|
|
3
|
+
MenuContext,
|
|
4
|
+
MenuRoot,
|
|
5
|
+
type TypeMenuContextProps,
|
|
6
|
+
type TypeMenuContext,
|
|
7
|
+
type TypeMenuRootProps,
|
|
8
|
+
} from "./index";
|
|
9
|
+
|
|
10
|
+
export const testDefaultContext = (
|
|
11
|
+
context: Partial<TypeMenuContextProps> = {}
|
|
12
|
+
): TypeMenuContext => ({
|
|
13
|
+
items: [],
|
|
14
|
+
itemsMap: context.items
|
|
15
|
+
? context.items.reduce((map, item) => {
|
|
16
|
+
map[`${item.id}`] = item;
|
|
17
|
+
return map;
|
|
18
|
+
}, {} as TypeMenuContext["itemsMap"])
|
|
19
|
+
: {},
|
|
20
|
+
// TODO: Make type on MenuContext more agnostic for other use cases
|
|
21
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
22
|
+
// @ts-expect-error - mocked function does not match real type
|
|
23
|
+
getItemProps: (props) => ({ role: "option", ...props }),
|
|
24
|
+
isListbox: true,
|
|
25
|
+
...context,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* TestWithMenuRoot
|
|
30
|
+
* Given children and context, render the children with the MenuRoot component.
|
|
31
|
+
* Used for integration testing components with the real MenuProvider and MenuRoot.
|
|
32
|
+
*
|
|
33
|
+
* @param props - valid context props to override the defaults for the MenuProvider and any other MenuRoot props
|
|
34
|
+
* @returns - test component using the MenuRoot
|
|
35
|
+
*/
|
|
36
|
+
export const TestWithMenuRoot = ({
|
|
37
|
+
children,
|
|
38
|
+
triggerElement,
|
|
39
|
+
...props
|
|
40
|
+
}: {
|
|
41
|
+
children: TypeMenuRootProps["children"];
|
|
42
|
+
triggerElement: TypeMenuRootProps["triggerElement"];
|
|
43
|
+
} & Partial<Omit<TypeMenuRootProps, "children" | "triggerElement">>) => {
|
|
44
|
+
return (
|
|
45
|
+
<MenuRoot triggerElement={triggerElement} {...testDefaultContext(props)}>
|
|
46
|
+
{children}
|
|
47
|
+
</MenuRoot>
|
|
48
|
+
);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* TestWithMockedMenu
|
|
53
|
+
* Given a children and context, render the children with the raw MenuContext with a mocked value.
|
|
54
|
+
* Used for unit testing components without the real MenuProvider or MenuRoot.
|
|
55
|
+
*
|
|
56
|
+
* @param children - react children to be loaded inside of the MenuContext.Provider
|
|
57
|
+
* @param context - mocked context props to override the testDefaultContext
|
|
58
|
+
* @returns - rendered component with mocked MenuContext
|
|
59
|
+
*/
|
|
60
|
+
export const TestWithMockedMenu = ({
|
|
61
|
+
children,
|
|
62
|
+
context = {},
|
|
63
|
+
}: {
|
|
64
|
+
children: React.ReactNode;
|
|
65
|
+
context: Partial<TypeMenuContextProps>;
|
|
66
|
+
}) => {
|
|
67
|
+
return (
|
|
68
|
+
<MenuContext.Provider value={testDefaultContext(context)}>
|
|
69
|
+
{children}
|
|
70
|
+
</MenuContext.Provider>
|
|
71
|
+
);
|
|
72
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { useSelect } from "downshift";
|
|
2
|
+
|
|
3
|
+
import { useSelectStateReducer } from "../useSelectStateReducer";
|
|
4
|
+
|
|
5
|
+
const defaultTestState = {
|
|
6
|
+
isOpen: false,
|
|
7
|
+
highlightedIndex: -1,
|
|
8
|
+
selectedItem: null,
|
|
9
|
+
inputValue: "",
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
describe("useSelectStateReducer", () => {
|
|
13
|
+
it("should keep isOpen true when the type is ToggleButtonBlur", () => {
|
|
14
|
+
const state = { ...defaultTestState, isOpen: true, highlightedIndex: 1 };
|
|
15
|
+
const changes = { isOpen: false, highlightedIndex: -1 };
|
|
16
|
+
const type = useSelect.stateChangeTypes.ToggleButtonBlur;
|
|
17
|
+
|
|
18
|
+
const result = useSelectStateReducer(state, { type, changes });
|
|
19
|
+
|
|
20
|
+
expect(result).toEqual({
|
|
21
|
+
...changes,
|
|
22
|
+
isOpen: true,
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("should make no changes when the type is not ToggleButtonBlur", () => {
|
|
27
|
+
const state = { ...defaultTestState, isOpen: true, highlightedIndex: 1 };
|
|
28
|
+
const changes = { isOpen: false, highlightedIndex: -1 };
|
|
29
|
+
const type = useSelect.stateChangeTypes.FunctionToggleMenu;
|
|
30
|
+
|
|
31
|
+
const result = useSelectStateReducer(state, { type, changes });
|
|
32
|
+
|
|
33
|
+
expect(result).toEqual({
|
|
34
|
+
...changes,
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { useSelect } from "downshift";
|
|
2
|
+
import type { UseSelectProps } from "downshift";
|
|
3
|
+
import type { TypeDefaultItemProps } from "../types";
|
|
4
|
+
|
|
5
|
+
export const useSelectStateReducer: Required<
|
|
6
|
+
UseSelectProps<TypeDefaultItemProps>
|
|
7
|
+
>["stateReducer"] = (state, actionAndChanges) => {
|
|
8
|
+
const { type, changes } = actionAndChanges;
|
|
9
|
+
switch (type) {
|
|
10
|
+
/** If downshift is blurred by tabbing within the menu, stay open */
|
|
11
|
+
case useSelect.stateChangeTypes.ToggleButtonBlur:
|
|
12
|
+
return {
|
|
13
|
+
...changes,
|
|
14
|
+
// keep menu open on blur
|
|
15
|
+
isOpen: true,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/** Return standard change payload */
|
|
19
|
+
default:
|
|
20
|
+
return changes;
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const defaultUseSelectProps = {
|
|
25
|
+
stateReducer: useSelectStateReducer,
|
|
26
|
+
isItemDisabled: (item) => !!item.disabled,
|
|
27
|
+
};
|
package/src/styled.d.ts
ADDED
package/src/types.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { TypeMenuItemProps } from "./MenuItem";
|
|
2
|
+
|
|
3
|
+
export interface TypeItemElements {
|
|
4
|
+
element: React.ReactElement<TypeMenuItemProps>;
|
|
5
|
+
parents: readonly React.ReactNode[];
|
|
6
|
+
}
|
|
7
|
+
export interface TypeDefaultItemProps extends Partial<TypeItemElements> {
|
|
8
|
+
id: string;
|
|
9
|
+
index: number;
|
|
10
|
+
disabled?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type TypeNotEmptyChildren =
|
|
14
|
+
| React.ReactChild
|
|
15
|
+
| React.ReactFragment
|
|
16
|
+
| React.ReactPortal;
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "@sproutsocial/seeds-tsconfig/react-library.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"noImplicitAny": false,
|
|
5
|
+
"noUncheckedIndexedAccess": false,
|
|
6
|
+
"baseUrl": ".",
|
|
7
|
+
"types": ["@testing-library/jest-dom"],
|
|
8
|
+
"verbatimModuleSyntax": true
|
|
9
|
+
},
|
|
10
|
+
"include": ["src/**/*"],
|
|
11
|
+
"exclude": ["node_modules", "dist"]
|
|
12
|
+
}
|