import React from 'react'; import { Label, Listbox, Transition, ListboxButton, ListboxOption, ListboxOptions, } from '@headlessui/react'; import type { Option, OptionWithIcon, DropdownValueSetter } from '~/common'; import CheckMark from '~/components/svg/CheckMark'; import { useMultiSearch } from './MultiSearch'; import { useLocalize } from '~/hooks'; import { cn } from '~/utils/'; type SelectDropDownProps = { id?: string; title?: string; disabled?: boolean; value: string | null | Option | OptionWithIcon; setValue: DropdownValueSetter | ((value: string) => void); tabIndex?: number; availableValues: string[] | Option[] | OptionWithIcon[]; emptyTitle?: boolean; showAbove?: boolean; showLabel?: boolean; iconSide?: 'left' | 'right'; optionIconSide?: 'left' | 'right'; renderOption?: () => React.ReactNode; containerClassName?: string; currentValueClass?: string; optionsListClass?: string; optionsClass?: string; subContainerClassName?: string; className?: string; placeholder?: string; searchClassName?: string; searchPlaceholder?: string; showOptionIcon?: boolean; }; function getOptionText(option: string | Option | OptionWithIcon): string { if (typeof option === 'string') { return option; } if ('label' in option) { return option.label ?? ''; } if ('value' in option) { return (option.value ?? '') + ''; } return ''; } function SelectDropDown({ title: _title, value, disabled, setValue, availableValues, showAbove = false, showLabel = true, emptyTitle = false, iconSide = 'right', optionIconSide = 'left', placeholder, containerClassName, optionsListClass, optionsClass, currentValueClass, subContainerClassName, className, renderOption, searchClassName, searchPlaceholder, showOptionIcon = false, }: SelectDropDownProps) { const localize = useLocalize(); const transitionProps = { className: 'top-full mt-3' }; if (showAbove) { transitionProps.className = 'bottom-full mb-3'; } let title = _title; if (emptyTitle) { title = ''; } else if (!(title ?? '')) { title = localize('com_ui_model'); } // Detemine if we should to convert this component into a searchable select. If we have enough elements, a search // input will appear near the top of the menu, allowing correct filtering of different model menu items. This will // reset once the component is unmounted (as per a normal search) const [filteredValues, searchRender] = useMultiSearch({ availableOptions: availableValues, placeholder: searchPlaceholder, getTextKeyOverride: (option) => getOptionText(option).toUpperCase(), className: searchClassName, disabled, }); const hasSearchRender = searchRender != null; const options = hasSearchRender ? filteredValues : availableValues; const renderIcon = showOptionIcon && value != null && (value as OptionWithIcon).icon != null; return (
{({ open }) => ( <> {' '} {showLabel && ( )} {!showLabel && !emptyTitle && ( {title}: )} {renderIcon && optionIconSide !== 'right' && ( {(value as OptionWithIcon).icon} )} {renderIcon && ( {(value as OptionWithIcon).icon} )} {(() => { if (!value) { return {placeholder}; } if (typeof value !== 'string') { return value.label ?? ''; } return value; })()} {renderOption && ( {renderOption()} )} {searchRender} {options.map((option: string | Option, i: number) => { if (!option) { return null; } const currentLabel = typeof option === 'string' ? option : option.label ?? option.value ?? ''; const currentValue = typeof option === 'string' ? option : option.value ?? ''; const currentIcon = typeof option === 'string' ? null : (option.icon as React.ReactNode) ?? null; let activeValue: string | number | null | Option = value; if (typeof activeValue !== 'string') { activeValue = activeValue?.value ?? ''; } return ( cn( 'group relative flex h-[42px] cursor-pointer select-none items-center overflow-hidden pl-3 pr-9 text-gray-800 hover:bg-gray-20 dark:text-white dark:hover:bg-gray-600', active ? 'bg-surface-active text-text-primary' : '', optionsClass ?? '', ) } > {currentIcon != null && ( {currentIcon} )} {currentLabel} {currentValue === activeValue && ( )} ); })} )}
); } export default SelectDropDown;