import type {ValidationResult} from "@react-types/shared"; import React, {Key} from "react"; import {Meta} from "@storybook/react"; import {useForm} from "react-hook-form"; import {useFilter} from "@react-aria/i18n"; import {autocomplete, input, button} from "@nextui-org/theme"; import { Pokemon, usePokemonList, animalsData, usersData, Animal, User, } from "@nextui-org/stories-utils"; import {useAsyncList} from "@react-stately/data"; import {useInfiniteScroll} from "@nextui-org/use-infinite-scroll"; import {PetBoldIcon, SearchLinearIcon, SelectorIcon} from "@nextui-org/shared-icons"; import {Avatar} from "@nextui-org/avatar"; import {Button} from "@nextui-org/button"; import {Form} from "@nextui-org/form"; import {Autocomplete, AutocompleteItem, AutocompleteProps, AutocompleteSection} from "../src"; export default { title: "Components/Autocomplete", component: Autocomplete, argTypes: { variant: { control: { type: "select", }, options: ["flat", "faded", "bordered", "underlined"], }, color: { control: { type: "select", }, options: ["default", "primary", "secondary", "success", "warning", "danger"], }, radius: { control: { type: "select", }, options: ["none", "sm", "md", "lg", "full"], }, size: { control: { type: "select", }, options: ["sm", "md", "lg"], }, labelPlacement: { control: { type: "select", }, options: ["inside", "outside", "outside-left"], }, isDisabled: { control: { type: "boolean", }, }, isReadonly: { control: { type: "boolean", }, }, validationBehavior: { control: { type: "select", }, options: ["aria", "native"], }, }, decorators: [ (Story) => (
), ], } as Meta; type SWCharacter = { name: string; height: string; mass: string; birth_year: string; }; const defaultProps = { ...input.defaultVariants, ...autocomplete.defaultVariants, className: "max-w-xs", }; const items = animalsData.map((item) => ( {item.label} )); interface LargeDatasetSchema { label: string; value: string; description: string; } function generateLargeDataset(n: number): LargeDatasetSchema[] { const dataset: LargeDatasetSchema[] = []; const items = [ "Cat", "Dog", "Elephant", "Lion", "Tiger", "Giraffe", "Dolphin", "Penguin", "Zebra", "Shark", "Whale", "Otter", "Crocodile", ]; for (let i = 0; i < n; i++) { const item = items[i % items.length]; dataset.push({ label: `${item}${i}`, value: `${item.toLowerCase()}${i}`, description: "Sample description", }); } return dataset; } const LargeDatasetTemplate = (args: AutocompleteProps & {numItems: number}) => { const largeDataset = generateLargeDataset(args.numItems); return ( {largeDataset.map((item, index) => ( {item.label} ))} ); }; const Template = (args: AutocompleteProps) => ( Red Panda Cat Dog Crocodile Elephant Lion Tiger Aardvark Kangaroo Koala Panda Giraffe Otter Snake Dolphin Penguin Whale Zebra Shark ); const DynamicTemplate = ({color, variant, ...args}: AutocompleteProps) => ( {(item) => {item.label}} ); const FormTemplate = ({color, variant, ...args}: AutocompleteProps) => { return (
{ alert(`Submitted value: ${e.target["favorite-animal"].value}`); e.preventDefault(); }} > {items}
); }; const FullyControlledTemplate = () => { // Store Autocomplete input value, selected option, open state, and items // in a state tracker const [fieldState, setFieldState] = React.useState({ selectedKey: "", inputValue: "", items: animalsData, }); // Implement custom filtering logic and control what items are // available to the Autocomplete. const {startsWith} = useFilter({sensitivity: "base"}); // Specify how each of the Autocomplete values should change when an // option is selected from the list box const onSelectionChange = (key) => { // eslint-disable-next-line no-console console.log(`onSelectionChange ${key}`); setFieldState((prevState) => { let selectedItem = prevState.items.find((option) => option.value === key); return { inputValue: selectedItem?.label || "", selectedKey: key, items: animalsData.filter((item) => startsWith(item.label, selectedItem?.label || "")), }; }); }; // Specify how each of the Autocomplete values should change when the input // field is altered by the user const onInputChange = (value) => { // eslint-disable-next-line no-console console.log(`onInputChange ${value}`); setFieldState((prevState: any) => ({ inputValue: value, selectedKey: value === "" ? null : prevState.selectedKey, items: animalsData.filter((item) => startsWith(item.label, value)), })); }; // Show entire list if user opens the menu manually const onOpenChange = (isOpen, menuTrigger) => { if (menuTrigger === "manual" && isOpen) { setFieldState((prevState) => ({ inputValue: prevState.inputValue, selectedKey: prevState.selectedKey, items: animalsData, })); } }; return ( {(item) => {item.label}} ); }; const MirrorTemplate = ({color, variant, ...args}: AutocompleteProps) => (
{items} {items}
); const LabelPlacementTemplate = ({color, variant, ...args}: AutocompleteProps) => (

Without placeholder

{items} {items} {items}

With placeholder

{items} {items} {items}
); const AsyncFilteringTemplate = ({color, variant, ...args}: AutocompleteProps) => { let list = useAsyncList({ async load({signal, filterText}) { let res = await fetch(`https://swapi.py4e.com/api/people/?search=${filterText}`, {signal}); let json = await res.json(); return { items: json.results, }; }, }); return ( {(item) => ( {item.name} )} ); }; const AsyncLoadingTemplate = ({color, variant, ...args}: AutocompleteProps) => { const [isOpen, setIsOpen] = React.useState(false); const {items, hasMore, isLoading, onLoadMore} = usePokemonList({fetchDelay: 1500}); const [, scrollerRef] = useInfiniteScroll({ hasMore, distance: 20, isEnabled: isOpen, shouldUseLoader: false, // We don't want to show the loader at the bottom of the list onLoadMore, }); return ( {(item) => ( {item.name} )} ); }; const StartContentTemplate = ({color, variant, ...args}: AutocompleteProps) => ( } variant={variant} {...args} > {items} ); const EndContentTemplate = ({color, variant, ...args}: AutocompleteProps) => ( } label="Favorite Animal" variant={variant} {...args} > {items} ); const DynamicTemplateWithDescriptions = ({color, variant, ...args}: AutocompleteProps) => ( {(item) => ( {item.label} )} ); const ItemStartContentTemplate = ({color, variant, ...args}: AutocompleteProps) => ( } > Argentina } > Venezuela } > Brazil } > Switzerland } > Germany } > Spain } > France } > Italy } > Mexico ); const ControlledTemplate = ({color, variant, ...args}: AutocompleteProps) => { const [value, setValue] = React.useState("cat"); const handleSelectionChange = (key: Key | null) => { setValue(key); }; return (
{(item) => {item.label}}

Selected: {value}

); }; const CustomItemsTemplate = ({color, variant, ...args}: AutocompleteProps) => ( {(item) => (
{item.name} {item.email}
)}
); const WithSectionsTemplate = ({color, variant, ...args}: AutocompleteProps) => ( Lion Tiger Elephant Kangaroo Panda Giraffe Zebra Cheetah Eagle Parrot Penguin Ostrich Peacock Swan Falcon Flamingo ); const WithCustomSectionsStylesTemplate = ({color, variant, ...args}: AutocompleteProps) => { const headingClasses = "flex w-full sticky top-1 z-20 py-1.5 px-2 bg-default-100 shadow-small rounded-small"; return ( Lion Tiger Elephant Kangaroo Panda Giraffe Zebra Cheetah Eagle Parrot Penguin Ostrich Peacock Swan Falcon Flamingo ); }; const WithAriaLabelTemplate = ({color, variant, ...args}: AutocompleteProps) => ( {items} ); const CustomStylesTemplate = ({color, variant, ...args}: AutocompleteProps) => { return ( {(item) => (
{item.name} {item.email}
)}
); }; const CustomStylesWithCustomItemsTemplate = ({color, ...args}: AutocompleteProps) => { return ( } {...args} radius="full" variant="bordered" > {(item) => (
{item.name} {item.team}
)}
); }; const WithReactHookFormTemplate = (args: AutocompleteProps) => { const { register, formState: {errors}, handleSubmit, } = useForm({ defaultValues: { withDefaultValue: "cat", withoutDefaultValue: "", requiredField: "", }, }); const onSubmit = (data: any) => { // eslint-disable-next-line no-console console.log(data); alert("Submitted value: " + JSON.stringify(data)); }; return (
{items} {items} {items} {errors.requiredField && This field is required}
); }; const ServerValidationTemplate = (args: AutocompleteProps) => { const [serverErrors, setServerErrors] = React.useState({}); const onSubmit = (e) => { e.preventDefault(); setServerErrors({ animals: "Please select a valid animal.", }); }; return (
Red Panda Cat Dog
); }; export const Default = { render: Template, args: { ...defaultProps, placeholder: "Select an animal", }, }; export const Required = { render: FormTemplate, args: { ...defaultProps, isRequired: true, }, }; export const ReadOnly = { render: Template, args: { ...defaultProps, selectedKey: "cat", isReadOnly: true, }, }; export const Disabled = { render: Template, args: { ...defaultProps, selectedKey: "cat", variant: "faded", isDisabled: true, }, }; export const DisabledOptions = { render: Template, args: { ...defaultProps, disabledKeys: ["zebra", "tiger", "lion", "elephant", "crocodile", "whale"], }, }; export const LabelPlacement = { render: LabelPlacementTemplate, args: { ...defaultProps, }, }; export const AsyncFiltering = { render: AsyncFilteringTemplate, args: { ...defaultProps, }, }; export const AsyncLoading = { render: AsyncLoadingTemplate, args: { ...defaultProps, }, }; export const StartContent = { render: StartContentTemplate, args: { ...defaultProps, }, }; export const EndContent = { render: EndContentTemplate, args: { ...defaultProps, }, }; export const IsInvalid = { render: Template, args: { ...defaultProps, isInvalid: true, variant: "bordered", defaultSelectedKey: "dog", errorMessage: "Please select a valid animal", }, }; export const WithDescription = { render: MirrorTemplate, args: { ...defaultProps, description: "Select your favorite animal", }, }; export const WithoutScrollShadow = { render: Template, args: { ...defaultProps, scrollShadowProps: { isEnabled: false, }, }, }; export const WithItemDescriptions = { render: DynamicTemplateWithDescriptions, args: { ...defaultProps, }, }; export const WithItemStartContent = { render: ItemStartContentTemplate, args: { ...defaultProps, }, }; export const WithErrorMessage = { render: DynamicTemplate, args: { ...defaultProps, isInvalid: true, errorMessage: "Please select an animal", }, }; export const WithErrorMessageFunction = { render: FormTemplate, args: { ...defaultProps, isRequired: true, errorMessage: (value: ValidationResult) => { if (value.validationDetails.valueMissing) { return "Value is required"; } }, }, }; export const WithValidation = { render: FormTemplate, args: { ...defaultProps, label: "Select Cat or Dog", validate: (value) => { if (value.selectedKey == null || value.selectedKey === "cat" || value.selectedKey === "dog") { return; } return "Please select a valid animal"; }, }, }; export const WithServerValidation = { render: ServerValidationTemplate, args: { ...defaultProps, }, }; export const WithSections = { render: WithSectionsTemplate, args: { ...defaultProps, }, }; export const WithCustomSectionsStyles = { render: WithCustomSectionsStylesTemplate, args: { ...defaultProps, }, }; export const WithAriaLabel = { render: WithAriaLabelTemplate, args: { ...defaultProps, label: "Select an animal 🐹", "aria-label": "Select an animal", }, }; export const WithReactHookForm = { render: WithReactHookFormTemplate, args: { ...defaultProps, }, }; export const Controlled = { render: ControlledTemplate, args: { ...defaultProps, }, }; export const CustomSelectorIcon = { render: Template, args: { ...defaultProps, disableSelectorIconRotation: true, selectorIcon: , }, }; export const CustomItems = { render: CustomItemsTemplate, args: { ...defaultProps, }, }; export const CustomStyles = { render: CustomStylesTemplate, args: { ...defaultProps, variant: "bordered", }, }; export const CustomStylesWithCustomItems = { render: CustomStylesWithCustomItemsTemplate, args: { ...defaultProps, }, }; export const FullyControlled = { render: FullyControlledTemplate, args: { ...defaultProps, }, }; export const OneThousandList = { render: LargeDatasetTemplate, args: { ...defaultProps, placeholder: "Search...", numItems: 1000, }, }; export const TenThousandList = { render: LargeDatasetTemplate, args: { ...defaultProps, placeholder: "Search...", numItems: 10000, }, }; export const CustomMaxListboxHeight = { render: LargeDatasetTemplate, args: { ...defaultProps, placeholder: "Search...", numItems: 1000, maxListboxHeight: 400, }, }; export const CustomItemHeight = { render: LargeDatasetTemplate, args: { ...defaultProps, placeholder: "Search...", numItems: 1000, maxListboxHeight: 400, itemHeight: 40, }, }; export const PopoverTopOrBottom = { args: { ...defaultProps, }, render: (args) => (