diff --git a/src/renderer/src/components/ui/Modal.tsx b/src/renderer/src/components/ui/Modal.tsx index b112601..c391b4c 100644 --- a/src/renderer/src/components/ui/Modal.tsx +++ b/src/renderer/src/components/ui/Modal.tsx @@ -4,8 +4,10 @@ * A reusable modal dialog component. */ -import React, { useEffect, useCallback } from 'react' +import React, { useEffect, useCallback, useRef } from 'react' import { X } from 'lucide-react' +import FocusLock from 'react-focus-lock' +import { useDialogFocus } from '../../hooks/useDialogFocus' interface ModalProps { isOpen: boolean @@ -14,6 +16,10 @@ interface ModalProps { children: React.ReactNode size?: 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' showCloseButton?: boolean + /** Ref to the element that triggered opening the modal (for focus restoration) */ + triggerRef?: React.RefObject + /** ID of the title element (for aria-labelledby) */ + titleId?: string } const sizeStyles: Record = { @@ -31,9 +37,24 @@ export function Modal({ title, children, size = 'md', - showCloseButton = true + showCloseButton = true, + triggerRef, + titleId }: ModalProps) { - // Handle escape key + const dialogRef = useRef(null) + + // Generate unique title ID if not provided + const generatedTitleId = titleId || `modal-title-${Math.random().toString(36).substr(2, 9)}` + + // Setup focus management + const { focusLockProps } = useDialogFocus({ + isOpen, + dialogRef, + onClose, + triggerRef: triggerRef || undefined + }) + + // Handle escape key (maintained for backward compatibility, useDialogFocus also handles this) const handleKeyDown = useCallback( (event: KeyboardEvent) => { if (event.key === 'Escape') { @@ -46,48 +67,59 @@ export function Modal({ useEffect(() => { if (isOpen) { document.addEventListener('keydown', handleKeyDown) - document.body.style.overflow = 'hidden' } return () => { document.removeEventListener('keydown', handleKeyDown) - document.body.style.overflow = 'unset' } }, [isOpen, handleKeyDown]) if (!isOpen) return null return ( -
- {/* Backdrop */} -
- - {/* Modal container */} -
+ +
+ {/* Backdrop */}
e.stopPropagation()} - > - {/* Header */} - {(title || showCloseButton) && ( -
- {title &&

{title}

} - {showCloseButton && ( - - )} -
- )} + className="fixed inset-0 bg-black bg-opacity-50 transition-opacity" + onClick={onClose} + /> - {/* Content */} -
{children}
+ {/* Modal container */} +
+
e.stopPropagation()} + > + {/* Header */} + {(title || showCloseButton) && ( +
+ {title && ( +

+ {title} +

+ )} + {showCloseButton && ( + + )} +
+ )} + + {/* Content */} +
{children}
+
-
+
) }