Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/clean-seahorses-cheat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@heroui/autocomplete": patch
---

ensure focused item matches selected item after filter, selection (#5277)
126 changes: 126 additions & 0 deletions packages/components/autocomplete/__tests__/autocomplete.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -940,3 +940,129 @@ describe("Autocomplete with React Hook Form", () => {
expect(onSubmit).toHaveBeenCalledTimes(1);
});
});

describe("focusedKey management with selected key", () => {
let user: UserEvent;

beforeEach(() => {
user = userEvent.setup();
});

it("should set focusedKey to the first non-disabled item when selectedKey is null", async () => {
const wrapper = render(
<Autocomplete
aria-label="Favorite Animal"
data-testid="autocomplete"
disabledKeys={["penguin"]}
label="Favorite Animal"
>
<AutocompleteItem key="penguin" isDisabled>
Penguin
</AutocompleteItem>
<AutocompleteItem key="zebra">Zebra</AutocompleteItem>
<AutocompleteItem key="shark">Shark</AutocompleteItem>
</Autocomplete>,
);

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

// open the select listbox
await user.click(autocomplete);

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

// first non-disabled item is zebra
const optionItem = options[1];

expect(optionItem).toHaveAttribute("data-focus", "true");
});

it("should set focusedKey to the item's key when an item is selected", async () => {
const wrapper = render(
<Autocomplete aria-label="Favorite Animal" data-testid="autocomplete" label="Favorite Animal">
<AutocompleteItem key="penguin">Penguin</AutocompleteItem>
<AutocompleteItem key="zebra">Zebra</AutocompleteItem>
<AutocompleteItem key="shark">Shark</AutocompleteItem>
</Autocomplete>,
);

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

// open the select listbox
await user.click(autocomplete);

// select the target item using keyboard
await user.keyboard("penguin");
await user.keyboard("{Enter}");
await user.click(autocomplete);

const options = wrapper.getAllByRole("option");
const optionItem = options[0];

expect(optionItem).toHaveAttribute("data-focus", "true");
});

it("should set focusedKey to the item's key when selectedKey prop is passed", async () => {
const wrapper = render(
<Autocomplete
aria-label="Favorite Animal"
data-testid="autocomplete"
label="Favorite Animal"
selectedKey="penguin"
>
<AutocompleteItem key="penguin">Penguin</AutocompleteItem>
<AutocompleteItem key="zebra">Zebra</AutocompleteItem>
<AutocompleteItem key="shark">Shark</AutocompleteItem>
</Autocomplete>,
);

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

// open the select listbox
await user.click(autocomplete);

const options = wrapper.getAllByRole("option");
const optionItem = options[0];

expect(optionItem).toHaveAttribute("data-focus", "true");
});

it("should set focusedKey to the default item's key when using react-hook-form defaultValues", async () => {
const {result} = renderHook(() =>
useForm({
defaultValues: {
withDefaultValue: "zebra",
withoutDefaultValue: "",
requiredField: "",
},
}),
);

const {register} = result.current;

const wrapper = render(
<form>
<Autocomplete
{...register("withDefaultValue")}
aria-label="Favorite Animal"
data-testid="autocomplete"
label="Favorite Animal"
>
<AutocompleteItem key="penguin">Penguin</AutocompleteItem>
<AutocompleteItem key="zebra">Zebra</AutocompleteItem>
<AutocompleteItem key="shark">Shark</AutocompleteItem>
</Autocomplete>
</form>,
);

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

// open the select listbox
await user.click(autocomplete);

const options = wrapper.getAllByRole("option");
const optionItem = options[1];

expect(optionItem).toHaveAttribute("data-focus", "true");
});
});
26 changes: 19 additions & 7 deletions packages/components/autocomplete/src/use-autocomplete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -339,15 +339,27 @@ export function useAutocomplete<T extends object>(originalProps: UseAutocomplete
}
}, [inputRef.current]);

// focus first non-disabled item
// Ensure the focused item in the dropdown correctly reflects the
// selected key when the component mounts or relevant state changes.
useEffect(() => {
let key = state.collection.getFirstKey();

while (key && state.disabledKeys.has(key)) {
key = state.collection.getKeyAfter(key);
let keyToFocus: React.Key | null;

if (
state.selectedKey !== null &&
state.collection.getItem(state.selectedKey) &&
!state.disabledKeys.has(state.selectedKey)
) {
keyToFocus = state.selectedKey;
} else {
let firstAvailableKey = state.collection.getFirstKey();

while (firstAvailableKey && state.disabledKeys.has(firstAvailableKey)) {
firstAvailableKey = state.collection.getKeyAfter(firstAvailableKey);
}
keyToFocus = firstAvailableKey;
}
state.selectionManager.setFocusedKey(key);
}, [state.collection, state.disabledKeys]);
state.selectionManager.setFocusedKey(keyToFocus);
}, [state.collection, state.disabledKeys, state.selectedKey]);

useEffect(() => {
if (isOpen) {
Expand Down