Here I need to implement a model like this and need some animation with slide-down
So my concern is for wrapping the button. I have to use a separate div and model for another div.
So I need to get to know if it is possible to wrap the icon and show the modal in only one div using clip path because now I am facing the problem in the slide-down animation.
If you closely see, I have got some slight border below in the wrapped dive at the time of animation, and I have achieved the box shadow in the model the same way it should be there in the wrapped div. How do I achieve this?
my code is reflecting
jsx code for that model
import React, {
useEffect,
useRef,
useState,
useCallback,
forwardRef,
} from "react";
import { createPortal } from "react-dom";
import "./MiniEditModal.scss";
interface MiniEditModalProps {
anchorRef: React.RefObject<HTMLButtonElement>;
headerProps?: { text: string; color: string; bgColor: string };
border?: string;
childContent: JSX.Element;
cancelButtonProps: { text: string; onClick: () => void };
proceedButtonProps: { text: string; onClick: () => void };
footerContent?: JSX.Element;
showBackdrop?: boolean;
isWrapped?: boolean;
isAnimated?: boolean;
placement?: "left" | "center" | "right";
}
const MiniModel = forwardRef<HTMLDivElement, MiniEditModalProps>(
(
{
anchorRef,
headerProps,
border,
childContent,
cancelButtonProps,
proceedButtonProps,
footerContent,
showBackdrop = false,
isWrapped = true,
isAnimated = false,
placement = "left", // default placement is bottom
},
ref
) => {
const [modalPosition, setModalPosition] = useState({
top: 0,
left: 0,
right: 0,
});
const [isVisible, setIsVisible] = useState(false);
const modalRef = useRef<HTMLDivElement>(null);
// Simplified positioning based on top or bottom placement
const calculatePosition = useCallback(() => {
if (anchorRef.current && modalRef.current) {
const buttonRect = anchorRef.current.getBoundingClientRect();
const modalRect = modalRef.current.getBoundingClientRect();
const right = buttonRect.right;
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
let top = buttonRect.bottom + window.scrollY;
let left;
switch (placement) {
case "left":
left = buttonRect.left;
break;
case "right":
left = buttonRect.right - modalRect.width;
break;
case "center":
left = buttonRect.left + buttonRect.width / 2 - modalRect.width / 2;
break;
default:
left = buttonRect.left;
}
// Adjust modal position if it overflows the viewport
if (left + modalRect.width > viewportWidth) {
left = viewportWidth - modalRect.width - 10; // Align to the right edge with padding
} else if (left < 0) {
left = 10; // Align to the left edge with padding
}
if (top + modalRect.height > viewportHeight) {
top = buttonRect.top - modalRect.height; // Place above the button if there's no space below
}
setModalPosition({ top, left, right });
}
}, [anchorRef, placement]);
const closeModal = useCallback(() => {
cancelButtonProps.onClick();
document.body.style.overflow = "";
}, [cancelButtonProps]);
useEffect(() => {
calculatePosition();
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape") {
closeModal();
} else if (event.key === "Enter") {
proceedButtonProps.onClick();
}
};
document.addEventListener("keydown", handleKeyDown);
return () => {
document.removeEventListener("keydown", handleKeyDown);
};
}, [calculatePosition, closeModal, anchorRef, proceedButtonProps]);
useEffect(() => {
const timeoutId = setTimeout(() => setIsVisible(true), 100);
return () => clearTimeout(timeoutId);
}, []);
if (!anchorRef.current) return null;
return createPortal(
<>
{showBackdrop && (
<span
className="modal-backdrop"
onClick={closeModal}
aria-hidden="true"
style={{
position: "fixed",
top: 0,
left: 0,
width: "100%",
height: "100%",
backgroundColor: "rgba(0, 0, 0, 0.5)",
zIndex: 999,
}}
/>
)}
{isWrapped && (
<div
className="wrapped-div"
style={{
width: "47px",
height: "70px",
backgroundColor: "white",
position: "absolute",
top: `${modalPosition.top - 50}px`,
left: `${anchorRef.current.getBoundingClientRect().x}px`,
zIndex: 1001,
borderTopLeftRadius: "12px",
borderTopRightRadius: "12px",
opacity: isVisible ? 1 : 0,
visibility: isVisible ? "visible" : "hidden",
animation:
isVisible && isAnimated
? "slideDown 0.5s ease, opacity 0.5s ease"
: "none",
}}
></div>
)}
<div
className="mini-edit-modal-container"
ref={ref || modalRef}
style={{
position: "absolute",
top: `${isWrapped ? modalPosition.top + 10 : modalPosition.top}px`,
left: `${
isWrapped && placement === "left"
? modalPosition.left - 20
: isWrapped && placement === "right"
? modalPosition.left + 30
: modalPosition.left
}px`,
zIndex: 100,
visibility: isVisible ? "visible" : "hidden",
animation:
isVisible && isAnimated
? "slideDown 0.5s ease, opacity 0.5s ease"
: "none",
}}
>
<div className="mini-edit-modal">
<header
className="modal-header"
style={{
backgroundColor: headerProps?.bgColor || "#434DB8",
color: headerProps?.color || "#fff",
borderBottom: border ?? "2px solid transparent",
}}
>
<h2>{headerProps?.text ?? "Header text"}</h2>
</header>
<div className="modal-content">{childContent}</div>
{footerContent ? (
<footer>{footerContent}</footer>
) : (
<footer className="modal-footer" style={{ borderTop: border }}>
<button
type="button"
className="btn-cancel"
onClick={cancelButtonProps.onClick}
>
{cancelButtonProps.text}
</button>
<button
type="button"
className="btn-proceed"
onClick={proceedButtonProps?.onClick}
>
{proceedButtonProps?.text}
</button>
</footer>
)}
</div>
</div>
</>,
document.body
);
}
);
export default MiniModel;
and for style
.mini-edit-modal-container {
background: transparent;
display: flex;
justify-content: center;
border-radius: 6px;
box-shadow: black 0px 2px 4px 0px, black 0px 2px 16px 0px;
}
.mini-edit-modal {
background-color: white;
padding: 10px;
width: 100%;
height: 100%;
box-shadow: black 0px 2px 4px 0px, black 0px 2px 16px 0px;
border-radius: 6px;
transition: transform 0.3s ease-in-out;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
background-color: blue;
color: white;
font-size: 8px;
font-weight: 400;
height: 24px;
transition: transform 0.3s ease-in-out;
}
.modal-footer {
display: flex;
justify-content: flex-end;
align-items: center;
gap: 5px;
background-color: gray;
height: 24px;
padding-right: 10px;
transition: transform 0.3s ease-in-out;
}
.btn-proceed,
.btn-cancel {
width: 40px;
height: 16px;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 10px;
line-height: 1;
transition: transform 0.3s ease-in-out;
}
.btn-proceed {
background-color: white;
color: blue;
border: 1px solid blue;
}
.btn-cancel {
background-color: white;
color: gray;
border: 1px solid gray;
}
.edit-form {
min-height: 55px;
max-height: 250px;
padding: 20px;
overflow: auto;
}
.modal-backdrop {
position: fixed;
top: 0;
left: 0;
width: 100%;
background-color: black;
z-index: 999;
}
@keyframes bounceIn {
0% {
transform: scale(0.5);
opacity: 0;
}
60% {
transform: scale(1.1);
opacity: 1;
}
100% {
transform: scale(1);
opacity: 1;
}
}
@keyframes slideDown {
from {
transform: translateY(-50px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.wrapped-div {
width: 55px;
height: 70px;
background-color: white;
position: absolute;
top: 45px;
left: 87px;
z-index: 10;
border-top-left-radius: 12px;
border-top-right-radius: 12px;
box-shadow: black 0px 2px 4px 0px, black 0px 2px 16px 0px;
}
/* For the bottom-left corner */
.wrapped-div::before {
position: absolute;
content: "";
width: 25px;
height: 25px;
background: transparent;
border-radius: 50%;
bottom: 7px;
left: -24px;
box-shadow: 9px 7px 0 white;
}
/* For the bottom-right corner */
.wrapped-div::after {
position: absolute;
content: "";
width: 25px;
height: 24px;
background: transparent;
border-radius: 50%;
bottom: 10px;
right: -25px;
box-shadow: -11px 10px 0 white;
}



