UNPKG

react-vite-themes

Version:

A test/experimental React theme system created for learning purposes. Features atomic design components, SCSS variables, and dark/light theme support. Not intended for production use.

71 lines (70 loc) 3.64 kB
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; import React, { useEffect, useRef, useCallback, useState } from 'react'; import { cn } from '../../../utils'; import { Icon } from '../Icon'; export const Modal = ({ isOpen, onClose, title, children, className, size = 'md', variant = 'default', showCloseButton = true, closeOnBackdrop = true, closeOnEscape = true, preventScroll = true, ...props }) => { const modalRef = useRef(null); const previousActiveElement = useRef(null); const [isDismissing, setIsDismissing] = useState(false); // Handle close with animation const handleClose = useCallback(() => { if (isDismissing) return; // Prevent multiple calls setIsDismissing(true); // Wait for animation to complete before calling onClose setTimeout(() => { onClose?.(); setIsDismissing(false); }, 300); // Match the animation duration }, [onClose, isDismissing]); // Handle escape key const handleEscape = useCallback((event) => { if (event.key === 'Escape' && closeOnEscape && !isDismissing) { handleClose(); } }, [closeOnEscape, isDismissing, handleClose]); // Handle backdrop click const handleBackdropClick = useCallback((event) => { if (event.target === event.currentTarget && closeOnBackdrop && !isDismissing) { handleClose(); } }, [closeOnBackdrop, isDismissing, handleClose]); // Focus management useEffect(() => { if (isOpen) { // Store the previously focused element previousActiveElement.current = document.activeElement; // Focus the modal if (modalRef.current) { modalRef.current.focus(); } // Prevent body scroll if (preventScroll) { document.body.style.overflow = 'hidden'; } // Add escape key listener document.addEventListener('keydown', handleEscape); return () => { document.removeEventListener('keydown', handleEscape); if (preventScroll) { document.body.style.overflow = ''; } // Restore focus to the previously focused element if (previousActiveElement.current) { previousActiveElement.current.focus(); } }; } }, [isOpen, handleEscape, preventScroll]); if (!isOpen && !isDismissing) return null; const sizeClasses = { xs: 'modal--xs', sm: 'modal--sm', md: 'modal--md', lg: 'modal--lg', xl: 'modal--xl', full: 'modal--full', }; return (_jsx("div", { className: cn('modal-overlay', isDismissing ? 'modal-overlay--dismissing' : 'modal-overlay--entering'), onClick: handleBackdropClick, role: "dialog", "aria-modal": "true", "aria-labelledby": title ? 'modal-title' : undefined, children: _jsxs("div", { ref: modalRef, className: cn('modal', `modal--${variant}`, sizeClasses[size], isDismissing && 'modal--dismissing', className), tabIndex: -1, ...props, children: [(title || showCloseButton) && (_jsxs("div", { className: "modal__header", children: [title && (_jsx("h2", { id: "modal-title", className: "modal__title", children: title })), showCloseButton && (_jsx("button", { className: "modal__close", onClick: handleClose, "aria-label": "Close modal", type: "button", children: _jsx(Icon, { name: "close", size: "sm" }) }))] })), _jsx("div", { className: "modal__content", children: children })] }) })); };