@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.
Files changed (91) hide show
  1. package/.eslintrc.js +4 -0
  2. package/.turbo/turbo-build.log +21 -0
  3. package/CHANGELOG.md +46 -0
  4. package/dist/esm/index.js +2651 -0
  5. package/dist/esm/index.js.map +1 -0
  6. package/dist/index.d.mts +197 -0
  7. package/dist/index.d.ts +197 -0
  8. package/dist/index.js +2670 -0
  9. package/dist/index.js.map +1 -0
  10. package/jest.config.js +12 -0
  11. package/package.json +50 -0
  12. package/src/ActionMenu/ActionMenu.feature +20 -0
  13. package/src/ActionMenu/ActionMenu.tsx +113 -0
  14. package/src/ActionMenu/__tests__/ActionMenu.test.tsx +99 -0
  15. package/src/ActionMenu/__tests__/ActionMenu.typetest.tsx +73 -0
  16. package/src/ActionMenu/index.ts +1 -0
  17. package/src/ComboBox/ComboBox.feature +96 -0
  18. package/src/ComboBox/TokenInput.feature +84 -0
  19. package/src/MenuContent/MenuContent.tsx +28 -0
  20. package/src/MenuContent/MenuContentTypes.ts +7 -0
  21. package/src/MenuContent/__tests__/MenuContent.test.tsx +59 -0
  22. package/src/MenuContent/__tests__/MenuContent.typetest.tsx +18 -0
  23. package/src/MenuContent/index.tsx +2 -0
  24. package/src/MenuContent/styles.tsx +10 -0
  25. package/src/MenuContext.tsx +116 -0
  26. package/src/MenuFooter/MenuFooter.feature +30 -0
  27. package/src/MenuFooter/MenuFooter.tsx +7 -0
  28. package/src/MenuFooter/MenuFooterTypes.ts +1 -0
  29. package/src/MenuFooter/__tests__/MenuFooter.test.tsx +76 -0
  30. package/src/MenuFooter/__tests__/MenuFooter.typetest.tsx +20 -0
  31. package/src/MenuFooter/index.ts +2 -0
  32. package/src/MenuFooter/styles.tsx +7 -0
  33. package/src/MenuGroup/MenuGroup.feature +114 -0
  34. package/src/MenuGroup/MenuGroup.tsx +33 -0
  35. package/src/MenuGroup/MenuGroupTypes.ts +29 -0
  36. package/src/MenuGroup/__tests__/MenuGroup.test.tsx +95 -0
  37. package/src/MenuGroup/__tests__/MenuGroup.typetest.tsx +45 -0
  38. package/src/MenuGroup/index.tsx +8 -0
  39. package/src/MenuGroup/styles.tsx +37 -0
  40. package/src/MenuHeader/MenuHeader.feature +59 -0
  41. package/src/MenuHeader/MenuHeader.tsx +30 -0
  42. package/src/MenuHeader/MenuHeaderTypes.ts +26 -0
  43. package/src/MenuHeader/__tests__/MenuHeader.test.tsx +67 -0
  44. package/src/MenuHeader/__tests__/MenuHeader.typetest.tsx +27 -0
  45. package/src/MenuHeader/index.ts +2 -0
  46. package/src/MenuHeader/styles.tsx +75 -0
  47. package/src/MenuItem/MenuItem.feature +88 -0
  48. package/src/MenuItem/MenuItem.tsx +44 -0
  49. package/src/MenuItem/MenuItemTypes.ts +37 -0
  50. package/src/MenuItem/__tests__/MenuItem.test.tsx +228 -0
  51. package/src/MenuItem/__tests__/MenuItem.typetest.tsx +41 -0
  52. package/src/MenuItem/index.ts +5 -0
  53. package/src/MenuItem/styles.tsx +71 -0
  54. package/src/MenuItemSelectable/MenuItemSelectable.feature +99 -0
  55. package/src/MenuItemSelectable/MenuItemSelectable.tsx +136 -0
  56. package/src/MenuItemSelectable/MenuItemSelectableTypes.ts +15 -0
  57. package/src/MenuItemSelectable/__tests__/MenuItemSelectable.test.tsx +303 -0
  58. package/src/MenuItemSelectable/__tests__/MenuItemSelectable.typetest.tsx +56 -0
  59. package/src/MenuItemSelectable/index.ts +5 -0
  60. package/src/MenuItemSelectable/styles.tsx +21 -0
  61. package/src/MenuLabel.tsx +12 -0
  62. package/src/MenuRoot/MenuRoot.feature +93 -0
  63. package/src/MenuRoot/MenuRoot.tsx +70 -0
  64. package/src/MenuRoot/__tests__/MenuRoot.test.tsx +197 -0
  65. package/src/MenuRoot/__tests__/MenuRoot.typetest.tsx +76 -0
  66. package/src/MenuRoot/index.ts +1 -0
  67. package/src/MenuToggleButton.tsx +29 -0
  68. package/src/MultiSelectMenu/MultiSelectMenu.feature +70 -0
  69. package/src/MultiSelectMenu/MultiSelectMenu.tsx +133 -0
  70. package/src/MultiSelectMenu/MultiSelectMenuTypes.ts +8 -0
  71. package/src/MultiSelectMenu/__tests__/MultiSelectMenu.test.tsx +349 -0
  72. package/src/MultiSelectMenu/__tests__/MultiSelectMenu.typetest.tsx +78 -0
  73. package/src/MultiSelectMenu/index.ts +1 -0
  74. package/src/SingleSelectMenu/SingleSelectMenu.feature +23 -0
  75. package/src/SingleSelectMenu/SingleSelectMenu.tsx +56 -0
  76. package/src/SingleSelectMenu/SingleSelectMenuTypes.ts +9 -0
  77. package/src/SingleSelectMenu/__tests__/SingleSelectMenu.test.tsx +167 -0
  78. package/src/SingleSelectMenu/__tests__/SingleSelectMenu.typetest.tsx +95 -0
  79. package/src/SingleSelectMenu/index.ts +3 -0
  80. package/src/__tests__/Menu.test.tsx +423 -0
  81. package/src/constants.ts +17 -0
  82. package/src/hooks/useItemsFromChildren.tsx +81 -0
  83. package/src/index.ts +20 -0
  84. package/src/menuTestingUtils.tsx +72 -0
  85. package/src/reducers/__tests__/useSelectStateReducer.test.tsx +37 -0
  86. package/src/reducers/useSelectStateReducer.tsx +27 -0
  87. package/src/styled.d.ts +7 -0
  88. package/src/types.ts +16 -0
  89. package/src/utils/itemToString.ts +5 -0
  90. package/tsconfig.json +12 -0
  91. 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
+ };
@@ -0,0 +1,7 @@
1
+ import "styled-components";
2
+ import { TypeTheme } from "@sproutsocial/seeds-react-theme";
3
+
4
+ declare module "styled-components" {
5
+ // eslint-disable-next-line @typescript-eslint/no-empty-interface
6
+ export interface DefaultTheme extends TypeTheme {}
7
+ }
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;
@@ -0,0 +1,5 @@
1
+ import type { TypeDefaultItemProps } from "../types";
2
+
3
+ export const itemToString = (item: TypeDefaultItemProps | null) => {
4
+ return item ? item.id : "";
5
+ };
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
+ }
package/tsup.config.ts ADDED
@@ -0,0 +1,11 @@
1
+ import { defineConfig } from "tsup";
2
+
3
+ export default defineConfig({
4
+ entry: ["src/index.ts"],
5
+ format: ["cjs", "esm"],
6
+ clean: true,
7
+ legacyOutput: true,
8
+ dts: false,
9
+ sourcemap: true,
10
+ external: ["react"],
11
+ });