Adding HoverPopup effect.

This commit is contained in:
Kartik Sharma 2025-11-29 12:47:07 +05:30
parent 38a1b140fb
commit c1ac69a292
3 changed files with 62 additions and 64 deletions

View File

@ -205,7 +205,7 @@ const TaskReportList = () => {
id="total_pending_task" id="total_pending_task"
title="Total Pending Task" title="Total Pending Task"
content={ content={
<div className="text-wrap" style={{ maxWidth: "200px" }}> <div className="text-wrap" style={{ minWidth: "200px" }}>
This shows the total pending tasks for each activity on that date. This shows the total pending tasks for each activity on that date.
</div> </div>
} }

View File

@ -69,7 +69,7 @@ const ManageJobTicket = ({ Job }) => {
id="STATUS_CHANEG" id="STATUS_CHANEG"
Mode="click" Mode="click"
className="" className=""
align="right" align="left"
content={ content={
<ChangeStatus <ChangeStatus
statusId={data?.status?.id} statusId={data?.status?.id}
@ -149,7 +149,8 @@ const ManageJobTicket = ({ Job }) => {
<HoverPopup <HoverPopup
id="BRANCH_DETAILS" id="BRANCH_DETAILS"
Mode="click" Mode="click"
align="auto" align="right"
minWidth="340px"
boundaryRef={drawerRef} boundaryRef={drawerRef}
content={<BranchDetails branch={data?.projectBranch?.id} />} content={<BranchDetails branch={data?.projectBranch?.id} />}
> >

View File

@ -1,15 +1,11 @@
import React, { useEffect, useRef } from "react"; import React, { useEffect, useRef } from "react";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { closePopup, openPopup, togglePopup } from "../../slices/localVariablesSlice"; import {
closePopup,
openPopup,
togglePopup,
} from "../../slices/localVariablesSlice";
/**
* 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 = ({ const HoverPopup = ({
id, id,
title, title,
@ -17,29 +13,26 @@ const HoverPopup = ({
children, children,
className = "", className = "",
Mode = "hover", Mode = "hover",
align = "auto", align = "auto", // <-- dynamic placement
minWidth = "250px",
maxWidth = "350px",
boundaryRef = null, boundaryRef = null,
}) => { }) => {
const dispatch = useDispatch(); 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 triggerRef = useRef(null);
const popupRef = useRef(null); const popupRef = useRef(null);
const handleMouseEnter = () => { const handleMouseEnter = () => Mode === "hover" && dispatch(openPopup(id));
if (Mode === "hover") dispatch(openPopup(id)); const handleMouseLeave = () => Mode === "hover" && dispatch(closePopup(id));
};
const handleMouseLeave = () => {
if (Mode === "hover") dispatch(closePopup(id));
};
const handleClick = (e) => { const handleClick = (e) => {
if (Mode === "click") { if (Mode !== "click") return;
e.stopPropagation(); e.stopPropagation();
dispatch(togglePopup(id)); dispatch(togglePopup(id));
}
}; };
// Close on outside click when in click mode // Close popup when clicking outside (click mode)
useEffect(() => { useEffect(() => {
if (Mode !== "click" || !visible) return; if (Mode !== "click" || !visible) return;
@ -56,9 +49,9 @@ const HoverPopup = ({
document.addEventListener("click", handler); document.addEventListener("click", handler);
return () => document.removeEventListener("click", handler); return () => document.removeEventListener("click", handler);
}, [Mode, visible, dispatch, id]); }, [visible, Mode, id, dispatch]);
// Positioning: compute left/top relative to popup.offsetParent, but clamp using boundaryRef (if provided) // ---------- DYNAMIC POSITIONING LOGIC ----------
useEffect(() => { useEffect(() => {
if (!visible || !popupRef.current || !triggerRef.current) return; if (!visible || !popupRef.current || !triggerRef.current) return;
@ -66,50 +59,55 @@ const HoverPopup = ({
const popup = popupRef.current; const popup = popupRef.current;
const trigger = triggerRef.current; const trigger = triggerRef.current;
const boundaryEl =
(boundaryRef && boundaryRef.current) || popup.parentElement;
const boundaryRect = boundaryEl.getBoundingClientRect();
const triggerRect = trigger.getBoundingClientRect(); const triggerRect = trigger.getBoundingClientRect();
const popupRect = popup.getBoundingClientRect(); const popupRect = popup.getBoundingClientRect();
// Find offsetParent (element the absolute popup is positioned relative to) let left;
const offsetParent = popup.offsetParent || document.documentElement;
const offsetParentRect = offsetParent.getBoundingClientRect();
// Global positions we want to clamp against (in viewport coordinates) // AUTO ALIGN (smart)
let desiredLeftGlobal; if (align === "auto") {
if (align === "left") { const center =
desiredLeftGlobal = triggerRect.left; triggerRect.left +
} else if (align === "right") { triggerRect.width / 2 -
desiredLeftGlobal = triggerRect.right - popupRect.width; boundaryRect.left -
} else { popupRect.width / 2;
// auto / center: center popup under trigger
desiredLeftGlobal = triggerRect.left + triggerRect.width / 2 - popupRect.width / 2; left = Math.max(
0,
Math.min(center, boundaryRect.width - popupRect.width)
);
} }
// Compute boundaries in global coordinates (use boundaryRef if provided, else offsetParent) // LEFT ALIGN
const boundaryEl = (boundaryRef && boundaryRef.current) || offsetParent; else if (align === "left") {
const boundaryRect = boundaryEl.getBoundingClientRect(); left = triggerRect.left - boundaryRect.left;
if (left + popupRect.width > boundaryRect.width) {
left = boundaryRect.width - popupRect.width; // clamp right
}
}
// Clamp desiredLeftGlobal to boundaryRect // RIGHT ALIGN
const minLeftGlobal = boundaryRect.left; else if (align === "right") {
const maxLeftGlobal = boundaryRect.right - popupRect.width; left =
let clampedLeftGlobal = Math.min(Math.max(desiredLeftGlobal, minLeftGlobal), maxLeftGlobal); triggerRect.left +
triggerRect.width -
boundaryRect.left -
popupRect.width;
// Convert to coordinates relative to offsetParent if (left < 0) left = 0; // clamp left
const leftRelativeToOffsetParent = clampedLeftGlobal - offsetParentRect.left; }
// Compute top: place popup just below trigger with small gap popup.style.left = `${left}px`;
const gap = 8; popup.style.top = `100%`;
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 = "";
}); });
}, [visible, align, boundaryRef]); }, [visible, align, boundaryRef]);
// ------------------------------------------------
return ( return (
<div className="d-inline-block position-relative" style={{ overflow: "visible" }}> <div className="d-inline-block position-relative" style={{ overflow: "visible" }}>
<div <div
@ -128,15 +126,14 @@ const HoverPopup = ({
className={`hover-popup bg-white border rounded shadow-sm p-3 position-absolute mt-2 ${className}`} className={`hover-popup bg-white border rounded shadow-sm p-3 position-absolute mt-2 ${className}`}
style={{ style={{
zIndex: 2000, zIndex: 2000,
minWidth: "200px", minWidth,
maxWidth: "300px", maxWidth,
wordWrap: "break-word", wordWrap: "break-word",
// we set left/top from JS; ensure positionAbsolute context exists via offsetParent
}} }}
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
> >
{title && <h6 className="fw-semibold mb-2">{title}</h6>} {title && <h6 className="fw-semibold mb-2">{title}</h6>}
<div>{content}</div> {content}
</div> </div>
)} )}
</div> </div>