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) => (
))}
>
)}
)}
);
}
);