Skip to content
Closed
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/clever-bikes-pull.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@heroui/autocomplete": patch
---

Fix autocomplete auto-selection on blur when input is empty. Now, when the input is empty and loses focus, no option is automatically selected (#5396)
77 changes: 71 additions & 6 deletions packages/components/autocomplete/__tests__/autocomplete.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type {UserEvent} from "@testing-library/user-event";
import type {AutocompleteProps} from "../src";

import * as React from "react";
import {within, render, renderHook, act} from "@testing-library/react";
import {within, render, renderHook, act, screen} from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import {spy, shouldIgnoreReactWarning} from "@heroui/test-utils";
import {useForm} from "react-hook-form";
Expand Down Expand Up @@ -971,7 +971,7 @@ describe("focusedKey management with selected key", () => {
user = userEvent.setup();
});

it("should set focusedKey to the first non-disabled item when selectedKey is null", async () => {
it("should not auto-focus any item when selectedKey is null and input is empty", async () => {
const wrapper = render(
<Autocomplete
aria-label="Favorite Animal"
Expand All @@ -994,10 +994,10 @@ describe("focusedKey management with selected key", () => {

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

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

expect(optionItem).toHaveAttribute("data-focus", "true");
// no option should be focused when input is empty and no selection
options.forEach((option) => {
expect(option).not.toHaveAttribute("data-focus", "true");
});
});

it("should set focusedKey to the item's key when an item is selected", async () => {
Expand Down Expand Up @@ -1089,3 +1089,68 @@ describe("focusedKey management with selected key", () => {
expect(optionItem).toHaveAttribute("data-focus", "true");
});
});

describe("Autocomplete - Blur Behavior", () => {
let user: UserEvent;

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

const animals = [
{key: "cat", label: "Cat"},
{key: "dog", label: "Dog"},
{key: "elephant", label: "Elephant"},
];

const renderAutocomplete = (props = {}) => {
return render(
<Autocomplete
data-testid="autocomplete"
label="Favorite Animal"
placeholder="Select an animal"
{...props}
>
{animals.map((animal) => (
<AutocompleteItem key={animal.key}>{animal.label}</AutocompleteItem>
))}
</Autocomplete>,
);
};

it("should not auto-select first option when tabbing away from empty input", async () => {
renderAutocomplete();

const input = screen.getByRole("combobox");

await user.click(input);

expect(screen.getByRole("listbox")).toBeInTheDocument();

// tab away from the input while it's empty
await user.tab();

// verify no option was selected and input remains empty
expect(input).toHaveValue("");
expect(input).not.toHaveAttribute("aria-activedescendant");
});

it("should preserve selection when tabbing away from non-empty input", async () => {
renderAutocomplete();

const input = screen.getByRole("combobox");

await user.click(input);
await user.type(input, "Cat");

const catOption = screen.getByText("Cat");

await user.click(catOption);

expect(input).toHaveValue("Cat");

await user.tab();

expect(input).toHaveValue("Cat");
});
});
10 changes: 7 additions & 3 deletions packages/components/autocomplete/src/use-autocomplete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -349,15 +349,19 @@ export function useAutocomplete<T extends object>(originalProps: UseAutocomplete
// Ensure the focused item in the dropdown correctly reflects the
// selected key when the component mounts or relevant state changes.
useEffect(() => {
let keyToFocus: React.Key | null;
if (!state.isOpen) {
return;
}

let keyToFocus: React.Key | null = null;

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

while (firstAvailableKey && state.disabledKeys.has(firstAvailableKey)) {
Expand All @@ -366,7 +370,7 @@ export function useAutocomplete<T extends object>(originalProps: UseAutocomplete
keyToFocus = firstAvailableKey;
}
state.selectionManager.setFocusedKey(keyToFocus);
}, [state.collection, state.disabledKeys, state.selectedKey]);
}, [state.collection, state.disabledKeys, state.selectedKey, state.isOpen, state.inputValue]);

// scroll the listbox to the selected item
useEffect(() => {
Expand Down