@sproutsocial/seeds-react-menu 0.5.0 → 1.0.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 +72 -0
- package/dist/esm/index.js +1529 -440
- package/dist/esm/index.js.map +1 -1
- package/dist/index.d.mts +275 -92
- package/dist/index.d.ts +275 -92
- package/dist/index.js +1558 -444
- package/dist/index.js.map +1 -1
- package/package.json +15 -12
- package/src/ActionMenu/ActionMenu.stories.tsx +422 -0
- package/src/ActionMenu/ActionMenu.tsx +24 -27
- package/src/ActionMenu/ActionMenuTypes.ts +8 -7
- package/src/ActionMenu/__tests__/ActionMenu.test.tsx +17 -8
- package/src/ActionMenu/__tests__/ActionMenu.typetest.tsx +8 -8
- package/src/Autocomplete/Autocomplete.stories.tsx +38 -3
- package/src/Autocomplete/Autocomplete.tsx +15 -80
- package/src/Autocomplete/AutocompleteInput.tsx +18 -27
- package/src/Autocomplete/AutocompleteTokenInput.tsx +50 -0
- package/src/Autocomplete/AutocompleteTypes.ts +25 -12
- package/src/Autocomplete/MultiAutocomplete.tsx +188 -0
- package/src/Autocomplete/SingleAutocomplete.tsx +74 -0
- package/src/Autocomplete/__tests__/Autocomplete.test.tsx +207 -3
- package/src/Autocomplete/__tests__/Autocomplete.typetest.tsx +90 -8
- package/src/MenuContent/MenuContent.stories.tsx +164 -0
- package/src/MenuContent/MenuContent.tsx +28 -12
- package/src/MenuContent/MenuContentTypes.ts +6 -4
- package/src/MenuContent/__tests__/MenuContent.typetest.tsx +8 -8
- package/src/MenuContext.tsx +192 -37
- package/src/MenuFooter/MenuFooter.stories.tsx +202 -0
- package/src/MenuFooter/__tests__/MenuFooter.test.tsx +16 -11
- package/src/MenuFooter/styles.tsx +1 -1
- package/src/MenuGroup/MenuGroup.feature +16 -16
- package/src/MenuGroup/MenuGroup.stories.tsx +248 -0
- package/src/MenuGroup/MenuGroup.tsx +12 -7
- package/src/MenuGroup/MenuGroupTypes.ts +6 -2
- package/src/MenuGroup/__tests__/MenuGroup.test.tsx +6 -6
- package/src/MenuGroup/__tests__/MenuGroup.typetest.tsx +7 -7
- package/src/MenuGroup/styles.tsx +4 -2
- package/src/MenuHeader/MenuHeader.stories.tsx +276 -0
- package/src/MenuHeader/MenuHeader.tsx +12 -7
- package/src/MenuHeader/__tests__/MenuHeader.test.tsx +23 -13
- package/src/MenuHeader/styles.tsx +22 -14
- package/src/MenuItem/MenuItem.stories.tsx +507 -0
- package/src/MenuItem/MenuItem.tsx +23 -9
- package/src/MenuItem/MenuItemTypes.ts +9 -4
- package/src/MenuItem/__tests__/MenuItem.test.tsx +6 -6
- package/src/MenuItem/styles.tsx +23 -5
- package/src/MenuItem/useOptionProps.tsx +11 -9
- package/src/MenuLabel.tsx +2 -2
- package/src/MenuRoot/MenuRoot.tsx +52 -24
- package/src/MenuRoot/__tests__/MenuRoot.test.tsx +59 -32
- package/src/MenuSearchInput/MenuSearchInput.tsx +85 -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 +108 -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 +222 -0
- package/src/MultiSelectMenu/MultiSelectMenu.tsx +38 -68
- package/src/MultiSelectMenu/MultiSelectMenuTypes.ts +8 -6
- package/src/MultiSelectMenu/__tests__/MultiSelectMenu.test.tsx +46 -17
- package/src/MultiSelectMenu/__tests__/MultiSelectMenu.typetest.tsx +6 -6
- 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 +75 -0
- package/src/NestedMenu/NestedMenuContext.tsx +56 -0
- package/src/NestedMenu/NestedMenuHeader.tsx +25 -0
- package/src/NestedMenu/NestedMenuItem.tsx +41 -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 +233 -0
- package/src/SingleSelectMenu/SingleSelectMenu.tsx +23 -26
- package/src/SingleSelectMenu/SingleSelectMenuTypes.ts +7 -5
- package/src/SingleSelectMenu/__tests__/SingleSelectMenu.typetest.tsx +6 -6
- package/src/SubmenuItem/SubmenuItem.feature +82 -0
- package/src/SubmenuItem/SubmenuItem.stories.tsx +163 -0
- package/src/SubmenuItem/SubmenuItem.tsx +258 -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/hooks/useDownshiftDefaults.tsx +117 -0
- package/src/hooks/useItemFiltering.tsx +68 -0
- package/src/hooks/useMenuChildren.tsx +205 -131
- package/src/index.ts +6 -0
- 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 -30
- package/src/utils/downshiftDefaults.ts +9 -0
- package/src/utils/getMultiSelectOverrides.ts +21 -0
- package/src/utils/reduceReducers.ts +19 -0
- package/src/utils/itemToString.ts +0 -5
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { Box } from "@sproutsocial/seeds-react-box";
|
|
3
|
+
|
|
4
|
+
import { TEST_BOOKS } from "../__fixtures__/menu-test-data";
|
|
5
|
+
import {
|
|
6
|
+
MenuItem,
|
|
7
|
+
SingleSelectMenu,
|
|
8
|
+
MenuToggleButton,
|
|
9
|
+
MenuContent,
|
|
10
|
+
} from "../index";
|
|
11
|
+
|
|
12
|
+
const meta: Meta<typeof MenuToggleButton> = {
|
|
13
|
+
title: "Under Development/Primitives/MenuToggleButton",
|
|
14
|
+
component: MenuToggleButton,
|
|
15
|
+
decorators: [
|
|
16
|
+
(Story) => (
|
|
17
|
+
<Box display="flex" flexDirection="column" gap="16px" maxWidth={300}>
|
|
18
|
+
<Story />
|
|
19
|
+
</Box>
|
|
20
|
+
),
|
|
21
|
+
],
|
|
22
|
+
argTypes: {
|
|
23
|
+
appearance: {
|
|
24
|
+
description: "All Button props are available",
|
|
25
|
+
options: [
|
|
26
|
+
"primary",
|
|
27
|
+
"secondary",
|
|
28
|
+
"pill",
|
|
29
|
+
"destructive",
|
|
30
|
+
"unstyled",
|
|
31
|
+
"placeholder",
|
|
32
|
+
],
|
|
33
|
+
control: { type: "select" },
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
args: {
|
|
37
|
+
appearance: "primary",
|
|
38
|
+
disabled: false,
|
|
39
|
+
size: "default",
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export default meta;
|
|
44
|
+
type Story = StoryObj<typeof MenuToggleButton>;
|
|
45
|
+
|
|
46
|
+
export const MenuTriggerOptions: Story = {
|
|
47
|
+
name: "Menu Trigger",
|
|
48
|
+
|
|
49
|
+
render: function MenuTriggerOptions(args) {
|
|
50
|
+
return (
|
|
51
|
+
<SingleSelectMenu
|
|
52
|
+
defaultIsOpen
|
|
53
|
+
initialIsOpen={false}
|
|
54
|
+
menuToggleElement={
|
|
55
|
+
<MenuToggleButton {...args} appearance={args.appearance}>
|
|
56
|
+
Select a book ↓
|
|
57
|
+
</MenuToggleButton>
|
|
58
|
+
}
|
|
59
|
+
>
|
|
60
|
+
<MenuContent>
|
|
61
|
+
{TEST_BOOKS.map((book) => {
|
|
62
|
+
return (
|
|
63
|
+
<MenuItem key={book.id} id={book.id}>
|
|
64
|
+
{book.title}
|
|
65
|
+
</MenuItem>
|
|
66
|
+
);
|
|
67
|
+
})}
|
|
68
|
+
</MenuContent>
|
|
69
|
+
</SingleSelectMenu>
|
|
70
|
+
);
|
|
71
|
+
},
|
|
72
|
+
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import React, { type ReactNode
|
|
1
|
+
import React, { type ReactNode } from "react";
|
|
2
2
|
import { Button, type TypeButtonProps } from "@sproutsocial/seeds-react-button";
|
|
3
|
-
import {
|
|
3
|
+
import { useMenuToggleContext } from "../MenuContext";
|
|
4
4
|
|
|
5
5
|
export interface TypeMenuToggleButtonProps extends Partial<TypeButtonProps> {
|
|
6
6
|
children: ReactNode;
|
|
@@ -10,15 +10,16 @@ export const MenuToggleButton = ({
|
|
|
10
10
|
children,
|
|
11
11
|
...rest
|
|
12
12
|
}: TypeMenuToggleButtonProps) => {
|
|
13
|
-
const { getToggleButtonProps, isListbox, getDropdownProps } =
|
|
14
|
-
|
|
13
|
+
const { getToggleButtonProps, isListbox, getDropdownProps, ref, ariaProps } =
|
|
14
|
+
useMenuToggleContext();
|
|
15
15
|
return (
|
|
16
16
|
<Button
|
|
17
17
|
appearance="secondary"
|
|
18
18
|
{...getToggleButtonProps?.({
|
|
19
19
|
// Decide if we want the preventKeyAction stuff. Maybe create a prop to allow for this to be overriden.
|
|
20
|
-
...getDropdownProps?.({ preventKeyAction: true }),
|
|
20
|
+
...(getDropdownProps?.({ ref, preventKeyAction: true }) || { ref }),
|
|
21
21
|
refKey: "innerRef",
|
|
22
|
+
...ariaProps,
|
|
22
23
|
...(isListbox ? {} : { role: "button", "aria-haspopup": "menu" }),
|
|
23
24
|
})}
|
|
24
25
|
{...rest}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./MenuToggleButton";
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import styled from "styled-components";
|
|
3
|
+
import {
|
|
4
|
+
TokenInput,
|
|
5
|
+
type TypeTokenInputProps,
|
|
6
|
+
type TypeTokenSpec,
|
|
7
|
+
} from "@sproutsocial/seeds-react-token-input";
|
|
8
|
+
import type { TypeInternalItemProps } from "./types";
|
|
9
|
+
import { MenuContentContext, MenuToggleContext } from "./MenuContext";
|
|
10
|
+
|
|
11
|
+
const StyledTokenInputWrapper = styled.div`
|
|
12
|
+
> div {
|
|
13
|
+
padding: 4px;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
input {
|
|
17
|
+
line-height: 21px;
|
|
18
|
+
margin: 0;
|
|
19
|
+
padding: 2px 4px;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.TokenInput-token button {
|
|
23
|
+
margin-top: 0;
|
|
24
|
+
padding: 1px 4px;
|
|
25
|
+
}
|
|
26
|
+
`;
|
|
27
|
+
|
|
28
|
+
export interface TypeMenuTokenInputProps extends TypeTokenInputProps {
|
|
29
|
+
/* A function that can be passed to override the default token props */
|
|
30
|
+
getTokenProps?: (
|
|
31
|
+
itemId: TypeInternalItemProps["id"]
|
|
32
|
+
) => Partial<TypeTokenSpec>;
|
|
33
|
+
MenuContext?: typeof MenuToggleContext | typeof MenuContentContext;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const MenuTokenInput = ({
|
|
37
|
+
inputProps = {},
|
|
38
|
+
getTokenProps,
|
|
39
|
+
onKeyDown,
|
|
40
|
+
MenuContext = MenuToggleContext,
|
|
41
|
+
...tokenInputProps
|
|
42
|
+
}: TypeMenuTokenInputProps) => {
|
|
43
|
+
const {
|
|
44
|
+
itemToString,
|
|
45
|
+
getSelectedItemProps,
|
|
46
|
+
removeSelectedItem,
|
|
47
|
+
setActiveIndex,
|
|
48
|
+
selectedItems = [],
|
|
49
|
+
} = React.useContext(MenuContext);
|
|
50
|
+
|
|
51
|
+
const removeToken = (tokenId: string) => {
|
|
52
|
+
const tokenToRemove = selectedItems?.find((item) => item.id === tokenId);
|
|
53
|
+
tokenToRemove && removeSelectedItem?.(tokenToRemove);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<StyledTokenInputWrapper>
|
|
58
|
+
<TokenInput
|
|
59
|
+
onKeyDown={(e, value) => {
|
|
60
|
+
// set active index to last item when arrow left is pressed and input is empty
|
|
61
|
+
if (e.key === "ArrowLeft" && !value && selectedItems.length) {
|
|
62
|
+
setActiveIndex?.(selectedItems.length - 1);
|
|
63
|
+
}
|
|
64
|
+
onKeyDown?.(e, value);
|
|
65
|
+
}}
|
|
66
|
+
inputProps={{
|
|
67
|
+
autoComplete: "off",
|
|
68
|
+
type: "search",
|
|
69
|
+
...inputProps,
|
|
70
|
+
}}
|
|
71
|
+
tokens={selectedItems.map((item, index) => {
|
|
72
|
+
const {
|
|
73
|
+
tokenProps: { ref, onKeyDown, ...restTokenProps } = {
|
|
74
|
+
ref: undefined,
|
|
75
|
+
onKeyDown: undefined,
|
|
76
|
+
},
|
|
77
|
+
...rest
|
|
78
|
+
} = getTokenProps?.(item.id) || {};
|
|
79
|
+
return {
|
|
80
|
+
id: item.id,
|
|
81
|
+
value: itemToString?.(item) || item.id,
|
|
82
|
+
tokenProps: {
|
|
83
|
+
...getSelectedItemProps?.({
|
|
84
|
+
selectedItem: item,
|
|
85
|
+
index,
|
|
86
|
+
ref,
|
|
87
|
+
refKey: "innerRef",
|
|
88
|
+
onKeyDown,
|
|
89
|
+
}),
|
|
90
|
+
// nullify the onclick provided from downshift
|
|
91
|
+
// TokenInput deletes the token on click
|
|
92
|
+
onClick: undefined,
|
|
93
|
+
tabIndex: 0,
|
|
94
|
+
...restTokenProps,
|
|
95
|
+
},
|
|
96
|
+
...rest,
|
|
97
|
+
};
|
|
98
|
+
})}
|
|
99
|
+
onRemoveToken={removeToken}
|
|
100
|
+
{...tokenInputProps}
|
|
101
|
+
/>
|
|
102
|
+
</StyledTokenInputWrapper>
|
|
103
|
+
);
|
|
104
|
+
};
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/* eslint-disable react-hooks/rules-of-hooks */
|
|
2
|
+
import React from "react";
|
|
3
|
+
|
|
4
|
+
import { Box } from "@sproutsocial/seeds-react-box";
|
|
5
|
+
import { action } from "@storybook/addon-actions";
|
|
6
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
7
|
+
import { StorybookMenuToggleButton } from "../storybookUtilities/menu-storybook-components";
|
|
8
|
+
import { TEST_BOOKS, EXTRA_BOOKS } from "../__fixtures__/menu-test-data";
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
MultiSelectMenu,
|
|
12
|
+
MenuContent,
|
|
13
|
+
MenuGroup,
|
|
14
|
+
MenuHeader,
|
|
15
|
+
MenuItem,
|
|
16
|
+
MenuSearchTokenInput,
|
|
17
|
+
type TypeInternalItemProps,
|
|
18
|
+
type TypeMultiSelectMenuProps,
|
|
19
|
+
type TypeMultiSelectMenuBaseProps,
|
|
20
|
+
} from "../index";
|
|
21
|
+
|
|
22
|
+
type TypeMultiSelectMenuStoryProps = TypeMultiSelectMenuProps & {
|
|
23
|
+
inputType: "checkbox" | "switch";
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const meta: Meta<TypeMultiSelectMenuStoryProps> = {
|
|
27
|
+
title: "Under Development/MultiSelectMenu",
|
|
28
|
+
component: MultiSelectMenu,
|
|
29
|
+
decorators: [
|
|
30
|
+
(Story) => (
|
|
31
|
+
<Box display="flex" flexDirection="column" gap="16px" maxWidth={300}>
|
|
32
|
+
<Story />
|
|
33
|
+
</Box>
|
|
34
|
+
),
|
|
35
|
+
],
|
|
36
|
+
args: {
|
|
37
|
+
menuToggleElement: <StorybookMenuToggleButton />,
|
|
38
|
+
},
|
|
39
|
+
argTypes: {
|
|
40
|
+
inputType: {
|
|
41
|
+
options: ["checkbox", "switch", "radio"],
|
|
42
|
+
control: { type: "select" },
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export default meta;
|
|
48
|
+
|
|
49
|
+
type Story = StoryObj<TypeMultiSelectMenuStoryProps>;
|
|
50
|
+
|
|
51
|
+
const MultiSelectMenuStory = ({ inputType, ...args }) => {
|
|
52
|
+
return (
|
|
53
|
+
<MultiSelectMenu menuToggleElement={args.menuToggleElement} {...args}>
|
|
54
|
+
<MenuContent>
|
|
55
|
+
<MenuGroup id="books">
|
|
56
|
+
{TEST_BOOKS.map((itemsToRender) => (
|
|
57
|
+
<MenuItem
|
|
58
|
+
inputType={inputType}
|
|
59
|
+
key={itemsToRender.id}
|
|
60
|
+
id={itemsToRender.id}
|
|
61
|
+
>
|
|
62
|
+
{itemsToRender.title}
|
|
63
|
+
</MenuItem>
|
|
64
|
+
))}
|
|
65
|
+
</MenuGroup>
|
|
66
|
+
</MenuContent>
|
|
67
|
+
</MultiSelectMenu>
|
|
68
|
+
);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export const MultiSelectMenuExample: Story = {
|
|
72
|
+
name: "Multi select menu",
|
|
73
|
+
args: {
|
|
74
|
+
inputType: "checkbox",
|
|
75
|
+
},
|
|
76
|
+
render: (args) => <MultiSelectMenuStory {...args} />,
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export const MultiSelectMenuWithItemsAsProps: Story = {
|
|
80
|
+
name: "With Items as Props",
|
|
81
|
+
args: {
|
|
82
|
+
inputType: "checkbox",
|
|
83
|
+
},
|
|
84
|
+
render: ({ inputType, ...args }) => {
|
|
85
|
+
const menuItems = TEST_BOOKS.map((book) => ({
|
|
86
|
+
id: book.id,
|
|
87
|
+
children: book.title,
|
|
88
|
+
inputType,
|
|
89
|
+
}));
|
|
90
|
+
return (
|
|
91
|
+
<MultiSelectMenu
|
|
92
|
+
{...args}
|
|
93
|
+
menuToggleElement={args.menuToggleElement}
|
|
94
|
+
menuItems={menuItems}
|
|
95
|
+
/>
|
|
96
|
+
);
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export const MultiSelectMenuWithGroupsExample: Story = {
|
|
101
|
+
name: "Multi select menu with groups",
|
|
102
|
+
args: {
|
|
103
|
+
inputType: "checkbox",
|
|
104
|
+
},
|
|
105
|
+
render: ({ inputType, ...args }) => {
|
|
106
|
+
return (
|
|
107
|
+
<MultiSelectMenu
|
|
108
|
+
menuToggleElement={<StorybookMenuToggleButton />}
|
|
109
|
+
{...args}
|
|
110
|
+
>
|
|
111
|
+
<MenuContent>
|
|
112
|
+
<MenuGroup title="Classics" id="classics">
|
|
113
|
+
{TEST_BOOKS.slice(0, 5).map((itemsToRender) => (
|
|
114
|
+
<MenuItem
|
|
115
|
+
inputType={inputType}
|
|
116
|
+
key={itemsToRender.id}
|
|
117
|
+
id={itemsToRender.id}
|
|
118
|
+
>
|
|
119
|
+
{itemsToRender.title}
|
|
120
|
+
</MenuItem>
|
|
121
|
+
))}
|
|
122
|
+
</MenuGroup>
|
|
123
|
+
<MenuGroup title="Essentials" id="essentials" isSingleSelect>
|
|
124
|
+
{EXTRA_BOOKS.slice(0, 5).map((itemsToRender) => (
|
|
125
|
+
<MenuItem
|
|
126
|
+
inputType="radio"
|
|
127
|
+
key={itemsToRender.id}
|
|
128
|
+
id={itemsToRender.id}
|
|
129
|
+
>
|
|
130
|
+
{itemsToRender.title}
|
|
131
|
+
</MenuItem>
|
|
132
|
+
))}
|
|
133
|
+
</MenuGroup>
|
|
134
|
+
</MenuContent>
|
|
135
|
+
</MultiSelectMenu>
|
|
136
|
+
);
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const TestControlledMultiSelectMenu = ({
|
|
141
|
+
inputType,
|
|
142
|
+
...args
|
|
143
|
+
}: TypeMultiSelectMenuBaseProps & {
|
|
144
|
+
inputType: "checkbox" | "switch";
|
|
145
|
+
}) => {
|
|
146
|
+
const [isOpen, setIsOpen] = React.useState<boolean>(false);
|
|
147
|
+
const [selectedItems, setSelectedItems] = React.useState<
|
|
148
|
+
TypeInternalItemProps[]
|
|
149
|
+
>([]);
|
|
150
|
+
return (
|
|
151
|
+
<MultiSelectMenu
|
|
152
|
+
{...args}
|
|
153
|
+
isOpen={isOpen}
|
|
154
|
+
onIsOpenChange={({ isOpen }) => setIsOpen(isOpen)}
|
|
155
|
+
selectedItems={selectedItems}
|
|
156
|
+
onSelectedItemsChange={({ selectedItems: newSelectedItems }) => {
|
|
157
|
+
setSelectedItems(newSelectedItems);
|
|
158
|
+
}}
|
|
159
|
+
menuToggleElement={<StorybookMenuToggleButton />}
|
|
160
|
+
>
|
|
161
|
+
<MenuContent>
|
|
162
|
+
<MenuGroup title="Classics" id="classics">
|
|
163
|
+
{TEST_BOOKS.slice(0, 5).map((itemsToRender) => (
|
|
164
|
+
<MenuItem
|
|
165
|
+
inputType={inputType}
|
|
166
|
+
key={itemsToRender.id}
|
|
167
|
+
id={itemsToRender.id}
|
|
168
|
+
>
|
|
169
|
+
{itemsToRender.title}
|
|
170
|
+
</MenuItem>
|
|
171
|
+
))}
|
|
172
|
+
</MenuGroup>
|
|
173
|
+
<MenuGroup title="Essentials" id="essentials">
|
|
174
|
+
{EXTRA_BOOKS.slice(0, 5).map((itemsToRender) => (
|
|
175
|
+
<MenuItem
|
|
176
|
+
inputType={inputType}
|
|
177
|
+
key={itemsToRender.id}
|
|
178
|
+
id={itemsToRender.id}
|
|
179
|
+
>
|
|
180
|
+
{itemsToRender.title}
|
|
181
|
+
</MenuItem>
|
|
182
|
+
))}
|
|
183
|
+
</MenuGroup>
|
|
184
|
+
</MenuContent>
|
|
185
|
+
</MultiSelectMenu>
|
|
186
|
+
);
|
|
187
|
+
};
|
|
188
|
+
export const MultiSelectMenuControlled: Story = {
|
|
189
|
+
name: "Controlled State",
|
|
190
|
+
args: {
|
|
191
|
+
inputType: "checkbox",
|
|
192
|
+
},
|
|
193
|
+
render: (args) => <TestControlledMultiSelectMenu {...args} />,
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
export const MultiSelectMenuWithSearch: Story = {
|
|
197
|
+
name: "With Search Token Input",
|
|
198
|
+
render: ({ inputType, ...args }) => {
|
|
199
|
+
return (
|
|
200
|
+
<MultiSelectMenu
|
|
201
|
+
itemToString={(item) => (item ? TEST_BOOKS[item.index].title : "")}
|
|
202
|
+
menuToggleElement={<StorybookMenuToggleButton />}
|
|
203
|
+
{...(args as object)}
|
|
204
|
+
>
|
|
205
|
+
<MenuHeader title="Select Book">
|
|
206
|
+
<MenuSearchTokenInput
|
|
207
|
+
id="single-select-search"
|
|
208
|
+
name="search"
|
|
209
|
+
aria-label="Search Books"
|
|
210
|
+
/>
|
|
211
|
+
</MenuHeader>
|
|
212
|
+
<MenuContent>
|
|
213
|
+
{TEST_BOOKS.map(({ id, title }) => (
|
|
214
|
+
<MenuItem key={id} id={id} inputType={inputType}>
|
|
215
|
+
{title}
|
|
216
|
+
</MenuItem>
|
|
217
|
+
))}
|
|
218
|
+
</MenuContent>
|
|
219
|
+
</MultiSelectMenu>
|
|
220
|
+
);
|
|
221
|
+
},
|
|
222
|
+
};
|
|
@@ -1,39 +1,13 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import {
|
|
3
|
-
useSelect,
|
|
4
|
-
useMultipleSelection,
|
|
5
|
-
type UseSelectState,
|
|
6
|
-
} from "downshift";
|
|
2
|
+
import { useSelect, useMultipleSelection } from "downshift";
|
|
7
3
|
import { useMenuChildren } from "../hooks/useMenuChildren";
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
} from "../
|
|
12
|
-
import {
|
|
13
|
-
import { MenuRoot } from "../MenuRoot";
|
|
4
|
+
import { useDownshiftDefaults } from "../hooks/useDownshiftDefaults";
|
|
5
|
+
import { itemToString } from "../utils/downshiftDefaults";
|
|
6
|
+
import { reduceReducers } from "../utils/reduceReducers";
|
|
7
|
+
import { MenuRoot, type TypeMenuRootProps } from "../MenuRoot";
|
|
8
|
+
import { type TypeMenuItemProps } from "../MenuItem";
|
|
14
9
|
import type { TypeMultiSelectMenuProps } from "./MultiSelectMenuTypes";
|
|
15
|
-
import type {
|
|
16
|
-
import { MenuItem } from "../MenuItem/index";
|
|
17
|
-
|
|
18
|
-
const selectReducerForMultiSelect: TypeMultiSelectMenuProps["stateReducer"] = (
|
|
19
|
-
state,
|
|
20
|
-
actionAndChanges
|
|
21
|
-
) => {
|
|
22
|
-
const { changes, type } = actionAndChanges;
|
|
23
|
-
switch (type) {
|
|
24
|
-
case useSelect.stateChangeTypes.ToggleButtonKeyDownEnter:
|
|
25
|
-
case useSelect.stateChangeTypes.ToggleButtonKeyDownSpaceButton:
|
|
26
|
-
case useSelect.stateChangeTypes.ItemClick:
|
|
27
|
-
return {
|
|
28
|
-
...changes,
|
|
29
|
-
isOpen: true, // keep the menu open after selection.
|
|
30
|
-
// TODO: Confirm that keeping highlightedIndex the same is the desired UX
|
|
31
|
-
// highlightedIndex: 0, // with the first option highlighted.
|
|
32
|
-
highlightedIndex: state.highlightedIndex, // keep highlightedIndex the same instead of returning to 0
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
return changes;
|
|
36
|
-
};
|
|
10
|
+
import type { TypeInternalItemProps } from "../types";
|
|
37
11
|
|
|
38
12
|
/**
|
|
39
13
|
* @link https://seeds.sproutsocial.com/components/multiselect-menu/
|
|
@@ -43,35 +17,36 @@ const selectReducerForMultiSelect: TypeMultiSelectMenuProps["stateReducer"] = (
|
|
|
43
17
|
* @see {@link https://seeds.sproutsocial.com/components/menu-group/ | MenuGroup}
|
|
44
18
|
*/
|
|
45
19
|
|
|
46
|
-
export const MultiSelectMenu =
|
|
20
|
+
export const MultiSelectMenu = <
|
|
21
|
+
I extends TypeMenuItemProps = TypeMenuItemProps
|
|
22
|
+
>({
|
|
47
23
|
children: childrenProp,
|
|
48
|
-
|
|
49
|
-
MenuItemComponent
|
|
24
|
+
menuItems,
|
|
25
|
+
MenuItemComponent,
|
|
26
|
+
onSelectedItemChange: externalOnSelectedItemChange,
|
|
50
27
|
stateReducer: externalStateReducer,
|
|
51
28
|
getA11yMultiStatusMessage,
|
|
52
29
|
onMultiSelectStateChange,
|
|
53
30
|
multiSelectStateReducer,
|
|
54
31
|
...useSelectProps
|
|
55
|
-
}: TypeMultiSelectMenuProps) => {
|
|
32
|
+
}: TypeMultiSelectMenuProps<I>) => {
|
|
56
33
|
const { allMenuItems, menuItemsMap, children } = useMenuChildren({
|
|
57
34
|
children: childrenProp,
|
|
58
|
-
|
|
35
|
+
menuItems,
|
|
59
36
|
MenuItemComponent,
|
|
60
37
|
});
|
|
38
|
+
const { defaultDownshiftProps, ...restDefaults } = useDownshiftDefaults({
|
|
39
|
+
isCombobox: false,
|
|
40
|
+
isMulti: true,
|
|
41
|
+
// handles the prop name mapping for multi select
|
|
42
|
+
multiSelectProps: {
|
|
43
|
+
getA11yMultiStatusMessage,
|
|
44
|
+
onMultiSelectStateChange,
|
|
45
|
+
multiSelectStateReducer,
|
|
46
|
+
...useSelectProps,
|
|
47
|
+
},
|
|
48
|
+
});
|
|
61
49
|
|
|
62
|
-
const multiSelectOverrides = Object.entries({
|
|
63
|
-
...useSelectProps,
|
|
64
|
-
getA11yStatusMessage: getA11yMultiStatusMessage,
|
|
65
|
-
onStateChange: onMultiSelectStateChange,
|
|
66
|
-
stateReducer: multiSelectStateReducer,
|
|
67
|
-
}).reduce(
|
|
68
|
-
(a, [k, v]) => (v ? ((a[k] = v), a) : a),
|
|
69
|
-
{} as {
|
|
70
|
-
getA11yStatusMessage?: TypeMultiSelectMenuProps["getA11yMultiStatusMessage"];
|
|
71
|
-
onStateChange?: TypeMultiSelectMenuProps["onMultiSelectStateChange"];
|
|
72
|
-
stateReducer?: TypeMultiSelectMenuProps["multiSelectStateReducer"];
|
|
73
|
-
}
|
|
74
|
-
);
|
|
75
50
|
const {
|
|
76
51
|
getSelectedItemProps,
|
|
77
52
|
addSelectedItem,
|
|
@@ -79,29 +54,20 @@ export const MultiSelectMenu = ({
|
|
|
79
54
|
selectedItems,
|
|
80
55
|
getDropdownProps,
|
|
81
56
|
...useMultipleSelectionReturnProps
|
|
82
|
-
} = useMultipleSelection<
|
|
57
|
+
} = useMultipleSelection<TypeInternalItemProps>(defaultDownshiftProps.multi);
|
|
83
58
|
|
|
84
59
|
const { isOpen, ...useSelectReturnProps } = useSelect({
|
|
85
|
-
...
|
|
60
|
+
...defaultDownshiftProps.select,
|
|
86
61
|
items: allMenuItems,
|
|
87
62
|
itemToString,
|
|
88
63
|
selectedItem: null,
|
|
89
64
|
defaultHighlightedIndex: 0,
|
|
90
|
-
stateReducer: (
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
});
|
|
97
|
-
return externalStateReducer
|
|
98
|
-
? externalStateReducer(state, {
|
|
99
|
-
...actionAndChanges,
|
|
100
|
-
changes: newState,
|
|
101
|
-
})
|
|
102
|
-
: newState;
|
|
103
|
-
},
|
|
104
|
-
onStateChange: ({ type, selectedItem: newSelectedItem }) => {
|
|
65
|
+
stateReducer: reduceReducers(
|
|
66
|
+
defaultDownshiftProps.select.stateReducer,
|
|
67
|
+
externalStateReducer
|
|
68
|
+
),
|
|
69
|
+
onSelectedItemChange: (changes) => {
|
|
70
|
+
const { type, selectedItem: newSelectedItem } = changes;
|
|
105
71
|
switch (type) {
|
|
106
72
|
case useSelect.stateChangeTypes.ToggleButtonKeyDownEnter:
|
|
107
73
|
case useSelect.stateChangeTypes.ToggleButtonKeyDownSpaceButton:
|
|
@@ -124,6 +90,9 @@ export const MultiSelectMenu = ({
|
|
|
124
90
|
default:
|
|
125
91
|
break;
|
|
126
92
|
}
|
|
93
|
+
|
|
94
|
+
// Call the consumer's onSelectedItemChange callback
|
|
95
|
+
externalOnSelectedItemChange?.(changes);
|
|
127
96
|
},
|
|
128
97
|
...useSelectProps,
|
|
129
98
|
});
|
|
@@ -147,6 +116,7 @@ export const MultiSelectMenu = ({
|
|
|
147
116
|
...useSelectProps,
|
|
148
117
|
...useSelectReturnProps,
|
|
149
118
|
...useMultipleSelectionReturnProps,
|
|
119
|
+
...restDefaults,
|
|
150
120
|
}}
|
|
151
121
|
>
|
|
152
122
|
{children}
|
|
@@ -1,14 +1,16 @@
|
|
|
1
|
-
import type { TypeMenuRootOwnProps } from "../MenuRoot";
|
|
1
|
+
import type { TypeMenuRootOwnProps, TypeMenuRootProps } from "../MenuRoot";
|
|
2
2
|
import type {
|
|
3
3
|
TypeDownshiftSelectProps,
|
|
4
4
|
TypeDownshiftMultiSelectProps,
|
|
5
5
|
} from "../MenuContext";
|
|
6
|
+
import type { TypeMenuItemProps } from "../MenuItem";
|
|
6
7
|
import type { TypeChildrenOrItems } from "../types";
|
|
7
8
|
|
|
8
|
-
interface TypeMultiSelectMenuBaseProps
|
|
9
|
+
export interface TypeMultiSelectMenuBaseProps
|
|
9
10
|
extends TypeMenuRootOwnProps,
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
TypeDownshiftSelectProps,
|
|
12
|
+
TypeDownshiftMultiSelectProps {}
|
|
12
13
|
|
|
13
|
-
export type TypeMultiSelectMenuProps
|
|
14
|
-
|
|
14
|
+
export type TypeMultiSelectMenuProps<
|
|
15
|
+
I extends TypeMenuItemProps = TypeMenuItemProps
|
|
16
|
+
> = TypeMultiSelectMenuBaseProps & TypeChildrenOrItems<I>;
|