121 lines
4.6 KiB
JavaScript
121 lines
4.6 KiB
JavaScript
import React, { useState, useEffect, useRef } from 'react';
|
|
import { Search, ChevronDown, Check } from 'lucide-react';
|
|
|
|
const AutocompleteInput = ({
|
|
label,
|
|
value,
|
|
onChange,
|
|
options = [],
|
|
onSelect,
|
|
placeholder = "Pesquisar...",
|
|
displayKey = "label", // key to show in list
|
|
valueKey = "value", // key to use for value
|
|
searchKeys = [], // keys to search in. if empty, searches displayKey
|
|
className = "",
|
|
disabled = false,
|
|
required = false
|
|
}) => {
|
|
const [isOpen, setIsOpen] = useState(false);
|
|
const [searchTerm, setSearchTerm] = useState(value || '');
|
|
const wrapperRef = useRef(null);
|
|
|
|
useEffect(() => {
|
|
setSearchTerm(value || '');
|
|
}, [value]);
|
|
|
|
useEffect(() => {
|
|
const handleClickOutside = (event) => {
|
|
if (wrapperRef.current && !wrapperRef.current.contains(event.target)) {
|
|
setIsOpen(false);
|
|
}
|
|
};
|
|
document.addEventListener('mousedown', handleClickOutside);
|
|
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
}, []);
|
|
|
|
const filteredOptions = options.filter(option => {
|
|
if (!searchTerm) return true;
|
|
const term = searchTerm.toLowerCase();
|
|
|
|
// If searchKeys provided, search in those
|
|
if (searchKeys.length > 0) {
|
|
return searchKeys.some(key => {
|
|
const val = option[key];
|
|
return val && String(val).toLowerCase().includes(term);
|
|
});
|
|
}
|
|
|
|
// Otherwise search in displayKey or direct string
|
|
const label = typeof option === 'object' ? option[displayKey] : option;
|
|
return String(label).toLowerCase().includes(term);
|
|
});
|
|
|
|
const handleSelect = (option) => {
|
|
const displayVal = typeof option === 'object' ? option[displayKey] : option;
|
|
const actualVal = typeof option === 'object' ? (option[valueKey] || option[displayKey]) : option;
|
|
|
|
setSearchTerm(displayVal);
|
|
onChange(displayVal); // Update the input value
|
|
if (onSelect) onSelect(option); // Trigger specific selection logic
|
|
setIsOpen(false);
|
|
};
|
|
|
|
return (
|
|
<div className={`space-y-1.5 relative ${className}`} ref={wrapperRef}>
|
|
{label && (
|
|
<label className="text-[10px] uppercase font-bold text-slate-500 dark:text-slate-400 tracking-wider ml-1">
|
|
{label} {required && <span className="text-red-500">*</span>}
|
|
</label>
|
|
)}
|
|
<div className="relative">
|
|
<input
|
|
type="text"
|
|
className="w-full bg-slate-50 dark:bg-[#141414] border border-slate-200 dark:border-[#333] rounded-lg pl-3 pr-8 py-2 text-sm text-slate-700 dark:text-slate-200 focus:outline-none focus:border-emerald-500 focus:ring-1 focus:ring-emerald-500 transition-all placeholder:text-slate-400 dark:placeholder:text-slate-700"
|
|
placeholder={placeholder}
|
|
value={searchTerm}
|
|
onChange={(e) => {
|
|
setSearchTerm(e.target.value);
|
|
onChange(e.target.value);
|
|
setIsOpen(true);
|
|
}}
|
|
onFocus={() => setIsOpen(true)}
|
|
disabled={disabled}
|
|
/>
|
|
<ChevronDown
|
|
size={16}
|
|
className={`absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 transition-transform duration-200 ${isOpen ? 'rotate-180' : ''}`}
|
|
/>
|
|
|
|
{isOpen && filteredOptions.length > 0 && (
|
|
<ul className="absolute z-50 w-full mt-1 bg-white dark:bg-[#1c1c1c] border border-slate-200 dark:border-[#333] rounded-lg shadow-xl max-h-60 overflow-auto custom-scrollbar">
|
|
{filteredOptions.map((option, index) => {
|
|
const display = typeof option === 'object' ? option[displayKey] : option;
|
|
const isSelected = display === value;
|
|
|
|
return (
|
|
<li
|
|
key={index}
|
|
className={`px-3 py-2 text-sm cursor-pointer flex items-center justify-between hover:bg-slate-100 dark:hover:bg-[#2a2a2a] ${
|
|
isSelected ? 'bg-emerald-50 dark:bg-emerald-900/20 text-emerald-600 dark:text-emerald-400 font-medium' : 'text-slate-700 dark:text-slate-300'
|
|
}`}
|
|
onClick={() => handleSelect(option)}
|
|
>
|
|
<span>{display}</span>
|
|
{isSelected && <Check size={14} />}
|
|
</li>
|
|
);
|
|
})}
|
|
</ul>
|
|
)}
|
|
{isOpen && filteredOptions.length === 0 && searchTerm && (
|
|
<div className="absolute z-50 w-full mt-1 bg-white dark:bg-[#1c1c1c] border border-slate-200 dark:border-[#333] rounded-lg shadow-xl p-3 text-sm text-slate-500 text-center">
|
|
Sem resultados
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default AutocompleteInput;
|