Skip to content

Commit

Permalink
fix(autocomplete): remove unnecessary state.close (#3464)
Browse files Browse the repository at this point in the history
* fix(autocomplete): remove unnecessary state.close

* feat(autocomplete): add fully controlled template

* feat(autocomplete): should clear value after clicking clear button (controlled)
  • Loading branch information
wingkwong authored Jul 16, 2024
1 parent b762141 commit bbebb79
Show file tree
Hide file tree
Showing 3 changed files with 122 additions and 1 deletion.
44 changes: 44 additions & 0 deletions packages/components/autocomplete/__tests__/autocomplete.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,50 @@ describe("Autocomplete", () => {
expect(autocomplete).toHaveFocus();
});

it("should clear value after clicking clear button (controlled)", async () => {
const wrapper = render(
<ControlledAutocomplete data-testid="autocomplete" items={itemsData}>
{(item) => <AutocompleteItem key={item.value}>{item.value}</AutocompleteItem>}
</ControlledAutocomplete>,
);

const autocomplete = wrapper.getByTestId("autocomplete");

// open the select listbox
await act(async () => {
await userEvent.click(autocomplete);
});

// assert that the autocomplete listbox is open
expect(autocomplete).toHaveAttribute("aria-expanded", "true");

let options = wrapper.getAllByRole("option");

// select the target item
await act(async () => {
await userEvent.click(options[0]);
});

const {container} = wrapper;

const clearButton = container.querySelector(
"[data-slot='inner-wrapper'] button:nth-of-type(1)",
)!;

expect(clearButton).not.toBeNull();

// select the target item
await act(async () => {
await userEvent.click(clearButton);
});

// assert that the input has empty value
expect(autocomplete).toHaveValue("");

// assert that input is focused
expect(autocomplete).toHaveFocus();
});

it("should open and close listbox by clicking selector button", async () => {
const wrapper = render(
<Autocomplete aria-label="Favorite Animal" data-testid="autocomplete" label="Favorite Animal">
Expand Down
1 change: 0 additions & 1 deletion packages/components/autocomplete/src/use-autocomplete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -364,7 +364,6 @@ export function useAutocomplete<T extends object>(originalProps: UseAutocomplete
const onClear = useCallback(() => {
state.setInputValue("");
state.setSelectedKey(null);
state.close();
}, [state]);

const onFocus = useCallback(
Expand Down
78 changes: 78 additions & 0 deletions packages/components/autocomplete/stories/autocomplete.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type {ValidationResult} from "@react-types/shared";
import React, {Key} from "react";
import {Meta} from "@storybook/react";
import {useForm} from "react-hook-form";
import {useFilter} from "@react-aria/i18n";
import {autocomplete, input, button} from "@nextui-org/theme";
import {
Pokemon,
Expand Down Expand Up @@ -161,6 +162,76 @@ const FormTemplate = ({color, variant, ...args}: AutocompleteProps) => {
);
};

const FullyControlledTemplate = () => {
// Store Autocomplete input value, selected option, open state, and items
// in a state tracker
const [fieldState, setFieldState] = React.useState({
selectedKey: "",
inputValue: "",
items: animalsData,
});

// Implement custom filtering logic and control what items are
// available to the Autocomplete.
const {startsWith} = useFilter({sensitivity: "base"});

// Specify how each of the Autocomplete values should change when an
// option is selected from the list box
const onSelectionChange = (key) => {
// eslint-disable-next-line no-console
console.log(`onSelectionChange ${key}`);
setFieldState((prevState) => {
let selectedItem = prevState.items.find((option) => option.value === key);

return {
inputValue: selectedItem?.label || "",
selectedKey: key,
items: animalsData.filter((item) => startsWith(item.label, selectedItem?.label || "")),
};
});
};

// Specify how each of the Autocomplete values should change when the input
// field is altered by the user
const onInputChange = (value) => {
// eslint-disable-next-line no-console
console.log(`onInputChange ${value}`);
setFieldState((prevState: any) => ({
inputValue: value,
selectedKey: value === "" ? null : prevState.selectedKey,
items: animalsData.filter((item) => startsWith(item.label, value)),
}));
};

// Show entire list if user opens the menu manually
const onOpenChange = (isOpen, menuTrigger) => {
if (menuTrigger === "manual" && isOpen) {
setFieldState((prevState) => ({
inputValue: prevState.inputValue,
selectedKey: prevState.selectedKey,
items: animalsData,
}));
}
};

return (
<Autocomplete
className="max-w-xs"
inputValue={fieldState.inputValue}
items={fieldState.items}
label="Favorite Animal"
placeholder="Search an animal"
selectedKey={fieldState.selectedKey}
variant="bordered"
onInputChange={onInputChange}
onOpenChange={onOpenChange}
onSelectionChange={onSelectionChange}
>
{(item) => <AutocompleteItem key={item.value}>{item.label}</AutocompleteItem>}
</Autocomplete>
);
};

const MirrorTemplate = ({color, variant, ...args}: AutocompleteProps) => (
<div className="w-full max-w-xl flex flex-row gap-4">
<Autocomplete
Expand Down Expand Up @@ -983,3 +1054,10 @@ export const CustomStylesWithCustomItems = {
...defaultProps,
},
};

export const FullyControlled = {
render: FullyControlledTemplate,
args: {
...defaultProps,
},
};

0 comments on commit bbebb79

Please sign in to comment.