diff --git a/src/components/common/HoverPopup.jsx b/src/components/common/HoverPopup.jsx index 232ced5d..0afb63e0 100644 --- a/src/components/common/HoverPopup.jsx +++ b/src/components/common/HoverPopup.jsx @@ -1,14 +1,14 @@ import React, { useEffect, useRef } from "react"; import { useDispatch, useSelector } from "react-redux"; -import { - closePopup, - openPopup, - togglePopup, -} from "../../slices/localVariablesSlice"; +import { closePopup, openPopup, togglePopup } from "../../slices/localVariablesSlice"; /** - * align: "auto" | "left" | "right" - * boundaryRef: optional ref to the drawer/container element to use as boundary + * Props: + * id, title, content, children + * className = "" + * Mode = "hover" | "click" + * align = "auto" | "left" | "right" + * boundaryRef = optional ref to DOM element to constrain popup within (getBoundingClientRect used) */ const HoverPopup = ({ id, @@ -21,7 +21,7 @@ const HoverPopup = ({ boundaryRef = null, }) => { const dispatch = useDispatch(); - const visible = useSelector((s) => s.localVariables.popups[id] || false); + const visible = useSelector((s) => s.localVariables.popups?.[id] || false); const triggerRef = useRef(null); const popupRef = useRef(null); @@ -39,7 +39,7 @@ const HoverPopup = ({ } }; - // Close on outside click when using click mode + // Close on outside click when in click mode useEffect(() => { if (Mode !== "click" || !visible) return; @@ -58,38 +58,61 @@ const HoverPopup = ({ return () => document.removeEventListener("click", handler); }, [Mode, visible, dispatch, id]); + // Positioning: compute left/top relative to popup.offsetParent, but clamp using boundaryRef (if provided) useEffect(() => { if (!visible || !popupRef.current || !triggerRef.current) return; requestAnimationFrame(() => { const popup = popupRef.current; - const boundaryEl = (boundaryRef && boundaryRef.current) || popup.parentElement; - if (!boundaryEl) return; + const trigger = triggerRef.current; + const triggerRect = trigger.getBoundingClientRect(); + const popupRect = popup.getBoundingClientRect(); + + // Find offsetParent (element the absolute popup is positioned relative to) + const offsetParent = popup.offsetParent || document.documentElement; + const offsetParentRect = offsetParent.getBoundingClientRect(); + + // Global positions we want to clamp against (in viewport coordinates) + let desiredLeftGlobal; + if (align === "left") { + desiredLeftGlobal = triggerRect.left; + } else if (align === "right") { + desiredLeftGlobal = triggerRect.right - popupRect.width; + } else { + // auto / center: center popup under trigger + desiredLeftGlobal = triggerRect.left + triggerRect.width / 2 - popupRect.width / 2; + } + + // Compute boundaries in global coordinates (use boundaryRef if provided, else offsetParent) + const boundaryEl = (boundaryRef && boundaryRef.current) || offsetParent; const boundaryRect = boundaryEl.getBoundingClientRect(); - const triggerRect = triggerRef.current.getBoundingClientRect(); - popup.style.left = ""; + + // Clamp desiredLeftGlobal to boundaryRect + const minLeftGlobal = boundaryRect.left; + const maxLeftGlobal = boundaryRect.right - popupRect.width; + let clampedLeftGlobal = Math.min(Math.max(desiredLeftGlobal, minLeftGlobal), maxLeftGlobal); + + // Convert to coordinates relative to offsetParent + const leftRelativeToOffsetParent = clampedLeftGlobal - offsetParentRect.left; + + // Compute top: place popup just below trigger with small gap + const gap = 8; + const desiredTopGlobal = triggerRect.bottom + gap; + // Convert to offsetParent-relative top + const topRelativeToOffsetParent = desiredTopGlobal - offsetParentRect.top; + + // Apply styles + popup.style.left = `${Math.round(leftRelativeToOffsetParent)}px`; + popup.style.top = `${Math.round(topRelativeToOffsetParent)}px`; popup.style.right = ""; popup.style.transform = ""; - popup.style.top = ""; - - const popupRect = popup.getBoundingClientRect(); - const triggerCenterX = triggerRect.left + triggerRect.width / 2 - boundaryRect.left; - let left = triggerCenterX - popupRect.width / 2; - - // Clamp to boundaries - left = Math.max(0, Math.min(left, boundaryRect.width - popupRect.width)); - popup.style.left = `${left}px`; }); }, [visible, align, boundaryRef]); return ( -
+
e.stopPropagation()} > @@ -118,7 +141,6 @@ const HoverPopup = ({ )}
); - }; export default HoverPopup;