/** * @fileoverview Unsaved changes confirmation modal * * Modal dialog that prompts users when they attempt to navigate away * from content with unsaved changes. Provides options to save or discard. * Cancel is available via X button, clicking outside, or pressing Escape. * Includes focus trap for accessibility compliance. * * @module @writenex/astro/client/components/UnsavedChangesModal */ import { AlertTriangle, X } from "lucide-react"; import { useCallback, useEffect, useRef } from "react"; import { useFocusTrap } from "../../hooks/useFocusTrap"; import "./UnsavedChangesModal.css"; /** * Props for UnsavedChangesModal component */ interface UnsavedChangesModalProps { /** Whether the modal is open */ isOpen: boolean; /** Callback to close the modal (cancel action) */ onClose: () => void; /** Callback when user chooses to discard changes and continue */ onDiscard: () => void; /** Callback when user chooses to save and continue */ onSave: () => void; /** Whether save is in progress */ isSaving?: boolean; } /** * Modal dialog for unsaved changes confirmation * * Features: * - Two action buttons: Don't Save (discard), Save * - Cancel via X button, click outside, or Escape key * - Loading state during save * - Consistent styling with design system * * @component * @example * ```tsx * setShowUnsavedModal(false)} * onDiscard={handleDiscard} * onSave={handleSave} * /> * ``` */ export function UnsavedChangesModal({ isOpen, onClose, onDiscard, onSave, isSaving = false, }: UnsavedChangesModalProps): React.ReactElement | null { const triggerRef = useRef(null); const discardButtonRef = useRef(null); // Store the trigger element when modal opens useEffect(() => { if (isOpen) { triggerRef.current = document.activeElement as HTMLElement; } }, [isOpen]); // Focus trap for accessibility const { containerRef } = useFocusTrap({ enabled: isOpen, onEscape: isSaving ? undefined : onClose, returnFocusTo: triggerRef.current, }); // Focus first button when modal opens useEffect(() => { if (isOpen) { setTimeout(() => { discardButtonRef.current?.focus(); }, 50); } }, [isOpen]); const handleOverlayClick = useCallback( (e: React.MouseEvent) => { if (e.target === e.currentTarget && !isSaving) { onClose(); } }, [onClose, isSaving] ); if (!isOpen) return null; return (
{/* Header */}

Unsaved Changes

{/* Body */}

You have unsaved changes. Save them before continuing?

{/* Footer */}
); }