"use client" import * as React from "react" import { CheckIcon, ChevronsUpDownIcon, Loader2Icon, PlusIcon, SearchIcon, XIcon } from "lucide-react" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover" import { cn, stopInteractivePropagation } from "@/lib/utils" export type AsyncSelectOption = { value: TValue label: React.ReactNode disabled?: boolean disabledReason?: React.ReactNode description?: React.ReactNode data?: TData } export type AsyncSelectOptionGroup< TValue extends string = string, TData = unknown, TOption extends AsyncSelectOption = AsyncSelectOption, > = { label?: React.ReactNode options: TOption[] } export type AsyncSelectOptionsResult< TValue extends string = string, TData = unknown, TOption extends AsyncSelectOption = AsyncSelectOption, > = TOption[] | AsyncSelectOptionGroup[] export type AsyncSelectRenderState< TValue extends string = string, TData = unknown, TOption extends AsyncSelectOption = AsyncSelectOption, > = { search: string minSearchLength: number options: TOption[] selectedCount?: number maxSelected?: number } export type AsyncSelectStateRenderer< TValue extends string = string, TData = unknown, TOption extends AsyncSelectOption = AsyncSelectOption, > = (state: AsyncSelectRenderState) => React.ReactNode export type AsyncSelectLabels = { placeholder?: string multiPlaceholder?: string searchPlaceholder?: string loading?: string creating?: string empty?: string error?: string clear?: string clearAll?: string selectAll?: string minSearchLength?: (minSearchLength: number) => string maxSelected?: (maxSelected: number) => string selectedCount?: (count: number) => string hiddenSelected?: (count: number) => string } export type AsyncSelectProps< TValue extends string = string, TData = unknown, TOption extends AsyncSelectOption = AsyncSelectOption, > = Omit, "onChange"> & { isMulti?: false value?: TValue selectedOption?: TOption | null onValueChange?: (value: TValue | undefined, option?: TOption) => void loadOptions: (search: string, signal?: AbortSignal) => Promise> loadSelectedOption?: (value: TValue, signal?: AbortSignal) => Promise defaultOptions?: AsyncSelectOptionsResult disabled?: boolean clearable?: boolean cacheOptions?: boolean cacheTtl?: number debounceMs?: number minSearchLength?: number labels?: AsyncSelectLabels renderOption?: (option: TOption, state: { selected: boolean }) => React.ReactNode renderValue?: (option: TOption) => React.ReactNode renderLoading?: AsyncSelectStateRenderer renderEmpty?: AsyncSelectStateRenderer renderError?: AsyncSelectStateRenderer renderMinSearch?: AsyncSelectStateRenderer onCreateOption?: (search: string) => Promise | TOption createOptionLabel?: (search: string) => React.ReactNode showCreateOption?: (search: string, options: TOption[]) => boolean showSelectedDescription?: boolean triggerClassName?: string contentClassName?: string searchClassName?: string optionClassName?: string invalid?: boolean } export type AsyncSelectMultiModeProps< TValue extends string = string, TData = unknown, TOption extends AsyncSelectOption = AsyncSelectOption, > = AsyncMultiSelectProps & { isMulti: true } export type AsyncMultiSelectProps< TValue extends string = string, TData = unknown, TOption extends AsyncSelectOption = AsyncSelectOption, > = Omit, "onChange"> & { value?: TValue[] selectedOptions?: TOption[] onValueChange?: (value: TValue[], options: TOption[]) => void loadOptions: (search: string, signal?: AbortSignal) => Promise> loadSelectedOptions?: (values: TValue[], signal?: AbortSignal) => Promise defaultOptions?: AsyncSelectOptionsResult disabled?: boolean clearable?: boolean cacheOptions?: boolean cacheTtl?: number closeOnSelect?: boolean debounceMs?: number minSearchLength?: number maxSelected?: number maxVisibleTags?: number showSelectAll?: boolean labels?: AsyncSelectLabels renderOption?: (option: TOption, state: { selected: boolean }) => React.ReactNode renderValue?: (option: TOption) => React.ReactNode renderTag?: (option: TOption, state: { remove: () => void }) => React.ReactNode renderLoading?: AsyncSelectStateRenderer renderEmpty?: AsyncSelectStateRenderer renderError?: AsyncSelectStateRenderer renderMinSearch?: AsyncSelectStateRenderer renderMaxSelected?: AsyncSelectStateRenderer onCreateOption?: (search: string) => Promise | TOption createOptionLabel?: (search: string) => React.ReactNode showCreateOption?: (search: string, options: TOption[]) => boolean showSelectedDescription?: boolean triggerClassName?: string contentClassName?: string searchClassName?: string optionClassName?: string tagClassName?: string invalid?: boolean } type AsyncSelectCacheEntry< TValue extends string, TData, TOption extends AsyncSelectOption, > = { createdAt: number groups: AsyncSelectOptionGroup[] } function useDebouncedValue(value: TValue, delay: number) { const [debouncedValue, setDebouncedValue] = React.useState(value) React.useEffect(() => { const timer = window.setTimeout(() => { setDebouncedValue(value) }, delay) return () => { window.clearTimeout(timer) } }, [delay, value]) return debouncedValue } function isOptionGroup< TValue extends string, TData, TOption extends AsyncSelectOption, >( item: TOption | AsyncSelectOptionGroup ): item is AsyncSelectOptionGroup { return typeof item === "object" && item !== null && "options" in item } function normalizeOptionGroups< TValue extends string, TData, TOption extends AsyncSelectOption, >( result?: AsyncSelectOptionsResult ): AsyncSelectOptionGroup[] { if (!result || result.length === 0) return [] const firstItem = result[0] if (isOptionGroup(firstItem)) { return result as AsyncSelectOptionGroup[] } return [{ options: result as TOption[] }] } function flattenOptionGroups< TValue extends string, TData, TOption extends AsyncSelectOption, >(groups: AsyncSelectOptionGroup[]) { return groups.flatMap((group) => group.options) } function optionToComparableText(option: AsyncSelectOption) { if (typeof option.label === "string" || typeof option.label === "number") { return String(option.label).toLowerCase() } return option.value.toLowerCase() } function defaultShowCreateOption< TValue extends string, TData, TOption extends AsyncSelectOption, >(search: string, options: TOption[]) { const normalizedSearch = search.trim().toLowerCase() if (!normalizedSearch) return false return !options.some( (option) => option.value.toLowerCase() === normalizedSearch || optionToComparableText(option) === normalizedSearch ) } function findSelectedOption< TValue extends string, TData, TOption extends AsyncSelectOption, >( value: TValue | undefined, selectedOption: TOption | null | undefined, options: TOption[], defaultOptions: TOption[], preloadedOption?: TOption | null ) { if (!value) return undefined if (selectedOption?.value === value) return selectedOption if (preloadedOption?.value === value) return preloadedOption return [...options, ...defaultOptions].find((option) => option.value === value) } function mergeUniqueOptions< TValue extends string, TData, TOption extends AsyncSelectOption, >(...optionLists: (TOption[] | undefined)[]) { const map = new Map() optionLists.forEach((optionList) => { optionList?.forEach((option) => { map.set(option.value, option) }) }) return Array.from(map.values()) } function getOptionsByValue< TValue extends string, TData, TOption extends AsyncSelectOption, >(values: TValue[], options: TOption[]) { const map = new Map(options.map((option) => [option.value, option])) return values.map((value) => map.get(value)).filter((option): option is TOption => Boolean(option)) } function getCachedOptionGroups< TValue extends string, TData, TOption extends AsyncSelectOption, >( cache: Map>, key: string, cacheTtl: number ) { const entry = cache.get(key) if (!entry) return undefined if (Number.isFinite(cacheTtl) && Date.now() - entry.createdAt > cacheTtl) { cache.delete(key) return undefined } return entry.groups } function setCachedOptionGroups< TValue extends string, TData, TOption extends AsyncSelectOption, >( cache: Map>, key: string, groups: AsyncSelectOptionGroup[] ) { cache.set(key, { groups, createdAt: Date.now(), }) } function normalizeAsyncSearch(search: string) { return search.trim().toLowerCase() } function AsyncStateMessage({ className, children, }: { className?: string children: React.ReactNode }) { return (
{children}
) } function getOptionLabelText(option: AsyncSelectOption) { if (typeof option.label === "string" || typeof option.label === "number") { return String(option.label) } return option.value } function AsyncOptionButton< TValue extends string, TData, TOption extends AsyncSelectOption, >({ option, selected, renderOption, optionClassName, onSelect, }: { option: TOption selected: boolean renderOption?: (option: TOption, state: { selected: boolean }) => React.ReactNode optionClassName?: string onSelect: (option: TOption) => void }) { return ( ) } function AsyncCreateButton({ search, isCreating, label, onCreate, }: { search: string isCreating: boolean label?: (search: string) => React.ReactNode onCreate: () => void }) { return ( ) } function AsyncSingleSelect< TValue extends string = string, TData = unknown, TOption extends AsyncSelectOption = AsyncSelectOption, >(props: AsyncSelectProps) { const { className, value, selectedOption, onValueChange, loadOptions, loadSelectedOption, defaultOptions, disabled = false, clearable = true, cacheOptions = true, cacheTtl = Number.POSITIVE_INFINITY, debounceMs = 250, minSearchLength = 0, labels, renderOption, renderValue, renderLoading, renderEmpty, renderError, renderMinSearch, onCreateOption, createOptionLabel, showCreateOption, showSelectedDescription = true, triggerClassName, contentClassName, searchClassName, optionClassName, invalid, ...rootProps } = props const resolvedDefaultGroups = React.useMemo(() => normalizeOptionGroups(defaultOptions), [defaultOptions]) const defaultFlatOptions = React.useMemo(() => flattenOptionGroups(resolvedDefaultGroups), [resolvedDefaultGroups]) const [open, setOpen] = React.useState(false) const [search, setSearch] = React.useState("") const [optionGroups, setOptionGroups] = React.useState(resolvedDefaultGroups) const [preloadedOption, setPreloadedOption] = React.useState(null) const [isLoading, setIsLoading] = React.useState(false) const [isCreating, setIsCreating] = React.useState(false) const [hasError, setHasError] = React.useState(false) const cacheRef = React.useRef(new Map>()) const loadOptionsRequestRef = React.useRef(0) const loadSelectedOptionRequestRef = React.useRef(0) const debouncedSearch = useDebouncedValue(search, debounceMs) const searchKey = debouncedSearch.trim() const flatOptions = React.useMemo(() => flattenOptionGroups(optionGroups), [optionGroups]) const state = React.useMemo>( () => ({ search, minSearchLength, options: flatOptions }), [flatOptions, minSearchLength, search] ) const currentOption = findSelectedOption( value, selectedOption, flatOptions, defaultFlatOptions, preloadedOption ) const canClear = clearable && Boolean(value) && !disabled const searchTooShort = searchKey.length < minSearchLength const canCreate = !searchTooShort && Boolean(onCreateOption) && (showCreateOption ?? defaultShowCreateOption)(search, flatOptions) React.useEffect(() => { cacheRef.current.clear() }, [loadOptions]) React.useEffect(() => { setOptionGroups(resolvedDefaultGroups) }, [resolvedDefaultGroups]) React.useEffect(() => { if (!value || currentOption || !loadSelectedOption) return const controller = new AbortController() const requestId = ++loadSelectedOptionRequestRef.current async function run() { const option = await loadSelectedOption?.(value as TValue, controller.signal) if (!controller.signal.aborted && requestId === loadSelectedOptionRequestRef.current) { setPreloadedOption(option ?? null) } } void run() return () => { controller.abort() } }, [currentOption, loadSelectedOption, value]) React.useEffect(() => { if (!open) return if (searchTooShort) { setOptionGroups(resolvedDefaultGroups) setIsLoading(false) setHasError(false) return } const cachedGroups = cacheOptions ? getCachedOptionGroups(cacheRef.current, searchKey, cacheTtl) : undefined if (cachedGroups) { setOptionGroups(cachedGroups) setIsLoading(false) setHasError(false) return } const controller = new AbortController() const requestId = ++loadOptionsRequestRef.current async function run() { setIsLoading(true) setHasError(false) try { const nextOptions = normalizeOptionGroups(await loadOptions(searchKey, controller.signal)) if (!controller.signal.aborted && requestId === loadOptionsRequestRef.current) { if (cacheOptions) { setCachedOptionGroups(cacheRef.current, searchKey, nextOptions) } setOptionGroups(nextOptions) } } catch { if (!controller.signal.aborted && requestId === loadOptionsRequestRef.current) { setHasError(true) setOptionGroups([]) } } finally { if (!controller.signal.aborted && requestId === loadOptionsRequestRef.current) { setIsLoading(false) } } } void run() return () => { controller.abort() } }, [cacheOptions, cacheTtl, loadOptions, open, resolvedDefaultGroups, searchKey, searchTooShort]) const handleSelect = (option: TOption) => { if (option.disabled) return onValueChange?.(option.value, option) setOpen(false) setSearch("") } const clearSelection = () => { onValueChange?.(undefined) setSearch("") } const handleClear = (event: React.MouseEvent) => { stopInteractivePropagation(event) clearSelection() } const handleTriggerKeyDown = (event: React.KeyboardEvent) => { if (disabled) return if (event.key === "ArrowDown" || event.key === "Enter" || event.key === " ") { event.preventDefault() setOpen(true) return } if ((event.key === "Backspace" || event.key === "Delete") && value) { event.preventDefault() clearSelection() } } const handleCreate = async () => { if (!onCreateOption || !search.trim() || searchTooShort) return setIsCreating(true) try { const createdOption = await onCreateOption(search.trim()) const normalizedSearch = normalizeAsyncSearch(search) setOptionGroups((previousGroups) => [{ options: [createdOption] }, ...previousGroups]) if (cacheOptions && normalizedSearch) { setCachedOptionGroups(cacheRef.current, normalizedSearch, [{ options: [createdOption] }]) } onValueChange?.(createdOption.value, createdOption) setSearch("") setOpen(false) } finally { setIsCreating(false) } } return (
} > {currentOption ? ( {renderValue?.(currentOption) ?? currentOption.label} {showSelectedDescription && (currentOption.description || (currentOption.disabled && currentOption.disabledReason)) ? ( {currentOption.description ?? currentOption.disabledReason} ) : null} ) : ( {labels?.placeholder ?? "Select"} )} {canClear && ( { if (event.key !== "Enter" && event.key !== " ") return event.preventDefault() stopInteractivePropagation(event) clearSelection() }} > )}
setSearch(event.target.value)} placeholder={labels?.searchPlaceholder ?? "Search..."} className={cn("pl-8", searchClassName)} />
{searchTooShort && flatOptions.length === 0 && ( renderMinSearch?.(state) ?? ( {labels?.minSearchLength?.(minSearchLength) ?? `Type at least ${minSearchLength} characters`} ) )} {isLoading && (renderLoading?.(state) ?? ( {labels?.loading ?? "Loading..."} ))} {!isLoading && hasError && (renderError?.(state) ?? ( {labels?.error ?? "Could not load options"} ))} {!isLoading && !hasError && !searchTooShort && flatOptions.length === 0 && !canCreate && (renderEmpty?.(state) ?? ( {labels?.empty ?? "No options found"} ))} {!isLoading && !hasError && canCreate && ( )} {!isLoading && !hasError && optionGroups.map((group, groupIndex) => (
{group.label && (
{group.label}
)} {group.options.map((option) => ( ))}
))}
) } /** * @deprecated Prefer `AsyncSelect` with `isMulti` for new usage. */ function AsyncMultiSelect< TValue extends string = string, TData = unknown, TOption extends AsyncSelectOption = AsyncSelectOption, >({ className, value, selectedOptions, onValueChange, loadOptions, loadSelectedOptions, defaultOptions, disabled = false, clearable = true, cacheOptions = true, cacheTtl = Number.POSITIVE_INFINITY, closeOnSelect = false, debounceMs = 250, minSearchLength = 0, maxSelected, maxVisibleTags, showSelectAll = false, labels, renderOption, renderValue, renderTag, renderLoading, renderEmpty, renderError, renderMinSearch, renderMaxSelected, onCreateOption, createOptionLabel, showCreateOption, showSelectedDescription = true, triggerClassName, contentClassName, searchClassName, optionClassName, tagClassName, invalid, ...props }: AsyncMultiSelectProps) { const values = React.useMemo(() => value ?? [], [value]) const selectedValueKey = values.join("|") const resolvedDefaultGroups = React.useMemo(() => normalizeOptionGroups(defaultOptions), [defaultOptions]) const defaultFlatOptions = React.useMemo(() => flattenOptionGroups(resolvedDefaultGroups), [resolvedDefaultGroups]) const [open, setOpen] = React.useState(false) const [search, setSearch] = React.useState("") const [optionGroups, setOptionGroups] = React.useState(resolvedDefaultGroups) const [preloadedOptions, setPreloadedOptions] = React.useState([]) const [isLoading, setIsLoading] = React.useState(false) const [isCreating, setIsCreating] = React.useState(false) const [hasError, setHasError] = React.useState(false) const cacheRef = React.useRef(new Map>()) const loadOptionsRequestRef = React.useRef(0) const loadSelectedOptionsRequestRef = React.useRef(0) const debouncedSearch = useDebouncedValue(search, debounceMs) const searchKey = debouncedSearch.trim() const flatOptions = React.useMemo(() => flattenOptionGroups(optionGroups), [optionGroups]) const selectedSet = React.useMemo(() => new Set(values), [values]) const allKnownOptions = React.useMemo( () => mergeUniqueOptions(selectedOptions, preloadedOptions, flatOptions, defaultFlatOptions), [defaultFlatOptions, flatOptions, preloadedOptions, selectedOptions] ) const currentOptions = React.useMemo( () => getOptionsByValue(values, allKnownOptions), [allKnownOptions, values] ) const state = React.useMemo>( () => ({ search, minSearchLength, options: flatOptions, selectedCount: values.length, maxSelected }), [flatOptions, maxSelected, minSearchLength, search, values.length] ) const hasValue = values.length > 0 const canClear = clearable && hasValue && !disabled const isMaxReached = typeof maxSelected === "number" && values.length >= maxSelected const resolvedMaxVisibleTags = Math.max(maxVisibleTags ?? values.length, 1) const searchTooShort = searchKey.length < minSearchLength const canCreate = !searchTooShort && Boolean(onCreateOption) && (showCreateOption ?? defaultShowCreateOption)(search, flatOptions) const visibleSelectableOptions = flatOptions.filter((option) => !option.disabled) const unselectedVisibleOptions = visibleSelectableOptions.filter((option) => !selectedSet.has(option.value)) const canSelectAll = showSelectAll && unselectedVisibleOptions.length > 0 && !isMaxReached const visibleTagOptions = currentOptions.slice(0, resolvedMaxVisibleTags) const hiddenTagCount = Math.max(currentOptions.length - visibleTagOptions.length, 0) React.useEffect(() => { cacheRef.current.clear() }, [loadOptions]) React.useEffect(() => { setOptionGroups(resolvedDefaultGroups) }, [resolvedDefaultGroups]) React.useEffect(() => { if (!values.length || !loadSelectedOptions) return const knownValues = new Set(allKnownOptions.map((option) => option.value)) const missingValues = values.filter((item) => !knownValues.has(item)) if (!missingValues.length) return const controller = new AbortController() const requestId = ++loadSelectedOptionsRequestRef.current async function run() { const loadedOptions = await loadSelectedOptions?.(missingValues, controller.signal) if (!controller.signal.aborted && requestId === loadSelectedOptionsRequestRef.current && loadedOptions) { setPreloadedOptions((previousOptions) => mergeUniqueOptions(previousOptions, loadedOptions)) } } void run() return () => { controller.abort() } }, [allKnownOptions, loadSelectedOptions, selectedValueKey, values]) React.useEffect(() => { if (!open) return if (searchTooShort) { setOptionGroups(resolvedDefaultGroups) setIsLoading(false) setHasError(false) return } const cachedGroups = cacheOptions ? getCachedOptionGroups(cacheRef.current, searchKey, cacheTtl) : undefined if (cachedGroups) { setOptionGroups(cachedGroups) setIsLoading(false) setHasError(false) return } const controller = new AbortController() const requestId = ++loadOptionsRequestRef.current async function run() { setIsLoading(true) setHasError(false) try { const nextOptions = normalizeOptionGroups(await loadOptions(searchKey, controller.signal)) if (!controller.signal.aborted && requestId === loadOptionsRequestRef.current) { if (cacheOptions) { setCachedOptionGroups(cacheRef.current, searchKey, nextOptions) } setOptionGroups(nextOptions) } } catch { if (!controller.signal.aborted && requestId === loadOptionsRequestRef.current) { setHasError(true) setOptionGroups([]) } } finally { if (!controller.signal.aborted && requestId === loadOptionsRequestRef.current) { setIsLoading(false) } } } void run() return () => { controller.abort() } }, [cacheOptions, cacheTtl, loadOptions, open, resolvedDefaultGroups, searchKey, searchTooShort]) const emitChange = (nextValues: TValue[], nextKnownOptions: TOption[]) => { onValueChange?.(nextValues, getOptionsByValue(nextValues, nextKnownOptions)) } const removeValue = (nextValue: TValue) => { emitChange(values.filter((item) => item !== nextValue), allKnownOptions) } const handleSelect = (option: TOption) => { if (option.disabled) return const isSelected = selectedSet.has(option.value) const nextValues = isSelected ? values.filter((item) => item !== option.value) : isMaxReached ? values : [...values, option.value] const nextKnownOptions = mergeUniqueOptions(allKnownOptions, [option]) emitChange(nextValues, nextKnownOptions) if (closeOnSelect) { setOpen(false) setSearch("") } } const handleTagRemoveKeyDown = (event: React.KeyboardEvent, option: TOption) => { if (event.key !== "Enter" && event.key !== " " && event.key !== "Backspace" && event.key !== "Delete") return event.preventDefault() stopInteractivePropagation(event) removeValue(option.value) } const handleTriggerKeyDown = (event: React.KeyboardEvent) => { if (disabled) return if (event.key === "ArrowDown" || event.key === "Enter" || event.key === " ") { event.preventDefault() setOpen(true) return } if (search.length > 0 || values.length === 0) return if (event.key === "Backspace" || event.key === "Delete") { event.preventDefault() removeValue(values[values.length - 1]) } } const handleSelectAllVisible = () => { if (!unselectedVisibleOptions.length) return const remainingCount = typeof maxSelected === "number" ? Math.max(maxSelected - values.length, 0) : unselectedVisibleOptions.length const nextOptions = unselectedVisibleOptions.slice(0, remainingCount) const nextValues = Array.from(new Set([...values, ...nextOptions.map((option) => option.value)])) const nextKnownOptions = mergeUniqueOptions(allKnownOptions, nextOptions) emitChange(nextValues, nextKnownOptions) } const removeOption = (option: TOption) => { removeValue(option.value) } const clearAllSelection = () => { onValueChange?.([], []) setSearch("") } const handleClear = (event: React.MouseEvent) => { stopInteractivePropagation(event) clearAllSelection() } const handleCreate = async () => { if (!onCreateOption || !search.trim() || searchTooShort) return setIsCreating(true) try { const createdOption = await onCreateOption(search.trim()) const normalizedSearch = normalizeAsyncSearch(search) const nextKnownOptions = mergeUniqueOptions(allKnownOptions, [createdOption]) const nextValues = selectedSet.has(createdOption.value) ? values : isMaxReached ? values : [...values, createdOption.value] setOptionGroups((previousGroups) => [{ options: [createdOption] }, ...previousGroups]) if (cacheOptions && normalizedSearch) { setCachedOptionGroups(cacheRef.current, normalizedSearch, [{ options: [createdOption] }]) } emitChange(nextValues, nextKnownOptions) setSearch("") if (closeOnSelect) { setOpen(false) } } finally { setIsCreating(false) } } return (
} > {currentOptions.length > 0 ? ( <> {visibleTagOptions.map((option) => ( {renderTag?.(option, { remove: () => removeOption(option) }) ?? renderValue?.(option) ?? option.label} {showSelectedDescription && (option.description || (option.disabled && option.disabledReason)) ? ( {option.description ?? option.disabledReason} ) : null} {!disabled && ( { stopInteractivePropagation(event) removeOption(option) }} onKeyDown={(event) => handleTagRemoveKeyDown(event, option)} > )} ))} {hiddenTagCount > 0 ? ( {labels?.hiddenSelected?.(hiddenTagCount) ?? `+${hiddenTagCount} more`} ) : null} ) : ( {labels?.multiPlaceholder ?? labels?.placeholder ?? "Select"} )} {canClear && ( { if (event.key !== "Enter" && event.key !== " ") return event.preventDefault() stopInteractivePropagation(event) clearAllSelection() }} > )}
setSearch(event.target.value)} placeholder={labels?.searchPlaceholder ?? "Search..."} className={cn("pl-8", searchClassName)} />
{hasValue && labels?.selectedCount && {labels.selectedCount(values.length)}} {isMaxReached && (renderMaxSelected?.(state) ?? ( {labels?.maxSelected?.(maxSelected ?? values.length) ?? `Maximum ${maxSelected} selected`} ))}
{canSelectAll && ( )} {canClear && ( )}
{searchTooShort && flatOptions.length === 0 && ( renderMinSearch?.(state) ?? ( {labels?.minSearchLength?.(minSearchLength) ?? `Type at least ${minSearchLength} characters`} ) )} {isLoading && (renderLoading?.(state) ?? ( {labels?.loading ?? "Loading..."} ))} {!isLoading && hasError && (renderError?.(state) ?? ( {labels?.error ?? "Could not load options"} ))} {!isLoading && !hasError && !searchTooShort && flatOptions.length === 0 && !canCreate && (renderEmpty?.(state) ?? ( {labels?.empty ?? "No options found"} ))} {!isLoading && !hasError && canCreate && ( )} {!isLoading && !hasError && optionGroups.map((group, groupIndex) => (
{group.label && (
{group.label}
)} {group.options.map((option) => ( ))}
))}
) } function AsyncSelect< TValue extends string = string, TData = unknown, TOption extends AsyncSelectOption = AsyncSelectOption, >(props: AsyncSelectProps | AsyncSelectMultiModeProps) { if (props.isMulti) { const { isMulti, ...multiProps } = props return } return } export { AsyncSelect, AsyncMultiSelect }