I am building a reusable dropdown component in React that can be toggled both by clicking its button and by clicking on external elements (watchElements). The dropdown works fine individually, but when I render multiple instances of the dropdown, clicking any button or external element toggles all dropdowns at once.
I want each dropdown instance to manage its own state independently so that clicking on a specific button or external element toggles only that specific dropdown.
Here is the current code for the Dropdown component:
import React, { useState, useEffect, useRef } from "react";
mport Button from "./Button";
import { RiArrowDownCircleFill } from "@remixicon/react";
import { constants } from "../constants";
const Dropdown = ({
content,
img = {
url: "",
alt: ""
},
img_classes = "",
btn_classes = "",
className = "text-center",
content_wrapper_classes = "",
btn_child = (
<RiArrowDownCircleFill
size={constants.static_sytles.icon_size_sm}
className="text-white animate-bounce"
/>
),
children,
watchElements = []
}) => {
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef(null);
const toggleDropdown = (event) => {
// console.log(dropdownRef.current)
console.log('here is the event', event.target)
console.log(dropdownRef.current.contains(event.target))
if (dropdownRef.current && dropdownRef.current.contains(event.target)) {
setIsOpen((prev) => !prev);
}
};
const closeDropdown = () => {
setIsOpen(false);
};
const handleClickOutside = (event) => {
if (
dropdownRef.current &&
!dropdownRef.current.contains(event.target) &&
!watchElements.some(
(el) => el && el.contains && el.contains(event.target)
)
) {
closeDropdown();
}
};
useEffect(() => {
document.addEventListener("mousedown", handleClickOutside);
if (watchElements.length) {
watchElements.forEach((watchElement) => {
if (watchElement) {
watchElement.addEventListener("click", toggleDropdown);
}
});
}
return () => {
document.removeEventListener("mousedown", handleClickOutside);
if (watchElements.length) {
watchElements.forEach((watchElement) => {
if (watchElement) {
watchElement.removeEventListener("click", toggleDropdown);
}
});
}
};
}, [watchElements]);
return (
<div className={`${className}`}>
<Button
className={`${btn_classes} mt-3 flex items-center justify-center w-full`}
// onClick={toggleDropdown}
isDefault={false}
>
{btn_child}
</Button>
{/* Dropdown Menu */}
<div
ref={dropdownRef}
className={` ${content_wrapper_classes} right-0 top-full w-full bg-white ring-opacity-5 overflow-hidden transition-all duration-700 ease-in-out ${
isOpen ? "max-h-screen animate-flip-down" : "max-h-0"
}`}
>
{img.url && (
<img className={`${img_classes}`} src={img.url} alt={img.alt} />
)}
{children}
</div>
</div>
);
};
export default Dropdown;
Usage:
const containerRefs= React.useRef([]);
{constants.home_services.map((service, index) => (
<div
className="w-full relative"
key={index}
ref={(el) => (containerRefs.current[index] = el)}
>
<div
className={`${constants.static_sytles.bg_color_secondary_heavy} shadow-sm ${constants.static_sytles.shadow_secondary_heavy} px-4 py-2 rounded-md gap-y-2 border-2 border-[#9b9fab] flex justify-between items-center cursor-pointer`}
>
<h2 className="text-xl md:text-2xl font-light">{service.name}</h2>
<img
className="h-14 w-20 rounded-sm"
src={service.ImgUrl}
alt={service.name}
/>
</div>
<Dropdown
watchElements={containerRefs.current}
img={{ url: service.ImgUrl, alt: service.name }}
btn_classes=""
className=""
img_classes="w-full rounded-sm h-80"
content_wrapper_classes="rounded-lg"
btn_child={
<RiArrowDropDownFill
className={`${constants.static_sytles.icon_color_secondary} ${constants.static_sytles.icon_size_sm}`}
/>
}
>
<p className="mt-2 p-3">
Lorem ipsum dolor sit amet consectetur, adipisicing elit. Accusantium,
quisquam voluptatem odio sapiente mollitia, autem nostrum quod vitae
aliquam quam non exercitationem consequatur architecto distinctio
numquam rerum odit. Nemo, eaque!
</p>
</Dropdown>
</div>
))}
I have tried to use the watchElements prop to hold the references of external elements to attach eventListners and use them to toggle dropdowns