@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.
- package/.turbo/turbo-build.log +10 -10
- package/CHANGELOG.md +15 -0
- package/dist/esm/index.js +1486 -440
- package/dist/esm/index.js.map +1 -1
- package/dist/index.d.mts +248 -92
- package/dist/index.d.ts +248 -92
- package/dist/index.js +1500 -429
- 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 +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 +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 +90 -8
- package/src/MenuContent/MenuContent.stories.tsx +164 -0
- package/src/MenuContent/MenuContent.tsx +26 -12
- package/src/MenuContent/MenuContentTypes.ts +6 -4
- package/src/MenuContent/__tests__/MenuContent.typetest.tsx +8 -8
- 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 +16 -16
- package/src/MenuGroup/MenuGroup.stories.tsx +248 -0
- package/src/MenuGroup/MenuGroup.tsx +8 -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 +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 -8
- 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 +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 +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 +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 +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/hooks/useDownshiftDefaults.tsx +107 -0
- package/src/hooks/useItemFiltering.tsx +66 -0
- package/src/hooks/useMenuChildren.tsx +197 -128
- 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,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,15 +1,12 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import { useSelect
|
|
2
|
+
import { useSelect } from "downshift";
|
|
3
|
+
import type { TypeMenuItemProps } from "../MenuItem";
|
|
3
4
|
import { useMenuChildren } from "../hooks/useMenuChildren";
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
} from "../
|
|
8
|
-
import { itemToString } from "../utils/itemToString";
|
|
9
|
-
import { MenuRoot } from "../MenuRoot";
|
|
10
|
-
import type { TypeDefaultItemProps } from "../types";
|
|
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
|
-
import { MenuItem } from "../MenuItem/index";
|
|
13
10
|
|
|
14
11
|
/**
|
|
15
12
|
* @link https://seeds.sproutsocial.com/components/singleselect-menu/
|
|
@@ -20,37 +17,36 @@ import { MenuItem } from "../MenuItem/index";
|
|
|
20
17
|
* @see {@link https://github.com/sproutsocial/seeds/tree/dev/seeds-react/seeds-react-menu/src/ActionMenu | ActionMenu}
|
|
21
18
|
*/
|
|
22
19
|
|
|
23
|
-
export const SingleSelectMenu =
|
|
20
|
+
export const SingleSelectMenu = <
|
|
21
|
+
I extends TypeMenuItemProps = TypeMenuItemProps
|
|
22
|
+
>({
|
|
24
23
|
children: childrenProp,
|
|
25
24
|
stateReducer: externalStateReducer,
|
|
26
|
-
|
|
27
|
-
MenuItemComponent
|
|
25
|
+
menuItems,
|
|
26
|
+
MenuItemComponent,
|
|
28
27
|
...useSelectProps
|
|
29
|
-
}: TypeSingleSelectMenuProps) => {
|
|
30
|
-
/** Get a list of
|
|
28
|
+
}: TypeSingleSelectMenuProps<I>) => {
|
|
29
|
+
/** Get a list of menuItems from the children */
|
|
31
30
|
const { allMenuItems, menuItemsMap, children } = useMenuChildren({
|
|
32
31
|
children: childrenProp,
|
|
33
|
-
|
|
32
|
+
menuItems,
|
|
34
33
|
MenuItemComponent,
|
|
35
34
|
});
|
|
35
|
+
const { defaultDownshiftProps, ...restDefaults } = useDownshiftDefaults({
|
|
36
|
+
isCombobox: false,
|
|
37
|
+
});
|
|
36
38
|
|
|
37
39
|
/**
|
|
38
40
|
* Destructure Downshift props and call the core useSelect with items and state handler
|
|
39
41
|
*/
|
|
40
42
|
const { isOpen, ...useSelectReturnProps } = useSelect({
|
|
41
|
-
...
|
|
43
|
+
...defaultDownshiftProps.select,
|
|
42
44
|
items: allMenuItems,
|
|
43
45
|
itemToString,
|
|
44
|
-
stateReducer: (
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
? externalStateReducer(state, {
|
|
49
|
-
...actionAndChanges,
|
|
50
|
-
changes: newState,
|
|
51
|
-
})
|
|
52
|
-
: newState;
|
|
53
|
-
},
|
|
46
|
+
stateReducer: reduceReducers(
|
|
47
|
+
defaultDownshiftProps.select.stateReducer,
|
|
48
|
+
externalStateReducer
|
|
49
|
+
),
|
|
54
50
|
...useSelectProps,
|
|
55
51
|
});
|
|
56
52
|
|
|
@@ -64,6 +60,7 @@ export const SingleSelectMenu = ({
|
|
|
64
60
|
isOpen,
|
|
65
61
|
...useSelectProps,
|
|
66
62
|
...useSelectReturnProps,
|
|
63
|
+
...restDefaults,
|
|
67
64
|
}}
|
|
68
65
|
>
|
|
69
66
|
{children}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import type { TypeMenuRootOwnProps } from "../MenuRoot";
|
|
1
|
+
import type { TypeMenuRootOwnProps, TypeMenuRootProps } from "../MenuRoot";
|
|
2
2
|
import type { TypeDownshiftSelectProps } from "../MenuContext";
|
|
3
|
+
import type { TypeMenuItemProps } from "../MenuItem";
|
|
3
4
|
import type { TypeChildrenOrItems } from "../types";
|
|
4
5
|
|
|
5
|
-
interface TypeSingleSelectMenuBaseProps
|
|
6
|
+
export interface TypeSingleSelectMenuBaseProps
|
|
6
7
|
extends TypeMenuRootOwnProps,
|
|
7
|
-
|
|
8
|
+
TypeDownshiftSelectProps {}
|
|
8
9
|
|
|
9
|
-
export type TypeSingleSelectMenuProps
|
|
10
|
-
|
|
10
|
+
export type TypeSingleSelectMenuProps<
|
|
11
|
+
I extends TypeMenuItemProps = TypeMenuItemProps
|
|
12
|
+
> = TypeSingleSelectMenuBaseProps & TypeChildrenOrItems<I>;
|
|
@@ -20,15 +20,15 @@ export const SingleSelectMenuTypeTest = () => (
|
|
|
20
20
|
<MenuContent>Test</MenuContent>
|
|
21
21
|
<MenuFooter>Test</MenuFooter>
|
|
22
22
|
</SingleSelectMenu>
|
|
23
|
-
{/* Valid SingleSelectMenu allows
|
|
23
|
+
{/* Valid SingleSelectMenu allows menuItems to be passed using the menuItems prop */}
|
|
24
24
|
<SingleSelectMenu
|
|
25
25
|
menuToggleElement={<>test</>}
|
|
26
|
-
|
|
26
|
+
menuItems={[{ children: "Item Name", id: "item-1" }]}
|
|
27
27
|
/>
|
|
28
|
-
{/* Valid SingleSelectMenu allows
|
|
28
|
+
{/* Valid SingleSelectMenu allows menuItems prop to be rendered using a MenuItemComponent prop */}
|
|
29
29
|
<SingleSelectMenu
|
|
30
30
|
menuToggleElement={<>test</>}
|
|
31
|
-
|
|
31
|
+
menuItems={[{ children: "Item Name", id: "item-1" }]}
|
|
32
32
|
MenuItemComponent={MenuItem}
|
|
33
33
|
/>
|
|
34
34
|
{/* Valid SingleSelectMenu accepts Downshift props and functions for useSelect */}
|
|
@@ -61,8 +61,8 @@ export const SingleSelectMenuTypeTest = () => (
|
|
|
61
61
|
>
|
|
62
62
|
Children
|
|
63
63
|
</SingleSelectMenu>
|
|
64
|
-
{/* @ts-expect-error - when both children and
|
|
65
|
-
<SingleSelectMenu menuToggleElement={<>test</>}
|
|
64
|
+
{/* @ts-expect-error - when both children and menuItems provided */}
|
|
65
|
+
<SingleSelectMenu menuToggleElement={<>test</>} menuItems={[]}>
|
|
66
66
|
Children
|
|
67
67
|
</SingleSelectMenu>
|
|
68
68
|
<SingleSelectMenu
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
Scenario: SubmenuItem is expandable via click
|
|
2
|
+
Given a SubmenuItem is not disabled
|
|
3
|
+
When the SubmenuItem is clicked
|
|
4
|
+
Then the submenu should be displayed
|
|
5
|
+
|
|
6
|
+
Scenario Outline: SubmenuItem can be expanded via keyboard
|
|
7
|
+
Given a SubmenuItem is focused
|
|
8
|
+
And the SubmenuItem is not disabled
|
|
9
|
+
When the SubmenuItem is triggered via <interaction>
|
|
10
|
+
Then the submenu should be displayed
|
|
11
|
+
|
|
12
|
+
Examples:
|
|
13
|
+
| interaction |
|
|
14
|
+
| pressing the Enter key |
|
|
15
|
+
| pressing the Space key |
|
|
16
|
+
| pressing the Right Arrow key |
|
|
17
|
+
|
|
18
|
+
Scenario Outline: SubmenuItem is disabled
|
|
19
|
+
Given a SubmenuItem is disabled
|
|
20
|
+
When the SubmenuItem is triggered via <interaction>
|
|
21
|
+
Then no events should fire
|
|
22
|
+
And the submenu should not be displayed
|
|
23
|
+
|
|
24
|
+
Examples:
|
|
25
|
+
| interaction |
|
|
26
|
+
| clicking |
|
|
27
|
+
| focusing and pressing the Enter key |
|
|
28
|
+
| focusing and pressing the Space key |
|
|
29
|
+
| focusing and pressing the Right Arrow key |
|
|
30
|
+
|
|
31
|
+
Scenario: SubmenuItem displays an arrow indicator
|
|
32
|
+
Given a SubmenuItem is rendered
|
|
33
|
+
Then it should display an arrow icon indicating a submenu
|
|
34
|
+
|
|
35
|
+
Scenario: SubmenuItem can be collapsed via keyboard
|
|
36
|
+
Given a SubmenuItem is expanded
|
|
37
|
+
And the submenu is displayed
|
|
38
|
+
When the Left Arrow key is pressed
|
|
39
|
+
Then the submenu should be hidden
|
|
40
|
+
|
|
41
|
+
Rule: API
|
|
42
|
+
Background:
|
|
43
|
+
Given an engineer is developing using the component
|
|
44
|
+
|
|
45
|
+
Scenario Outline: SubmenuItem accepts children
|
|
46
|
+
When <child component(s)> provided
|
|
47
|
+
Then the SubmenuItem should render the <component(s)>
|
|
48
|
+
|
|
49
|
+
Examples: child components
|
|
50
|
+
| child component(s) | component(s) |
|
|
51
|
+
| a Link is | Link |
|
|
52
|
+
| a Text is | Text |
|
|
53
|
+
| an Icon is | Icon |
|
|
54
|
+
| a Text then an Icon are | Text and Icon |
|
|
55
|
+
| an Icon and Text are | Icon and Text |
|
|
56
|
+
|
|
57
|
+
Scenario Outline: A SubmenuItem accepts system props
|
|
58
|
+
When a system prop from the category <systemPropCategory> is passed a value
|
|
59
|
+
Then the SubmenuItem will have <systemPropName> styling
|
|
60
|
+
|
|
61
|
+
Examples: System Props
|
|
62
|
+
| systemPropCategory | systemPropName |
|
|
63
|
+
| color | bg |
|
|
64
|
+
| border | borderColor |
|
|
65
|
+
| layout | display |
|
|
66
|
+
| position | zIndex |
|
|
67
|
+
| flexbox | flex |
|
|
68
|
+
| grid | gridGap |
|
|
69
|
+
| space | p |
|
|
70
|
+
|
|
71
|
+
Scenario Outline: SubmenuItem validates prop types
|
|
72
|
+
When any <propName> is passed to the SubmenuItem
|
|
73
|
+
Then the SubmenuItem should check that each <propName> is of the expected <propType>
|
|
74
|
+
|
|
75
|
+
Examples: Prop Types
|
|
76
|
+
| propName | propType |
|
|
77
|
+
| children | ReactNode |
|
|
78
|
+
| disabled | boolean |
|
|
79
|
+
| elevation | low, medium, or high |
|
|
80
|
+
| href | string |
|
|
81
|
+
| onClick | React.MouseEventHandler |
|
|
82
|
+
| role | string |
|