import React, { useEffect, useState, useRef, useCallback } from "react";
interface TodoItem {
id: number;
title: string;
completed: boolean;
}
const DropDownWithSearch = () => {
const [selectedOption, setSelectedOption] = useState<string>("");
const [data, setData] = useState<TodoItem[]>([]);
const [error, setError] = useState<string>("");
const [searchTerm, setSearchTerm] = useState<string>("");
const [filteredData, setFilteredData] = useState<TodoItem[]>([]);
const [isOpen, setIsOpen] = useState<boolean>(false);
const dropdownRef = useRef<HTMLDivElement>(null);
// ---------- FIXED & STABLE DEBOUNCE HOOK ----------
const useDebounce = (fn: (...args: any[]) => void, delay: number) => {
const timeoutRef = useRef<any | null>(null);
return useCallback(
(...args: any[]) => {
if (timeoutRef.current) clearTimeout(timeoutRef.current);
timeoutRef.current = setTimeout(() => fn(...args), delay);
},
[fn, delay]
);
};
// ---------------------------------------------------
// fetch data
useEffect(() => {
const fetchPost = async () => {
try {
const response = await fetch("https://jsonplaceholder.typicode.com/todos");
if (!response.ok) {
setError("Failed to fetch data");
return;
}
const postData: TodoItem[] = await response.json();
setData(postData);
setFilteredData(postData);
} catch (err) {
setError("An error occurred while fetching data");
}
};
fetchPost();
}, []);
// filter list
useEffect(() => {
if (searchTerm) {
const filtered = data.filter((item) =>
item.title.toLowerCase().includes(searchTerm.toLowerCase())
);
setFilteredData(filtered);
} else {
setFilteredData(data);
}
}, [searchTerm, data]);
// detect outside click
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setIsOpen(false);
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, []);
// triggered after debounce delay
const onSearchApply = (value: string) => {
console.log("search applied");
setSearchTerm(value);
setIsOpen(true);
};
// debounce wrapper
const onSearchChange = useDebounce((value: string) => {
onSearchApply(value);
}, 5000);
const handleSearchInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearchTerm(e?.target.value);
onSearchChange(e.target.value);
};
const onSelectOption = (title: string) => {
setSelectedOption(title);
setSearchTerm(title);
setIsOpen(false);
};
const toggleDropdown = () => setIsOpen(!isOpen);
return (
<div
ref={dropdownRef}
style={{
padding: "20px",
width: "350px",
position: "relative",
fontFamily: "'Segoe UI', Tahoma, Geneva, Verdana, sans-serif",
}}
>
<h3
style={{
marginBottom: "15px",
fontSize: "18px",
color: "red",
fontWeight: 600,
}}
>
Dropdown with Search
</h3>
<div
style={{
display: "flex",
alignItems: "center",
border: `1px solid ${isOpen ? "#007BFF" : "#ced4da"}`,
borderRadius: "6px",
padding: "10px",
cursor: "pointer",
boxShadow: isOpen ? "0 0 0 2px rgba(0, 123, 255, 0.2)" : "none",
}}
onClick={toggleDropdown}
>
<input
type="text"
placeholder={selectedOption || "Search or select an option..."}
value={searchTerm}
onChange={handleSearchInputChange}
onClick={(e) => {
e.stopPropagation();
setIsOpen(true);
}}
style={{
border: "none",
outline: "none",
flex: 1,
padding: "0",
fontSize: "15px",
}}
/>
<span style={{ marginLeft: "10px" }}>{isOpen ? "▲" : "▼"}</span>
</div>
{isOpen && filteredData.length > 0 && (
<ul
style={{
position: "absolute",
top: "calc(100% + 8px)",
left: 0,
right: 0,
maxHeight: "250px",
overflowY: "auto",
border: "1px solid #ced4da",
borderRadius: "6px",
margin: 0,
padding: 0,
listStyle: "none",
zIndex: 1000,
}}
>
{filteredData.map((each) => (
<li
key={each.id}
onClick={() => onSelectOption(each.title)}
style={{
padding: "12px 16px",
cursor: "pointer",
borderBottom:
filteredData[filteredData.length - 1].id === each.id
? "none"
: "1px solid #e9ecef",
backgroundColor:
selectedOption === each.title ? "#f1f8ff" : "transparent",
}}
>
{each.title}
</li>
))}
</ul>
)}
{error && (
<p style={{ color: "#dc3545", marginTop: "12px" }}>{error}</p>
)}
{selectedOption && !isOpen && (
<p style={{ marginTop: "12px", color: "#28a745" }}>
Selected: {selectedOption}
</p>
)}
</div>
);
};
export default DropDownWithSearch;
For further actions, you may consider blocking this person and/or reporting abuse
Top comments (0)