import React, { useEffect, useMemo, useState } from "react"; import { STATUS_VARIANT } from "../../types"; import { FormGroup } from "../FormGroup"; import { Icon, ICON_TYPE } from "../Icon"; import { Label } from "../Label"; import { Stack } from "../Stack"; import { Text } from "../Text"; import { SelectOptionCategory } from "./shared/SelectOptionCategory"; import { SingleSelect } from "./SingleSelect"; import { SelectOption, SelectOptionsByCategory, SingleSelectedOption, } from "./shared/types"; import { useDisclosure } from "../../hooks"; import { Button } from "../Button"; import { Modal } from "../Modal"; export default { title: "Components/SelectNew/SingleSelect", component: SingleSelect, parameters: { docs: { description: { component: `A single select component is used to select a single option from a list of options. The options can be displayed in a list or in categories. The options can be filtered by typing in the input. The options can be created by typing in the input and pressing enter. The options can be cleared by clicking the clear button. The component can be disabled. The component can be loading. The component can be used in a modal.`, }, }, }, argTypes: { optionsDisplay: { control: { type: "select", options: ["list", "categories"], }, }, variant: { control: { type: "select", options: Object.values(STATUS_VARIANT), }, }, }, }; export const DemoSingleSelectSimple = () => { const [selected, setSelected] = useState(null); const OPTIONS = [ { label: "Butter Pecan", value: "butter-pecan" }, { label: "Chocolate", value: "chocolate" }, { label: "Chocolate Chip Cookie Dough", value: "chocolate-chip-cookie-dough", }, { label: "Cookie Dough", value: "cookie-dough" }, { label: "Cookies and Cream", value: "cookies-and-cream" }, { label: "Mint Chocolate Chip", value: "mint-chocolate-chip" }, { label: "Neapolitan", value: "neapolitan" }, { label: "Moose Tracks", value: "moose-tracks" }, { label: "Rocky Road", value: "rocky-road" }, { label: "Strawberry", value: "strawberry" }, { label: "Vanilla", value: "vanilla" }, ]; return ( {OPTIONS.map((o) => ( ))} ); }; interface IceCreamExtended extends SelectOption { dairyFree: boolean; } export const DemoSingleSelectCategories = () => { const OPTIONS_WITH_CATEGORIES: SelectOptionsByCategory[] = useMemo( () => [ { title: "Nuts", options: [ { label: "Maple Walnut", value: "maple-walnut", dairyFree: false }, { label: "Moose Tracks", value: "moose-tracks", dairyFree: false }, { label: "Pistachio", value: "pistachio", dairyFree: false }, { label: "Rocky Road", value: "rocky-road", dairyFree: false }, ], }, { title: "No Nuts", options: [ { label: "Butter Pecan", value: "butter-pecan", dairyFree: false }, { label: "Chocolate", value: "chocolate", dairyFree: true }, { label: "Chocolate Chip Cookie Dough", value: "chocolate-chip-cookie-dough", dairyFree: false, }, { label: "Cookie Dough", value: "cookie-dough", dairyFree: false }, { label: "Cookies and Cream", value: "cookies-and-cream", dairyFree: true, }, { label: "Strawberry", value: "strawberry", dairyFree: true }, { label: "Vanilla", value: "vanilla", dairyFree: false }, ], }, ], [], ); const renderIceCream = (option: IceCreamExtended) => option.dairyFree ? `${option.label} - Dairy Free` : `${option.label} 🐮`; const [selected, setSelected] = useState< SingleSelectedOption >(null); const [search, setSearch] = useState(""); const filteredOptions: SelectOptionsByCategory[] = useMemo(() => { if (!search) return OPTIONS_WITH_CATEGORIES; return OPTIONS_WITH_CATEGORIES.reduce< SelectOptionsByCategory[] >((acc, cur) => { const filteredCatOptions = cur.options.filter( (o) => o.label.toLowerCase().indexOf(search.toLowerCase()) >= 0, ); return filteredCatOptions.length > 0 ? [...acc, { ...cur, options: filteredCatOptions }] : acc; }, []); }, [search, OPTIONS_WITH_CATEGORIES]); return ( label="Favorite Ice Cream" optionsDisplay="categories" options={filteredOptions} selected={selected} onChange={setSelected} renderSelected={({ selectedItem }) => { if (!selectedItem) return null; return renderIceCream(selectedItem); }} inputValue={search} onInputChange={setSearch} isClearable > {filteredOptions.map((cat) => ( {cat.options.map((o) => ( {renderIceCream(o)} ))} ))} ); }; export const DemoSingleSelectForm = () => { const [selected, setSelected] = useState(null); const OPTIONS = [ { label: "Chocolate", value: "chocolate" }, { label: "Strawberry", value: "strawberry" }, { label: "Vanilla", value: "vanilla" }, ]; return ( {OPTIONS.map((o) => ( {`👉 ${o.label}`} ))} ); }; export const DemoSingleSelectNoResults = () => { const [selected, setSelected] = useState(null); const [search, setSearch] = useState(""); const OPTIONS = useMemo( () => [ { label: "Chocolate", value: "chocolate" }, { label: "Strawberry", value: "strawberry" }, { label: "Vanilla", value: "vanilla" }, ], [], ); const filteredOptions = useMemo( () => OPTIONS.filter( (o) => o.label.toLowerCase().indexOf(search.toLowerCase()) >= 0, ), [search, OPTIONS], ); return ( {filteredOptions.map((o) => ( ))} ); }; export const DemoSingleSelectNoResultsCustom = () => { const [selected, setSelected] = useState(null); const [search, setSearch] = useState(""); const OPTIONS = useMemo( () => [ { label: "Chocolate", value: "chocolate" }, { label: "Strawberry", value: "strawberry" }, { label: "Vanilla", value: "vanilla" }, ], [], ); const filteredOptions = useMemo( () => OPTIONS.filter( (o) => o.label.toLowerCase().indexOf(search.toLowerCase()) >= 0, ), [search, OPTIONS], ); return ( {filteredOptions.length > 0 ? ( filteredOptions.map((o) => ( )) ) : ( No ice cream found 🍦❌ )} ); }; export const DemoSingleSelectLoading = () => { return ( {}} isLoading > {[]} ); }; export const DemoSingleSelectLoadingCustom = () => { return ( {}} isLoading > brb fetching ice cream... 🏃‍♀️ ); }; export const DemoSingleSelectAsyncExample = () => { const [selected, setSelected] = useState(null); const [search, setSearch] = useState(""); const [options, setOptions] = useState([]); const [isLoading, setIsLoading] = useState(false); const OPTIONS = useMemo( () => [ { label: "Chocolate", value: "chocolate" }, { label: "Strawberry", value: "strawberry" }, { label: "Vanilla", value: "vanilla" }, ], [], ); // Emulate server request useEffect(() => { if (search) { setIsLoading(true); setTimeout(() => { setIsLoading(false); setOptions(OPTIONS); }, 1000); } }, [search, OPTIONS]); const filteredOptions = useMemo( () => options.filter( (o) => o.label.toLowerCase().indexOf(search.toLowerCase()) >= 0, ), [options, search], ); return ( {options.length === 0 && !isLoading ? ( Search for ice cream ) : ( filteredOptions.map((o) => ( )) )} ); }; export const DemoSingleSelectPlaceholder = () => { const [selected, setSelected] = useState(null); const OPTIONS = [ { label: "Chocolate", value: "chocolate" }, { label: "Strawberry", value: "strawberry" }, { label: "Vanilla", value: "vanilla" }, ]; return ( {OPTIONS.map((o) => ( ))} ); }; export const DemoSingleSelectPlaceholderElements = () => { const [selected, setSelected] = useState(null); const OPTIONS = [ { label: "Chocolate", value: "chocolate" }, { label: "Strawberry", value: "strawberry" }, { label: "Vanilla", value: "vanilla" }, ]; return ( Favorite ice cream? } > {OPTIONS.map((o) => ( ))} ); }; export const DemoSingleSelectClearable = () => { const [selected, setSelected] = useState(null); const OPTIONS = [ { label: "Chocolate", value: "chocolate" }, { label: "Strawberry", value: "strawberry" }, { label: "Vanilla", value: "vanilla" }, ]; return ( {OPTIONS.map((o) => ( ))} ); }; export const DemoSingleSelectCreatable = () => { const [selected, setSelected] = useState(null); const [search, setSearch] = useState(""); const [isLoading, setIsLoading] = useState(false); const OPTIONS = [ { label: "Chocolate", value: "chocolate" }, { label: "Strawberry", value: "strawberry" }, { label: "Vanilla", value: "vanilla" }, ]; const [options, setOptions] = useState(OPTIONS); return ( { // Create the new option and format the value however you want const newOption = { label: newLabel, value: newLabel.toLowerCase().replace(/\W/g, ""), }; // Create the new options array and sort it const newOptions = [...options, newOption].sort((a, b) => a.label.toLowerCase().localeCompare(b.label.toLowerCase()), ); // set selected state setSelected(newOption); // emulate creating option on server setIsLoading(true); setTimeout(() => { setIsLoading(false); // set the new options setOptions(newOptions); }, 1000); }} > {options.map((o) => ( ))} ); }; export const DemoSingleSelectCategoriesCreatable = () => { const OPTIONS_WITH_CATEGORIES: SelectOptionsByCategory[] = [ { title: "Nuts", options: [ { label: "Maple Walnut", value: "maple-walnut", dairyFree: false }, { label: "Moose Tracks", value: "moose-tracks", dairyFree: false }, { label: "Pistachio", value: "pistachio", dairyFree: false }, { label: "Rocky Road", value: "rocky-road", dairyFree: false }, ], }, { title: "No Nuts", options: [ { label: "Butter Pecan", value: "butter-pecan", dairyFree: false }, { label: "Chocolate", value: "chocolate", dairyFree: true }, { label: "Chocolate Chip Cookie Dough", value: "chocolate-chip-cookie-dough", dairyFree: false, }, { label: "Cookie Dough", value: "cookie-dough", dairyFree: false }, { label: "Cookies and Cream", value: "cookies-and-cream", dairyFree: true, }, { label: "Strawberry", value: "strawberry", dairyFree: true }, { label: "Vanilla", value: "vanilla", dairyFree: false }, ], }, ]; const [options, setOptions] = useState(OPTIONS_WITH_CATEGORIES); const [isLoading, setIsLoading] = useState(false); const renderIceCream = (option: IceCreamExtended) => option.dairyFree ? `${option.label} - Dairy Free` : `${option.label} 🐮`; const [selected, setSelected] = useState< SingleSelectedOption >(null); const [search, setSearch] = useState(""); const filteredOptions: SelectOptionsByCategory[] = useMemo(() => { if (!search) return options; return options.reduce[]>( (acc, cur) => { const filteredCatOptions = cur.options.filter( (o) => o.label.toLowerCase().indexOf(search.toLowerCase()) >= 0, ); return filteredCatOptions.length > 0 ? [...acc, { ...cur, options: filteredCatOptions }] : acc; }, [], ); }, [search, options]); return ( label="Favorite Ice Cream" optionsDisplay="categories" options={filteredOptions} selected={selected} onChange={setSelected} renderSelected={({ selectedItem }) => { if (!selectedItem) return null; return renderIceCream(selectedItem); }} inputValue={search} onInputChange={setSearch} isClearable isLoading={isLoading} onCreateOption={(newLabel) => { const hasNuts = newLabel.includes("nuts"); const hasDairy = newLabel.includes("dairy"); let trimmedLabel = newLabel; if (hasNuts) { trimmedLabel = trimmedLabel.replace("nuts", ""); } if (hasDairy) { trimmedLabel = trimmedLabel.replace("dairy", ""); } // Create the new option and format the value however you want const newOption: IceCreamExtended = { label: trimmedLabel, value: trimmedLabel.toLowerCase().replace(/\W/g, ""), dairyFree: !hasDairy, }; const newOptions = [...options]; const categoryTitle = hasNuts ? "Nuts" : "No Nuts"; const category = newOptions.find((c) => c.title === categoryTitle); if (category) { category.options = [...category.options, newOption].sort((a, b) => a.label.toLowerCase().localeCompare(b.label.toLowerCase()), ); } // set selected state locally setSelected(newOption); // emulate creating option on server setIsLoading(true); setTimeout(() => { setIsLoading(false); // set the new options setOptions(newOptions); }, 1000); }} > {filteredOptions.map((cat) => ( {cat.options.map((o) => ( {renderIceCream(o)} ))} ))} ); }; export const DemoSingleSelectVariants = () => { return Object.values(STATUS_VARIANT).map((v) => ( {}} variant={v} placeholder={v} > No options here 🤷‍♀️ )); }; export const DemoSingleSelectDisabledOption = () => { interface IceCreamWithStock extends SelectOption { inStock: boolean; } const [selected, setSelected] = useState< SingleSelectedOption >(null); const OPTIONS: IceCreamWithStock[] = [ { label: "Butter Pecan", value: "butter-pecan", inStock: true }, { label: "Chocolate", value: "chocolate", inStock: true }, { label: "Chocolate Chip Cookie Dough", value: "chocolate-chip-cookie-dough", inStock: false, }, { label: "Cookie Dough", value: "cookie-dough", inStock: true }, { label: "Cookies and Cream", value: "cookies-and-cream", inStock: true }, { label: "Mint Chocolate Chip", value: "mint-chocolate-chip", inStock: true, }, { label: "Neapolitan", value: "neapolitan", inStock: true }, { label: "Moose Tracks", value: "moose-tracks", inStock: false }, { label: "Rocky Road", value: "rocky-road", inStock: true }, { label: "Strawberry", value: "strawberry", inStock: false }, { label: "Vanilla", value: "vanilla", inStock: true }, ]; return ( {OPTIONS.map((o) => ( {`${o.label}${o.inStock ? "" : " ❌ Sold out"}`} ))} ); }; export const DemoSingleSelectDisabled = () => { return ( {}} isDisabled > {[]} ); }; export const DemoSelectInModal = () => { const { isOpen, onOpen, onClose } = useDisclosure(); const [selected, setSelected] = useState(null); const OPTIONS = [ { label: "Butter Pecan", value: "butter-pecan" }, { label: "Chocolate", value: "chocolate" }, { label: "Chocolate Chip Cookie Dough", value: "chocolate-chip-cookie-dough", }, { label: "Cookie Dough", value: "cookie-dough" }, { label: "Cookies and Cream", value: "cookies-and-cream" }, { label: "Mint Chocolate Chip", value: "mint-chocolate-chip" }, { label: "Neapolitan", value: "neapolitan" }, { label: "Moose Tracks", value: "moose-tracks" }, { label: "Rocky Road", value: "rocky-road" }, { label: "Strawberry", value: "strawberry" }, { label: "Vanilla", value: "vanilla" }, ]; return ( <> {OPTIONS.map((o) => ( ))} ); };