UNPKG

at-react-autocomplete-1

Version:

An auto complete dropdown component for React with TypeScript support.

187 lines (185 loc) 6.05 kB
"use client"; // src/Index.tsx import { useState, useEffect, useRef, useCallback } from "react"; import { jsx, jsxs } from "react/jsx-runtime"; function AutocompleteDropdown({ suggestions, onSelect, onInputChange, renderItem, getDisplayValue, placeholder = "Search...", isLoading = false, inputValue, setInputValue, minSearchLength = 2, debounceDelay = 300, className = "", inputClassName = "", onEnter }) { const [internalValue, setInternalValue] = useState(""); const value = inputValue !== void 0 ? inputValue : internalValue; const updateValue = setInputValue !== void 0 ? setInputValue : setInternalValue; const [showDropdown, setShowDropdown] = useState(false); const [highlightedIndex, setHighlightedIndex] = useState(-1); const dropdownRef = useRef(null); const suggestionsListRef = useRef(null); const inputRef = useRef(null); const abortControllerRef = useRef(null); const defaultGetDisplayValue = (item) => { if (typeof item === "string") return item; if (item && typeof item === "object" && "label" in item) { return String(item.label); } return ""; }; const getDisplay = getDisplayValue != null ? getDisplayValue : defaultGetDisplayValue; const render = renderItem != null ? renderItem : (item) => /* @__PURE__ */ jsx("span", { children: getDisplay(item) }); const debounce = useCallback( (func) => { let timeoutId; return (...args) => { clearTimeout(timeoutId); timeoutId = setTimeout(() => func(...args), debounceDelay); }; }, [debounceDelay] ); const debouncedInputChange = useCallback( debounce((val) => { if (abortControllerRef.current) { abortControllerRef.current.abort(); } abortControllerRef.current = new AbortController(); if (val.length >= minSearchLength) { onInputChange(val); } else { onInputChange(""); } }), [onInputChange, minSearchLength] ); const handleInputChange = (e) => { const val = e.target.value; updateValue(val); debouncedInputChange(val); setShowDropdown(val.length > 0); setHighlightedIndex(-1); }; const handleSuggestionClick = (item) => { var _a; onSelect(item); setShowDropdown(false); updateValue(getDisplay(item)); (_a = inputRef.current) == null ? void 0 : _a.focus(); }; const handleKeyDown = (e) => { if (!showDropdown) return; switch (e.key) { case "ArrowDown": e.preventDefault(); setHighlightedIndex( (prev) => prev < suggestions.length - 1 ? prev + 1 : prev ); scrollIntoView(highlightedIndex + 1); break; case "ArrowUp": e.preventDefault(); setHighlightedIndex((prev) => prev > 0 ? prev - 1 : 0); scrollIntoView(highlightedIndex - 1); break; case "Enter": e.preventDefault(); if (highlightedIndex >= 0 && highlightedIndex < suggestions.length) { const item = suggestions[highlightedIndex]; onSelect(item); setShowDropdown(false); updateValue(getDisplay(item)); } else { setShowDropdown(false); if (onEnter) { onEnter(value); } } break; case "Escape": e.preventDefault(); setShowDropdown(false); break; } }; const scrollIntoView = (index) => { if (suggestionsListRef.current && index >= 0 && index < suggestions.length) { const itemElement = suggestionsListRef.current.children[index]; if (itemElement) { itemElement.scrollIntoView({ block: "nearest" }); } } }; useEffect(() => { const handleClickOutside = (event) => { if (dropdownRef.current && !dropdownRef.current.contains(event.target)) { setShowDropdown(false); } }; document.addEventListener("mousedown", handleClickOutside); return () => { document.removeEventListener("mousedown", handleClickOutside); if (abortControllerRef.current) { abortControllerRef.current.abort(); } }; }, []); useEffect(() => { var _a; if (((_a = value == null ? void 0 : value.length) != null ? _a : 0) > 0 && suggestions.length > 0) { setShowDropdown(true); } else { setShowDropdown(false); } }, [suggestions, value]); useEffect(() => { setHighlightedIndex(-1); }, [suggestions]); return /* @__PURE__ */ jsxs("div", { className: `relative w-full border ${className}`, ref: dropdownRef, children: [ /* @__PURE__ */ jsx( "input", { type: "text", ref: inputRef, value, onChange: handleInputChange, onKeyDown: handleKeyDown, onFocus: () => { var _a; if (((_a = value == null ? void 0 : value.length) != null ? _a : 0) >= minSearchLength) { setShowDropdown(true); } }, className: `w-full focus:outline-none ${inputClassName}`, placeholder } ), isLoading && /* @__PURE__ */ jsx("div", { className: "absolute z-10 w-full p-2 border rounded-md shadow-lg", children: /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center p-2", children: /* @__PURE__ */ jsx("div", { className: "animate-spin rounded-full h-5 w-5 border-b-2 mr-2 " }) }) }), showDropdown && !isLoading && suggestions.length > 0 && /* @__PURE__ */ jsx("div", { className: "absolute z-10 w-full mt-2 border rounded-md shadow-lg max-h-60 overflow-y-auto", children: /* @__PURE__ */ jsx("ul", { ref: suggestionsListRef, children: suggestions.map((item, index) => /* @__PURE__ */ jsx( "li", { className: ` cursor-pointer ${highlightedIndex === index ? "bg-accent" : ""}`, onMouseDown: () => handleSuggestionClick(item), onMouseEnter: () => setHighlightedIndex(index), children: render(item) }, index )) }) }) ] }); } var Index_default = AutocompleteDropdown; export { Index_default as default };