@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
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
ActionMenu,
|
|
3
|
+
MenuContent,
|
|
4
|
+
MenuHeader,
|
|
5
|
+
MenuFooter,
|
|
6
|
+
MenuItem,
|
|
7
|
+
} from "../../index";
|
|
2
8
|
|
|
3
9
|
export const ActionMenuTypeTest = () => (
|
|
4
10
|
<>
|
|
@@ -14,6 +20,17 @@ export const ActionMenuTypeTest = () => (
|
|
|
14
20
|
<MenuContent>Test</MenuContent>
|
|
15
21
|
<MenuFooter>Test</MenuFooter>
|
|
16
22
|
</ActionMenu>
|
|
23
|
+
{/* Valid ActionMenu allows menuItems to be passed using the menuItems prop */}
|
|
24
|
+
<ActionMenu
|
|
25
|
+
menuToggleElement={<>test</>}
|
|
26
|
+
menuItems={[{ children: "Item Name", id: "item-1" }]}
|
|
27
|
+
/>
|
|
28
|
+
{/* Valid ActionMenu allows menuItems prop to be rendered using a MenuItemComponent prop */}
|
|
29
|
+
<ActionMenu
|
|
30
|
+
menuToggleElement={<>test</>}
|
|
31
|
+
menuItems={[{ children: "Item Name", id: "item-1" }]}
|
|
32
|
+
MenuItemComponent={MenuItem}
|
|
33
|
+
/>
|
|
17
34
|
{/* Valid ActionMenu accepts Downshift props and functions for useSelect other than selectedItem */}
|
|
18
35
|
<ActionMenu
|
|
19
36
|
menuToggleElement={<>test</>}
|
|
@@ -34,8 +51,20 @@ export const ActionMenuTypeTest = () => (
|
|
|
34
51
|
>
|
|
35
52
|
Test
|
|
36
53
|
</ActionMenu>
|
|
37
|
-
{/* @ts-expect-error - when
|
|
38
|
-
<ActionMenu menuToggleElement=
|
|
54
|
+
{/* @ts-expect-error - when both children and MenuItemComponent provided */}
|
|
55
|
+
<ActionMenu menuToggleElement={<>test</>} MenuItemComponent={MenuItem}>
|
|
56
|
+
Children
|
|
57
|
+
</ActionMenu>
|
|
58
|
+
{/* @ts-expect-error - when both children and menuItems provided */}
|
|
59
|
+
<ActionMenu menuToggleElement={<>test</>} menuItems={[]}>
|
|
60
|
+
Children
|
|
61
|
+
</ActionMenu>
|
|
62
|
+
{/* @ts-expect-error - when no children or items provided */}
|
|
63
|
+
<ActionMenu menuToggleElement={<>test</>} />
|
|
64
|
+
{/* @ts-expect-error - when MenuItemComponent is of the wrong type */}
|
|
65
|
+
<ActionMenu menuToggleElement={<>test</>} MenuItemComponent={MenuContent}>
|
|
66
|
+
Children
|
|
67
|
+
</ActionMenu>
|
|
39
68
|
|
|
40
69
|
<ActionMenu
|
|
41
70
|
menuToggleElement={<>test</>}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/* eslint-disable react-hooks/rules-of-hooks */
|
|
2
2
|
import React from "react";
|
|
3
3
|
import type { Meta, StoryObj } from "@storybook/react";
|
|
4
|
+
import { action } from "@storybook/addon-actions";
|
|
4
5
|
|
|
5
6
|
import { Box } from "@sproutsocial/seeds-react-box";
|
|
6
7
|
import { Icon } from "@sproutsocial/seeds-react-icon";
|
|
@@ -10,6 +11,7 @@ import { TEST_BOOKS } from "../__fixtures__/menu-test-data";
|
|
|
10
11
|
|
|
11
12
|
import { Autocomplete } from "./Autocomplete";
|
|
12
13
|
import { AutocompleteInput } from "./AutocompleteInput";
|
|
14
|
+
import { AutocompleteTokenInput } from "./AutocompleteTokenInput";
|
|
13
15
|
|
|
14
16
|
type CustomAutocompleteArgs = React.ComponentProps<typeof Autocomplete>;
|
|
15
17
|
|
|
@@ -23,7 +25,7 @@ const meta: Meta<CustomAutocompleteArgs> = {
|
|
|
23
25
|
},
|
|
24
26
|
decorators: [
|
|
25
27
|
(Story) => (
|
|
26
|
-
<Box display="flex" flexDirection="column" gap="16px" maxWidth={
|
|
28
|
+
<Box display="flex" flexDirection="column" gap="16px" maxWidth={350}>
|
|
27
29
|
<Story />
|
|
28
30
|
</Box>
|
|
29
31
|
),
|
|
@@ -62,6 +64,30 @@ export const AutocompleteTextInputExample: Story = {
|
|
|
62
64
|
},
|
|
63
65
|
};
|
|
64
66
|
|
|
67
|
+
export const AutocompleteWithItemsAsProps: Story = {
|
|
68
|
+
name: "With Items as Props",
|
|
69
|
+
render: (args) => {
|
|
70
|
+
return (
|
|
71
|
+
<Autocomplete
|
|
72
|
+
itemToString={(item) => (item ? TEST_BOOKS[item.index].title : "")}
|
|
73
|
+
menuToggleElement={
|
|
74
|
+
<>
|
|
75
|
+
<MenuLabel htmlFor="my-combobox" mb={300}>
|
|
76
|
+
Choose a book
|
|
77
|
+
</MenuLabel>
|
|
78
|
+
<AutocompleteInput />
|
|
79
|
+
</>
|
|
80
|
+
}
|
|
81
|
+
menuItems={TEST_BOOKS.map(({ id, title }) => ({
|
|
82
|
+
children: title,
|
|
83
|
+
id,
|
|
84
|
+
}))}
|
|
85
|
+
{...(args as object)}
|
|
86
|
+
/>
|
|
87
|
+
);
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
|
|
65
91
|
export const AutocompleteTextWithButtonExample: Story = {
|
|
66
92
|
name: "Text Input w/ Button",
|
|
67
93
|
render: (args) => {
|
|
@@ -95,12 +121,45 @@ export const AutocompleteTextWithButtonExample: Story = {
|
|
|
95
121
|
},
|
|
96
122
|
};
|
|
97
123
|
|
|
124
|
+
export const AutocompleteTokenInputExample: Story = {
|
|
125
|
+
name: "Token Input w/ Button",
|
|
126
|
+
render: (args) => {
|
|
127
|
+
return (
|
|
128
|
+
<Autocomplete
|
|
129
|
+
itemToString={(item) => (item ? TEST_BOOKS[item.index].title : "")}
|
|
130
|
+
menuToggleElement={
|
|
131
|
+
<>
|
|
132
|
+
<MenuLabel htmlFor="my-combobox" mb={300}>
|
|
133
|
+
Choose a book
|
|
134
|
+
</MenuLabel>
|
|
135
|
+
<AutocompleteTokenInput />
|
|
136
|
+
<MenuToggleButton mt={200} width="100%" p="0">
|
|
137
|
+
<Icon name="chevron-down" aria-label="Open Menu" />
|
|
138
|
+
</MenuToggleButton>
|
|
139
|
+
</>
|
|
140
|
+
}
|
|
141
|
+
onStateChange={(changes) => action("onStateChange")(changes)}
|
|
142
|
+
multiSelect
|
|
143
|
+
{...(args as object)}
|
|
144
|
+
>
|
|
145
|
+
<MenuContent>
|
|
146
|
+
{TEST_BOOKS.map(({ id, title }) => (
|
|
147
|
+
<MenuItem key={id} id={id}>
|
|
148
|
+
{title}
|
|
149
|
+
</MenuItem>
|
|
150
|
+
))}
|
|
151
|
+
</MenuContent>
|
|
152
|
+
</Autocomplete>
|
|
153
|
+
);
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
|
|
98
157
|
// export const AutocompleteControlled: Story = {
|
|
99
158
|
// name: "Controlled State",
|
|
100
159
|
// render: ({ inputType, ...args }) => {
|
|
101
160
|
// const [isOpen, setIsOpen] = React.useState<boolean>(false);
|
|
102
161
|
// const [selectedItem, setSelectedItem] =
|
|
103
|
-
// React.useState<
|
|
162
|
+
// React.useState<TypeInternalItemProps | null>(null);
|
|
104
163
|
// return (
|
|
105
164
|
// <Autocomplete
|
|
106
165
|
// isOpen={isOpen}
|
|
@@ -1,83 +1,24 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
defaultUseComboboxProps,
|
|
6
|
-
useComboboxStateReducer,
|
|
7
|
-
} from "../reducers/useComboboxStateReducer";
|
|
8
|
-
import { itemToString as defaultItemToString } from "../utils/itemToString";
|
|
9
|
-
import { MenuRoot } from "../MenuRoot";
|
|
2
|
+
|
|
3
|
+
import type { TypeMenuItemProps } from "../MenuItem";
|
|
4
|
+
import type { TypeMenuRootProps } from "../MenuRoot";
|
|
10
5
|
import type { TypeAutocompleteProps } from "./AutocompleteTypes";
|
|
11
|
-
import
|
|
6
|
+
import { SingleAutocomplete } from "./SingleAutocomplete";
|
|
7
|
+
import { MultiAutocomplete } from "./MultiAutocomplete";
|
|
12
8
|
|
|
13
9
|
/**
|
|
14
|
-
* @link https://seeds.sproutsocial.com/components/
|
|
10
|
+
* @link https://seeds.sproutsocial.com/components/autocomplete/
|
|
15
11
|
*
|
|
16
|
-
* @description A Autocomplete allows
|
|
12
|
+
* @description A Autocomplete allows selecting a {@link https://github.com/sproutsocial/seeds/tree/dev/seeds-react/seeds-react-menu/src/MenuItem | MenuItems} to be selected from a list or groups of lists to populate an input.
|
|
17
13
|
*
|
|
18
|
-
* @see {@link https://seeds.sproutsocial.com/components/menu-group/ | MenuGroup}
|
|
19
14
|
*/
|
|
15
|
+
export const Autocomplete = <I extends TypeMenuItemProps = TypeMenuItemProps>({
|
|
16
|
+
multiSelect,
|
|
17
|
+
...rest
|
|
18
|
+
}: TypeAutocompleteProps<I>) => {
|
|
19
|
+
if (multiSelect) {
|
|
20
|
+
return <MultiAutocomplete {...rest} />;
|
|
21
|
+
}
|
|
20
22
|
|
|
21
|
-
|
|
22
|
-
children,
|
|
23
|
-
stateReducer: externalStateReducer,
|
|
24
|
-
itemToString = defaultItemToString,
|
|
25
|
-
getIsItemVisible = (item, inputValue) =>
|
|
26
|
-
itemToString(item).toLowerCase().startsWith(inputValue.toLowerCase()),
|
|
27
|
-
...useComboboxProps
|
|
28
|
-
}: TypeAutocompleteProps) => {
|
|
29
|
-
/** Get a list of items from the children */
|
|
30
|
-
const { allMenuItems } = useItemsFromChildren({ children });
|
|
31
|
-
const [items, setItems] = React.useState(allMenuItems);
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Destructure Downshift props and call the core useCombobox with items and state handler
|
|
35
|
-
*/
|
|
36
|
-
const { isOpen, ...useComboboxReturnProps } = useCombobox({
|
|
37
|
-
...defaultUseComboboxProps,
|
|
38
|
-
items,
|
|
39
|
-
itemToString,
|
|
40
|
-
onInputValueChange({ inputValue }) {
|
|
41
|
-
setItems(
|
|
42
|
-
inputValue
|
|
43
|
-
? allMenuItems.reduce(
|
|
44
|
-
(items, item) => [
|
|
45
|
-
...items,
|
|
46
|
-
{ ...item, hidden: !getIsItemVisible(item, inputValue) },
|
|
47
|
-
],
|
|
48
|
-
[] as TypeDefaultItemProps[]
|
|
49
|
-
)
|
|
50
|
-
: allMenuItems
|
|
51
|
-
);
|
|
52
|
-
},
|
|
53
|
-
stateReducer: (state, actionAndChanges) => {
|
|
54
|
-
let newState: Partial<UseComboboxState<TypeDefaultItemProps>>;
|
|
55
|
-
newState = useComboboxStateReducer(state, actionAndChanges);
|
|
56
|
-
return externalStateReducer
|
|
57
|
-
? externalStateReducer(state, {
|
|
58
|
-
...actionAndChanges,
|
|
59
|
-
changes: newState,
|
|
60
|
-
})
|
|
61
|
-
: newState;
|
|
62
|
-
},
|
|
63
|
-
...useComboboxProps,
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
return (
|
|
67
|
-
<MenuRoot
|
|
68
|
-
{...{
|
|
69
|
-
isListbox: true,
|
|
70
|
-
items,
|
|
71
|
-
itemToString,
|
|
72
|
-
isOpen,
|
|
73
|
-
...useComboboxProps,
|
|
74
|
-
...useComboboxReturnProps,
|
|
75
|
-
}}
|
|
76
|
-
popoutProps={{
|
|
77
|
-
focusOnContent: false,
|
|
78
|
-
}}
|
|
79
|
-
>
|
|
80
|
-
{children}
|
|
81
|
-
</MenuRoot>
|
|
82
|
-
);
|
|
23
|
+
return <SingleAutocomplete {...rest} />;
|
|
83
24
|
};
|
|
@@ -1,21 +1,10 @@
|
|
|
1
1
|
import { useContext } from "react";
|
|
2
|
-
import type {
|
|
3
|
-
UseComboboxGetInputPropsOptions,
|
|
4
|
-
GetInputPropsReturnValue,
|
|
5
|
-
} from "downshift";
|
|
6
2
|
import { Input, type TypeInputProps } from "@sproutsocial/seeds-react-input";
|
|
7
|
-
import {
|
|
3
|
+
import type { TypeDownshiftMergedInputProps } from "../types";
|
|
4
|
+
import { useMenuToggleContext } from "../MenuContext";
|
|
8
5
|
|
|
9
|
-
type TypeInputEventListeners =
|
|
10
|
-
| "onKeyDown"
|
|
11
|
-
| "onChange"
|
|
12
|
-
| "onInput"
|
|
13
|
-
| "onBlur"
|
|
14
|
-
| "onClick"
|
|
15
|
-
| "refKey";
|
|
16
6
|
export interface TypeAutocompleteInputProps
|
|
17
|
-
extends
|
|
18
|
-
Omit<Partial<TypeInputProps>, TypeInputEventListeners> {}
|
|
7
|
+
extends TypeDownshiftMergedInputProps<TypeInputProps> {}
|
|
19
8
|
|
|
20
9
|
export const AutocompleteInput = ({
|
|
21
10
|
onKeyDown,
|
|
@@ -26,23 +15,25 @@ export const AutocompleteInput = ({
|
|
|
26
15
|
inputProps,
|
|
27
16
|
...rest
|
|
28
17
|
}: TypeAutocompleteInputProps) => {
|
|
29
|
-
const {
|
|
18
|
+
const { getInputComponentProps, inputValue, ref, ariaProps } =
|
|
19
|
+
useMenuToggleContext();
|
|
20
|
+
|
|
30
21
|
return (
|
|
31
22
|
<Input
|
|
32
23
|
id="autocomplete_input"
|
|
33
24
|
name="autocomplete_input"
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
...
|
|
45
|
-
}}
|
|
25
|
+
{...getInputComponentProps({
|
|
26
|
+
onKeyDown,
|
|
27
|
+
onChange,
|
|
28
|
+
onInput,
|
|
29
|
+
onBlur,
|
|
30
|
+
onClick,
|
|
31
|
+
value: inputValue,
|
|
32
|
+
ref,
|
|
33
|
+
refKey: "innerRef",
|
|
34
|
+
inputProps,
|
|
35
|
+
...ariaProps,
|
|
36
|
+
})}
|
|
46
37
|
autoComplete={false}
|
|
47
38
|
value={inputValue}
|
|
48
39
|
{...rest}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import React, { useContext } from "react";
|
|
2
|
+
import type { TypeDownshiftMergedInputProps } from "../types";
|
|
3
|
+
import {
|
|
4
|
+
MenuTokenInput,
|
|
5
|
+
type TypeMenuTokenInputProps,
|
|
6
|
+
} from "../MenuTokenInput";
|
|
7
|
+
import { useMenuToggleContext } from "../MenuContext";
|
|
8
|
+
|
|
9
|
+
export interface TypeAutocompleteTokenInputProps
|
|
10
|
+
extends TypeDownshiftMergedInputProps<TypeMenuTokenInputProps> {}
|
|
11
|
+
|
|
12
|
+
export const AutocompleteTokenInput = ({
|
|
13
|
+
onKeyDown,
|
|
14
|
+
onChange,
|
|
15
|
+
onInput,
|
|
16
|
+
onBlur,
|
|
17
|
+
onClick,
|
|
18
|
+
inputProps,
|
|
19
|
+
getTokenProps,
|
|
20
|
+
...rest
|
|
21
|
+
}: TypeAutocompleteTokenInputProps) => {
|
|
22
|
+
const {
|
|
23
|
+
getInputComponentProps,
|
|
24
|
+
getDropdownProps,
|
|
25
|
+
inputValue,
|
|
26
|
+
isOpen,
|
|
27
|
+
ref,
|
|
28
|
+
ariaProps,
|
|
29
|
+
} = useMenuToggleContext();
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<MenuTokenInput
|
|
33
|
+
id="autocomplete_input"
|
|
34
|
+
name="autocomplete_input"
|
|
35
|
+
{...getInputComponentProps({
|
|
36
|
+
...(getDropdownProps?.({ ref, preventKeyAction: isOpen }) || { ref }),
|
|
37
|
+
onKeyDown,
|
|
38
|
+
onChange,
|
|
39
|
+
onInput,
|
|
40
|
+
onBlur,
|
|
41
|
+
onClick,
|
|
42
|
+
value: inputValue,
|
|
43
|
+
refKey: "innerRef",
|
|
44
|
+
inputProps,
|
|
45
|
+
...ariaProps,
|
|
46
|
+
})}
|
|
47
|
+
{...rest}
|
|
48
|
+
/>
|
|
49
|
+
);
|
|
50
|
+
};
|
|
@@ -1,15 +1,31 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { TypeMenuRootOwnProps } from "../MenuRoot";
|
|
2
2
|
import type {
|
|
3
3
|
TypeDownshiftComboboxProps,
|
|
4
|
-
|
|
4
|
+
TypeDownshiftMultiSelectProps,
|
|
5
5
|
} from "../MenuContext";
|
|
6
|
+
import type { TypeGetIsItemVisibleFn } from "../hooks/useItemFiltering";
|
|
7
|
+
import type { TypeMenuRootProps } from "../MenuRoot";
|
|
8
|
+
import type { TypeMenuItemProps } from "../MenuItem";
|
|
9
|
+
import type { TypeChildrenOrItems } from "../types";
|
|
6
10
|
|
|
7
|
-
export
|
|
8
|
-
extends
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
export type TypeSingleAutocompleteProps<
|
|
12
|
+
I extends TypeMenuItemProps = TypeMenuItemProps
|
|
13
|
+
> = TypeMenuRootOwnProps &
|
|
14
|
+
TypeChildrenOrItems<I> &
|
|
15
|
+
TypeDownshiftComboboxProps & {
|
|
16
|
+
getIsItemVisible?: TypeGetIsItemVisibleFn;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type TypeMultiAutocompleteProps<
|
|
20
|
+
I extends TypeMenuItemProps = TypeMenuItemProps
|
|
21
|
+
> = TypeSingleAutocompleteProps<I> &
|
|
22
|
+
TypeDownshiftMultiSelectProps & {
|
|
23
|
+
/* Determine whether or not the combobox should display selected items in the dropdown */
|
|
24
|
+
showSelectedItemsInDropdown?: boolean;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type TypeAutocompleteProps<
|
|
28
|
+
I extends TypeMenuItemProps = TypeMenuItemProps
|
|
29
|
+
> =
|
|
30
|
+
| (TypeSingleAutocompleteProps<I> & { multiSelect?: false })
|
|
31
|
+
| (TypeMultiAutocompleteProps<I> & { multiSelect: true });
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { useCombobox, useMultipleSelection } from "downshift";
|
|
3
|
+
import { useMenuChildren } from "../hooks/useMenuChildren";
|
|
4
|
+
import {
|
|
5
|
+
itemToString as defaultItemToString,
|
|
6
|
+
itemToKey as defaultItemToKey,
|
|
7
|
+
} from "../utils/downshiftDefaults";
|
|
8
|
+
import { reduceReducers } from "../utils/reduceReducers";
|
|
9
|
+
import { useDownshiftDefaults } from "../hooks/useDownshiftDefaults";
|
|
10
|
+
import {
|
|
11
|
+
useItemFiltering,
|
|
12
|
+
getDefaultGetIsItemVisible,
|
|
13
|
+
defaultGetShouldFilter,
|
|
14
|
+
type TypeGetShouldFilterFn,
|
|
15
|
+
} from "../hooks/useItemFiltering";
|
|
16
|
+
import { getSelectedItemIds } from "../MenuContext";
|
|
17
|
+
import { MenuRoot } from "../MenuRoot";
|
|
18
|
+
|
|
19
|
+
import type { TypeMenuItemProps } from "../MenuItem";
|
|
20
|
+
import type { TypeMenuRootProps } from "../MenuRoot";
|
|
21
|
+
import type { TypeMultiAutocompleteProps } from "./AutocompleteTypes";
|
|
22
|
+
|
|
23
|
+
export const MultiAutocomplete = <
|
|
24
|
+
I extends TypeMenuItemProps = TypeMenuItemProps
|
|
25
|
+
>({
|
|
26
|
+
children: childrenProp,
|
|
27
|
+
stateReducer: externalStateReducer,
|
|
28
|
+
menuItems,
|
|
29
|
+
MenuItemComponent,
|
|
30
|
+
itemToString = defaultItemToString,
|
|
31
|
+
itemToKey = defaultItemToKey,
|
|
32
|
+
getIsItemVisible = getDefaultGetIsItemVisible(itemToString),
|
|
33
|
+
showSelectedItemsInDropdown = false,
|
|
34
|
+
initialSelectedItems = [],
|
|
35
|
+
onStateChange,
|
|
36
|
+
onSelectedItemsChange,
|
|
37
|
+
getA11yMultiStatusMessage,
|
|
38
|
+
onMultiSelectStateChange,
|
|
39
|
+
multiSelectStateReducer,
|
|
40
|
+
...useComboboxProps
|
|
41
|
+
}: TypeMultiAutocompleteProps<I>) => {
|
|
42
|
+
/** Get a list of menuItems and children */
|
|
43
|
+
const { allMenuItems, children } = useMenuChildren({
|
|
44
|
+
children: childrenProp,
|
|
45
|
+
menuItems,
|
|
46
|
+
MenuItemComponent,
|
|
47
|
+
});
|
|
48
|
+
const [selectedItems, setSelectedItems] =
|
|
49
|
+
React.useState(initialSelectedItems);
|
|
50
|
+
const [selectedItemIds, setSelectedItemIds] = React.useState<
|
|
51
|
+
Record<string, boolean>
|
|
52
|
+
>(getSelectedItemIds(initialSelectedItems));
|
|
53
|
+
|
|
54
|
+
// function that checks that item should be visible based on the selected items
|
|
55
|
+
const getIsItemVisibleMulti = React.useCallback<typeof getIsItemVisible>(
|
|
56
|
+
(item, inputValue) => {
|
|
57
|
+
return (
|
|
58
|
+
(showSelectedItemsInDropdown || !selectedItemIds[item.id]) &&
|
|
59
|
+
getIsItemVisible(item, inputValue)
|
|
60
|
+
);
|
|
61
|
+
},
|
|
62
|
+
[selectedItemIds, showSelectedItemsInDropdown, getIsItemVisible]
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
// function that checks that item should be filtered
|
|
66
|
+
const getShouldFilterMulti = React.useCallback<TypeGetShouldFilterFn>(
|
|
67
|
+
(args) => {
|
|
68
|
+
return (
|
|
69
|
+
(!showSelectedItemsInDropdown &&
|
|
70
|
+
Object.keys(selectedItemIds).length > 0) ||
|
|
71
|
+
defaultGetShouldFilter(args)
|
|
72
|
+
);
|
|
73
|
+
},
|
|
74
|
+
[selectedItemIds, showSelectedItemsInDropdown, getIsItemVisible]
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
const { setHiddenItemsMap, defaultDownshiftProps, ...restDefaults } =
|
|
78
|
+
useDownshiftDefaults({
|
|
79
|
+
isCombobox: true,
|
|
80
|
+
isMulti: true,
|
|
81
|
+
// handles the prop name mapping for multi select
|
|
82
|
+
multiSelectProps: {
|
|
83
|
+
getA11yMultiStatusMessage,
|
|
84
|
+
onMultiSelectStateChange,
|
|
85
|
+
multiSelectStateReducer,
|
|
86
|
+
...useComboboxProps,
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// used to handle filtering of items
|
|
91
|
+
const { updateFilteredItems } = useItemFiltering({
|
|
92
|
+
allMenuItems,
|
|
93
|
+
itemToString,
|
|
94
|
+
getIsItemVisible: getIsItemVisibleMulti,
|
|
95
|
+
getShouldFilter: getShouldFilterMulti,
|
|
96
|
+
setHiddenItemsMap,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// update the filtered items when the selectedItemIds changes
|
|
100
|
+
React.useEffect(() => {
|
|
101
|
+
// the inputValue should be empty whenever the selected items change
|
|
102
|
+
// if the inputValue is needed here then it will need to be stored on a new state
|
|
103
|
+
updateFilteredItems({ inputValue: "" });
|
|
104
|
+
}, [selectedItemIds]);
|
|
105
|
+
|
|
106
|
+
const useMultipleSelectionReturnProps = useMultipleSelection({
|
|
107
|
+
...defaultDownshiftProps.multi,
|
|
108
|
+
itemToKey,
|
|
109
|
+
selectedItems,
|
|
110
|
+
onSelectedItemsChange: ({ selectedItems, ...changes }) => {
|
|
111
|
+
setSelectedItems(selectedItems);
|
|
112
|
+
setSelectedItemIds(getSelectedItemIds(selectedItems));
|
|
113
|
+
onSelectedItemsChange &&
|
|
114
|
+
onSelectedItemsChange({ selectedItems, ...changes });
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Destructure Downshift props and call the core useCombobox with items and state handler
|
|
120
|
+
*/
|
|
121
|
+
const { isOpen, ...useComboboxReturnProps } = useCombobox<
|
|
122
|
+
(typeof allMenuItems)[number]
|
|
123
|
+
>({
|
|
124
|
+
...defaultDownshiftProps.combobox,
|
|
125
|
+
items: allMenuItems,
|
|
126
|
+
itemToString,
|
|
127
|
+
itemToKey,
|
|
128
|
+
stateReducer: reduceReducers(
|
|
129
|
+
defaultDownshiftProps.combobox.stateReducer,
|
|
130
|
+
externalStateReducer
|
|
131
|
+
),
|
|
132
|
+
onStateChange(changes) {
|
|
133
|
+
const { type, selectedItem: newSelectedItem, inputValue } = changes;
|
|
134
|
+
switch (type) {
|
|
135
|
+
// Add the selected item to the selected items list
|
|
136
|
+
case useCombobox.stateChangeTypes.InputKeyDownEnter:
|
|
137
|
+
case useCombobox.stateChangeTypes.ItemClick:
|
|
138
|
+
if (newSelectedItem) {
|
|
139
|
+
const newSelectedItems = [...selectedItems, newSelectedItem];
|
|
140
|
+
setSelectedItems(newSelectedItems);
|
|
141
|
+
setSelectedItemIds({
|
|
142
|
+
...selectedItemIds,
|
|
143
|
+
[newSelectedItem.id]: true,
|
|
144
|
+
});
|
|
145
|
+
onSelectedItemsChange &&
|
|
146
|
+
onSelectedItemsChange({
|
|
147
|
+
selectedItems: newSelectedItems,
|
|
148
|
+
type: useMultipleSelection.stateChangeTypes
|
|
149
|
+
.FunctionSetSelectedItems,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
break;
|
|
153
|
+
default:
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
// Update the filtered items based on the input value
|
|
157
|
+
if (inputValue !== undefined) updateFilteredItems({ inputValue });
|
|
158
|
+
onStateChange && onStateChange(changes);
|
|
159
|
+
},
|
|
160
|
+
...useComboboxProps,
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
return (
|
|
164
|
+
<MenuRoot
|
|
165
|
+
{...{
|
|
166
|
+
isListbox: true,
|
|
167
|
+
items: allMenuItems,
|
|
168
|
+
itemToString,
|
|
169
|
+
isOpen,
|
|
170
|
+
initialSelectedItems,
|
|
171
|
+
onStateChange,
|
|
172
|
+
onSelectedItemsChange,
|
|
173
|
+
getA11yMultiStatusMessage,
|
|
174
|
+
onMultiSelectStateChange,
|
|
175
|
+
multiSelectStateReducer,
|
|
176
|
+
setHiddenItemsMap,
|
|
177
|
+
...useComboboxProps,
|
|
178
|
+
...useComboboxReturnProps,
|
|
179
|
+
...useMultipleSelectionReturnProps,
|
|
180
|
+
...restDefaults,
|
|
181
|
+
}}
|
|
182
|
+
popoutProps={{
|
|
183
|
+
focusOnContent: false,
|
|
184
|
+
}}
|
|
185
|
+
>
|
|
186
|
+
{children}
|
|
187
|
+
</MenuRoot>
|
|
188
|
+
);
|
|
189
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { useCombobox } from "downshift";
|
|
3
|
+
import { useMenuChildren } from "../hooks/useMenuChildren";
|
|
4
|
+
import { itemToString as defaultItemToString } from "../utils/downshiftDefaults";
|
|
5
|
+
import { reduceReducers } from "../utils/reduceReducers";
|
|
6
|
+
import { useDownshiftDefaults } from "../hooks/useDownshiftDefaults";
|
|
7
|
+
import { useItemFiltering } from "../hooks/useItemFiltering";
|
|
8
|
+
import { MenuRoot } from "../MenuRoot";
|
|
9
|
+
|
|
10
|
+
import type { TypeMenuItemProps } from "../MenuItem";
|
|
11
|
+
import type { TypeMenuRootProps } from "../MenuRoot";
|
|
12
|
+
import type { TypeAutocompleteProps } from "./AutocompleteTypes";
|
|
13
|
+
|
|
14
|
+
export const SingleAutocomplete = <
|
|
15
|
+
I extends TypeMenuItemProps = TypeMenuItemProps
|
|
16
|
+
>({
|
|
17
|
+
children: childrenProp,
|
|
18
|
+
stateReducer: externalStateReducer,
|
|
19
|
+
menuItems,
|
|
20
|
+
MenuItemComponent,
|
|
21
|
+
itemToString = defaultItemToString,
|
|
22
|
+
getIsItemVisible,
|
|
23
|
+
...useComboboxProps
|
|
24
|
+
}: TypeAutocompleteProps<I>) => {
|
|
25
|
+
/** Get a list of menuItems and children */
|
|
26
|
+
const { allMenuItems, children } = useMenuChildren({
|
|
27
|
+
children: childrenProp,
|
|
28
|
+
menuItems,
|
|
29
|
+
MenuItemComponent,
|
|
30
|
+
});
|
|
31
|
+
const { setHiddenItemsMap, defaultDownshiftProps, ...restDefaults } =
|
|
32
|
+
useDownshiftDefaults({ isCombobox: true });
|
|
33
|
+
const { updateFilteredItems } = useItemFiltering({
|
|
34
|
+
allMenuItems,
|
|
35
|
+
itemToString,
|
|
36
|
+
getIsItemVisible,
|
|
37
|
+
setHiddenItemsMap,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Destructure Downshift props and call the core useCombobox with items and state handler
|
|
42
|
+
*/
|
|
43
|
+
const { isOpen, ...useComboboxReturnProps } = useCombobox({
|
|
44
|
+
...defaultDownshiftProps.combobox,
|
|
45
|
+
items: allMenuItems,
|
|
46
|
+
itemToString,
|
|
47
|
+
onInputValueChange: updateFilteredItems,
|
|
48
|
+
stateReducer: reduceReducers(
|
|
49
|
+
defaultDownshiftProps.combobox.stateReducer,
|
|
50
|
+
externalStateReducer
|
|
51
|
+
),
|
|
52
|
+
...useComboboxProps,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<MenuRoot
|
|
57
|
+
{...{
|
|
58
|
+
isListbox: true,
|
|
59
|
+
items: allMenuItems,
|
|
60
|
+
itemToString,
|
|
61
|
+
isOpen,
|
|
62
|
+
setHiddenItemsMap,
|
|
63
|
+
...useComboboxProps,
|
|
64
|
+
...useComboboxReturnProps,
|
|
65
|
+
...restDefaults,
|
|
66
|
+
}}
|
|
67
|
+
popoutProps={{
|
|
68
|
+
focusOnContent: false,
|
|
69
|
+
}}
|
|
70
|
+
>
|
|
71
|
+
{children}
|
|
72
|
+
</MenuRoot>
|
|
73
|
+
);
|
|
74
|
+
};
|