@sproutsocial/seeds-react-menu 0.4.1 → 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.
- package/.turbo/turbo-build.log +10 -10
- package/CHANGELOG.md +32 -0
- package/dist/esm/index.js +1553 -423
- package/dist/esm/index.js.map +1 -1
- package/dist/index.d.mts +303 -122
- package/dist/index.d.ts +303 -122
- package/dist/index.js +1561 -405
- package/dist/index.js.map +1 -1
- package/package.json +4 -1
- package/src/ActionMenu/ActionMenu.stories.tsx +426 -0
- package/src/ActionMenu/ActionMenu.tsx +30 -26
- package/src/ActionMenu/ActionMenuTypes.ts +13 -8
- package/src/ActionMenu/__tests__/ActionMenu.test.tsx +17 -30
- package/src/ActionMenu/__tests__/ActionMenu.typetest.tsx +32 -3
- package/src/Autocomplete/Autocomplete.stories.tsx +61 -2
- package/src/Autocomplete/Autocomplete.tsx +15 -74
- package/src/Autocomplete/AutocompleteInput.tsx +18 -27
- package/src/Autocomplete/AutocompleteTokenInput.tsx +50 -0
- package/src/Autocomplete/AutocompleteTypes.ts +27 -11
- package/src/Autocomplete/MultiAutocomplete.tsx +189 -0
- package/src/Autocomplete/SingleAutocomplete.tsx +74 -0
- package/src/Autocomplete/__tests__/Autocomplete.test.tsx +171 -3
- package/src/Autocomplete/__tests__/Autocomplete.typetest.tsx +116 -3
- package/src/MenuContent/MenuContent.stories.tsx +164 -0
- package/src/MenuContent/MenuContent.tsx +35 -8
- package/src/MenuContent/MenuContentTypes.ts +7 -5
- package/src/MenuContent/__tests__/MenuContent.typetest.tsx +18 -2
- package/src/MenuContext.tsx +191 -37
- package/src/MenuFooter/MenuFooter.stories.tsx +202 -0
- package/src/MenuFooter/__tests__/MenuFooter.test.tsx +16 -11
- package/src/MenuGroup/MenuGroup.feature +21 -21
- package/src/MenuGroup/MenuGroup.stories.tsx +248 -0
- package/src/MenuGroup/MenuGroup.tsx +9 -7
- package/src/MenuGroup/MenuGroupTypes.ts +8 -6
- package/src/MenuGroup/__tests__/MenuGroup.test.tsx +6 -6
- package/src/MenuGroup/__tests__/MenuGroup.typetest.tsx +23 -4
- package/src/MenuGroup/styles.tsx +4 -2
- package/src/MenuHeader/MenuHeader.stories.tsx +276 -0
- package/src/MenuHeader/MenuHeader.tsx +3 -1
- package/src/MenuHeader/__tests__/MenuHeader.test.tsx +23 -13
- package/src/MenuHeader/styles.tsx +18 -14
- package/src/MenuItem/MenuItem.stories.tsx +507 -0
- package/src/MenuItem/MenuItem.tsx +18 -9
- package/src/MenuItem/MenuItemTypes.ts +9 -4
- package/src/MenuItem/__tests__/MenuItem.test.tsx +6 -6
- package/src/MenuItem/styles.tsx +21 -5
- package/src/MenuItem/useOptionProps.tsx +10 -8
- package/src/MenuLabel.tsx +2 -2
- package/src/MenuRoot/MenuRoot.tsx +37 -23
- package/src/MenuRoot/__tests__/MenuRoot.test.tsx +59 -32
- package/src/MenuSearchInput/MenuSearchInput.tsx +63 -0
- package/src/MenuSearchInput/__tests__/MenuSearchInput.test.tsx +157 -0
- package/src/MenuSearchInput/__tests__/MenuSearchInput.typetest.tsx +31 -0
- package/src/MenuSearchInput/index.ts +1 -0
- package/src/MenuSearchTokenInput/MenuSearchTokenInput.tsx +98 -0
- package/src/MenuSearchTokenInput/__tests__/MenuSearchTokenInput.test.tsx +247 -0
- package/src/MenuSearchTokenInput/__tests__/MenuSearchTokenInput.typetest.tsx +33 -0
- package/src/MenuSearchTokenInput/index.ts +1 -0
- package/src/MenuToggleButton/MenuToggleButton.stories.tsx +72 -0
- package/src/{MenuToggleButton.tsx → MenuToggleButton/MenuToggleButton.tsx} +6 -5
- package/src/MenuToggleButton/index.ts +1 -0
- package/src/MenuTokenInput.tsx +104 -0
- package/src/MultiSelectMenu/MultiSelectMenu.stories.tsx +214 -0
- package/src/MultiSelectMenu/MultiSelectMenu.tsx +44 -67
- package/src/MultiSelectMenu/MultiSelectMenuTypes.ts +9 -5
- package/src/MultiSelectMenu/__tests__/MultiSelectMenu.test.tsx +46 -60
- package/src/MultiSelectMenu/__tests__/MultiSelectMenu.typetest.tsx +29 -2
- package/src/NestedMenu/NestedMenu.feature +109 -0
- package/src/NestedMenu/NestedMenu.stories.tsx +157 -0
- package/src/NestedMenu/NestedMenu.tsx +102 -0
- package/src/NestedMenu/NestedMenuContent.tsx +71 -0
- package/src/NestedMenu/NestedMenuContext.tsx +56 -0
- package/src/NestedMenu/NestedMenuHeader.tsx +25 -0
- package/src/NestedMenu/NestedMenuItem.tsx +37 -0
- package/src/NestedMenu/NestedMenuTypes.ts +47 -0
- package/src/NestedMenu/__tests__/NestedMenu.test.tsx +31 -0
- package/src/NestedMenu/__tests__/NestedMenu.typetest.tsx +134 -0
- package/src/NestedMenu/index.ts +5 -0
- package/src/SingleSelectMenu/SingleSelectMenu.stories.tsx +227 -0
- package/src/SingleSelectMenu/SingleSelectMenu.tsx +29 -25
- package/src/SingleSelectMenu/SingleSelectMenuTypes.ts +9 -5
- package/src/SingleSelectMenu/__tests__/SingleSelectMenu.typetest.tsx +32 -2
- package/src/SubmenuItem/SubmenuItem.feature +82 -0
- package/src/SubmenuItem/SubmenuItem.stories.tsx +163 -0
- package/src/SubmenuItem/SubmenuItem.tsx +254 -0
- package/src/SubmenuItem/SubmenuItemTypes.ts +24 -0
- package/src/SubmenuItem/__tests__/SubmenuItem.test.tsx +3 -0
- package/src/SubmenuItem/__tests__/SubmenuItem.typetest.tsx +112 -0
- package/src/SubmenuItem/index.ts +2 -0
- package/src/__tests__/Menu.test.tsx +38 -0
- package/src/hooks/useDownshiftDefaults.tsx +107 -0
- package/src/hooks/useItemFiltering.tsx +66 -0
- package/src/hooks/useMenuChildren.tsx +279 -0
- package/src/index.ts +7 -1
- package/src/menuTestingUtils.tsx +21 -10
- package/src/reducers/nestedMenuStateReducer.tsx +23 -0
- package/src/reducers/useComboboxStateReducer.tsx +26 -8
- package/src/reducers/useSelectStateReducer.tsx +24 -6
- package/src/storybookUtilities/menu-storybook-components.tsx +5 -3
- package/src/types.ts +38 -5
- package/src/utils/downshiftDefaults.ts +9 -0
- package/src/utils/getMultiSelectOverrides.ts +21 -0
- package/src/utils/reduceReducers.ts +19 -0
- package/src/hooks/useItemsFromChildren.tsx +0 -131
- package/src/utils/itemToString.ts +0 -5
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import {
|
|
2
|
+
NestedMenu,
|
|
3
|
+
ActionMenu,
|
|
4
|
+
MenuToggleButton,
|
|
5
|
+
NestedMenuItem,
|
|
6
|
+
MenuItem,
|
|
7
|
+
NestedMenuContent,
|
|
8
|
+
type TypeNestedMenuItemProps,
|
|
9
|
+
} from "../../index";
|
|
10
|
+
|
|
11
|
+
export const NestedMenuTypeTest = () => (
|
|
12
|
+
<>
|
|
13
|
+
{/* Valid NestedMenu with all props */}
|
|
14
|
+
<NestedMenu
|
|
15
|
+
initialMenuId={"root"}
|
|
16
|
+
menuToggleElement={<button>Open</button>}
|
|
17
|
+
onBackButtonClick={(menuId) => console.log(menuId)}
|
|
18
|
+
// The menus can use different menu components and props
|
|
19
|
+
menus={{
|
|
20
|
+
root: {
|
|
21
|
+
MenuComponent: ActionMenu,
|
|
22
|
+
menuProps: {
|
|
23
|
+
// menuItems prop is supported
|
|
24
|
+
menuItems: [
|
|
25
|
+
{ id: "item1", children: "Item 1", childMenuId: "child" },
|
|
26
|
+
{ id: "item2", children: "Item 2" },
|
|
27
|
+
],
|
|
28
|
+
MenuItemComponent: NestedMenuItem,
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
child: {
|
|
32
|
+
MenuComponent: ActionMenu,
|
|
33
|
+
menuProps: {
|
|
34
|
+
// children prop is supported
|
|
35
|
+
children: (
|
|
36
|
+
<NestedMenuContent>
|
|
37
|
+
<NestedMenuItem children="Child Item 1" id="child-1" />
|
|
38
|
+
</NestedMenuContent>
|
|
39
|
+
),
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
}}
|
|
43
|
+
/>
|
|
44
|
+
|
|
45
|
+
{/*Required/optional props*/}
|
|
46
|
+
<NestedMenu
|
|
47
|
+
// onBackButtonClick is not required
|
|
48
|
+
onBackButtonClick={undefined}
|
|
49
|
+
// @ts-expect-error initialMenuId is required
|
|
50
|
+
initialMenuId={undefined}
|
|
51
|
+
// @ts-expect-error menuToggleElement is required
|
|
52
|
+
menuToggleElement={undefined}
|
|
53
|
+
// @ts-expect-error menus is required
|
|
54
|
+
menus={undefined}
|
|
55
|
+
/>
|
|
56
|
+
|
|
57
|
+
{/*Invalid props*/}
|
|
58
|
+
<NestedMenu
|
|
59
|
+
// @ts-expect-error initialMenuId must be a string
|
|
60
|
+
initialMenuId={3}
|
|
61
|
+
// @ts-expect-error menuToggleElement must be a React element, not a component
|
|
62
|
+
menuToggleElement={MenuToggleButton}
|
|
63
|
+
// @ts-expect-error menus must be an object, not an array
|
|
64
|
+
menus={[
|
|
65
|
+
{
|
|
66
|
+
MenuComponent: ActionMenu,
|
|
67
|
+
menuProps: {
|
|
68
|
+
menuItems: [
|
|
69
|
+
{ id: "item1", children: "Item 1", childMenuId: "child" },
|
|
70
|
+
{ id: "item2", children: "Item 2" },
|
|
71
|
+
] as TypeNestedMenuItemProps[],
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
]}
|
|
75
|
+
/>
|
|
76
|
+
|
|
77
|
+
{/*menuToggleElement is not allowed on menuProps*/}
|
|
78
|
+
<NestedMenu
|
|
79
|
+
initialMenuId={"1"}
|
|
80
|
+
menuToggleElement={<span />}
|
|
81
|
+
menus={{
|
|
82
|
+
"1": {
|
|
83
|
+
MenuComponent: ActionMenu,
|
|
84
|
+
menuProps: {
|
|
85
|
+
menuItems: [
|
|
86
|
+
{ id: "item1", children: "Item 1", childMenuId: "2" },
|
|
87
|
+
{ id: "item2", children: "Item 2" },
|
|
88
|
+
] as TypeNestedMenuItemProps[],
|
|
89
|
+
// @ts-expect-error menuToggleElement is not allowed here
|
|
90
|
+
menuToggleElement: <span />,
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
}}
|
|
94
|
+
/>
|
|
95
|
+
|
|
96
|
+
{/*menuProps cannot have unexpected values*/}
|
|
97
|
+
<NestedMenu
|
|
98
|
+
initialMenuId={"1"}
|
|
99
|
+
menuToggleElement={<span />}
|
|
100
|
+
menus={{
|
|
101
|
+
"1": {
|
|
102
|
+
MenuComponent: ActionMenu,
|
|
103
|
+
menuProps: {
|
|
104
|
+
menuItems: [
|
|
105
|
+
{ id: "item1", children: "Item 1", childMenuId: "2" },
|
|
106
|
+
{ id: "item2", children: "Item 2" },
|
|
107
|
+
] as TypeNestedMenuItemProps[],
|
|
108
|
+
// @ts-expect-error unexpected properties are not allowed
|
|
109
|
+
random: "asdf",
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
}}
|
|
113
|
+
/>
|
|
114
|
+
|
|
115
|
+
{/*menuProps cannot combine menuItems and children*/}
|
|
116
|
+
<NestedMenu
|
|
117
|
+
initialMenuId={"1"}
|
|
118
|
+
menuToggleElement={<span />}
|
|
119
|
+
menus={{
|
|
120
|
+
"1": {
|
|
121
|
+
MenuComponent: ActionMenu,
|
|
122
|
+
// @ts-expect-error menuItems and children cannot be used together
|
|
123
|
+
menuProps: {
|
|
124
|
+
menuItems: [
|
|
125
|
+
{ id: "item1", children: "Item 1", childMenuId: "2" },
|
|
126
|
+
{ id: "item2", children: "Item 2" },
|
|
127
|
+
] as TypeNestedMenuItemProps[],
|
|
128
|
+
children: "asdf",
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
}}
|
|
132
|
+
/>
|
|
133
|
+
</>
|
|
134
|
+
);
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/* eslint-disable react-hooks/rules-of-hooks */
|
|
2
|
+
import React from "react";
|
|
3
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
4
|
+
import { action } from "@storybook/addon-actions";
|
|
5
|
+
|
|
6
|
+
import { Box } from "@sproutsocial/seeds-react-box";
|
|
7
|
+
import { Icon, type TypeIconName } from "@sproutsocial/seeds-react-icon";
|
|
8
|
+
import { StorybookMenuToggleButton } from "../storybookUtilities/menu-storybook-components";
|
|
9
|
+
import { TEST_BOOKS } from "../__fixtures__/menu-test-data";
|
|
10
|
+
import {
|
|
11
|
+
SingleSelectMenu,
|
|
12
|
+
MenuContent,
|
|
13
|
+
MenuGroup,
|
|
14
|
+
MenuItem,
|
|
15
|
+
MenuHeader,
|
|
16
|
+
MenuSearchInput,
|
|
17
|
+
type InputType,
|
|
18
|
+
type TypeInternalItemProps,
|
|
19
|
+
type TypeSingleSelectMenuBaseProps,
|
|
20
|
+
type TypeSingleSelectMenuProps,
|
|
21
|
+
} from "../index";
|
|
22
|
+
|
|
23
|
+
type CustomSingleSelectArgs = TypeSingleSelectMenuProps & {
|
|
24
|
+
inputType: InputType;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const meta: Meta<CustomSingleSelectArgs> = {
|
|
28
|
+
title: "Under Development/SingleSelectMenu",
|
|
29
|
+
component: SingleSelectMenu,
|
|
30
|
+
parameters: {
|
|
31
|
+
controls: {
|
|
32
|
+
expanded: true,
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
argTypes: {
|
|
36
|
+
inputType: {
|
|
37
|
+
options: ["radio", "checkbox", "icon", "switch", undefined],
|
|
38
|
+
control: { type: "select" },
|
|
39
|
+
description: "The type of input to use for the menu items",
|
|
40
|
+
table: {
|
|
41
|
+
category: "Menu Item Selectable",
|
|
42
|
+
defaultValue: { summary: "undefined" },
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
args: {
|
|
47
|
+
inputType: "radio",
|
|
48
|
+
},
|
|
49
|
+
decorators: [
|
|
50
|
+
(Story) => (
|
|
51
|
+
<Box display="flex" flexDirection="column" gap="16px" maxWidth={300}>
|
|
52
|
+
<Story />
|
|
53
|
+
</Box>
|
|
54
|
+
),
|
|
55
|
+
],
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export default meta;
|
|
59
|
+
|
|
60
|
+
type Story = StoryObj<CustomSingleSelectArgs>;
|
|
61
|
+
|
|
62
|
+
export const SingleSelectMenuExample: Story = {
|
|
63
|
+
name: "Simple Single Select Menu",
|
|
64
|
+
render: ({ inputType, ...args }) => {
|
|
65
|
+
return (
|
|
66
|
+
<SingleSelectMenu
|
|
67
|
+
itemToString={(item) => (item ? TEST_BOOKS[item.index].title : "")}
|
|
68
|
+
menuToggleElement={<StorybookMenuToggleButton />}
|
|
69
|
+
{...(args as object)}
|
|
70
|
+
>
|
|
71
|
+
<MenuContent>
|
|
72
|
+
<MenuGroup id="books">
|
|
73
|
+
{TEST_BOOKS.map(({ id, title }) => (
|
|
74
|
+
<MenuItem key={id} id={id} inputType={inputType}>
|
|
75
|
+
{title}
|
|
76
|
+
</MenuItem>
|
|
77
|
+
))}
|
|
78
|
+
</MenuGroup>
|
|
79
|
+
</MenuContent>
|
|
80
|
+
</SingleSelectMenu>
|
|
81
|
+
);
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
type permissionsObj = {
|
|
86
|
+
id: string;
|
|
87
|
+
index: number;
|
|
88
|
+
label: string;
|
|
89
|
+
iconName: TypeIconName;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const permissionsOptions: permissionsObj[] = [
|
|
93
|
+
{ id: "no-access", index: 0, label: "No access", iconName: "circle-minus" },
|
|
94
|
+
{ id: "read-only", index: 1, label: "Read only", iconName: "circle-half" },
|
|
95
|
+
{
|
|
96
|
+
id: "need-approval",
|
|
97
|
+
index: 2,
|
|
98
|
+
label: "Needs Approval",
|
|
99
|
+
iconName: "circle-half",
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
id: "can-reply",
|
|
103
|
+
index: 3,
|
|
104
|
+
label: "Can Reply",
|
|
105
|
+
iconName: "circle-half",
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
id: "full-publishing",
|
|
109
|
+
index: 4,
|
|
110
|
+
label: "Full Publishing",
|
|
111
|
+
iconName: "circle-fill",
|
|
112
|
+
},
|
|
113
|
+
];
|
|
114
|
+
|
|
115
|
+
export const SingleSelectMenuWithItemsAsProps: Story = {
|
|
116
|
+
name: "With Items as Props",
|
|
117
|
+
render: ({ inputType, ...args }) => {
|
|
118
|
+
const items = TEST_BOOKS.map((book) => ({
|
|
119
|
+
id: book.id,
|
|
120
|
+
children: book.title,
|
|
121
|
+
inputType,
|
|
122
|
+
}));
|
|
123
|
+
return (
|
|
124
|
+
<SingleSelectMenu
|
|
125
|
+
{...args}
|
|
126
|
+
menuToggleElement={<StorybookMenuToggleButton />}
|
|
127
|
+
menuItems={items}
|
|
128
|
+
/>
|
|
129
|
+
);
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
export const SingleSelectWithIcons: Story = {
|
|
134
|
+
name: "Single Select with icons",
|
|
135
|
+
render: ({ inputType, ...args }) => {
|
|
136
|
+
return (
|
|
137
|
+
<SingleSelectMenu
|
|
138
|
+
itemToString={(item) =>
|
|
139
|
+
item ? permissionsOptions[item.index].label : ""
|
|
140
|
+
}
|
|
141
|
+
menuToggleElement={
|
|
142
|
+
<StorybookMenuToggleButton buttonText="Select permissions" />
|
|
143
|
+
}
|
|
144
|
+
{...(args as object)}
|
|
145
|
+
>
|
|
146
|
+
<MenuContent>
|
|
147
|
+
<MenuGroup id="permissions">
|
|
148
|
+
{permissionsOptions.map(({ id, label, iconName }) => (
|
|
149
|
+
<MenuItem
|
|
150
|
+
key={id}
|
|
151
|
+
id={id}
|
|
152
|
+
display="flex"
|
|
153
|
+
alignContent="top"
|
|
154
|
+
inputType={inputType}
|
|
155
|
+
>
|
|
156
|
+
<Icon name={iconName} mr={300} />
|
|
157
|
+
{label}
|
|
158
|
+
</MenuItem>
|
|
159
|
+
))}
|
|
160
|
+
</MenuGroup>
|
|
161
|
+
</MenuContent>
|
|
162
|
+
</SingleSelectMenu>
|
|
163
|
+
);
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
export const SingleSelectControlled: Story = {
|
|
168
|
+
name: "Controlled State",
|
|
169
|
+
render: ({ inputType, ...args }) => {
|
|
170
|
+
const [isOpen, setIsOpen] = React.useState<boolean>(false);
|
|
171
|
+
const [selectedItem, setSelectedItem] =
|
|
172
|
+
React.useState<TypeInternalItemProps | null>(null);
|
|
173
|
+
return (
|
|
174
|
+
<SingleSelectMenu
|
|
175
|
+
isOpen={isOpen}
|
|
176
|
+
onIsOpenChange={({ isOpen }) => setIsOpen(isOpen)}
|
|
177
|
+
selectedItem={selectedItem}
|
|
178
|
+
onSelectedItemChange={({ selectedItem: newSelectedItem }) => {
|
|
179
|
+
setSelectedItem(newSelectedItem);
|
|
180
|
+
}}
|
|
181
|
+
itemToString={(item) => (item ? TEST_BOOKS[item.index].title : "")}
|
|
182
|
+
menuToggleElement={<StorybookMenuToggleButton />}
|
|
183
|
+
{...(args as object)}
|
|
184
|
+
>
|
|
185
|
+
<MenuContent>
|
|
186
|
+
<MenuGroup id="books">
|
|
187
|
+
{TEST_BOOKS.map(({ id, title }) => (
|
|
188
|
+
<MenuItem key={id} id={id} inputType={inputType}>
|
|
189
|
+
{title}
|
|
190
|
+
</MenuItem>
|
|
191
|
+
))}
|
|
192
|
+
</MenuGroup>
|
|
193
|
+
</MenuContent>
|
|
194
|
+
</SingleSelectMenu>
|
|
195
|
+
);
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
export const SingleSelectMenuWithSearch: Story = {
|
|
200
|
+
name: "Single Select Menu with Search",
|
|
201
|
+
render: ({ inputType, ...args }) => {
|
|
202
|
+
return (
|
|
203
|
+
<SingleSelectMenu
|
|
204
|
+
itemToString={(item) => (item ? TEST_BOOKS[item.index].title : "")}
|
|
205
|
+
menuToggleElement={<StorybookMenuToggleButton />}
|
|
206
|
+
onStateChange={(changes) => action("onStateChange")(changes)}
|
|
207
|
+
{...(args as object)}
|
|
208
|
+
>
|
|
209
|
+
<MenuHeader title="Select Book">
|
|
210
|
+
<MenuSearchInput
|
|
211
|
+
id="single-select-search"
|
|
212
|
+
name="search"
|
|
213
|
+
type="search"
|
|
214
|
+
aria-label="Search Books"
|
|
215
|
+
/>
|
|
216
|
+
</MenuHeader>
|
|
217
|
+
<MenuContent>
|
|
218
|
+
{TEST_BOOKS.map(({ id, title }) => (
|
|
219
|
+
<MenuItem key={id} id={id} inputType={inputType}>
|
|
220
|
+
{title}
|
|
221
|
+
</MenuItem>
|
|
222
|
+
))}
|
|
223
|
+
</MenuContent>
|
|
224
|
+
</SingleSelectMenu>
|
|
225
|
+
);
|
|
226
|
+
},
|
|
227
|
+
};
|
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import { useSelect
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
} from "../
|
|
8
|
-
import {
|
|
9
|
-
import { MenuRoot } from "../MenuRoot";
|
|
10
|
-
import type { TypeDefaultItemProps } from "../types";
|
|
2
|
+
import { useSelect } from "downshift";
|
|
3
|
+
import type { TypeMenuItemProps } from "../MenuItem";
|
|
4
|
+
import { useMenuChildren } from "../hooks/useMenuChildren";
|
|
5
|
+
import { useDownshiftDefaults } from "../hooks/useDownshiftDefaults";
|
|
6
|
+
import { itemToString } from "../utils/downshiftDefaults";
|
|
7
|
+
import { reduceReducers } from "../utils/reduceReducers";
|
|
8
|
+
import { MenuRoot, type TypeMenuRootProps } from "../MenuRoot";
|
|
11
9
|
import type { TypeSingleSelectMenuProps } from "./SingleSelectMenuTypes";
|
|
12
10
|
|
|
13
11
|
/**
|
|
@@ -19,31 +17,36 @@ import type { TypeSingleSelectMenuProps } from "./SingleSelectMenuTypes";
|
|
|
19
17
|
* @see {@link https://github.com/sproutsocial/seeds/tree/dev/seeds-react/seeds-react-menu/src/ActionMenu | ActionMenu}
|
|
20
18
|
*/
|
|
21
19
|
|
|
22
|
-
export const SingleSelectMenu =
|
|
23
|
-
|
|
20
|
+
export const SingleSelectMenu = <
|
|
21
|
+
I extends TypeMenuItemProps = TypeMenuItemProps
|
|
22
|
+
>({
|
|
23
|
+
children: childrenProp,
|
|
24
24
|
stateReducer: externalStateReducer,
|
|
25
|
+
menuItems,
|
|
26
|
+
MenuItemComponent,
|
|
25
27
|
...useSelectProps
|
|
26
|
-
}: TypeSingleSelectMenuProps) => {
|
|
27
|
-
/** Get a list of
|
|
28
|
-
const { allMenuItems, menuItemsMap } =
|
|
28
|
+
}: TypeSingleSelectMenuProps<I>) => {
|
|
29
|
+
/** Get a list of menuItems from the children */
|
|
30
|
+
const { allMenuItems, menuItemsMap, children } = useMenuChildren({
|
|
31
|
+
children: childrenProp,
|
|
32
|
+
menuItems,
|
|
33
|
+
MenuItemComponent,
|
|
34
|
+
});
|
|
35
|
+
const { defaultDownshiftProps, ...restDefaults } = useDownshiftDefaults({
|
|
36
|
+
isCombobox: false,
|
|
37
|
+
});
|
|
29
38
|
|
|
30
39
|
/**
|
|
31
40
|
* Destructure Downshift props and call the core useSelect with items and state handler
|
|
32
41
|
*/
|
|
33
42
|
const { isOpen, ...useSelectReturnProps } = useSelect({
|
|
34
|
-
...
|
|
43
|
+
...defaultDownshiftProps.select,
|
|
35
44
|
items: allMenuItems,
|
|
36
45
|
itemToString,
|
|
37
|
-
stateReducer: (
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
? externalStateReducer(state, {
|
|
42
|
-
...actionAndChanges,
|
|
43
|
-
changes: newState,
|
|
44
|
-
})
|
|
45
|
-
: newState;
|
|
46
|
-
},
|
|
46
|
+
stateReducer: reduceReducers(
|
|
47
|
+
defaultDownshiftProps.select.stateReducer,
|
|
48
|
+
externalStateReducer
|
|
49
|
+
),
|
|
47
50
|
...useSelectProps,
|
|
48
51
|
});
|
|
49
52
|
|
|
@@ -57,6 +60,7 @@ export const SingleSelectMenu = ({
|
|
|
57
60
|
isOpen,
|
|
58
61
|
...useSelectProps,
|
|
59
62
|
...useSelectReturnProps,
|
|
63
|
+
...restDefaults,
|
|
60
64
|
}}
|
|
61
65
|
>
|
|
62
66
|
{children}
|
|
@@ -1,8 +1,12 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { TypeMenuRootOwnProps, TypeMenuRootProps } from "../MenuRoot";
|
|
2
2
|
import type { TypeDownshiftSelectProps } from "../MenuContext";
|
|
3
|
+
import type { TypeMenuItemProps } from "../MenuItem";
|
|
4
|
+
import type { TypeChildrenOrItems } from "../types";
|
|
3
5
|
|
|
4
|
-
export interface
|
|
6
|
+
export interface TypeSingleSelectMenuBaseProps
|
|
5
7
|
extends TypeMenuRootOwnProps,
|
|
6
|
-
TypeDownshiftSelectProps {
|
|
7
|
-
|
|
8
|
-
|
|
8
|
+
TypeDownshiftSelectProps {}
|
|
9
|
+
|
|
10
|
+
export type TypeSingleSelectMenuProps<
|
|
11
|
+
I extends TypeMenuItemProps = TypeMenuItemProps
|
|
12
|
+
> = TypeSingleSelectMenuBaseProps & TypeChildrenOrItems<I>;
|