import React, { forwardRef, Fragment, useEffect, useRef, useState, } from 'react'; import { Combobox as ComboboxLib, ComboboxInputProps as ComboboxLibInputProps, Transition, } from '@headlessui/react'; import { Icon, IconName } from '@/components/core/icon'; import { Flex } from '@/components/layout'; import { useDebounce } from '@/hooks/use-debounce'; import { Separator } from '../separator.styles'; import { cleanString } from './combobox.utils'; type ComboboxInputProps = { /** * If it's true, the list of options will be displayed */ open: boolean; /** * Name of the left icon to display in the input */ leftIcon: IconName; /** * Value to indicate it's invalid */ error?: boolean; } & ComboboxLibInputProps<'input', ComboboxItem>; const ComboboxInput = ({ open, leftIcon, error, ...props }: ComboboxInputProps) => (
selectedValue.label} {...props} />
); type ComboboxOptionProps = { option: ComboboxItem; }; const ComboboxOption = ({ option }: ComboboxOptionProps) => ( `relative cursor-default select-none py-2 px-3.5 text-slate11 rounded-xl mb-2 text-sm ${ active ? 'bg-slate5 text-slate12' : 'bg-transparent' }` } > {({ selected, active }) => ( {option.icon} {option.label} {selected && } )} ); export const NoResults = ({ css }: { css?: string }) => (
Nothing found.
); export type ComboboxItem = { /** * The key of the item. */ value: string; /** * The label to display of the item. */ label: string; /** * Optional icon to display on the left of the item. */ icon?: React.ReactNode; }; export type ComboboxProps = { /** * List of items to be displayed in the combobox. */ items: ComboboxItem[]; /** * The selected value of the combobox. */ selectedValue: ComboboxItem | undefined; /** * If true, the combobox will add the input if it doesn't exist in the list of items. */ withAutocomplete?: boolean; /** * Name of the left icon to display in the input. Defualt is "search". */ leftIcon?: IconName; /** * Callback when the selected value changes. */ onChange: (option: ComboboxItem) => void; /** * Function to handle the input blur */ onBlur?: () => void; /** * Value to indicate it's invalid */ error?: boolean; }; export const Combobox = forwardRef( ( { items, selectedValue = { value: '', label: '' }, withAutocomplete = false, leftIcon = 'search', onChange, onBlur, error = false, }, ref ) => { const [filteredItems, setFilteredItems] = useState([]); const [autocompleteItems, setAutocompleteItems] = useState( [] ); useEffect(() => { // If the selected value doesn't exist in the list of items, we add it if ( items.filter((item) => item === selectedValue).length === 0 && selectedValue.value !== undefined && autocompleteItems.length === 0 && withAutocomplete ) { setAutocompleteItems([selectedValue]); } }, [selectedValue]); useEffect(() => { setFilteredItems(items); }, [items]); const buttonRef = useRef(null); const handleSearch = useDebounce((searchValue: string) => { if (searchValue === '') { setFilteredItems(items); if (withAutocomplete) { setAutocompleteItems([]); handleComboboxChange({} as ComboboxItem); } } else { const filteredValues = items.filter((item) => cleanString(item.label).startsWith(cleanString(searchValue)) ); if (withAutocomplete && filteredValues.length === 0) { // If the search value doesn't exist in the list of items, we add it setAutocompleteItems([{ value: searchValue, label: searchValue }]); } setFilteredItems(filteredValues); } }, 200); const handleInputChange = (event: React.ChangeEvent) => { event.stopPropagation(); handleSearch(event.target.value); }; const handleInputClick = () => { buttonRef.current?.click(); }; const handleComboboxChange = (optionSelected: ComboboxItem) => { onChange(optionSelected); }; const handleLeaveTransition = () => { setFilteredItems(items); if (selectedValue.value === undefined && withAutocomplete) { setAutocompleteItems([]); handleComboboxChange({} as ComboboxItem); } }; return ( {({ open }) => (
{[...autocompleteItems, ...filteredItems].length === 0 || filteredItems === undefined ? ( ) : ( <> {autocompleteItems.length > 0 && Create new} {autocompleteItems.map( (autocompleteOption: ComboboxItem) => ( ) )} {autocompleteItems.length > 0 && filteredItems.length > 0 && ( )} {filteredItems.map((option: ComboboxItem) => ( ))} )}
)}
); } );