fix(autocomplete): ensure focused item matches selected item after filter, selection (#5290)

* fix(autocomplete): ensure focused item matches selected item after filter, selection

* chore: apply type and default value

* chore: add perpose coment in updated code

* test: add focuskey management testcode

* docs: add changeset

* docs: update changeset

* chore: remove comment
This commit is contained in:
KumJungMin 2025-06-02 01:56:15 +09:00 committed by GitHub
parent d09e602b59
commit 360b2e77fd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 149 additions and 6 deletions

View File

@ -0,0 +1,5 @@
---
"@heroui/autocomplete": patch
---
ensure focused item matches selected item after filter, selection (#5277)

View File

@ -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");
});
});

View File

@ -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();
let keyToFocus: React.Key | null;
while (key && state.disabledKeys.has(key)) {
key = state.collection.getKeyAfter(key);
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]);
// scroll the listbox to the selected item
useEffect(() => {