'use client' import React, { useState, useRef, useEffect } from 'react' import { classNames } from '../utils/classNames' export interface AutocompleteRenderInputParams { value: string onChange: (e: React.ChangeEvent) => void onFocus: () => void onKeyDown: (e: React.KeyboardEvent) => void disabled: boolean } export interface AutocompleteRenderOptionState { index: number } export interface AutocompleteProps extends Omit, 'onChange'> { options?: T[] value?: T | T[] | null onChange?: (event: React.SyntheticEvent | null, value: T | T[] | null) => void inputValue?: string onInputChange?: (event: React.ChangeEvent, value: string) => void getOptionLabel?: (option: T) => string renderOption?: (option: T, state: AutocompleteRenderOptionState) => React.ReactNode renderInput?: (params: AutocompleteRenderInputParams) => React.ReactNode multiple?: boolean freeSolo?: boolean disabled?: boolean loading?: boolean loadingText?: string noOptionsText?: string placeholder?: string } export function Autocomplete({ options = [], value, onChange, inputValue, onInputChange, getOptionLabel = (option: any) => option?.label ?? option ?? '', renderOption, renderInput, multiple = false, freeSolo = false, disabled = false, loading = false, loadingText = 'Loading…', noOptionsText = 'No options', placeholder, className, ...props }: AutocompleteProps) { const [open, setOpen] = useState(false) const [internalInputValue, setInternalInputValue] = useState('') const [highlightedIndex, setHighlightedIndex] = useState(-1) const inputRef = useRef(null) const listRef = useRef(null) const controlledInputValue = inputValue ?? internalInputValue const filteredOptions = options.filter((option) => getOptionLabel(option).toLowerCase().includes(controlledInputValue.toLowerCase()) ) const handleInputChange = (e: React.ChangeEvent) => { const newValue = e.target.value if (onInputChange) { onInputChange(e, newValue) } else { setInternalInputValue(newValue) } setOpen(true) } const handleOptionClick = (option: T) => { if (multiple) { const currentValue = (value as T[]) || [] const newValue = [...currentValue, option] onChange?.(null, newValue) } else { onChange?.(null, option) setInternalInputValue(getOptionLabel(option)) } setOpen(false) } const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'ArrowDown') { e.preventDefault() setHighlightedIndex((prev) => Math.min(prev + 1, filteredOptions.length - 1)) } else if (e.key === 'ArrowUp') { e.preventDefault() setHighlightedIndex((prev) => Math.max(prev - 1, 0)) } else if (e.key === 'Enter' && highlightedIndex >= 0) { e.preventDefault() const selectedOption = filteredOptions[highlightedIndex] if (selectedOption !== undefined) { handleOptionClick(selectedOption) } } else if (e.key === 'Escape') { setOpen(false) } } useEffect(() => { const handleClickOutside = (e: MouseEvent) => { if (inputRef.current && !inputRef.current.contains(e.target as Node)) { setOpen(false) } } document.addEventListener('mousedown', handleClickOutside) return () => document.removeEventListener('mousedown', handleClickOutside) }, []) const defaultRenderInput = (params: AutocompleteRenderInputParams) => ( ) return (
{multiple && Array.isArray(value) && value.length > 0 && (
{value.map((item, index) => ( {getOptionLabel(item)} ))}
)} {renderInput ? ( renderInput({ value: controlledInputValue, onChange: handleInputChange, onFocus: () => setOpen(true), onKeyDown: handleKeyDown, disabled, }) ) : ( defaultRenderInput({ value: controlledInputValue, onChange: handleInputChange, onFocus: () => setOpen(true), onKeyDown: handleKeyDown, disabled, }) )}
{open && (
    {loading ? (
  • {loadingText}
  • ) : filteredOptions.length === 0 ? (
  • {noOptionsText}
  • ) : ( filteredOptions.map((option, index) => (
  • handleOptionClick(option)} onMouseEnter={() => setHighlightedIndex(index)} > {renderOption ? renderOption(option, { index }) : getOptionLabel(option)}
  • )) )}
)}
) } export default Autocomplete