diff --git a/.changeset/angry-pillows-accept.md b/.changeset/angry-pillows-accept.md
new file mode 100644
index 0000000000..83f72ae554
--- /dev/null
+++ b/.changeset/angry-pillows-accept.md
@@ -0,0 +1,6 @@
+---
+"@nextui-org/autocomplete": minor
+"@nextui-org/listbox": minor
+---
+
+add prop autoHighlight to enable/disable the automatic focus on autocomplete items (#2186)
diff --git a/apps/docs/content/components/autocomplete/auto-highlight.ts b/apps/docs/content/components/autocomplete/auto-highlight.ts
new file mode 100644
index 0000000000..84d210746f
--- /dev/null
+++ b/apps/docs/content/components/autocomplete/auto-highlight.ts
@@ -0,0 +1,53 @@
+const data = `export const animals = [
+ {label: "Cat", value: "cat", description: "The second most popular pet in the world"},
+ {label: "Dog", value: "dog", description: "The most popular pet in the world"},
+ {label: "Elephant", value: "elephant", description: "The largest land animal"},
+ {label: "Lion", value: "lion", description: "The king of the jungle"},
+ {label: "Tiger", value: "tiger", description: "The largest cat species"},
+ {label: "Giraffe", value: "giraffe", description: "The tallest land animal"},
+ {
+ label: "Dolphin",
+ value: "dolphin",
+ description: "A widely distributed and diverse group of aquatic mammals",
+ },
+ {label: "Penguin", value: "penguin", description: "A group of aquatic flightless birds"},
+ {label: "Zebra", value: "zebra", description: "A several species of African equids"},
+ {
+ label: "Shark",
+ value: "shark",
+ description: "A group of elasmobranch fish characterized by a cartilaginous skeleton",
+ },
+ {
+ label: "Whale",
+ value: "whale",
+ description: "Diverse group of fully aquatic placental marine mammals",
+ },
+ {label: "Otter", value: "otter", description: "A carnivorous mammal in the subfamily Lutrinae"},
+ {label: "Crocodile", value: "crocodile", description: "A large semiaquatic reptile"},
+];`;
+
+const App = `import {Autocomplete, AutocompleteItem} from "@nextui-org/react";
+import {animals} from "./data";
+
+export default function App() {
+ return (
+
+ {(item) => {item.label}}
+
+ );
+}`;
+
+const react = {
+ "/App.jsx": App,
+ "/data.js": data,
+};
+
+export default {
+ ...react,
+};
diff --git a/apps/docs/content/components/autocomplete/index.ts b/apps/docs/content/components/autocomplete/index.ts
index 76d57925a3..388cc7e1b0 100644
--- a/apps/docs/content/components/autocomplete/index.ts
+++ b/apps/docs/content/components/autocomplete/index.ts
@@ -26,6 +26,7 @@ import customSectionsStyle from "./custom-sections-style";
import customStyles from "./custom-styles";
import customEmptyContentMessage from "./custom-empty-content-message";
import readOnly from "./read-only";
+import autoHighlight from "./auto-highlight";
export const autocompleteContent = {
usage,
@@ -56,4 +57,5 @@ export const autocompleteContent = {
customStyles,
customEmptyContentMessage,
readOnly,
+ autoHighlight,
};
diff --git a/apps/docs/content/docs/components/autocomplete.mdx b/apps/docs/content/docs/components/autocomplete.mdx
index b826b21556..de29f4ea56 100644
--- a/apps/docs/content/docs/components/autocomplete.mdx
+++ b/apps/docs/content/docs/components/autocomplete.mdx
@@ -84,7 +84,7 @@ the end of the label and the autocomplete will be required.
### Read Only
-If you pass the `isReadOnly` property to the Autocomplete, the Listbox will open to display
+If you pass the `isReadOnly` property to the Autocomplete, the Listbox will open to display
all available options, but users won't be able to select any of the listed options.
@@ -322,6 +322,12 @@ You can use the `AutocompleteSection` component to group autocomplete items.
+### Auto Highlight
+
+If you pass the `autoHighlight` property to the Autocomplete, the Listbox will show the first list item automatically highlighted.
+
+
+
### Custom Sections Style
You can customize the sections style by using the `classNames` property of the `AutocompleteSection` component.
diff --git a/packages/components/autocomplete/__tests__/autocomplete.test.tsx b/packages/components/autocomplete/__tests__/autocomplete.test.tsx
index fb6162007c..79a32334d4 100644
--- a/packages/components/autocomplete/__tests__/autocomplete.test.tsx
+++ b/packages/components/autocomplete/__tests__/autocomplete.test.tsx
@@ -861,3 +861,75 @@ describe("Autocomplete with React Hook Form", () => {
expect(onSubmit).toHaveBeenCalledTimes(1);
});
});
+
+it("should auto-highlight the first non-disabled item when autoHighlight is true", async () => {
+ const {getByRole, getAllByRole} = render(
+
+ {(item) => {item.label}}
+ ,
+ );
+
+ const input = getByRole("combobox");
+
+ await act(async () => {
+ await userEvent.click(input);
+ });
+
+ const options = getAllByRole("option");
+
+ expect(options[0]).toHaveAttribute("data-hover", "true");
+});
+
+it("should skip disabled items when auto-highlighting", async () => {
+ const {getByRole, getAllByRole} = render(
+
+ {(item) => {item.label}}
+ ,
+ );
+
+ const input = getByRole("combobox");
+
+ await act(async () => {
+ await userEvent.click(input);
+ });
+
+ const options = getAllByRole("option");
+
+ expect(options[2]).toHaveAttribute("data-hover", "true");
+});
+
+it("should not auto-highlight when autoHighlight is false", async () => {
+ const {getByRole, getAllByRole} = render(
+
+ {(item) => {item.label}}
+ ,
+ );
+
+ const input = getByRole("combobox");
+
+ await act(async () => {
+ await userEvent.click(input);
+ });
+
+ const options = getAllByRole("option");
+
+ options.forEach((option) => {
+ expect(option).not.toHaveAttribute("data-hover", "true");
+ });
+});
diff --git a/packages/components/autocomplete/src/use-autocomplete.ts b/packages/components/autocomplete/src/use-autocomplete.ts
index 6f84d1c979..ccd63b6680 100644
--- a/packages/components/autocomplete/src/use-autocomplete.ts
+++ b/packages/components/autocomplete/src/use-autocomplete.ts
@@ -110,6 +110,11 @@ interface Props extends Omit, keyof ComboBoxProps
* Callback fired when the select menu is closed.
*/
onClose?: () => void;
+ /**
+ * Whether to automatically highlight the first item in the list as the user types.
+ * @default false
+ */
+ autoHighlight?: boolean;
}
export type UseAutocompleteProps = Props &
@@ -165,6 +170,7 @@ export function useAutocomplete(originalProps: UseAutocomplete
onOpenChange,
onClose,
isReadOnly = false,
+ autoHighlight = false,
...otherProps
} = props;
@@ -319,12 +325,14 @@ export function useAutocomplete(originalProps: UseAutocomplete
// focus first non-disabled item
useEffect(() => {
- let key = state.collection.getFirstKey();
+ if (autoHighlight) {
+ let key = state.collection.getFirstKey();
- while (key && state.disabledKeys.has(key)) {
- key = state.collection.getKeyAfter(key);
+ while (key && state.disabledKeys.has(key)) {
+ key = state.collection.getKeyAfter(key);
+ }
+ state.selectionManager.setFocusedKey(key);
}
- state.selectionManager.setFocusedKey(key);
}, [state.collection, state.disabledKeys]);
useEffect(() => {
@@ -432,6 +440,7 @@ export function useAutocomplete(originalProps: UseAutocomplete
...mergeProps(slotsProps.listboxProps, listBoxProps, {
shouldHighlightOnFocus: true,
}),
+ autoHighlight,
} as ListboxProps);
const getPopoverProps = (props: DOMAttributes = {}) => {
@@ -515,6 +524,7 @@ export function useAutocomplete(originalProps: UseAutocomplete
disableAnimation,
allowsCustomValue,
selectorIcon,
+ autoHighlight,
getBaseProps,
getInputProps,
getListBoxProps,
diff --git a/packages/components/autocomplete/stories/autocomplete.stories.tsx b/packages/components/autocomplete/stories/autocomplete.stories.tsx
index ca4db45df2..8dd487638a 100644
--- a/packages/components/autocomplete/stories/autocomplete.stories.tsx
+++ b/packages/components/autocomplete/stories/autocomplete.stories.tsx
@@ -65,6 +65,11 @@ export default {
type: "boolean",
},
},
+ autoHighlight: {
+ control: {
+ type: "boolean",
+ },
+ },
validationBehavior: {
control: {
type: "select",
@@ -803,6 +808,21 @@ const WithReactHookFormTemplate = (args: AutocompleteProps) => {
);
};
+const AutoHighlightTemplate = ({color, variant, ...args}: AutocompleteProps) => (
+
+ {(item) => {item.label}}
+
+);
+
export const Default = {
render: Template,
args: {
@@ -1061,3 +1081,10 @@ export const FullyControlled = {
...defaultProps,
},
};
+
+export const WithAutoHighlight = {
+ render: AutoHighlightTemplate,
+ args: {
+ ...defaultProps,
+ },
+};
diff --git a/packages/components/listbox/src/listbox.tsx b/packages/components/listbox/src/listbox.tsx
index ffc0fa6f82..521933e426 100644
--- a/packages/components/listbox/src/listbox.tsx
+++ b/packages/components/listbox/src/listbox.tsx
@@ -24,6 +24,7 @@ function Listbox(props: Props, ref: ForwardedRef({...props, ref});
const content = (
@@ -51,6 +52,7 @@ function Listbox(props: Props, ref: ForwardedRef
diff --git a/packages/components/listbox/src/use-listbox-item.ts b/packages/components/listbox/src/use-listbox-item.ts
index f4e0781644..29f6e61f72 100644
--- a/packages/components/listbox/src/use-listbox-item.ts
+++ b/packages/components/listbox/src/use-listbox-item.ts
@@ -21,6 +21,7 @@ import {ListState} from "@react-stately/list";
interface Props extends ListboxItemBaseProps {
item: Node;
state: ListState;
+ autoHighlighted?: boolean;
}
export type UseListboxItemProps = Props &
@@ -46,6 +47,7 @@ export function useListboxItem(originalProps: UseListboxItemPr
onPress,
onClick,
shouldHighlightOnFocus,
+ autoHighlighted = false,
hideSelectedIcon = false,
isReadOnly = false,
...otherProps
@@ -111,7 +113,8 @@ export function useListboxItem(originalProps: UseListboxItemPr
const isHighlighted =
(shouldHighlightOnFocus && isFocused) ||
- (isMobile ? isHovered || isPressed : isHovered || (isFocused && !isFocusVisible));
+ (isMobile ? isHovered || isPressed : isHovered || (isFocused && !isFocusVisible)) ||
+ autoHighlighted;
const getItemProps: PropGetter = (props = {}) => ({
ref: domRef,
@@ -178,6 +181,7 @@ export function useListboxItem(originalProps: UseListboxItemPr
selectedIcon,
hideSelectedIcon,
disableAnimation,
+ isHighlighted,
getItemProps,
getLabelProps,
getWrapperProps,
diff --git a/packages/components/listbox/src/use-listbox.ts b/packages/components/listbox/src/use-listbox.ts
index 6a59d6aeb9..3a8936e6cb 100644
--- a/packages/components/listbox/src/use-listbox.ts
+++ b/packages/components/listbox/src/use-listbox.ts
@@ -92,6 +92,11 @@ interface Props extends Omit, "children"> {
* The menu items classNames.
*/
itemClasses?: ListboxItemProps["classNames"];
+ /**
+ * Whether to automatically highlight the first item in the list.
+ * @default false
+ */
+ autoHighlight?: boolean;
}
export type UseListboxProps = Props & AriaListBoxOptions & ListboxVariantProps;
@@ -118,6 +123,7 @@ export function useListbox(props: UseListboxProps) {
hideEmptyContent = false,
shouldHighlightOnFocus = false,
classNames,
+ autoHighlight = false,
...otherProps
} = props;
@@ -181,6 +187,7 @@ export function useListbox(props: UseListboxProps) {
disableAnimation,
className,
itemClasses,
+ autoHighlight,
getBaseProps,
getListProps,
getEmptyContentProps,