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 (
);
};
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) => (
);
const LabelPlacementTemplate = ({color, variant, ...args}: AutocompleteProps) => (
);
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) => (
)}
);
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) => (
)}
);
};
const CustomStylesWithCustomItemsTemplate = ({color, ...args}: AutocompleteProps) => {
return (
}
{...args}
radius="full"
variant="bordered"
>
{(item) => (
)}
);
};
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 (
);
};
const ServerValidationTemplate = (args: AutocompleteProps) => {
const [serverErrors, setServerErrors] = React.useState({});
const onSubmit = (e) => {
e.preventDefault();
setServerErrors({
animals: "Please select a valid animal.",
});
};
return (
);
};
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) => (
),
};