import React, { useEffect, useRef } from "react"; import { useDispatch, useSelector } from "react-redux"; import { closePopup, openPopup, togglePopup, } from "../../slices/localVariablesSlice"; /** * align: "auto" | "left" | "right" * boundaryRef: optional ref to the drawer/container element to use as boundary */ const HoverPopup = ({ id, title, content, children, className = "", Mode = "hover", align = "auto", boundaryRef = null, }) => { const dispatch = useDispatch(); const visible = useSelector((s) => s.localVariables.popups[id] || false); const triggerRef = useRef(null); const popupRef = useRef(null); const handleMouseEnter = () => { if (Mode === "hover") dispatch(openPopup(id)); }; const handleMouseLeave = () => { if (Mode === "hover") dispatch(closePopup(id)); }; const handleClick = (e) => { if (Mode === "click") { e.stopPropagation(); dispatch(togglePopup(id)); } }; // Close on outside click when using click mode useEffect(() => { if (Mode !== "click" || !visible) return; const handler = (e) => { if ( popupRef.current && !popupRef.current.contains(e.target) && triggerRef.current && !triggerRef.current.contains(e.target) ) { dispatch(closePopup(id)); } }; document.addEventListener("click", handler); return () => document.removeEventListener("click", handler); }, [Mode, visible, dispatch, id]); // Positioning effect: respects align prop and stays inside boundary (drawer) useEffect(() => { if (!visible || !popupRef.current || !triggerRef.current) return; // run in next frame so DOM/layout settles requestAnimationFrame(() => { const popup = popupRef.current; // choose boundary: provided boundaryRef or nearest positioned parent (popup.parentElement) const boundaryEl = (boundaryRef && boundaryRef.current) || popup.parentElement; if (!boundaryEl) return; const boundaryRect = boundaryEl.getBoundingClientRect(); const triggerRect = triggerRef.current.getBoundingClientRect(); // reset styles first popup.style.left = ""; popup.style.right = ""; popup.style.transform = ""; popup.style.top = ""; const popupRect = popup.getBoundingClientRect(); const parentRect = boundaryRect; // alias // Convert trigger center to parent coordinates const triggerCenterX = triggerRect.left + triggerRect.width / 2 - parentRect.left; // preferred left so popup center aligns to trigger center: const preferredLeft = triggerCenterX - popupRect.width / 2; // Helpers to set styles in parent's coordinate system: const setLeft = (leftPx) => { popup.style.left = `${leftPx}px`; popup.style.right = "auto"; popup.style.transform = "none"; }; const setRight = (rightPx) => { popup.style.left = "auto"; popup.style.right = `${rightPx}px`; popup.style.transform = "none"; }; // If user forced align: if (align === "left") { // align popup's left to parent's left (0) setLeft(0); return; } if (align === "right") { // align popup's right to parent's right (0) setRight(0); return; } if (align === "center") { popup.style.left = "50%"; popup.style.right = "auto"; popup.style.transform = "translateX(-50%)"; return; } // align === "auto": try preferred centered position, but flip fully if overflow // clamp preferredLeft to boundaries so it doesn't render partially outside const leftIfCentered = Math.max( 0, Math.min(preferredLeft, parentRect.width - popupRect.width) ); // if centered fits, use it if (leftIfCentered === preferredLeft) { setLeft(leftIfCentered); return; } // if centering would overflow right -> stick popup fully to left (left=0) if (preferredLeft > parentRect.width - popupRect.width) { // place popup so its right aligns to parent's right // i.e., left = parent width - popup width setLeft(parentRect.width - popupRect.width); return; } // if centering would overflow left -> stick popup fully to left=0 if (preferredLeft < 0) { setLeft(0); return; } // fallback center setLeft(leftIfCentered); }); }, [visible, align, boundaryRef]); return (