Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 | 1x 1x 1x 4x 4x 4x 4x 3x 3x 3x 1x 1x 4x 4x 4x 4x 4x 4x 4x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 4x 4x 4x 4x 1x | import React, { useEffect, useRef } from 'react';
import classNames from 'classnames';
interface ModalProps {
isOpen: boolean;
onClose: () => void;
title: string;
children: React.ReactNode;
footer?: React.ReactNode;
size?: 'sm' | 'md' | 'lg' | 'xl';
}
const Modal: React.FC<ModalProps> = ({ isOpen, onClose, title, children, footer, size = 'md' }) => {
const modalRef = useRef<HTMLDivElement>(null);
const previousFocusRef = useRef<HTMLElement | null>(null);
useEffect(() => {
if (isOpen) {
// Store the element that had focus before the modal opened
previousFocusRef.current = document.activeElement as HTMLElement;
// Focus the modal
modalRef.current?.focus();
} else {
// Return focus to the previously focused element
previousFocusRef.current?.focus();
}
}, [isOpen]);
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape' && isOpen) {
onClose();
}
// Focus trap: keep focus within modal
if (e.key === 'Tab' && isOpen && modalRef.current) {
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;
if (e.shiftKey) {
// Shift+Tab: if at first element, move to last
if (document.activeElement === firstElement) {
e.preventDefault();
lastElement?.focus();
}
} else {
// Tab: if at last element, move to first
if (document.activeElement === lastElement) {
e.preventDefault();
firstElement?.focus();
}
}
}
};
document.addEventListener('keydown', handleKeyDown);
return () => document.removeEventListener('keydown', handleKeyDown);
}, [isOpen, onClose]);
if (!isOpen) return null;
const sizeClasses = {
sm: 'max-w-sm',
md: 'max-w-md',
lg: 'max-w-lg',
xl: 'max-w-xl',
};
return (
<>
{/* Backdrop */}
<div
className="fixed inset-0 bg-black bg-opacity-50 z-40 transition-opacity"
onClick={onClose}
aria-hidden="true"
/>
{/* Modal */}
<div
className="fixed inset-0 flex items-center justify-center z-50 p-4"
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
>
<div
ref={modalRef}
className={classNames(
'bg-oxford-blue rounded-lg shadow-2xl w-full',
'border border-neon-blue border-opacity-20',
'animate-slideIn',
sizeClasses[size]
)}
onClick={(e) => e.stopPropagation()}
tabIndex={-1}
>
{/* Header */}
<div className="px-6 py-4 border-b border-neon-blue border-opacity-20 flex items-center justify-between">
<h2 id="modal-title" className="text-xl font-bold text-white">{title}</h2>
<button
onClick={onClose}
className="p-1 hover:bg-sea-green hover:bg-opacity-20 rounded transition-colors text-french-gray"
aria-label="Close modal"
>
✕
</button>
</div>
{/* Content */}
<div className="px-6 py-4 max-h-96 overflow-y-auto">
{children}
</div>
{/* Footer */}
{footer && (
<div className="px-6 py-4 border-t border-neon-blue border-opacity-20 flex justify-end gap-3">
{footer}
</div>
)}
</div>
</div>
</>
);
};
export default Modal;
|