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
JavaScript
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 })] }) }));
};