From b58c8e7dd59decf1b5c7e895dfb9a82f9cc16313 Mon Sep 17 00:00:00 2001 From: Mahmud1087 Date: Sat, 18 Oct 2025 16:31:00 +0100 Subject: [PATCH] feat: add reusable modal component for React --- components/modal/react/modal/Modal.jsx | 237 ++++++++++++++++ components/modal/react/modal/README.md | 290 ++++++++++++++++++++ components/modal/react/modal/component.json | 113 ++++++++ 3 files changed, 640 insertions(+) create mode 100644 components/modal/react/modal/Modal.jsx create mode 100644 components/modal/react/modal/README.md create mode 100644 components/modal/react/modal/component.json diff --git a/components/modal/react/modal/Modal.jsx b/components/modal/react/modal/Modal.jsx new file mode 100644 index 0000000..08c42b1 --- /dev/null +++ b/components/modal/react/modal/Modal.jsx @@ -0,0 +1,237 @@ +import React, { useEffect, useRef } from "react" +import { IoMdClose } from "react-icons/io" + +// Modal Context +const ModalContext = React.createContext(undefined) + +// Main Modal Component +const Modal = ({ + open, + onClose, + children, + size = "md", + closeOnOverlay = true, + closeOnEsc = true, + showCloseButton = true, +}) => { + const modalRef = useRef(null) + const previousActiveElement = useRef(null) + + // Size classes + const sizeClasses = { + sm: "max-w-sm", + md: "max-w-md", + lg: "max-w-lg", + xl: "max-w-xl", + full: "max-w-full mx-4", + } + + // Handle ESC key + useEffect(() => { + if (!open || !closeOnEsc) return + + const handleEsc = e => { + if (e.key === "Escape") { + onClose() + } + } + + document.addEventListener("keydown", handleEsc) + return () => document.removeEventListener("keydown", handleEsc) + }, [open, closeOnEsc, onClose]) + + // Focus management + useEffect(() => { + if (open) { + previousActiveElement.current = document.activeElement + modalRef.current?.focus() + } else { + previousActiveElement.current?.focus() + } + }, [open]) + + // Prevent body scroll when modal is open + useEffect(() => { + if (open) { + document.body.style.overflow = "hidden" + } else { + document.body.style.overflow = "" + } + return () => { + document.body.style.overflow = "" + } + }, [open]) + + // Focus trap + const handleTabKey = e => { + if (e.key !== "Tab") return + + const focusableElements = modalRef.current?.querySelectorAll( + 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' + ) + + if (!focusableElements || focusableElements.length === 0) return + + const firstElement = focusableElements[0] + const lastElement = focusableElements[focusableElements.length - 1] + + if (e.shiftKey) { + if (document.activeElement === firstElement) { + lastElement.focus() + e.preventDefault() + } + } else { + if (document.activeElement === lastElement) { + firstElement.focus() + e.preventDefault() + } + } + } + + if (!open) return null + + return ( + +
+ {/* Overlay */} + + + + + ) +} + +// Modal Header +const ModalHeader = ({ children, className = "" }) => { + return ( +
+ +
+ ) +} + +// Modal Body +const ModalBody = ({ children, className = "" }) => { + return
{children}
+} + +// Modal Footer +const ModalFooter = ({ children, className = "" }) => { + return ( +
+ {children} +
+ ) +} + +// Attach compound components +Modal.Header = ModalHeader +Modal.Body = ModalBody +Modal.Footer = ModalFooter + +// Demo Component +const ModalDemo = () => { + const [previewOpen, setPreviewOpen] = React.useState(false) + + return ( +
+
+ {/* Preview Modal Trigger */} + + + {/* Preview Modal */} + setPreviewOpen(false)} + size="lg" + > + Image Preview + +
+ Preview Content +
+
+

+ Beautiful Gradient +

+

+ This is an example of a content preview modal. You can display + images, videos, or any other content here. +

+
+
+ + + +
+
+
+ ) +} + +export default ModalDemo +export { Modal } diff --git a/components/modal/react/modal/README.md b/components/modal/react/modal/README.md new file mode 100644 index 0000000..8690b1a --- /dev/null +++ b/components/modal/react/modal/README.md @@ -0,0 +1,290 @@ +# Accessible Modal Component + +A reusable modal component for displaying forms, confirmation boxes, and alerts. Should include accessibility features, animations, and flexible content placement. + +## Features + +- **Full Accessibility (WCAG Compliant)** + - Focus trap with keyboard navigation + - Automatic focus management + - ESC key to close + - Proper ARIA attributes + - Screen reader support + +- **Smooth Animations** + - Fade-in overlay + - Scale-in modal entrance + - CSS-based for optimal performance + +- **Flexible** + - Compound component pattern (Header, Body, Footer) + - Multiple size variants + - Customizable behaviors + +- **Responsive Design** + - Mobile-friendly + - Prevents body scroll when open + - Adaptive sizing + +- **Use Cases** + - Confirmation modals + - Form popups (login, create, edit) + - Alert notifications + - Content/image previews + +## Installation + +No external dependencies required beyond React and Tailwind CSS. + +1. Copy the component files to your project +2. Ensure Tailwind CSS is configured in your project +3. Import and use the component + +## Usage + +### Basic Example + +```jsx +import { useState } from "react" +import Modal from "./components/Modal" + +function App() { + const [open, setOpen] = useState(false) + + return ( + <> + + + setOpen(false)}> + Modal Title + +

This is the modal content.

+
+ + + +
+ + ) +} +``` + +### Confirmation Modal + +```jsx + setConfirmOpen(false)} size="sm"> + Delete Item? + +

+ Are you sure you want to delete this item? This action cannot be undone. +

+
+ + + + +
+``` + +### Form Modal + +```jsx + setFormOpen(false)} size="md"> + Create Account + +
+
+ + setFormData({ ...formData, name: e.target.value })} + /> +
+
+ + setFormData({ ...formData, email: e.target.value })} + /> +
+
+
+ + + + +
+``` + +### Alert Modal + +```jsx + setAlertOpen(false)} size="sm"> + Success! + +

Your changes have been saved successfully.

+
+ + + +
+``` + +### Without Close Button + +```jsx + setOpen(false)} + showCloseButton={false} + closeOnOverlay={false} + closeOnEsc={false} +> + Important Message + +

This modal requires explicit action.

+
+ + + +
+``` + +## Props Documentation + +### Modal Props + +| Prop | Type | Required | Default | Description | +| ----------------- | ---------------------------------------- | -------- | ------- | ---------------------------------------------------------------- | +| `open` | `boolean` | ✅ | - | Controls the visibility of the modal | +| `onClose` | `() => void` | ✅ | - | Callback function triggered when the modal should close | +| `children` | `React.ReactNode` | ✅ | - | Modal content (typically Modal.Header, Modal.Body, Modal.Footer) | +| `size` | `'sm' \| 'md' \| 'lg' \| 'xl' \| 'full'` | ❌ | `'md'` | Sets the maximum width of the modal | +| `closeOnOverlay` | `boolean` | ❌ | `true` | Whether clicking the overlay/backdrop closes the modal | +| `closeOnEsc` | `boolean` | ❌ | `true` | Whether pressing the ESC key closes the modal | +| `showCloseButton` | `boolean` | ❌ | `true` | Whether to display the X close button in the top-right corner | + +### Size Options + +- `sm` - max-width: 24rem (384px) +- `md` - max-width: 28rem (448px) +- `lg` - max-width: 32rem (512px) +- `xl` - max-width: 36rem (576px) +- `full` - Full width with margin + +### Modal.Header Props + +| Prop | Type | Required | Default | Description | +| ----------- | ----------------- | -------- | ------- | ------------------------------------------ | +| `children` | `React.ReactNode` | ✅ | - | Header content (typically the modal title) | +| `className` | `string` | ❌ | `''` | Additional CSS classes to apply | + +### Modal.Body Props + +| Prop | Type | Required | Default | Description | +| ----------- | ----------------- | -------- | ------- | ------------------------------- | +| `children` | `React.ReactNode` | ✅ | - | Main modal content | +| `className` | `string` | ❌ | `''` | Additional CSS classes to apply | + +### Modal.Footer Props + +| Prop | Type | Required | Default | Description | +| ----------- | ----------------- | -------- | ------- | ----------------------------------------- | +| `children` | `React.ReactNode` | ✅ | - | Footer content (typically action buttons) | +| `className` | `string` | ❌ | `''` | Additional CSS classes to apply | + +## Customization Guide + +### Styling + +The component uses Tailwind CSS classes. You can customize the appearance by: + +1. **Modifying default classes** in the component file +2. **Passing custom classes** via the `className` prop on sub-components +3. **Overriding Tailwind theme** in your `tailwind.config.js` + +### Custom Overlay Color + +```jsx +// Edit the overlay div in the Modal component +