modal-drawer-system

安装量: 42
排名: #17328

安装

npx skills add https://github.com/patricio0312rev/skills --skill modal-drawer-system

Modal & Drawer System Generator

Create accessible, polished modal dialogs and drawer components.

Core Workflow Choose type: Modal (center), Drawer (side), Bottom Sheet Setup portal: Render outside DOM hierarchy Focus management: Focus trap and restoration Accessibility: ARIA attributes, keyboard shortcuts Animations: Smooth enter/exit transitions Scroll lock: Prevent body scroll when open Backdrop: Click outside to close Base Modal Component "use client";

import { useEffect, useRef } from "react"; import { createPortal } from "react-dom"; import { X } from "lucide-react";

interface ModalProps { isOpen: boolean; onClose: () => void; title?: string; description?: string; children: React.ReactNode; size?: "sm" | "md" | "lg" | "xl" | "full"; closeOnEscape?: boolean; closeOnBackdrop?: boolean; }

export function Modal({ isOpen, onClose, title, description, children, size = "md", closeOnEscape = true, closeOnBackdrop = true, }: ModalProps) { const modalRef = useRef(null);

// ESC key handler useEffect(() => { if (!isOpen || !closeOnEscape) return;

const handleEscape = (e: KeyboardEvent) => {
  if (e.key === "Escape") onClose();
};

document.addEventListener("keydown", handleEscape);
return () => document.removeEventListener("keydown", handleEscape);

}, [isOpen, closeOnEscape, onClose]);

// Focus trap useEffect(() => { if (!isOpen) return;

const focusableElements = modalRef.current?.querySelectorAll(
  'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const firstElement = focusableElements?.[0] as HTMLElement;
const lastElement = focusableElements?.[
  focusableElements.length - 1
] as HTMLElement;

const handleTab = (e: KeyboardEvent) => {
  if (e.key !== "Tab") return;

  if (e.shiftKey && document.activeElement === firstElement) {
    e.preventDefault();
    lastElement?.focus();
  } else if (!e.shiftKey && document.activeElement === lastElement) {
    e.preventDefault();
    firstElement?.focus();
  }
};

firstElement?.focus();
document.addEventListener("keydown", handleTab);
return () => document.removeEventListener("keydown", handleTab);

}, [isOpen]);

// Body scroll lock useEffect(() => { if (isOpen) { document.body.style.overflow = "hidden"; } else { document.body.style.overflow = ""; } return () => { document.body.style.overflow = ""; }; }, [isOpen]);

if (!isOpen) return null;

const sizeClasses = { sm: "max-w-sm", md: "max-w-md", lg: "max-w-lg", xl: "max-w-xl", full: "max-w-full mx-4", };

return createPortal(

{/ Backdrop /}

  {/* Modal */}
  <div
    ref={modalRef}
    className={`relative z-10 w-full ${sizeClasses[size]} animate-in zoom-in-95 slide-in-from-bottom-4 duration-200`}
  >
    <div className="rounded-lg bg-white shadow-xl">
      {/* Header */}
      {(title || description) && (
        <div className="border-b px-6 py-4">
          {title && (
            <h2 id="modal-title" className="text-xl font-semibold">
              {title}
            </h2>
          )}
          {description && (
            <p
              id="modal-description"
              className="mt-1 text-sm text-gray-600"
            >
              {description}
            </p>
          )}
        </div>
      )}

      {/* Close button */}
      <button
        onClick={onClose}
        className="absolute right-4 top-4 rounded-lg p-1 hover:bg-gray-100"
        aria-label="Close modal"
      >
        <X className="h-5 w-5" />
      </button>

      {/* Content */}
      <div className="p-6">{children}</div>
    </div>
  </div>
</div>,
document.body

); }

Drawer Component interface DrawerProps { isOpen: boolean; onClose: () => void; position?: "left" | "right" | "bottom"; title?: string; children: React.ReactNode; }

export function Drawer({ isOpen, onClose, position = "right", title, children, }: DrawerProps) { // Similar hooks as Modal (ESC, focus trap, scroll lock)

const positionClasses = { left: "left-0 top-0 h-full w-80 animate-in slide-in-from-left", right: "right-0 top-0 h-full w-80 animate-in slide-in-from-right", bottom: "bottom-0 left-0 right-0 h-96 animate-in slide-in-from-bottom", };

if (!isOpen) return null;

return createPortal(

absolute ${positionClasses[position]} bg-white shadow-xl} >

{title}

{children}
, document.body ); }

Common Use Cases Confirmation Dialog interface ConfirmDialogProps { isOpen: boolean; onClose: () => void; onConfirm: () => void; title: string; description: string; confirmText?: string; cancelText?: string; variant?: "danger" | "default"; }

export function ConfirmDialog({ isOpen, onClose, onConfirm, title, description, confirmText = "Confirm", cancelText = "Cancel", variant = "default", }: ConfirmDialogProps) { return (

{title}

{description}

); }

Edit Form Modal export function EditUserModal({ user, isOpen, onClose }: EditUserModalProps) { const [isSaving, setIsSaving] = useState(false);

const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setIsSaving(true); // Save logic onClose(); };

return (

); }

Detail View Drawer export function UserDetailDrawer({ user, isOpen, onClose, }: UserDetailDrawerProps) { return (

{user.name}

{user.email}

Role

{user.role}

Department

{user.department}

Joined

{formatDate(user.joinedAt)}

); }

Best Practices Portal rendering: Render outside parent DOM Focus management: Trap focus, restore on close Keyboard support: ESC to close, Tab navigation ARIA attributes: role, aria-modal, aria-labelledby Scroll lock: Prevent body scroll when open Backdrop: Click outside to close (optional) Animations: Smooth enter/exit transitions Mobile responsive: Full screen on small devices Output Checklist Modal component with portal Drawer component (left/right/bottom) Focus trap implementation ESC key handler Scroll lock on body Backdrop with click-to-close ARIA attributes Smooth animations Close button Sample use cases (confirm, edit, detail)

返回排行榜