import { memo, useCallback, useEffect, useState } from 'react'; import clsx from 'clsx'; import type { AutocompleteRenderGetTagProps } from '@mui/material/Autocomplete'; import type { FilterOptionsState } from '@mui/material/useAutocomplete'; import Tooltip from '@mui/material/Tooltip'; import { ASSETS_URL } from '../../../consts/common'; import { Chips } from '../../chips'; import type { ChipsProps } from '../../chips'; import { CustomIcon } from '../../custom-icon'; import { truncateWithEllipsis } from '../../../utils'; import { AutocompleteWithListbox } from './components/autocomplete-with-listbox'; import type { AutocompleteProps, AutocompleteOptionProps } from './types'; import createClasses from './styles'; /** * The autocomplete is a normal text input enhanced by a panel of suggested options. * It's meant to be an improved version of the "react-select" and "downshift" packages. * @link https://mui.com/material-ui/react-autocomplete/ */ const Autocomplete = (props: AutocompleteProps) => { const { defaultLimitTags = 2, showLimitTagsTooltip, size = 'medium', handleClear, handleCreate, handleDelete, listItemsMapFN, menuContentProps, ...otherProps } = props; const classes = createClasses({ multiple: otherProps.multiple, size }); const defaultValue = (typeof otherProps.defaultValue === 'string' && otherProps.defaultValue) || ''; const hasStringInputValue = !otherProps.multiple && typeof otherProps.value === 'string'; const [inputText, setInputText] = useState(defaultValue); useEffect(() => { if (hasStringInputValue) setInputText(otherProps.value as string); }, [hasStringInputValue, setInputText, otherProps.value]); const getLimitTagsTooltip: AutocompleteProps['getLimitTagsText'] = more => { const tooltipTitle = (otherProps.value as AutocompleteOptionProps[]) ?.slice(defaultLimitTags) .map(option => option?.name) .join(', '); return ( {`+${more}`} ); }; const onBlur = useCallback>( e => { if (inputText && otherProps.freeSolo) { handleCreate?.(e, [inputText] as unknown as AutocompleteOptionProps[]); setInputText(''); } }, [handleCreate, inputText, otherProps.freeSolo] ); const onChange = useCallback>( (e, v, reason) => { switch (reason) { case 'clear': if (hasStringInputValue) { setInputText(''); } handleClear?.(e); break; case 'createOption': handleCreate?.(e, v as AutocompleteOptionProps[]); break; case 'removeOption': handleDelete?.(e, v as AutocompleteOptionProps[]); break; } }, [handleClear, handleCreate, handleDelete, hasStringInputValue] ); const getOptSearchWords = (option: AutocompleteOptionProps) => { const { name, searchWords: _searchWords = [], additionalProps } = option; const searchWords = [..._searchWords, name]; if (additionalProps) searchWords.push( ...Object.entries(additionalProps) .filter(ent => typeof ent[1] === 'string') .map(ent => ent[1] as string) ); return searchWords; }; const filterOptions = ( options: AutocompleteOptionProps[], params: FilterOptionsState ) => { const inputValue = (hasStringInputValue ? inputText : params.inputValue).toLowerCase(); const opts = options.filter(option => { const optSearchWords = getOptSearchWords(option); const found = optSearchWords.some(word => word?.toString().toLowerCase().includes(inputValue) ); return found; }); return opts; }; const renderTags = ( tagValue: AutocompleteOptionProps[], getTagProps: AutocompleteRenderGetTagProps ) => { return tagValue.map((option: AutocompleteOptionProps, index) => { const tagProps: ChipsProps = getTagProps({ index }); return ( ); }); }; const clearIcon = ( ); return ( (typeof option !== 'string' && option.name?.toString()) || ''} inputText={inputText} isOptionEqualToValue={(o, v) => o.name === v.name} onBlur={onBlur} onChange={onChange} renderOption={(_, option) => option} renderTags={renderTags} size={size} {...otherProps} listItemsMapFN={listItemsMapFN} menuContentProps={menuContentProps} setInputText={setInputText} /> ); }; const m = memo(Autocomplete); export { m as Autocomplete };