"use client"; import { useEffect, useRef, type ReactNode } from "react"; import { X, Maximize2, Minimize2 } from "lucide-react"; import { useState } from "react"; export interface ModalProps { isOpen: boolean; onClose: () => void; title?: string; children: ReactNode | ((isFullscreen: boolean) => ReactNode); size?: "sm" | "md" | "lg" | "xl" | "full"; showCloseButton?: boolean; closeOnClickOutside?: boolean; closeOnEsc?: boolean; showFullscreenToggle?: boolean; footer?: ReactNode; className?: string; } export function Modal({ isOpen, onClose, title, children, size = "md", showCloseButton = true, closeOnClickOutside = true, closeOnEsc = true, showFullscreenToggle = false, footer, className = "", }: ModalProps) { const [isFullscreen, setIsFullscreen] = useState(false); const [isAnimating, setIsAnimating] = useState(false); const modalRef = useRef(null); const contentRef = useRef(null); // Анимация появления useEffect(() => { if (isOpen) { setIsAnimating(true); // Блокируем прокрутку body document.body.style.overflow = "hidden"; } else { // Разблокируем прокрутку при закрытии document.body.style.overflow = ""; } return () => { document.body.style.overflow = ""; }; }, [isOpen]); // ESC для закрытия useEffect(() => { if (!isOpen || !closeOnEsc) return; const handleEsc = (e: KeyboardEvent) => { if (e.key === "Escape") { onClose(); } }; document.addEventListener("keydown", handleEsc); return () => document.removeEventListener("keydown", handleEsc); }, [isOpen, closeOnEsc, onClose]); // Focus trap useEffect(() => { if (!isOpen || !contentRef.current) return; const focusableElements = contentRef.current.querySelectorAll( 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' ); if (focusableElements.length === 0) return; const firstElement = focusableElements[0]; const lastElement = focusableElements[focusableElements.length - 1]; // Фокус на первый элемент при открытии firstElement?.focus(); const handleTab = (e: KeyboardEvent) => { if (e.key !== "Tab") return; if (e.shiftKey) { // Shift + Tab if (document.activeElement === firstElement) { lastElement?.focus(); e.preventDefault(); } } else { // Tab if (document.activeElement === lastElement) { firstElement?.focus(); e.preventDefault(); } } }; document.addEventListener("keydown", handleTab); return () => document.removeEventListener("keydown", handleTab); }, [isOpen]); if (!isOpen) return null; const handleBackdropClick = (e: React.MouseEvent) => { if (closeOnClickOutside && e.target === modalRef.current) { onClose(); } }; const sizeClasses = { sm: "max-w-md", md: "max-w-2xl", lg: "max-w-4xl", xl: "max-w-6xl", full: "max-w-[95vw]", }; const modalSizeClass = isFullscreen ? "w-screen h-screen max-w-none rounded-none" : `w-full ${sizeClasses[size]} max-h-[90vh]`; return (
{/* Header */} {(title || showCloseButton || showFullscreenToggle) && (
{title && ( )}
{showFullscreenToggle && ( )} {showCloseButton && ( )}
)} {/* Content */}
{typeof children === "function" ? children(isFullscreen) : children}
{/* Footer */} {footer && (
{footer}
)} {/* Bottom padding */}
); }