@sproutsocial/seeds-react-menu 0.3.0 → 0.4.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 +234 -80
- package/dist/esm/index.js.map +1 -1
- package/dist/index.d.mts +43 -17
- package/dist/index.d.ts +43 -17
- package/dist/index.js +228 -72
- package/dist/index.js.map +1 -1
- package/package.json +3 -1
- package/src/Autocomplete/Autocomplete.feature +14 -43
- package/src/Autocomplete/Autocomplete.stories.tsx +128 -0
- package/src/Autocomplete/Autocomplete.tsx +29 -11
- package/src/Autocomplete/AutocompleteInput.tsx +51 -0
- package/src/Autocomplete/AutocompleteTypes.ts +8 -1
- package/src/Autocomplete/__tests__/Autocomplete.test.tsx +142 -89
- package/src/Autocomplete/__tests__/Autocomplete.typetest.tsx +25 -43
- package/src/Autocomplete/index.ts +1 -0
- package/src/MenuContext.tsx +11 -5
- package/src/MenuGroup/MenuGroup.feature +6 -24
- package/src/MenuGroup/MenuGroup.tsx +12 -1
- package/src/MenuGroup/MenuGroupTypes.ts +4 -1
- package/src/MenuGroup/__tests__/MenuGroup.test.tsx +41 -18
- package/src/MenuGroup/__tests__/MenuGroup.typetest.tsx +4 -2
- package/src/MenuItem/MenuItem.tsx +8 -5
- package/src/MenuLabel.tsx +4 -4
- package/src/MenuRoot/MenuRoot.tsx +5 -3
- package/src/SingleSelectMenu/SingleSelectMenu.tsx +2 -2
- package/src/__fixtures__/menu-test-data.ts +36 -0
- package/src/__tests__/Menu.test.tsx +29 -96
- package/src/hooks/useItemsFromChildren.tsx +70 -27
- package/src/index.ts +1 -0
- package/src/reducers/useComboboxStateReducer.tsx +11 -8
- package/src/storybookUtilities/menu-storybook-components.tsx +39 -0
- package/src/types.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sproutsocial/seeds-react-menu",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Seeds React Menu",
|
|
5
5
|
"author": "Sprout Social, Inc.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -21,6 +21,8 @@
|
|
|
21
21
|
"@sproutsocial/seeds-react-button": "*",
|
|
22
22
|
"@sproutsocial/seeds-react-checkbox": "*",
|
|
23
23
|
"@sproutsocial/seeds-react-icon": "*",
|
|
24
|
+
"@sproutsocial/seeds-react-input": "*",
|
|
25
|
+
"@sproutsocial/seeds-react-label": "*",
|
|
24
26
|
"@sproutsocial/seeds-react-popout": "*",
|
|
25
27
|
"@sproutsocial/seeds-react-radio": "*",
|
|
26
28
|
"@sproutsocial/seeds-react-switch": "*",
|
|
@@ -13,17 +13,6 @@ Feature: Autocomplete
|
|
|
13
13
|
Then a list of matching items is displayed
|
|
14
14
|
And an item can be selected from the list
|
|
15
15
|
|
|
16
|
-
Scenario: User can add a new value
|
|
17
|
-
Given the Autocomplete Input has a value
|
|
18
|
-
When new values are allowed
|
|
19
|
-
And the value has no match
|
|
20
|
-
Then a new value can be created
|
|
21
|
-
|
|
22
|
-
Scenario: User gets a warning on error
|
|
23
|
-
Given the Autocomplete Input has a value
|
|
24
|
-
When the value is invalid
|
|
25
|
-
Then a warning message appears
|
|
26
|
-
|
|
27
16
|
Scenario: User can select an item from the list
|
|
28
17
|
Given the Autocomplete Input has text
|
|
29
18
|
When an item is selected
|
|
@@ -35,32 +24,22 @@ Feature: Autocomplete
|
|
|
35
24
|
When the Autocomplete input is focused
|
|
36
25
|
Then the screen reader announces it as a "combobox"
|
|
37
26
|
|
|
38
|
-
Scenario: Component handles
|
|
39
|
-
|
|
40
|
-
|
|
27
|
+
Scenario: Component handles selected item change
|
|
28
|
+
Given that the onSelectedItemChange prop is passed to Autocomplete
|
|
29
|
+
When an item is selected on the Autocomplete
|
|
30
|
+
Then the onSelectedItemChange callback is called
|
|
41
31
|
|
|
42
|
-
Scenario Outline: Custom
|
|
43
|
-
Given the
|
|
32
|
+
Scenario Outline: Custom input handlers
|
|
33
|
+
Given the AutocompleteInput is rendered with custom behavior
|
|
44
34
|
When the `<action>` is performed
|
|
45
35
|
Then the `<handler>` is executed
|
|
46
36
|
|
|
47
37
|
Examples: Custom Behavior
|
|
48
38
|
| action | handler |
|
|
49
|
-
|
|
|
39
|
+
| input changed | onChange |
|
|
50
40
|
| input focused | onFocus |
|
|
51
41
|
| input blurred | onBlur |
|
|
52
42
|
|
|
53
|
-
Scenario Outline: Autocomplete suggestions are displayed
|
|
54
|
-
Given the Autocomplete is rendered with items
|
|
55
|
-
When <text> is typed into the input
|
|
56
|
-
Then matching <suggestion> items are displayed
|
|
57
|
-
|
|
58
|
-
Examples: Autocomplete Suggestions
|
|
59
|
-
| text | suggestion |
|
|
60
|
-
| ---------- | ---------------------- |
|
|
61
|
-
| app | apple, application |
|
|
62
|
-
| ban | banana, band |
|
|
63
|
-
|
|
64
43
|
Rule: API
|
|
65
44
|
Background:
|
|
66
45
|
Given an engineer is developing using the component
|
|
@@ -73,24 +52,16 @@ Feature: Autocomplete
|
|
|
73
52
|
When a selected item is passed to the Autocomplete
|
|
74
53
|
Then the input reflects the selected item
|
|
75
54
|
|
|
76
|
-
Scenario: Autocomplete
|
|
77
|
-
When
|
|
78
|
-
Then
|
|
55
|
+
Scenario: Autocomplete accepts custom filter function
|
|
56
|
+
When getIsItemVisible prop is passed to the Autocomplete
|
|
57
|
+
Then filtering of the items uses the custom function
|
|
79
58
|
|
|
80
59
|
Scenario Outline: Autocomplete checks its types
|
|
81
60
|
When <propName> is passed a value
|
|
82
61
|
Then Autocomplete expects it to be of type <propType>
|
|
83
62
|
|
|
84
63
|
Examples: Prop Types
|
|
85
|
-
| propName
|
|
86
|
-
|
|
|
87
|
-
|
|
|
88
|
-
|
|
|
89
|
-
| onChange | function |
|
|
90
|
-
| onPaste | function |
|
|
91
|
-
| autoFocus | boolean |
|
|
92
|
-
| isInvalid | boolean |
|
|
93
|
-
| hasWarning | boolean |
|
|
94
|
-
| required | boolean |
|
|
95
|
-
| inputProps | any |
|
|
96
|
-
| autocomplete | string |
|
|
64
|
+
| propName | propType |
|
|
65
|
+
| selectedItem | object |
|
|
66
|
+
| itemToString | function |
|
|
67
|
+
| getIsItemVisible | function |
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/* eslint-disable react-hooks/rules-of-hooks */
|
|
2
|
+
import React from "react";
|
|
3
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
4
|
+
|
|
5
|
+
import { Box } from "@sproutsocial/seeds-react-box";
|
|
6
|
+
import { Icon } from "@sproutsocial/seeds-react-icon";
|
|
7
|
+
import { MenuContent, MenuItem, MenuLabel, MenuToggleButton } from "../index";
|
|
8
|
+
// eslint-disable-next-line no-restricted-imports
|
|
9
|
+
import { TEST_BOOKS } from "../__fixtures__/menu-test-data";
|
|
10
|
+
|
|
11
|
+
import { Autocomplete } from "./Autocomplete";
|
|
12
|
+
import { AutocompleteInput } from "./AutocompleteInput";
|
|
13
|
+
|
|
14
|
+
type CustomAutocompleteArgs = React.ComponentProps<typeof Autocomplete>;
|
|
15
|
+
|
|
16
|
+
const meta: Meta<CustomAutocompleteArgs> = {
|
|
17
|
+
title: "Under Development/Autocomplete",
|
|
18
|
+
component: Autocomplete,
|
|
19
|
+
parameters: {
|
|
20
|
+
controls: {
|
|
21
|
+
expanded: true,
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
decorators: [
|
|
25
|
+
(Story) => (
|
|
26
|
+
<Box display="flex" flexDirection="column" gap="16px" maxWidth={300}>
|
|
27
|
+
<Story />
|
|
28
|
+
</Box>
|
|
29
|
+
),
|
|
30
|
+
],
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export default meta;
|
|
34
|
+
|
|
35
|
+
type Story = StoryObj<CustomAutocompleteArgs>;
|
|
36
|
+
|
|
37
|
+
export const AutocompleteTextInputExample: Story = {
|
|
38
|
+
name: "Text Input",
|
|
39
|
+
render: (args) => {
|
|
40
|
+
return (
|
|
41
|
+
<Autocomplete
|
|
42
|
+
itemToString={(item) => (item ? TEST_BOOKS[item.index].title : "")}
|
|
43
|
+
menuToggleElement={
|
|
44
|
+
<>
|
|
45
|
+
<MenuLabel htmlFor="my-combobox" mb={300}>
|
|
46
|
+
Choose a book
|
|
47
|
+
</MenuLabel>
|
|
48
|
+
<AutocompleteInput />
|
|
49
|
+
</>
|
|
50
|
+
}
|
|
51
|
+
{...(args as object)}
|
|
52
|
+
>
|
|
53
|
+
<MenuContent>
|
|
54
|
+
{TEST_BOOKS.map(({ id, title }) => (
|
|
55
|
+
<MenuItem key={id} id={id}>
|
|
56
|
+
{title}
|
|
57
|
+
</MenuItem>
|
|
58
|
+
))}
|
|
59
|
+
</MenuContent>
|
|
60
|
+
</Autocomplete>
|
|
61
|
+
);
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export const AutocompleteTextWithButtonExample: Story = {
|
|
66
|
+
name: "Text Input w/ Button",
|
|
67
|
+
render: (args) => {
|
|
68
|
+
return (
|
|
69
|
+
<Autocomplete
|
|
70
|
+
itemToString={(item) => (item ? TEST_BOOKS[item.index].title : "")}
|
|
71
|
+
menuToggleElement={
|
|
72
|
+
<>
|
|
73
|
+
<MenuLabel htmlFor="my-combobox" mb={300}>
|
|
74
|
+
Choose a book
|
|
75
|
+
</MenuLabel>
|
|
76
|
+
<Box display="flex" alignItems="center" gap={200}>
|
|
77
|
+
<AutocompleteInput />
|
|
78
|
+
<MenuToggleButton>
|
|
79
|
+
<Icon name="chevron-down" aria-label="Open Menu" />
|
|
80
|
+
</MenuToggleButton>
|
|
81
|
+
</Box>
|
|
82
|
+
</>
|
|
83
|
+
}
|
|
84
|
+
{...(args as object)}
|
|
85
|
+
>
|
|
86
|
+
<MenuContent>
|
|
87
|
+
{TEST_BOOKS.map(({ id, title }) => (
|
|
88
|
+
<MenuItem key={id} id={id}>
|
|
89
|
+
{title}
|
|
90
|
+
</MenuItem>
|
|
91
|
+
))}
|
|
92
|
+
</MenuContent>
|
|
93
|
+
</Autocomplete>
|
|
94
|
+
);
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// export const AutocompleteControlled: Story = {
|
|
99
|
+
// name: "Controlled State",
|
|
100
|
+
// render: ({ inputType, ...args }) => {
|
|
101
|
+
// const [isOpen, setIsOpen] = React.useState<boolean>(false);
|
|
102
|
+
// const [selectedItem, setSelectedItem] =
|
|
103
|
+
// React.useState<TypeDefaultItemProps | null>(null);
|
|
104
|
+
// return (
|
|
105
|
+
// <Autocomplete
|
|
106
|
+
// isOpen={isOpen}
|
|
107
|
+
// onIsOpenChange={({ isOpen }) => setIsOpen(isOpen)}
|
|
108
|
+
// selectedItem={selectedItem}
|
|
109
|
+
// onSelectedItemChange={({ selectedItem: newSelectedItem }) => {
|
|
110
|
+
// setSelectedItem(newSelectedItem);
|
|
111
|
+
// }}
|
|
112
|
+
// itemToString={(item) => (item ? TEST_BOOKS[item.index].title : "")}
|
|
113
|
+
// menuToggleElement={<AutocompleteInput />}
|
|
114
|
+
// {...(args as object)}
|
|
115
|
+
// >
|
|
116
|
+
// <MenuContent>
|
|
117
|
+
// <MenuGroup>
|
|
118
|
+
// {TEST_BOOKS.map(({ id, title }) => (
|
|
119
|
+
// <MenuItem key={id} id={id} inputType={inputType}>
|
|
120
|
+
// {title}
|
|
121
|
+
// </MenuItem>
|
|
122
|
+
// ))}
|
|
123
|
+
// </MenuGroup>
|
|
124
|
+
// </MenuContent>
|
|
125
|
+
// </Autocomplete>
|
|
126
|
+
// );
|
|
127
|
+
// },
|
|
128
|
+
// };
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
defaultUseComboboxProps,
|
|
6
6
|
useComboboxStateReducer,
|
|
7
7
|
} from "../reducers/useComboboxStateReducer";
|
|
8
|
-
import { itemToString } from "../utils/itemToString";
|
|
8
|
+
import { itemToString as defaultItemToString } from "../utils/itemToString";
|
|
9
9
|
import { MenuRoot } from "../MenuRoot";
|
|
10
10
|
import type { TypeAutocompleteProps } from "./AutocompleteTypes";
|
|
11
11
|
import type { TypeDefaultItemProps } from "../types";
|
|
@@ -21,19 +21,35 @@ import type { TypeDefaultItemProps } from "../types";
|
|
|
21
21
|
export const Autocomplete = ({
|
|
22
22
|
children,
|
|
23
23
|
stateReducer: externalStateReducer,
|
|
24
|
+
itemToString = defaultItemToString,
|
|
25
|
+
getIsItemVisible = (item, inputValue) =>
|
|
26
|
+
itemToString(item).toLowerCase().startsWith(inputValue.toLowerCase()),
|
|
24
27
|
...useComboboxProps
|
|
25
28
|
}: TypeAutocompleteProps) => {
|
|
26
|
-
|
|
29
|
+
/** Get a list of items from the children */
|
|
30
|
+
const { allMenuItems } = useItemsFromChildren({ children });
|
|
31
|
+
const [items, setItems] = React.useState(allMenuItems);
|
|
27
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Destructure Downshift props and call the core useCombobox with items and state handler
|
|
35
|
+
*/
|
|
28
36
|
const { isOpen, ...useComboboxReturnProps } = useCombobox({
|
|
29
37
|
...defaultUseComboboxProps,
|
|
30
|
-
|
|
31
|
-
// setItems(books.filter(getBooksFilter(inputValue)));
|
|
32
|
-
// },
|
|
33
|
-
items: allMenuItems,
|
|
38
|
+
items,
|
|
34
39
|
itemToString,
|
|
35
|
-
|
|
36
|
-
|
|
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
|
+
},
|
|
37
53
|
stateReducer: (state, actionAndChanges) => {
|
|
38
54
|
let newState: Partial<UseComboboxState<TypeDefaultItemProps>>;
|
|
39
55
|
newState = useComboboxStateReducer(state, actionAndChanges);
|
|
@@ -44,20 +60,22 @@ export const Autocomplete = ({
|
|
|
44
60
|
})
|
|
45
61
|
: newState;
|
|
46
62
|
},
|
|
47
|
-
...
|
|
63
|
+
...useComboboxProps,
|
|
48
64
|
});
|
|
49
65
|
|
|
50
66
|
return (
|
|
51
67
|
<MenuRoot
|
|
52
68
|
{...{
|
|
53
69
|
isListbox: true,
|
|
54
|
-
items
|
|
55
|
-
itemsMap: menuItemsMap,
|
|
70
|
+
items,
|
|
56
71
|
itemToString,
|
|
57
72
|
isOpen,
|
|
58
73
|
...useComboboxProps,
|
|
59
74
|
...useComboboxReturnProps,
|
|
60
75
|
}}
|
|
76
|
+
popoutProps={{
|
|
77
|
+
focusOnContent: false,
|
|
78
|
+
}}
|
|
61
79
|
>
|
|
62
80
|
{children}
|
|
63
81
|
</MenuRoot>
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { useContext } from "react";
|
|
2
|
+
import type {
|
|
3
|
+
UseComboboxGetInputPropsOptions,
|
|
4
|
+
GetInputPropsReturnValue,
|
|
5
|
+
} from "downshift";
|
|
6
|
+
import { Input, type TypeInputProps } from "@sproutsocial/seeds-react-input";
|
|
7
|
+
import { MenuContext } from "../MenuContext";
|
|
8
|
+
|
|
9
|
+
type TypeInputEventListeners =
|
|
10
|
+
| "onKeyDown"
|
|
11
|
+
| "onChange"
|
|
12
|
+
| "onInput"
|
|
13
|
+
| "onBlur"
|
|
14
|
+
| "onClick"
|
|
15
|
+
| "refKey";
|
|
16
|
+
export interface TypeAutocompleteInputProps
|
|
17
|
+
extends Pick<UseComboboxGetInputPropsOptions, TypeInputEventListeners>,
|
|
18
|
+
Omit<Partial<TypeInputProps>, TypeInputEventListeners> {}
|
|
19
|
+
|
|
20
|
+
export const AutocompleteInput = ({
|
|
21
|
+
onKeyDown,
|
|
22
|
+
onChange,
|
|
23
|
+
onInput,
|
|
24
|
+
onBlur,
|
|
25
|
+
onClick,
|
|
26
|
+
inputProps,
|
|
27
|
+
...rest
|
|
28
|
+
}: TypeAutocompleteInputProps) => {
|
|
29
|
+
const { getInputProps, inputValue } = useContext(MenuContext);
|
|
30
|
+
return (
|
|
31
|
+
<Input
|
|
32
|
+
id="autocomplete_input"
|
|
33
|
+
name="autocomplete_input"
|
|
34
|
+
inputProps={{
|
|
35
|
+
...getInputProps?.({
|
|
36
|
+
// refKey: "innerRef",
|
|
37
|
+
onKeyDown,
|
|
38
|
+
onChange,
|
|
39
|
+
onInput,
|
|
40
|
+
onBlur,
|
|
41
|
+
onClick,
|
|
42
|
+
value: inputValue,
|
|
43
|
+
}),
|
|
44
|
+
...inputProps,
|
|
45
|
+
}}
|
|
46
|
+
autoComplete={false}
|
|
47
|
+
value={inputValue}
|
|
48
|
+
{...rest}
|
|
49
|
+
/>
|
|
50
|
+
);
|
|
51
|
+
};
|
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
import type { TypeMenuRootProps, TypeMenuRootOwnProps } from "../MenuRoot";
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
TypeDownshiftComboboxProps,
|
|
4
|
+
TypeMenuContext,
|
|
5
|
+
} from "../MenuContext";
|
|
3
6
|
|
|
4
7
|
export interface TypeAutocompleteProps
|
|
5
8
|
extends TypeMenuRootOwnProps,
|
|
6
9
|
TypeDownshiftComboboxProps {
|
|
7
10
|
children: TypeMenuRootProps["children"];
|
|
11
|
+
getIsItemVisible?: (
|
|
12
|
+
item: TypeMenuContext["items"][number],
|
|
13
|
+
inputValue: string
|
|
14
|
+
) => boolean;
|
|
8
15
|
}
|
|
@@ -1,113 +1,166 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
import React from "react";
|
|
2
|
+
import {
|
|
3
|
+
render,
|
|
4
|
+
screen,
|
|
5
|
+
waitFor,
|
|
6
|
+
act,
|
|
7
|
+
} from "@sproutsocial/seeds-react-testing-library";
|
|
8
|
+
import { MenuContent, MenuItem } from "../../index";
|
|
9
|
+
import { Autocomplete } from "../Autocomplete";
|
|
10
|
+
import { AutocompleteInput } from "../AutocompleteInput";
|
|
11
|
+
import type { TypeAutocompleteProps } from "../AutocompleteTypes";
|
|
9
12
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
const menuItems = [
|
|
14
|
+
{ id: "item-0", index: 0, label: "Item 0" },
|
|
15
|
+
{ id: "item-1", index: 1, label: "Item 1" },
|
|
16
|
+
{ id: "item-2", index: 2, label: "Item 2" },
|
|
17
|
+
{ id: "item-3", index: 3, label: "Item 3" },
|
|
18
|
+
];
|
|
16
19
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
20
|
+
const itemToString = (item) => (item ? menuItems[item.index].label : "");
|
|
21
|
+
const defaultAutocompleteProps: TypeAutocompleteProps = {
|
|
22
|
+
menuToggleElement: <AutocompleteInput />,
|
|
23
|
+
itemToString,
|
|
24
|
+
children: (
|
|
25
|
+
<MenuContent>
|
|
26
|
+
{menuItems.map(({ id, label }) => (
|
|
27
|
+
<MenuItem key={id} id={id}>
|
|
28
|
+
{label}
|
|
29
|
+
</MenuItem>
|
|
30
|
+
))}
|
|
31
|
+
</MenuContent>
|
|
32
|
+
),
|
|
33
|
+
};
|
|
26
34
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
// const suggestion = screen.getByText("test suggestion");
|
|
33
|
-
// fireEvent.click(suggestion);
|
|
34
|
-
// expect(inputElement.value).toBe("test suggestion");
|
|
35
|
-
// });
|
|
35
|
+
describe("Autocomplete Component", () => {
|
|
36
|
+
it("should render the Autocomplete component with input as a combobox", () => {
|
|
37
|
+
render(<Autocomplete {...defaultAutocompleteProps}>TEST</Autocomplete>);
|
|
38
|
+
const autocompleteElement = screen.getByRole("combobox");
|
|
39
|
+
expect(autocompleteElement).toBeInTheDocument();
|
|
36
40
|
});
|
|
37
41
|
|
|
38
|
-
it("should
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
42
|
+
it("should display suggestions when typing", async () => {
|
|
43
|
+
const { user } = render(<Autocomplete {...defaultAutocompleteProps} />);
|
|
44
|
+
const inputElement = screen.getByRole("combobox");
|
|
45
|
+
await act(async () => {
|
|
46
|
+
await user.click(inputElement);
|
|
47
|
+
await user.type(inputElement, "item");
|
|
48
|
+
});
|
|
49
|
+
await waitFor(() => {
|
|
50
|
+
const suggestions = screen.getAllByRole("option");
|
|
51
|
+
expect(suggestions.length).toBeGreaterThan(0);
|
|
52
|
+
});
|
|
47
53
|
});
|
|
48
54
|
|
|
49
|
-
it("should
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
55
|
+
it("should select a suggestion when clicked", async () => {
|
|
56
|
+
const { user } = render(<Autocomplete {...defaultAutocompleteProps} />);
|
|
57
|
+
const inputElement = screen.getByRole("combobox") as HTMLInputElement;
|
|
58
|
+
await act(async () => {
|
|
59
|
+
await user.click(inputElement);
|
|
60
|
+
await user.type(inputElement, "item");
|
|
61
|
+
});
|
|
62
|
+
const suggestion = screen.getByRole("option", { name: menuItems[0].label });
|
|
63
|
+
await act(() => user.click(suggestion));
|
|
64
|
+
await waitFor(() => expect(inputElement.value).toBe(menuItems[0].label));
|
|
57
65
|
});
|
|
58
66
|
|
|
59
67
|
it("should mark an item as selected and reflect it in the input", async () => {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
it('should announce the input as a "combobox" for screen readers', () => {
|
|
72
|
-
// render(<Autocomplete />);
|
|
73
|
-
// const inputElement = screen.getByRole("combobox");
|
|
74
|
-
// expect(inputElement).toHaveAttribute("aria-haspopup", "listbox");
|
|
68
|
+
const { user } = render(<Autocomplete {...defaultAutocompleteProps} />);
|
|
69
|
+
const inputElement = screen.getByRole("combobox") as HTMLInputElement;
|
|
70
|
+
await act(async () => {
|
|
71
|
+
await user.click(inputElement);
|
|
72
|
+
await user.type(inputElement, "item");
|
|
73
|
+
});
|
|
74
|
+
const suggestion = screen.getByRole("option", { name: menuItems[0].label });
|
|
75
|
+
await act(() => user.click(suggestion));
|
|
76
|
+
await waitFor(() => expect(inputElement.value).toBe(menuItems[0].label));
|
|
77
|
+
expect(suggestion).toHaveAttribute("aria-selected", "true");
|
|
75
78
|
});
|
|
76
79
|
|
|
77
|
-
it("should
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
80
|
+
it("should call onSelectedItemChange when an item is selected", async () => {
|
|
81
|
+
const handleChange = jest.fn();
|
|
82
|
+
const { user } = render(
|
|
83
|
+
<Autocomplete
|
|
84
|
+
{...defaultAutocompleteProps}
|
|
85
|
+
onSelectedItemChange={handleChange}
|
|
86
|
+
/>
|
|
87
|
+
);
|
|
88
|
+
const inputElement = screen.getByRole("combobox") as HTMLInputElement;
|
|
89
|
+
await act(() => user.click(inputElement));
|
|
90
|
+
const suggestion = screen.getByRole("option", { name: menuItems[0].label });
|
|
91
|
+
await act(() => user.click(suggestion));
|
|
92
|
+
await waitFor(() => expect(handleChange).toHaveBeenCalled());
|
|
83
93
|
});
|
|
84
94
|
|
|
85
95
|
it.each`
|
|
86
96
|
action | handler
|
|
87
|
-
${"
|
|
97
|
+
${"input changed"} | ${"onChange"}
|
|
88
98
|
${"input focused"} | ${"onFocus"}
|
|
89
99
|
${"input blurred"} | ${"onBlur"}
|
|
90
100
|
`(
|
|
91
101
|
"should execute $handler when $action is performed",
|
|
92
102
|
async ({ action, handler }) => {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
//
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
103
|
+
const handlerMock = jest.fn();
|
|
104
|
+
const props = { [handler]: handlerMock };
|
|
105
|
+
const { user } = render(
|
|
106
|
+
<Autocomplete
|
|
107
|
+
{...defaultAutocompleteProps}
|
|
108
|
+
menuToggleElement={<AutocompleteInput {...props} />}
|
|
109
|
+
/>
|
|
110
|
+
);
|
|
111
|
+
const inputElement = screen.getByRole("combobox");
|
|
112
|
+
await act(() => user.click(inputElement));
|
|
113
|
+
|
|
114
|
+
// Use switch case for each action
|
|
115
|
+
switch (action) {
|
|
116
|
+
case "input changed":
|
|
117
|
+
await act(async () => {
|
|
118
|
+
await user.type(inputElement, "item");
|
|
119
|
+
});
|
|
120
|
+
break;
|
|
121
|
+
case "input focused":
|
|
122
|
+
// Clicking on the input adds focus to it
|
|
123
|
+
break;
|
|
124
|
+
case "input blurred":
|
|
125
|
+
await act(async () => {
|
|
126
|
+
await user.keyboard("{tab}");
|
|
127
|
+
});
|
|
128
|
+
break;
|
|
129
|
+
default:
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
await waitFor(() => expect(handlerMock).toHaveBeenCalled());
|
|
111
134
|
}
|
|
112
135
|
);
|
|
136
|
+
|
|
137
|
+
it("should reflect the selected item in the input when a selected item is passed", () => {
|
|
138
|
+
const testItem = menuItems[0];
|
|
139
|
+
render(
|
|
140
|
+
<Autocomplete {...defaultAutocompleteProps} selectedItem={testItem} />
|
|
141
|
+
);
|
|
142
|
+
const inputElement = screen.getByRole("combobox") as HTMLInputElement;
|
|
143
|
+
expect(inputElement.value).toBe(testItem.label);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("should use the custom filter function when getIsItemVisible is passed", async () => {
|
|
147
|
+
const customFilter: TypeAutocompleteProps["getIsItemVisible"] = jest.fn(
|
|
148
|
+
(item, inputValue) => itemToString(item).includes(inputValue)
|
|
149
|
+
);
|
|
150
|
+
const { user } = render(
|
|
151
|
+
<Autocomplete
|
|
152
|
+
{...defaultAutocompleteProps}
|
|
153
|
+
getIsItemVisible={customFilter}
|
|
154
|
+
/>
|
|
155
|
+
);
|
|
156
|
+
const inputElement = screen.getByRole("combobox");
|
|
157
|
+
await act(async () => {
|
|
158
|
+
await user.click(inputElement);
|
|
159
|
+
await user.type(inputElement, "0");
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
await waitFor(() => expect(screen.getAllByRole("option").length).toBe(1));
|
|
163
|
+
|
|
164
|
+
expect(customFilter).toHaveBeenCalled();
|
|
165
|
+
});
|
|
113
166
|
});
|