/* 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 (
{ alert(`Submitted value: ${e.target["favorite-animal"].value}`); e.preventDefault(); }} >
); }; 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 (
{submittedData && (
You submitted: {JSON.stringify(submittedData)}
)}
); }; 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 (
{submittedData && (
You submitted: {JSON.stringify(submittedData)}
)}
); }; 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) => ( ); 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 (
{errors.requiredField && This field is required}
); }; 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) => (