@vincentwings/react-modal
Version:
A lightweight and flexible modal component for React, inspired by jquery-modal.
1 lines • 11.6 kB
Source Map (JSON)
{"version":3,"sources":["../src/index.js","../src/Modal.jsx","../src/ModalManager.jsx"],"sourcesContent":["export { default as Modal } from './Modal'\r\nexport { ModalProvider, useModal } from './ModalManager'\r\n","import React, { useEffect, useRef, useState } from 'react'\r\nimport './Modal.css'\r\n\r\n/**\r\n * A reusable modal component with animation, accessibility support, and custom styling options.\r\n *\r\n * @param {Object} props\r\n * @param {boolean} props.isOpen - Whether the modal should be visible or not\r\n * @param {Function} props.onClose - Function to close the modal\r\n * @param {number} [props.fadeDuration=300] - Duration of the fade animation in ms\r\n * @param {number} [props.fadeDelay=0.5] - Delay multiplier before content fades in\r\n * @param {boolean} [props.escapeClose=true] - Allows modal to be closed with Escape key\r\n * @param {boolean} [props.clickClose=true] - Allows closing by clicking outside the modal\r\n * @param {boolean} [props.showClose=true] - Show the close (×) button in top-right corner\r\n * @param {string} [props.closeText='×'] - The text or icon to display in the close button\r\n * @param {boolean} [props.useTransform=true] - Whether to animate with translateY\r\n * @param {boolean} [props.useBorderRadius=true] - Whether to apply border-radius\r\n * @param {string} [props.overlayColor='rgba(0, 0, 0, 0.4)'] - Background color of the overlay\r\n * @param {string} [props.backgroundColor='#fff'] - Background color of the modal\r\n * @param {string} [props.textColor='#2a2a2a'] - Text color inside the modal\r\n * @param {string} [props.borderRadius='12px'] - Border-radius for modal corners\r\n * @param {React.ReactNode} props.children - Content to render inside the modal\r\n */\r\n\r\nconst Modal = ({\r\n isOpen, // Whether modal is open\r\n onClose, // Function to call on close\r\n fadeDuration = 300, // ms of fade animation\r\n fadeDelay = 0.5, // multiplier delay before content animates\r\n escapeClose = true, // can close modal with Escape key\r\n clickClose = true, // can close modal by clicking overlay\r\n showClose = true, // show close \"×\" button in corner\r\n closeText = '×', // text/icon inside close button\r\n useTransform = true, // whether modal should slide in (translateY)\r\n useBorderRadius = true, // whether modal should have rounded corners\r\n overlayColor = 'rgba(0, 0, 0, 0.4)', // background of the overlay\r\n backgroundColor = '#fff', // background of modal itself\r\n textColor = '#2a2a2a', // default text color inside modal\r\n borderRadius = '12px', // default radius for modal box\r\n children // content of the modal\r\n}) => {\r\n const modalRef = useRef() // Ref to modal element to trap focus\r\n const [visible, setVisible] = useState(false) // Track if modal should be in DOM\r\n const [animating, setAnimating] = useState(false) // Track fade-in animation state\r\n\r\n // === 1. Handle modal visibility + fade animation ===\r\n useEffect(() => {\r\n if (isOpen) {\r\n setVisible(true)\r\n setTimeout(() => setAnimating(true), fadeDuration * fadeDelay)\r\n } else if (visible) {\r\n setAnimating(false)\r\n setTimeout(() => setVisible(false), fadeDuration)\r\n }\r\n }, [isOpen, fadeDuration, fadeDelay, visible])\r\n\r\n // === 2. Handle Escape + Tab key events ===\r\n useEffect(() => {\r\n if (!visible || !modalRef.current) return\r\n\r\n const focusables = modalRef.current.querySelectorAll(\r\n 'button, [href], input, select, textarea, [tabindex]:not([tabindex=\"-1\"])'\r\n )\r\n\r\n // Auto focus the first focusable element\r\n if (focusables.length > 0) focusables[0].focus()\r\n\r\n const handleKeyDown = (e) => {\r\n if (escapeClose && e.key === 'Escape') {\r\n onClose()\r\n return\r\n }\r\n\r\n if (e.key === 'Tab') {\r\n handleFocusTrap(e, focusables)\r\n }\r\n }\r\n\r\n document.addEventListener('keydown', handleKeyDown)\r\n return () => document.removeEventListener('keydown', handleKeyDown)\r\n }, [visible, escapeClose, onClose])\r\n\r\n /**\r\n * Prevent tabbing outside the modal (trap focus inside)\r\n * @param {KeyboardEvent} e\r\n * @param {NodeList} focusables\r\n */\r\n const handleFocusTrap = (e, focusables) => {\r\n const first = focusables[0]\r\n const last = focusables[focusables.length - 1]\r\n\r\n const isShiftTab = e.shiftKey && document.activeElement === first\r\n const isTabAtEnd = !e.shiftKey && document.activeElement === last\r\n\r\n if (isShiftTab || isTabAtEnd) {\r\n e.preventDefault()\r\n const target = isShiftTab ? last : first\r\n target.focus()\r\n }\r\n }\r\n\r\n // === 3. Don't render anything if modal is not visible ===\r\n if (!visible) return null\r\n\r\n // === 4. Render modal content ===\r\n return (\r\n <div\r\n className={`modal-overlay ${animating ? 'open' : ''}`}\r\n onClick={clickClose ? onClose : undefined}\r\n role=\"dialog\"\r\n aria-modal=\"true\"\r\n style={{\r\n backgroundColor: overlayColor,\r\n transition: `opacity ${fadeDuration}ms`,\r\n opacity: animating ? 1 : 0,\r\n pointerEvents: animating ? 'auto' : 'none'\r\n }}\r\n >\r\n <div\r\n className={`modal ${animating ? 'open' : ''}`}\r\n onClick={(e) => e.stopPropagation()} // prevent close on modal content click\r\n ref={modalRef}\r\n style={{\r\n transition: `opacity ${fadeDuration}ms, transform ${fadeDuration}ms`,\r\n opacity: animating ? 1 : 0,\r\n transform: useTransform ? (animating ? 'translateY(0)' : 'translateY(-20px)') : 'none',\r\n backgroundColor,\r\n color: textColor,\r\n borderRadius: useBorderRadius ? borderRadius : '0'\r\n }}\r\n >\r\n {showClose && (\r\n <button className=\"modal-close\" onClick={onClose}>\r\n {closeText}\r\n </button>\r\n )}\r\n\r\n <div className=\"modal-content\">\r\n {children}\r\n </div>\r\n </div>\r\n </div>\r\n )\r\n}\r\n\r\nexport default Modal","import React, { useState, useContext, createContext } from 'react'\r\nimport Modal from './Modal'\r\n\r\n// Create a React Context to provide modal functions and state\r\nconst ModalContext = createContext()\r\n\r\n/**\r\n * Provides modal context to the entire application\r\n * Wrap the app with <ModalProvider> to enable the use of modals anywhere\r\n *\r\n * @param {Object} props\r\n * @param {React.ReactNode} props.children - The content that should have access to the modal context\r\n */\r\nexport const ModalProvider = ({ children }) => {\r\n const [isOpen, setIsOpen] = useState(false) // Tracks if a modal is currently open\r\n const [modalContent, setModalContent] = useState(null) // Stores the content to display inside the modal\r\n const [modalOptions, setModalOptions] = useState({}) // Custom options to pass to the Modal component (animation, styles, etc.)\r\n\r\n /**\r\n * Opens a modal with the given content and optional configuration\r\n * It first closes any existing modal before opening the new one\r\n *\r\n * @param {React.ReactNode} content - The content to render inside the modal\r\n * @param {Object} options - Modal component props like backgroundColor, fadeDuration, etc.\r\n */\r\n const openModal = (content, options = {}) => {\r\n // Close any open modal first\r\n setIsOpen(false)\r\n\r\n // Then wait a tiny bit before opening a new one\r\n setTimeout(() => {\r\n setModalContent(content)\r\n setModalOptions(options) // Save modal customization options (like closeText or backgroundColor)\r\n setIsOpen(true)\r\n }, 10)\r\n }\r\n\r\n /**\r\n * Closes the modal and resets its content and options\r\n */\r\n const closeModal = () => {\r\n setIsOpen(false)\r\n setModalContent(null)\r\n setModalOptions({})\r\n }\r\n\r\n return (\r\n // Provide modal context to the children tree\r\n <ModalContext.Provider value={{ openModal, closeModal }}>\r\n {children}\r\n\r\n {/* Mounts the modal component with all saved options and content */}\r\n <Modal isOpen={isOpen} onClose={closeModal} {...modalOptions}>\r\n {modalContent}\r\n </Modal>\r\n </ModalContext.Provider>\r\n )\r\n}\r\n\r\n/**\r\n * Hook to use modal functionality from anywhere in the app\r\n * Usage: const { openModal, closeModal } = useModal()\r\n */\r\nexport const useModal = () => useContext(ModalContext)"],"mappings":"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","names":["React","import_react","React"]}