@sproutsocial/seeds-react-menu 0.5.0 → 0.6.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 (103) hide show
  1. package/.turbo/turbo-build.log +10 -10
  2. package/CHANGELOG.md +15 -0
  3. package/dist/esm/index.js +1486 -440
  4. package/dist/esm/index.js.map +1 -1
  5. package/dist/index.d.mts +248 -92
  6. package/dist/index.d.ts +248 -92
  7. package/dist/index.js +1500 -429
  8. package/dist/index.js.map +1 -1
  9. package/package.json +4 -1
  10. package/src/ActionMenu/ActionMenu.stories.tsx +426 -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 +189 -0
  21. package/src/Autocomplete/SingleAutocomplete.tsx +74 -0
  22. package/src/Autocomplete/__tests__/Autocomplete.test.tsx +171 -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 +26 -12
  26. package/src/MenuContent/MenuContentTypes.ts +6 -4
  27. package/src/MenuContent/__tests__/MenuContent.typetest.tsx +8 -8
  28. package/src/MenuContext.tsx +191 -37
  29. package/src/MenuFooter/MenuFooter.stories.tsx +202 -0
  30. package/src/MenuFooter/__tests__/MenuFooter.test.tsx +16 -11
  31. package/src/MenuGroup/MenuGroup.feature +16 -16
  32. package/src/MenuGroup/MenuGroup.stories.tsx +248 -0
  33. package/src/MenuGroup/MenuGroup.tsx +8 -7
  34. package/src/MenuGroup/MenuGroupTypes.ts +6 -2
  35. package/src/MenuGroup/__tests__/MenuGroup.test.tsx +6 -6
  36. package/src/MenuGroup/__tests__/MenuGroup.typetest.tsx +7 -7
  37. package/src/MenuGroup/styles.tsx +4 -2
  38. package/src/MenuHeader/MenuHeader.stories.tsx +276 -0
  39. package/src/MenuHeader/MenuHeader.tsx +3 -1
  40. package/src/MenuHeader/__tests__/MenuHeader.test.tsx +23 -13
  41. package/src/MenuHeader/styles.tsx +18 -14
  42. package/src/MenuItem/MenuItem.stories.tsx +507 -0
  43. package/src/MenuItem/MenuItem.tsx +18 -8
  44. package/src/MenuItem/MenuItemTypes.ts +9 -4
  45. package/src/MenuItem/__tests__/MenuItem.test.tsx +6 -6
  46. package/src/MenuItem/styles.tsx +21 -5
  47. package/src/MenuItem/useOptionProps.tsx +10 -8
  48. package/src/MenuLabel.tsx +2 -2
  49. package/src/MenuRoot/MenuRoot.tsx +37 -23
  50. package/src/MenuRoot/__tests__/MenuRoot.test.tsx +59 -32
  51. package/src/MenuSearchInput/MenuSearchInput.tsx +63 -0
  52. package/src/MenuSearchInput/__tests__/MenuSearchInput.test.tsx +157 -0
  53. package/src/MenuSearchInput/__tests__/MenuSearchInput.typetest.tsx +31 -0
  54. package/src/MenuSearchInput/index.ts +1 -0
  55. package/src/MenuSearchTokenInput/MenuSearchTokenInput.tsx +98 -0
  56. package/src/MenuSearchTokenInput/__tests__/MenuSearchTokenInput.test.tsx +247 -0
  57. package/src/MenuSearchTokenInput/__tests__/MenuSearchTokenInput.typetest.tsx +33 -0
  58. package/src/MenuSearchTokenInput/index.ts +1 -0
  59. package/src/MenuToggleButton/MenuToggleButton.stories.tsx +72 -0
  60. package/src/{MenuToggleButton.tsx → MenuToggleButton/MenuToggleButton.tsx} +6 -5
  61. package/src/MenuToggleButton/index.ts +1 -0
  62. package/src/MenuTokenInput.tsx +104 -0
  63. package/src/MultiSelectMenu/MultiSelectMenu.stories.tsx +214 -0
  64. package/src/MultiSelectMenu/MultiSelectMenu.tsx +38 -68
  65. package/src/MultiSelectMenu/MultiSelectMenuTypes.ts +8 -6
  66. package/src/MultiSelectMenu/__tests__/MultiSelectMenu.test.tsx +46 -17
  67. package/src/MultiSelectMenu/__tests__/MultiSelectMenu.typetest.tsx +6 -6
  68. package/src/NestedMenu/NestedMenu.feature +109 -0
  69. package/src/NestedMenu/NestedMenu.stories.tsx +157 -0
  70. package/src/NestedMenu/NestedMenu.tsx +102 -0
  71. package/src/NestedMenu/NestedMenuContent.tsx +71 -0
  72. package/src/NestedMenu/NestedMenuContext.tsx +56 -0
  73. package/src/NestedMenu/NestedMenuHeader.tsx +25 -0
  74. package/src/NestedMenu/NestedMenuItem.tsx +37 -0
  75. package/src/NestedMenu/NestedMenuTypes.ts +47 -0
  76. package/src/NestedMenu/__tests__/NestedMenu.test.tsx +31 -0
  77. package/src/NestedMenu/__tests__/NestedMenu.typetest.tsx +134 -0
  78. package/src/NestedMenu/index.ts +5 -0
  79. package/src/SingleSelectMenu/SingleSelectMenu.stories.tsx +227 -0
  80. package/src/SingleSelectMenu/SingleSelectMenu.tsx +23 -26
  81. package/src/SingleSelectMenu/SingleSelectMenuTypes.ts +7 -5
  82. package/src/SingleSelectMenu/__tests__/SingleSelectMenu.typetest.tsx +6 -6
  83. package/src/SubmenuItem/SubmenuItem.feature +82 -0
  84. package/src/SubmenuItem/SubmenuItem.stories.tsx +163 -0
  85. package/src/SubmenuItem/SubmenuItem.tsx +254 -0
  86. package/src/SubmenuItem/SubmenuItemTypes.ts +24 -0
  87. package/src/SubmenuItem/__tests__/SubmenuItem.test.tsx +3 -0
  88. package/src/SubmenuItem/__tests__/SubmenuItem.typetest.tsx +112 -0
  89. package/src/SubmenuItem/index.ts +2 -0
  90. package/src/hooks/useDownshiftDefaults.tsx +107 -0
  91. package/src/hooks/useItemFiltering.tsx +66 -0
  92. package/src/hooks/useMenuChildren.tsx +197 -128
  93. package/src/index.ts +6 -0
  94. package/src/menuTestingUtils.tsx +21 -10
  95. package/src/reducers/nestedMenuStateReducer.tsx +23 -0
  96. package/src/reducers/useComboboxStateReducer.tsx +26 -8
  97. package/src/reducers/useSelectStateReducer.tsx +24 -6
  98. package/src/storybookUtilities/menu-storybook-components.tsx +5 -3
  99. package/src/types.ts +38 -30
  100. package/src/utils/downshiftDefaults.ts +9 -0
  101. package/src/utils/getMultiSelectOverrides.ts +21 -0
  102. package/src/utils/reduceReducers.ts +19 -0
  103. package/src/utils/itemToString.ts +0 -5
@@ -0,0 +1,109 @@
1
+ Feature: NestedMenu
2
+
3
+ A menu component that allows users to navigate through hierarchical menus
4
+
5
+ Rule: User behavior
6
+ Background:
7
+ Given an end user is using the component
8
+
9
+ Scenario: User can open the NestedMenu root menu
10
+ Given the NestedMenu is closed
11
+ When the menu button is clicked
12
+ Then it should open the parent NestedMenu
13
+
14
+ Scenario: User can open a child menu
15
+ Given the NestedMenu is open
16
+ When an item with a childMenuId is clicked
17
+ Then it should display the corresponding child menu
18
+
19
+ Scenario: User can view one child menu at a time
20
+ Given the NestedMenu is open
21
+ When a child menu is displayed
22
+ Then the parent menu is hidden
23
+
24
+ Scenario: User can return to a previous menu
25
+ Given the NestedMenu is open
26
+ And a child menu is displayed
27
+ Then the back button is visible
28
+ When the back button is clicked
29
+ Then it should return to the previous menu
30
+
31
+ Scenario: User can select a child menu without closing the menu
32
+ Given the NestedMenu is open
33
+ When an menu item that triggers a child menu is clicked
34
+ Then the menu will stay open
35
+ And the menu will not use its default open/close behavior
36
+
37
+ Rule: API
38
+ Background:
39
+ Given an engineer is implementing a NestedMenu
40
+
41
+ Scenario: NestedMenu accepts an initialMenuId prop
42
+ Given the NestedMenu is open
43
+ And an initialMenuId is passed
44
+ Then the corresponding menu should be displayed
45
+
46
+ Scenario: NestedMenu requires a menus prop
47
+ Given the NestedMenu is open
48
+ And a menus prop is not passed
49
+ Then the NestedMenu should not render
50
+
51
+ Scenario: NestedMenu only displays one menu at a time
52
+ Given the NestedMenu is open
53
+ And a child menu is being displayed
54
+ Then none of the parent or sibling menus should be displayed
55
+
56
+ Scenario: NestedMenu accepts an onBackButtonClick prop
57
+ Given the NestedMenu is open
58
+ And an onBackButtonClick function is passed
59
+ When the back button is clicked
60
+ Then the onBackButtonClick function should be called
61
+
62
+ Scenario Outline: NestedMenu can render MenuComponents
63
+ Given the NestedMenu is open
64
+ When the visible menu's MenuComponent is <MenuComponent>
65
+ Then a <MenuComponent> should be rendered with menuProps
66
+ And the menuProps should be of type <menuPropsType>
67
+
68
+ Examples:
69
+ | MenuComponent | menuPropsType |
70
+ |------------------|--------------------------|
71
+ | ActionMenu | TypeActionMenuProps |
72
+ | SingleSelectMenu | TypeSingleSelectMenuProps|
73
+ | MultiSelectMenu | TypeMultiSelectMenuProps |
74
+
75
+
76
+ Scenario: A Menu stays open when a menu item with a childMenuId is clicked
77
+ Given the NestedMenu is open
78
+ When an menu item that triggers a child menu is clicked
79
+ Then the menu will stay open
80
+ And the menu will not use its default open/close behavior
81
+
82
+ Scenario Outline: A Menu inherits its MenuComponent's open/close behavior for items without a childMenuId
83
+ Given the NestedMenu is open
84
+ And the visible menu's MenuComponent is <MenuComponent>
85
+ When an item without a childMenuId is clicked
86
+ And the <MenuComponent> will inherit its default behavior of <onClickBehavior>
87
+
88
+ Examples:
89
+ | MenuComponent | onClickBehavior |
90
+ |------------------|-------------- |
91
+ | ActionMenu | closing |
92
+ | SingleSelectMenu | not closing |
93
+ | MultiSelectMenu | not closing |
94
+
95
+ Scenario: NestedMenu renders a MenuHeaderComponent
96
+ Given the NestedMenu is open
97
+ And the visible menu's MenuHeaderComponent is not null
98
+ Then the MenuHeaderComponent should be rendered with menuHeaderProps
99
+
100
+ Scenario Outline: NestedMenu checks its types
101
+ When `<propName>` is passed a value
102
+ Then NestedMenu expects it to be of type `<propType>`
103
+
104
+ Examples: Prop Types
105
+ | propName | propType |
106
+ | --------------------- | -------------------------------------------------------------------------------- |
107
+ | menus | Record<string, {MenuComponent, menuProps, MenuHeaderComponent, menuHeaderProps}> |
108
+ | selectedMenuId | string |
109
+ | onBackButtonClick | function |
@@ -0,0 +1,157 @@
1
+ /* eslint-disable react-hooks/rules-of-hooks */
2
+ import React from "react";
3
+ import type { Meta, StoryObj } from "@storybook/react";
4
+
5
+ import { Box } from "@sproutsocial/seeds-react-box";
6
+ // eslint-disable-next-line no-restricted-imports
7
+ import { TEST_BOOKS } from "../__fixtures__/menu-test-data";
8
+
9
+ import {
10
+ ActionMenu,
11
+ SingleSelectMenu,
12
+ MenuToggleButton,
13
+ MultiSelectMenu,
14
+ NestedMenu,
15
+ } from "../index";
16
+ import { NestedMenuItem } from "./NestedMenuItem";
17
+ import { NestedMenuContent } from "./NestedMenuContent";
18
+
19
+ type CustomNestedMenuArgs = React.ComponentProps<typeof NestedMenu>;
20
+
21
+ const meta: Meta<CustomNestedMenuArgs> = {
22
+ title: "Under Development/NestedMenu",
23
+ component: NestedMenu,
24
+ parameters: {
25
+ controls: {
26
+ expanded: true,
27
+ },
28
+ },
29
+ decorators: [
30
+ (Story) => (
31
+ <Box display="flex" flexDirection="column" gap="16px" maxWidth={300}>
32
+ <Story />
33
+ </Box>
34
+ ),
35
+ ],
36
+ };
37
+
38
+ export default meta;
39
+
40
+ type Story = StoryObj<CustomNestedMenuArgs>;
41
+
42
+ const menus = {
43
+ root: {
44
+ MenuComponent: ActionMenu,
45
+ menuProps: {
46
+ menuItems: [
47
+ {
48
+ id: "parent-1",
49
+ children: "Open inner menu",
50
+ childMenuId: "inner",
51
+ },
52
+ {
53
+ id: "other-root",
54
+ children: "No child menu",
55
+ },
56
+ ],
57
+ },
58
+ MenuHeaderComponent: null,
59
+ },
60
+ inner: {
61
+ MenuComponent: ActionMenu,
62
+ menuProps: {
63
+ children: (
64
+ <NestedMenuContent>
65
+ <NestedMenuItem
66
+ id="item-2"
67
+ children="Books as ActionMenu"
68
+ childMenuId="books-action"
69
+ />
70
+ <NestedMenuItem
71
+ id="item-3"
72
+ children="Books as MultiSelect"
73
+ childMenuId="books-multiselect"
74
+ />
75
+ <NestedMenuItem
76
+ id="item-4"
77
+ children="Books as SingleSelect"
78
+ childMenuId="books-select"
79
+ />
80
+ </NestedMenuContent>
81
+ ),
82
+ },
83
+ menuHeaderProps: {
84
+ title: "Inner Menu",
85
+ },
86
+ },
87
+ "books-action": {
88
+ MenuComponent: ActionMenu,
89
+ menuProps: {
90
+ menuItems: TEST_BOOKS.map((book) => ({
91
+ id: book.id,
92
+ children: book.title,
93
+ childMenuId: "edit-book",
94
+ })),
95
+ },
96
+ menuHeaderProps: {
97
+ title: "Manage Books",
98
+ },
99
+ },
100
+ "books-multiselect": {
101
+ MenuComponent: MultiSelectMenu,
102
+ menuProps: {
103
+ menuItems: TEST_BOOKS.map((book) => ({
104
+ id: book.id,
105
+ children: book.title,
106
+ })),
107
+ },
108
+ menuHeaderProps: {
109
+ title: "Select Books",
110
+ },
111
+ },
112
+ "books-select": {
113
+ MenuComponent: SingleSelectMenu,
114
+ menuProps: {
115
+ menuItems: TEST_BOOKS.map((book) => ({
116
+ id: book.id,
117
+ children: book.title,
118
+ })),
119
+ },
120
+ menuHeaderProps: {
121
+ title: "Select One Book",
122
+ },
123
+ },
124
+ "edit-book": {
125
+ MenuComponent: ActionMenu,
126
+ menuProps: {
127
+ menuItems: [
128
+ {
129
+ id: "archive",
130
+ children: "Archive",
131
+ },
132
+ {
133
+ id: "delete",
134
+ children: "Delete",
135
+ },
136
+ ],
137
+ },
138
+ menuHeaderProps: {
139
+ title: "Edit Book",
140
+ backArrowLabel: "Back to Books",
141
+ },
142
+ },
143
+ };
144
+
145
+ export const NestedMenuMultiLevelStory: Story = {
146
+ name: "Multi Level NestedMenu",
147
+ render: (args) => {
148
+ return (
149
+ <NestedMenu
150
+ initialMenuId="root"
151
+ menus={menus}
152
+ backArrowLabel="Go Back"
153
+ menuToggleElement={<MenuToggleButton>Open Menu</MenuToggleButton>}
154
+ />
155
+ );
156
+ },
157
+ };
@@ -0,0 +1,102 @@
1
+ import React, { useState } from "react";
2
+ import type { TypeMenuItemProps } from "../MenuItem";
3
+ import type {
4
+ TypeNestedMenuItemProps,
5
+ TypeNestedMenuProps,
6
+ } from "./NestedMenuTypes";
7
+ import { reduceReducers } from "../utils/reduceReducers";
8
+ import { NestedMenuHeader } from "./NestedMenuHeader";
9
+ import { nestedMenuStateReducer } from "../reducers/nestedMenuStateReducer";
10
+ import { NestedMenuContent } from "./NestedMenuContent";
11
+ import { NestedMenuProvider } from "./NestedMenuContext";
12
+ import { NestedMenuItem } from "./NestedMenuItem";
13
+
14
+ export const NestedMenu = <
15
+ I extends TypeMenuItemProps = TypeNestedMenuItemProps
16
+ >({
17
+ initialMenuId,
18
+ menus,
19
+ onBackButtonClick,
20
+ backArrowLabel,
21
+ menuToggleElement,
22
+ }: TypeNestedMenuProps<I>) => {
23
+ const [selectedMenuPath, setSelectedMenuPath] = useState([initialMenuId]);
24
+ const selectedMenuId = selectedMenuPath[selectedMenuPath.length - 1];
25
+
26
+ const setSelectedMenuId = (id: string) => {
27
+ setSelectedMenuPath((currentPath) => [...currentPath, id]);
28
+ };
29
+ const goBack = () => {
30
+ setSelectedMenuPath((currentPath) => currentPath.slice(0, -1));
31
+ onBackButtonClick?.(selectedMenuId);
32
+ };
33
+
34
+ const {
35
+ MenuComponent,
36
+ menuProps,
37
+ MenuHeaderComponent = NestedMenuHeader,
38
+ menuHeaderProps = {},
39
+ } = menus[selectedMenuId] || {};
40
+ const {
41
+ children,
42
+ menuItems,
43
+ MenuItemComponent,
44
+ stateReducer: externalStateReducer,
45
+ onStateChange: externalOnStateChange,
46
+ ...props
47
+ } = menuProps || {};
48
+
49
+ const stateReducer = reduceReducers(
50
+ nestedMenuStateReducer,
51
+ externalStateReducer
52
+ );
53
+
54
+ // Reactively reset the selected menu path when the menu closes
55
+ const onStateChange = (changes) => {
56
+ externalOnStateChange?.(changes);
57
+ if (changes.isOpen === false) {
58
+ // Wait for the menu to close before resetting to avoid flicker
59
+ setTimeout(() => {
60
+ setSelectedMenuPath([initialMenuId]);
61
+ }, 100);
62
+ }
63
+ };
64
+
65
+ const nestedMenuProps = {
66
+ menuToggleElement,
67
+ stateReducer,
68
+ onStateChange,
69
+ initialIsOpen: selectedMenuPath.length > 1,
70
+ children: (
71
+ <>
72
+ {/*This header can be suppressed by passing `null` for MenuHeaderComponent*/}
73
+ {MenuHeaderComponent && (
74
+ <MenuHeaderComponent
75
+ backArrowLabel={backArrowLabel}
76
+ {...menuHeaderProps}
77
+ />
78
+ )}
79
+ {/*children takes precedence over menuItems if both are passed.*/}
80
+ {children && children}
81
+ {menuItems && !children && (
82
+ <NestedMenuContent
83
+ menuItems={menuItems}
84
+ MenuItemComponent={MenuItemComponent ?? NestedMenuItem}
85
+ />
86
+ )}
87
+ </>
88
+ ),
89
+ ...props,
90
+ };
91
+
92
+ return (
93
+ <NestedMenuProvider
94
+ selectedMenuPath={selectedMenuPath}
95
+ setSelectedMenuPath={setSelectedMenuPath}
96
+ setSelectedMenuId={setSelectedMenuId}
97
+ goBack={goBack}
98
+ >
99
+ {MenuComponent && <MenuComponent {...nestedMenuProps} />}
100
+ </NestedMenuProvider>
101
+ );
102
+ };
@@ -0,0 +1,71 @@
1
+ import { MenuContent, type TypeMenuContentProps } from "../MenuContent/index";
2
+ import { useMenuContentContext } from "../MenuContext";
3
+ import type { TypeChildrenOrItems, TypeInternalItemProps } from "../types";
4
+ import { useNestedMenuContext } from "./NestedMenuContext";
5
+ import type { TypeMenuItemProps } from "../MenuItem/index";
6
+ import type { TypeNestedMenuItemProps } from "./NestedMenuTypes";
7
+ import { useEffect, useRef } from "react";
8
+ import { mergeRefs } from "@sproutsocial/seeds-react-utilities/src/index";
9
+ import { NestedMenuItem } from "./NestedMenuItem";
10
+
11
+ export const NestedMenuContent = <
12
+ I extends TypeMenuItemProps = TypeNestedMenuItemProps
13
+ >({
14
+ innerRef: innerRefProp = null,
15
+ onKeyDown: onKeyDownProp,
16
+ children,
17
+ menuItems,
18
+ MenuItemComponent = NestedMenuItem,
19
+ ...props
20
+ }: TypeMenuContentProps<I>) => {
21
+ const { highlightedIndex, items } = useMenuContentContext();
22
+ const { setSelectedMenuId, selectedMenuPath, goBack } =
23
+ useNestedMenuContext();
24
+ const internalRef = useRef<HTMLUListElement>(null);
25
+ // Merge our ref and the consumer's ref so they will both be updated
26
+ const innerRef = mergeRefs<HTMLUListElement>(internalRef, innerRefProp);
27
+
28
+ const contentProps = children
29
+ ? { children }
30
+ : ({ menuItems, MenuItemComponent } as TypeChildrenOrItems<I>);
31
+
32
+ // Focus the content on mount so that keyboard nav is available immediately
33
+ useEffect(() => {
34
+ internalRef?.current?.focus();
35
+ }, []);
36
+
37
+ const onKeyDown = (e) => {
38
+ // Call the consumer's onKeyDown handler first
39
+ onKeyDownProp?.(e);
40
+
41
+ // Handle navigating into and out of child menus
42
+ switch (e.key) {
43
+ case "ArrowRight":
44
+ case " ":
45
+ case "Enter":
46
+ if (highlightedIndex !== undefined && highlightedIndex !== -1) {
47
+ const item = items[
48
+ highlightedIndex
49
+ ] as TypeInternalItemProps<TypeNestedMenuItemProps>;
50
+ if (item?.menuItemProps?.childMenuId !== undefined) {
51
+ setSelectedMenuId?.(item?.menuItemProps?.childMenuId);
52
+ }
53
+ }
54
+ break;
55
+ case "ArrowLeft":
56
+ if (selectedMenuPath.length > 1) {
57
+ goBack?.();
58
+ }
59
+ break;
60
+ }
61
+ };
62
+
63
+ return (
64
+ <MenuContent
65
+ onKeyDown={onKeyDown}
66
+ innerRef={innerRef}
67
+ {...contentProps}
68
+ {...props}
69
+ />
70
+ );
71
+ };
@@ -0,0 +1,56 @@
1
+ import React, { createContext, useContext } from "react";
2
+ import type { TypeNotEmptyChildren } from "../types";
3
+
4
+ export interface TypeNestedMenuContext {
5
+ selectedMenuPath: string[];
6
+ setSelectedMenuId?: (id: string) => void;
7
+ setSelectedMenuPath?: (path: string[]) => void;
8
+ goBack?: () => void;
9
+ }
10
+
11
+ const defaultNestedMenuContext: TypeNestedMenuContext = {
12
+ selectedMenuPath: [],
13
+ setSelectedMenuId: () => {},
14
+ setSelectedMenuPath: () => {},
15
+ goBack: () => {},
16
+ };
17
+
18
+ export interface TypeNestedMenuProviderProps extends TypeNestedMenuContext {
19
+ children: TypeNotEmptyChildren;
20
+ }
21
+
22
+ /**
23
+ * NestedMenuContext factory, sets the type and the initial object
24
+ */
25
+ export const NestedMenuContext = createContext<TypeNestedMenuContext>(
26
+ defaultNestedMenuContext
27
+ );
28
+
29
+ /**
30
+ * Provider Component sets the context values in the provider
31
+ */
32
+ export const NestedMenuProvider = ({
33
+ children,
34
+ ...nestedMenuProps
35
+ }: TypeNestedMenuProviderProps) => {
36
+ return (
37
+ <NestedMenuContext.Provider value={nestedMenuProps}>
38
+ {children}
39
+ </NestedMenuContext.Provider>
40
+ );
41
+ };
42
+
43
+ /**
44
+ * Utility hook for consuming NestedMenuContext
45
+ */
46
+ export const useNestedMenuContext = (): TypeNestedMenuContext => {
47
+ const nestedMenuContextValue = useContext(NestedMenuContext);
48
+
49
+ if (nestedMenuContextValue === null) {
50
+ throw new Error(
51
+ "useNestedMenuContext must be used within a <NestedMenuContext.Provider />"
52
+ );
53
+ }
54
+
55
+ return nestedMenuContextValue;
56
+ };
@@ -0,0 +1,25 @@
1
+ import { MenuHeader } from "../MenuHeader/index";
2
+ import { useNestedMenuContext } from "./NestedMenuContext";
3
+ import { Button } from "@sproutsocial/seeds-react-button";
4
+ import { Icon } from "@sproutsocial/seeds-react-icon";
5
+ import type { TypeNestedMenuHeaderProps } from "./NestedMenuTypes";
6
+
7
+ export const NestedMenuHeader = ({
8
+ backArrowLabel,
9
+ ...props
10
+ }: TypeNestedMenuHeaderProps) => {
11
+ const { goBack, selectedMenuPath } = useNestedMenuContext();
12
+ const showBackArrow = selectedMenuPath.length > 1;
13
+ return (
14
+ <MenuHeader
15
+ leftAction={
16
+ showBackArrow ? (
17
+ <Button onClick={goBack} ariaLabel={backArrowLabel}>
18
+ <Icon name="arrow-left" />
19
+ </Button>
20
+ ) : undefined
21
+ }
22
+ {...props}
23
+ />
24
+ );
25
+ };
@@ -0,0 +1,37 @@
1
+ import { MenuItem, MenuItemActionRight } from "../MenuItem/index";
2
+ import { useNestedMenuContext } from "./NestedMenuContext";
3
+ import { Icon } from "@sproutsocial/seeds-react-icon";
4
+ import { Box } from "@sproutsocial/seeds-react-box";
5
+ import type { TypeNestedMenuItemProps } from "./NestedMenuTypes";
6
+
7
+ export const NestedMenuItem = ({
8
+ children,
9
+ childMenuId,
10
+ onClick: onClickProp,
11
+ ...rest
12
+ }: TypeNestedMenuItemProps) => {
13
+ const { setSelectedMenuId } = useNestedMenuContext();
14
+ // This method of passing onClick is necessary because passing undefined for onClick
15
+ // behaves differently from not passing anything at all.
16
+ const onClickProps = childMenuId
17
+ ? {
18
+ onClick: (e) => {
19
+ onClickProp?.(e);
20
+ setSelectedMenuId?.(childMenuId);
21
+ },
22
+ }
23
+ : {};
24
+
25
+ return (
26
+ <MenuItem {...onClickProps} {...rest}>
27
+ <Box display="flex" justifyContent="space-between">
28
+ {children}
29
+ {childMenuId && (
30
+ <MenuItemActionRight>
31
+ <Icon name="arrow-right-outline" />
32
+ </MenuItemActionRight>
33
+ )}
34
+ </Box>
35
+ </MenuItem>
36
+ );
37
+ };
@@ -0,0 +1,47 @@
1
+ import type { TypeActionMenuProps } from "../ActionMenu";
2
+ import type { TypeMultiSelectMenuProps } from "../MultiSelectMenu";
3
+ import type { TypeSingleSelectMenuProps } from "../SingleSelectMenu";
4
+ import type { TypeMenuItemProps } from "../MenuItem";
5
+ import type { TypeMenuRootProps } from "../MenuRoot";
6
+ import type { TypeMenuHeaderProps } from "../MenuHeader";
7
+ import type { TypeChildrenOrItems } from "../types";
8
+
9
+ export interface TypeNestedMenuMapProps<
10
+ P,
11
+ I extends TypeMenuItemProps = TypeNestedMenuItemProps
12
+ > {
13
+ MenuComponent: React.ComponentType<P>;
14
+ menuProps: Omit<P, "menuToggleElement"> & TypeChildrenOrItems<I>;
15
+ /** Defaults to NestedMenuHeader if not provided.
16
+ * This header can be suppressed by passing null.*/
17
+ MenuHeaderComponent?: React.ComponentType<TypeMenuHeaderProps> | null;
18
+ menuHeaderProps?: Partial<TypeMenuHeaderProps>;
19
+ }
20
+
21
+ export interface TypeNestedMenuHeaderProps extends TypeMenuHeaderProps {
22
+ /** Localized label for the back arrow in this child menu.
23
+ * Overrides the default label passed to NestedMenu. */
24
+ backArrowLabel?: string;
25
+ }
26
+
27
+ export interface TypeNestedMenuItemProps extends TypeMenuItemProps {
28
+ childMenuId?: string;
29
+ }
30
+
31
+ export interface TypeNestedMenuProps<
32
+ I extends TypeMenuItemProps = TypeNestedMenuItemProps
33
+ > {
34
+ initialMenuId: string;
35
+ menuToggleElement: TypeMenuRootProps["menuToggleElement"];
36
+ onBackButtonClick?: (menuId: string) => void;
37
+ /** Sets the default localized label for the back arrow in all child menus.
38
+ * This label can be overridden by including backArrowLabel in a menu's menuHeaderProps. */
39
+ backArrowLabel?: string;
40
+ /** An object of the menus and child menus to be displayed, keyed by id.*/
41
+ menus: {
42
+ [id: string]:
43
+ | TypeNestedMenuMapProps<TypeActionMenuProps<I>, I>
44
+ | TypeNestedMenuMapProps<TypeSingleSelectMenuProps<I>, I>
45
+ | TypeNestedMenuMapProps<TypeMultiSelectMenuProps<I>, I>;
46
+ };
47
+ }
@@ -0,0 +1,31 @@
1
+ /* eslint-disable testing-library/no-unnecessary-act */
2
+ import {
3
+ NestedMenu,
4
+ MenuContent,
5
+ MenuToggleButton,
6
+ MenuItem,
7
+ MenuGroup,
8
+ } from "../../index";
9
+ import {
10
+ render,
11
+ screen,
12
+ cleanup,
13
+ waitFor,
14
+ } from "@sproutsocial/seeds-react-testing-library";
15
+
16
+ const menuItems = [
17
+ { id: "test-item-1", index: 0, label: "Item 1" },
18
+ { id: "test-item-2", index: 1, label: "Item 2" },
19
+ { id: "test-item-3", index: 2, label: "Item 3" },
20
+ { id: "test-item-4", index: 3, label: "Item 4" },
21
+ ];
22
+
23
+ describe("NestedMenu", () => {
24
+ afterEach(() => {
25
+ cleanup();
26
+ });
27
+
28
+ it("runs", () => {
29
+ expect(true).toBe(true);
30
+ });
31
+ });