153 lines
4.0 KiB
TypeScript
153 lines
4.0 KiB
TypeScript
import React, { useState, useEffect, useRef } from "react";
|
|
import { createPortal } from "react-dom";
|
|
|
|
interface DropdownProps {
|
|
header: string;
|
|
options: string[];
|
|
onSelect: (option: string) => void;
|
|
search?: boolean;
|
|
onClick?: () => void;
|
|
onChange?: () => void;
|
|
}
|
|
|
|
const RegularDropDown: React.FC<DropdownProps> = ({
|
|
header,
|
|
options,
|
|
onSelect,
|
|
search = true,
|
|
}) => {
|
|
const [isOpen, setIsOpen] = useState(false);
|
|
const [selectedOption, setSelectedOption] = useState<string | null>(null);
|
|
const [searchTerm, setSearchTerm] = useState("");
|
|
const [filteredOptions, setFilteredOptions] = useState<string[]>(options);
|
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
|
const [position, setPosition] = useState<{
|
|
top: number;
|
|
left: number;
|
|
width: number;
|
|
}>({
|
|
top: 0,
|
|
left: 0,
|
|
width: 0,
|
|
});
|
|
|
|
// Reset when closed
|
|
useEffect(() => {
|
|
if (!isOpen) {
|
|
setSelectedOption(null);
|
|
setSearchTerm("");
|
|
setFilteredOptions(options);
|
|
}
|
|
}, [isOpen, options]);
|
|
|
|
// Reset when header changes
|
|
useEffect(() => {
|
|
setSelectedOption(null);
|
|
setSearchTerm("");
|
|
setFilteredOptions(options);
|
|
}, [header, options]);
|
|
|
|
// Close if clicked outside
|
|
useEffect(() => {
|
|
const handleClickOutside = (event: MouseEvent) => {
|
|
if (
|
|
dropdownRef.current &&
|
|
!dropdownRef.current.contains(event.target as Node)
|
|
) {
|
|
setIsOpen(false);
|
|
}
|
|
};
|
|
document.addEventListener("click", handleClickOutside);
|
|
return () => document.removeEventListener("click", handleClickOutside);
|
|
}, []);
|
|
|
|
// Recalculate position when opening
|
|
useEffect(() => {
|
|
if (isOpen && dropdownRef.current) {
|
|
const rect = dropdownRef.current.getBoundingClientRect();
|
|
setPosition({
|
|
top: rect.bottom + window.scrollY,
|
|
left: rect.left + window.scrollX,
|
|
width: rect.width,
|
|
});
|
|
}
|
|
}, [isOpen]);
|
|
|
|
const toggleDropdown = () => setIsOpen((prev) => !prev);
|
|
|
|
const handleOptionClick = (option: string) => {
|
|
setSelectedOption(option);
|
|
onSelect(option);
|
|
setIsOpen(false);
|
|
};
|
|
|
|
const handleSearchChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
const term = event.target.value;
|
|
setSearchTerm(term);
|
|
setFilteredOptions(
|
|
options.filter((option) =>
|
|
option.toLowerCase().includes(term.toLowerCase())
|
|
)
|
|
);
|
|
};
|
|
|
|
return (
|
|
<div
|
|
className="regularDropdown-container"
|
|
ref={dropdownRef}
|
|
onPointerLeave={() => {
|
|
setIsOpen(false);
|
|
}}
|
|
>
|
|
{/* Header */}
|
|
<div className="dropdown-header flex-sb" onClick={toggleDropdown}>
|
|
<div className="key">{selectedOption || header}</div>
|
|
<div className="icon">▾</div>
|
|
</div>
|
|
|
|
{/* Options rendered in portal */}
|
|
{isOpen &&
|
|
createPortal(
|
|
<div
|
|
className="dropdown-options"
|
|
style={{
|
|
position: "absolute",
|
|
top: position.top,
|
|
left: position.left,
|
|
width: position.width,
|
|
zIndex: 9999,
|
|
}}
|
|
>
|
|
{search && (
|
|
<div className="dropdown-search">
|
|
<input
|
|
type="text"
|
|
placeholder="Search..."
|
|
value={searchTerm}
|
|
onChange={handleSearchChange}
|
|
/>
|
|
</div>
|
|
)}
|
|
{filteredOptions.length > 0 ? (
|
|
filteredOptions.map((option, index) => (
|
|
<div
|
|
className="option"
|
|
key={index}
|
|
onClick={() => handleOptionClick(option)}
|
|
title={option}
|
|
>
|
|
{option}
|
|
</div>
|
|
))
|
|
) : (
|
|
<div className="no-options">No options found</div>
|
|
)}
|
|
</div>,
|
|
document.body
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default RegularDropDown;
|