@sproutsocial/seeds-react-menu 0.2.3 → 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.
Files changed (39) hide show
  1. package/.turbo/turbo-build.log +10 -10
  2. package/CHANGELOG.md +27 -0
  3. package/dist/esm/index.js +243 -75
  4. package/dist/esm/index.js.map +1 -1
  5. package/dist/index.d.mts +81 -49
  6. package/dist/index.d.ts +81 -49
  7. package/dist/index.js +237 -67
  8. package/dist/index.js.map +1 -1
  9. package/package.json +3 -1
  10. package/src/Autocomplete/Autocomplete.feature +14 -43
  11. package/src/Autocomplete/Autocomplete.stories.tsx +128 -0
  12. package/src/Autocomplete/Autocomplete.tsx +29 -11
  13. package/src/Autocomplete/AutocompleteInput.tsx +51 -0
  14. package/src/Autocomplete/AutocompleteTypes.ts +8 -1
  15. package/src/Autocomplete/__tests__/Autocomplete.test.tsx +142 -89
  16. package/src/Autocomplete/__tests__/Autocomplete.typetest.tsx +25 -43
  17. package/src/Autocomplete/index.ts +1 -0
  18. package/src/MenuContent/MenuContent.tsx +2 -2
  19. package/src/MenuContext.tsx +11 -5
  20. package/src/MenuGroup/MenuGroup.feature +19 -37
  21. package/src/MenuGroup/MenuGroup.tsx +18 -3
  22. package/src/MenuGroup/MenuGroupTypes.ts +6 -1
  23. package/src/MenuGroup/__tests__/MenuGroup.test.tsx +167 -9
  24. package/src/MenuGroup/__tests__/MenuGroup.typetest.tsx +27 -13
  25. package/src/MenuItem/MenuItem.tsx +7 -4
  26. package/src/MenuLabel.tsx +4 -4
  27. package/src/MenuRoot/MenuRoot.tsx +5 -3
  28. package/src/MenuRoot/__tests__/MenuRoot.test.tsx +1 -1
  29. package/src/MultiSelectMenu/MultiSelectMenu.tsx +8 -0
  30. package/src/MultiSelectMenu/__tests__/MultiSelectMenu.test.tsx +6 -6
  31. package/src/SingleSelectMenu/SingleSelectMenu.tsx +2 -2
  32. package/src/SingleSelectMenu/__tests__/SingleSelectMenu.test.tsx +3 -3
  33. package/src/__fixtures__/menu-test-data.ts +36 -0
  34. package/src/__tests__/Menu.test.tsx +29 -96
  35. package/src/hooks/useItemsFromChildren.tsx +71 -21
  36. package/src/index.ts +1 -0
  37. package/src/reducers/useComboboxStateReducer.tsx +11 -8
  38. package/src/storybookUtilities/menu-storybook-components.tsx +39 -0
  39. package/src/types.ts +3 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sproutsocial/seeds-react-menu",
3
- "version": "0.2.3",
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 input change
39
- When the input value changes
40
- Then the onChange callback fires
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 open/close behavior
43
- Given the Autocomplete is rendered with custom behavior
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
- | item selected | onSelect |
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 hasWarning
77
- When the input's validation has an error
78
- Then 'hasWarning' becomes true
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 | propType |
86
- | delimiters | array of strings |
87
- | value | string |
88
- | hasFocus | boolean |
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
- const { allMenuItems, menuItemsMap } = useItemsFromChildren({ children });
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
- // onInputValueChange({ inputValue }) {
31
- // setItems(books.filter(getBooksFilter(inputValue)));
32
- // },
33
- items: allMenuItems,
38
+ items,
34
39
  itemToString,
35
- selectedItem: null,
36
- defaultHighlightedIndex: 0,
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
- ...useCombobox,
63
+ ...useComboboxProps,
48
64
  });
49
65
 
50
66
  return (
51
67
  <MenuRoot
52
68
  {...{
53
69
  isListbox: true,
54
- items: allMenuItems,
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 { TypeDownshiftComboboxProps } from "../MenuContext";
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
- // import React from "react";
2
- // import {
3
- // render,
4
- // screen,
5
- // fireEvent,
6
- // waitFor,
7
- // } from "@sproutsocial/seeds-react-testing-library";
8
- // import { Autocomplete } from "../Autocomplete";
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
- xdescribe("Autocomplete Component", () => {
11
- it("should render the Autocomplete component", () => {
12
- // render(<Autocomplete />);
13
- // const autocompleteElement = screen.getByRole("combobox");
14
- // expect(autocompleteElement).toBeInTheDocument();
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
- it("should display suggestions when typing", async () => {
18
- // render(<Autocomplete />);
19
- // const inputElement = screen.getByRole("combobox");
20
- // fireEvent.change(inputElement, { target: { value: "test" } });
21
- // await waitFor(() => {
22
- // const suggestions = screen.getAllByRole("option");
23
- // expect(suggestions.length).toBeGreaterThan(0);
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
- it("should select a suggestion when clicked", async () => {
28
- // render(<Autocomplete />);
29
- // const inputElement = screen.getByRole("combobox");
30
- // fireEvent.change(inputElement, { target: { value: "test" } });
31
- // await waitFor(() => {
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 allow adding a new value when no match is found", async () => {
39
- // render(<Autocomplete allowNewValues />);
40
- // const inputElement = screen.getByRole("combobox");
41
- // fireEvent.change(inputElement, { target: { value: "new value" } });
42
- // await waitFor(() => {
43
- // const newValueOption = screen.getByText('Add "new value"');
44
- // fireEvent.click(newValueOption);
45
- // expect(inputElement.value).toBe("new value");
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 display a warning message when the value is invalid", async () => {
50
- // render(<Autocomplete />);
51
- // const inputElement = screen.getByRole("combobox");
52
- // fireEvent.change(inputElement, { target: { value: "invalid value" } });
53
- // await waitFor(() => {
54
- // const warningMessage = screen.getByText("Invalid value");
55
- // expect(warningMessage).toBeInTheDocument();
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
- // render(<Autocomplete />);
61
- // const inputElement = screen.getByRole("combobox");
62
- // fireEvent.change(inputElement, { target: { value: "test" } });
63
- // await waitFor(() => {
64
- // const suggestion = screen.getByText("test suggestion");
65
- // fireEvent.click(suggestion);
66
- // expect(inputElement.value).toBe("test suggestion");
67
- // expect(suggestion).toHaveAttribute("aria-selected", "true");
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 fire the onChange callback when the input value changes", () => {
78
- // const handleChange = jest.fn();
79
- // render(<Autocomplete onChange={handleChange} />);
80
- // const inputElement = screen.getByRole("combobox");
81
- // fireEvent.change(inputElement, { target: { value: "test" } });
82
- // expect(handleChange).toHaveBeenCalledWith("test");
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
- ${"item selected"} | ${"onSelect"}
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
- // const handlerMock = jest.fn();
94
- // const props = { [handler]: handlerMock };
95
- // render(<Autocomplete {...props} />);
96
- // const inputElement = screen.getByRole("combobox");
97
- // if (action === "item selected") {
98
- // fireEvent.change(inputElement, { target: { value: "test" } });
99
- // await waitFor(() => {
100
- // const suggestion = screen.getByText("test suggestion");
101
- // fireEvent.click(suggestion);
102
- // expect(handlerMock).toHaveBeenCalled();
103
- // });
104
- // } else if (action === "input focused") {
105
- // fireEvent.focus(inputElement);
106
- // expect(handlerMock).toHaveBeenCalled();
107
- // } else if (action === "input blurred") {
108
- // fireEvent.blur(inputElement);
109
- // expect(handlerMock).toHaveBeenCalled();
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
  });