/* eslint-disable react/display-name */
import type {ValidationResult} from "@react-types/shared";
import React, {ChangeEvent} from "react";
import {useForm} from "react-hook-form";
import {Meta} from "@storybook/react";
import {select, button} from "@nextui-org/theme";
import {PetBoldIcon, SelectorIcon} from "@nextui-org/shared-icons";
import {Avatar} from "@nextui-org/avatar";
import {Chip} from "@nextui-org/chip";
import {Button} from "@nextui-org/button";
import {Selection} from "@react-types/shared";
import {useInfiniteScroll} from "@nextui-org/use-infinite-scroll";
import {
Pokemon,
usePokemonList,
animalsData,
usersData,
Animal,
User,
} from "@nextui-org/stories-utils";
import {Form} from "@nextui-org/form";
import {Select, SelectedItems, SelectItem, SelectProps, SelectSection} from "../src";
export default {
title: "Components/Select",
component: Select,
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",
},
},
},
decorators: [
(Story) => (
),
],
} as Meta;
const defaultProps = {
...select.defaultVariants,
};
const items = animalsData.map((item) => (
{item.label}
));
const Template = ({color, variant, ...args}: SelectProps) => (
);
const DynamicTemplate = ({color, variant, ...args}: SelectProps) => (
);
const DynamicTemplateWithDescriptions = ({color, variant, ...args}: SelectProps) => (
);
const ItemStartContentTemplate = ({color, variant, ...args}: SelectProps) => (
);
const ControlledTemplate = ({color, variant, ...args}: SelectProps) => {
const [value, setValue] = React.useState(new Set(["cat"]));
const handleSelectionChange = (e: ChangeEvent) => {
setValue(new Set([e.target.value]));
};
return (
Selected: {value}
);
};
const ControlledOpenTemplate = ({color, variant, ...args}: SelectProps) => {
const [isOpen, setIsOpen] = React.useState(false);
return (
);
};
const ControlledMultipleTemplate = ({color, variant, ...args}: SelectProps) => {
const [values, setValues] = React.useState(new Set(["cat", "dog"]));
const handleSelectionChange = (items: Selection) => {
setValues(items);
};
return (
Selected: {[...values].join(", ")}
);
};
const FormTemplate = ({color, variant, ...args}: SelectProps) => {
return (
);
};
const ServerValidationTemplate = (args: SelectProps) => {
const [submittedData, setSubmittedData] = React.useState<{animal: string} | null>(null);
const [serverErrors, setServerErrors] = React.useState({});
const onSubmit = (e) => {
e.preventDefault();
const formData = new FormData(e.target as HTMLFormElement);
const value = formData.get("animal");
if (!value) {
setServerErrors({
animal: "Please select a valid value",
});
return;
}
if (!value || (value !== "cat" && value !== "dog")) {
setServerErrors({
animal: "Please select a cat or dog",
});
} else {
setServerErrors({});
setSubmittedData({animal: value});
}
};
return (
);
};
const ServerValidationTemplateWithMultiple = (args: SelectProps) => {
const [submittedData, setSubmittedData] = React.useState<{animals: string[]} | null>(null);
const [serverErrors, setServerErrors] = React.useState({});
const onSubmit = (e) => {
e.preventDefault();
const formData = new FormData(e.target as HTMLFormElement);
const values = formData.getAll("animals");
if (!values.length || !values.every((v) => v === "cat" || v === "dog")) {
setServerErrors({
animals: "Please select only cats and/or dogs",
});
} else {
setServerErrors({});
setSubmittedData({animals: values as string[]});
}
};
return (
);
};
const MirrorTemplate = ({color, variant, ...args}: SelectProps) => (
);
const LabelPlacementTemplate = ({color, variant, ...args}: SelectProps) => (
Without placeholder
With placeholder
With placeholder and description
);
const StartContentTemplate = ({color, variant, ...args}: SelectProps) => (
}
variant={variant}
{...args}
>
{items}
);
const EmptyTemplate = ({color, variant, ...args}: SelectProps) => (
);
const CustomItemsTemplate = ({color, variant, ...args}: SelectProps) => (
);
const WithSectionsTemplate = ({color, variant, ...args}: SelectProps) => (
);
const WithCustomSectionsStylesTemplate = ({color, variant, ...args}: SelectProps) => {
const headingClasses =
"flex w-full sticky top-1 z-20 py-1.5 px-2 bg-default-100 shadow-small rounded-small";
return (
);
};
const WithAriaLabelTemplate = ({color, variant, ...args}: SelectProps) => (
);
const CustomStylesTemplate = ({color, variant, ...args}: SelectProps) => {
return (
);
};
const AsyncLoadingTemplate = ({color, variant, ...args}: SelectProps) => {
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 (
);
};
const WithReactHookFormTemplate = (args: SelectProps) => {
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 ScrollableContainerTemplate = (args: SelectProps) => {
const categories = [
{
target: "Animals",
items: [
{name: "Lion", emoji: "ðĶ"},
{name: "Tiger", emoji: "ð
"},
{name: "Elephant", emoji: "ð"},
{name: "Kangaroo", emoji: "ðĶ"},
{name: "Panda", emoji: "ðž"},
{name: "Giraffe", emoji: "ðĶ"},
{name: "Zebra", emoji: "ðĶ"},
{name: "Cheetah", emoji: "ð"},
],
},
{
target: "Birds",
items: [
{name: "Eagle", emoji: "ðĶ
"},
{name: "Parrot", emoji: "ðĶ"},
{name: "Penguin", emoji: "ð§"},
{name: "Ostrich", emoji: "ðĶĒ"},
{name: "Peacock", emoji: "ðĶ"},
{name: "Swan", emoji: "ðĶĒ"},
{name: "Falcon", emoji: "ðĶ
"},
{name: "Flamingo", emoji: "ðĶĐ"},
],
},
];
const DEFAULT_CATEGORY = "Animals";
return (
<>
>
);
};
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: SelectProps & {numItems: number}) => {
const largeDataset = generateLargeDataset(args.numItems);
return (
);
};
const ValidationBehaviorAriaTemplate = (args: SelectProps) => {
// Custom validation example
const CustomValidationExample = () => {
return (
);
};
//Custom validation example multiple
const CustomValidationExampleMultiple = () => {
return (
);
};
// Server validation example
const ServerValidationExample = () => {
const [serverErrors, setServerErrors] = React.useState({});
const onSubmit = (e: React.FormEvent) => {
e.preventDefault();
const formData = new FormData(e.target as HTMLFormElement);
const value = formData.get("animal");
if (value === "penguin") {
setServerErrors({
animal: "Server says: No penguins allowed!",
});
} else {
setServerErrors({});
}
};
return (
);
};
return (
Custom Validation
Try selecting a penguin
Custom Validation Multiple
Try selecting a penguin
Server Validation
Select a penguin and click validate
);
};
export const Default = {
render: MirrorTemplate,
args: {
...defaultProps,
},
};
export const Multiple = {
render: Template,
args: {
...defaultProps,
selectionMode: "multiple",
},
};
export const Required = {
render: FormTemplate,
args: {
...defaultProps,
isRequired: 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 IsInvalid = {
render: Template,
args: {
...defaultProps,
isInvalid: true,
variant: "bordered",
defaultSelectedKeys: ["dog"],
errorMessage: "Please select a valid animal",
},
};
export const LabelPlacement = {
render: LabelPlacementTemplate,
args: {
...defaultProps,
},
};
export const AsyncLoading = {
render: AsyncLoadingTemplate,
args: {
...defaultProps,
},
};
export const StartContent = {
render: StartContentTemplate,
args: {
...defaultProps,
},
};
export const EmptyContent = {
render: EmptyTemplate,
args: {
...defaultProps,
},
};
export const WithDescription = {
render: MirrorTemplate,
args: {
...defaultProps,
description: "Select your favorite animal",
},
};
export const WithoutLabel = {
render: Template,
args: {
...defaultProps,
label: null,
"aria-label": "Select an animal",
placeholder: "Select an 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: DynamicTemplate,
args: {
...defaultProps,
isInvalid: true,
errorMessage: (value: ValidationResult) => {
if (value.isInvalid) {
return "Please select an animal";
}
},
},
};
export const WithChips = {
render: CustomItemsTemplate,
args: {
...defaultProps,
variant: "bordered",
selectionMode: "multiple",
isMultiline: true,
labelPlacement: "outside",
classNames: {
base: "max-w-xs",
trigger: "min-h-12 py-2",
},
renderValue: (items: SelectedItems) => {
return (
{items.map((item) => (
{item.data?.name}
))}
);
},
},
};
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 WithServerValidation = {
render: ServerValidationTemplate,
args: {
...defaultProps,
},
};
export const WithServerValidationMultiple = {
render: ServerValidationTemplateWithMultiple,
args: {
...defaultProps,
},
};
export const WithScrollableContainer = {
render: ScrollableContainerTemplate,
args: {
...defaultProps,
},
};
export const Controlled = {
render: ControlledTemplate,
args: {
...defaultProps,
},
};
export const ControlledMultiple = {
render: ControlledMultipleTemplate,
args: {
...defaultProps,
},
};
export const ControlledOpen = {
render: ControlledOpenTemplate,
args: {
...defaultProps,
},
};
export const CustomSelectorIcon = {
render: Template,
args: {
...defaultProps,
disableSelectorIconRotation: true,
selectorIcon: ,
},
};
export const CustomItems = {
render: CustomItemsTemplate,
args: {
...defaultProps,
},
};
export const CustomRenderValue = {
render: CustomItemsTemplate,
args: {
...defaultProps,
labelPlacement: "outside",
classNames: {
trigger: "h-12",
},
renderValue: (items: SelectedItems) => {
return items.map((item) => (
{item.data?.name}
({item.data?.email})
));
},
},
};
export const CustomStyles = {
render: CustomStylesTemplate,
args: {
...defaultProps,
variant: "bordered",
renderValue: (items: SelectedItems) => {
return items.map((item) => (
{item.data?.name}
({item.data?.email})
));
},
},
};
export const OneThousandList = {
render: LargeDatasetTemplate,
args: {
...defaultProps,
placeholder: "Select an item...",
numItems: 1000,
isVirtualized: true,
},
};
export const TenThousandList = {
render: LargeDatasetTemplate,
args: {
...defaultProps,
placeholder: "Select an item...",
numItems: 10000,
isVirtualized: true,
},
};
export const CustomMaxListboxHeight = {
render: LargeDatasetTemplate,
args: {
...defaultProps,
placeholder: "Select an item...",
numItems: 1000,
isVirtualized: true,
maxListboxHeight: 400,
},
};
export const CustomItemHeight = {
render: LargeDatasetTemplate,
args: {
...defaultProps,
placeholder: "Select an item...",
numItems: 1000,
isVirtualized: true,
maxListboxHeight: 400,
itemHeight: 40,
},
};
const AVATAR_DECORATIONS: {[key: string]: string[]} = {
arcane: ["jinx", "atlas-gauntlets", "flame-chompers", "fishbones", "hexcore", "shimmer"],
anime: ["cat-ears", "heart-bloom", "in-love", "in-tears", "soul-leaving-body", "starry-eyed"],
"lofi-vibes": ["chromawave", "cozy-cat", "cozy-headphones", "doodling", "rainy-mood"],
valorant: [
"a-hint-of-clove",
"blade-storm",
"cypher",
"frag-out",
"omen-cowl",
"reyna-leer",
"vct-supernova",
"viper",
"yoru",
"carnalito2",
"a-hint-of-clove2",
"blade-storm2",
"cypher2",
"frag-out2",
"omen-cowl2",
"reyna-leer2",
"vct-supernova2",
"viper2",
"yoru2",
"carnalito3",
"a-hint-of-clove3",
"blade-storm3",
"cypher3",
"frag-out3",
"omen-cowl3",
"reyna-leer3",
"vct-supernova3",
"viper3",
"yoru3",
"carnalito4",
"a-hint-of-clove4",
"blade-storm4",
"cypher4",
"frag-out4",
"omen-cowl4",
"reyna-leer4",
"vct-supernova4",
"viper4",
"yoru4",
],
spongebob: [
"flower-clouds",
"gary-the-snail",
"imagination",
"musclebob",
"sandy-cheeks",
"spongebob",
],
arcade: ["clyde-invaders", "hot-shot", "joystick", "mallow-jump", "pipedream", "snake"],
"street-fighter": ["akuma", "cammy", "chun-li", "guile", "juri", "ken", "m.bison", "ryu"],
};
export const NonVirtualizedVsVirtualizedWithSections = {
render: () => {
const SelectComponent = ({isVirtualized}: {isVirtualized: boolean}) => (
);
return (
);
},
};
export const ValidationBehaviorAria = {
render: ValidationBehaviorAriaTemplate,
args: {
...defaultProps,
},
};
export const PopoverTopOrBottom = {
args: {
...defaultProps,
},
render: (args) => (
),
};