diff --git a/.changeset/clean-seahorses-cheat.md b/.changeset/clean-seahorses-cheat.md new file mode 100644 index 0000000000..01c5ac2f1a --- /dev/null +++ b/.changeset/clean-seahorses-cheat.md @@ -0,0 +1,5 @@ +--- +"@heroui/autocomplete": patch +--- + +ensure focused item matches selected item after filter, selection (#5277) diff --git a/packages/components/autocomplete/__tests__/autocomplete.test.tsx b/packages/components/autocomplete/__tests__/autocomplete.test.tsx index 79c09386c0..c8a8f418da 100644 --- a/packages/components/autocomplete/__tests__/autocomplete.test.tsx +++ b/packages/components/autocomplete/__tests__/autocomplete.test.tsx @@ -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( + + + Penguin + + Zebra + Shark + , + ); + + 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( + + Penguin + Zebra + Shark + , + ); + + 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( + + Penguin + Zebra + Shark + , + ); + + 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( +
+ + Penguin + Zebra + Shark + +
, + ); + + 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"); + }); +}); diff --git a/packages/components/autocomplete/src/use-autocomplete.ts b/packages/components/autocomplete/src/use-autocomplete.ts index 38eef998e1..eecdd519e6 100644 --- a/packages/components/autocomplete/src/use-autocomplete.ts +++ b/packages/components/autocomplete/src/use-autocomplete.ts @@ -339,15 +339,27 @@ export function useAutocomplete(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) {