at-react-autocomplete-1
Version:
An auto complete dropdown component for React with TypeScript support.
187 lines (185 loc) • 6.05 kB
JavaScript
"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
};