generated from muhtadeetaron/nextjs-template
92 lines
2.6 KiB
TypeScript
92 lines
2.6 KiB
TypeScript
"use client";
|
||
import { useEffect, useRef } from "react";
|
||
import { createPortal } from "react-dom";
|
||
import { X } from "lucide-react";
|
||
|
||
interface ModalProps {
|
||
/** Control visibility */
|
||
open: boolean;
|
||
/** Callback for both explicit and implicit close actions */
|
||
onClose: () => void;
|
||
/** Optional heading */
|
||
title?: string;
|
||
children: React.ReactNode;
|
||
/** Center horizontally and vertically? (default true) */
|
||
center?: boolean;
|
||
}
|
||
|
||
export default function Modal({
|
||
open,
|
||
onClose,
|
||
title,
|
||
children,
|
||
center = true,
|
||
}: ModalProps) {
|
||
const dialogRef = useRef<HTMLDialogElement | null>(null);
|
||
|
||
// Open / close imperatively to keep <dialog> in sync with prop
|
||
useEffect(() => {
|
||
const dialog = dialogRef.current;
|
||
if (!dialog) return;
|
||
if (open && !dialog.open) dialog.showModal();
|
||
if (!open && dialog.open) dialog.close();
|
||
}, [open]);
|
||
|
||
// Close on native <dialog> close event
|
||
useEffect(() => {
|
||
const dialog = dialogRef.current;
|
||
if (!dialog) return;
|
||
const handleClose = () => onClose();
|
||
dialog.addEventListener("close", handleClose);
|
||
return () => dialog.removeEventListener("close", handleClose);
|
||
}, [onClose]);
|
||
|
||
// ESC -> close (for browsers without built‑in <dialog> handling)
|
||
useEffect(() => {
|
||
if (!open) return;
|
||
const onKey = (e: KeyboardEvent) => {
|
||
if (e.key === "Escape") onClose();
|
||
};
|
||
window.addEventListener("keydown", onKey);
|
||
return () => window.removeEventListener("keydown", onKey);
|
||
}, [open, onClose]);
|
||
|
||
if (typeof window === "undefined") return null; // SSR guard
|
||
|
||
return createPortal(
|
||
<dialog
|
||
ref={dialogRef}
|
||
className={`fixed top-0 right-0 h-[110vh] z-50 w-[87vw]
|
||
ml-auto transform-none /* 1 & 2 */
|
||
backdrop:bg-black/20
|
||
transition-[opacity,transform] duration-300
|
||
${
|
||
open
|
||
? "opacity-100 translate-x-0 translate-y-0"
|
||
: "opacity-0 translate-x-4"
|
||
}
|
||
${center ? "rounded-l-xl" : ""}
|
||
`}
|
||
>
|
||
{/* Card */}
|
||
<div className="bg-white rounded-xl overflow-hidden pb-10">
|
||
{title && (
|
||
<header className="flex items-center justify-between px-6 pt-10 pb-4 dark:border-zinc-700">
|
||
<h2 className="text-2xl font-semibold">{title}</h2>
|
||
<button
|
||
aria-label="Close"
|
||
onClick={onClose}
|
||
className="p-1 hover:bg-zinc-200 dark:hover:bg-zinc-800 rounded-full"
|
||
>
|
||
<X className="h-5 w-5" />
|
||
</button>
|
||
</header>
|
||
)}
|
||
|
||
<section className="px-6 ">{children}</section>
|
||
</div>
|
||
</dialog>,
|
||
document.body
|
||
);
|
||
}
|