/** * Modal - Accessible dialog overlay with animation. * * Uses a portal rendered into document.body. Locks background * scroll when open. Closes on Escape key and optional backdrop click. * Animated with framer-motion for enter/exit transitions. */ import { useEffect, useCallback, useRef } from "react"; import { createPortal } from "react-dom"; // eslint-disable-next-line no-unused-vars -- motion is used via import { motion, AnimatePresence } from "framer-motion"; import PropTypes from "prop-types"; const SIZE_CLASSES = { sm: "max-w-sm", md: "max-w-md", lg: "max-w-lg", xl: "max-w-xl", full: "max-w-4xl", }; const BACKDROP_VARIANTS = { hidden: { opacity: 0 }, visible: { opacity: 1 }, }; const PANEL_VARIANTS = { hidden: { opacity: 0, y: 20, scale: 0.97 }, visible: { opacity: 1, y: 0, scale: 1 }, }; function Modal({ isOpen, onClose, title, children, footer, size = "md", closeOnBackdrop = true, showCloseButton = true, className = "", }) { const panelRef = useRef(null); // Lock body scroll when modal is open useEffect(() => { if (isOpen) { document.body.style.overflow = "hidden"; } return () => { document.body.style.overflow = ""; }; }, [isOpen]); // Close on Escape key const handleKeyDown = useCallback( (event) => { if (event.key === "Escape" && onClose) { onClose(); } }, [onClose], ); useEffect(() => { if (isOpen) { document.addEventListener("keydown", handleKeyDown); } return () => document.removeEventListener("keydown", handleKeyDown); }, [isOpen, handleKeyDown]); // Focus trap: focus the panel when it opens useEffect(() => { if (isOpen && panelRef.current) { panelRef.current.focus(); } }, [isOpen]); const handleBackdropClick = () => { if (closeOnBackdrop && onClose) { onClose(); } }; const content = ( {isOpen && (
{/* Backdrop */}
)}
); return createPortal(content, document.body); } Modal.propTypes = { isOpen: PropTypes.bool.isRequired, onClose: PropTypes.func.isRequired, title: PropTypes.string, children: PropTypes.node.isRequired, footer: PropTypes.node, size: PropTypes.oneOf(["sm", "md", "lg", "xl", "full"]), closeOnBackdrop: PropTypes.bool, showCloseButton: PropTypes.bool, className: PropTypes.string, }; export default Modal;