pmtool / src /components /asset-management /shared /CustomDialog.tsx
devarshia5's picture
Upload 487 files
d97b8f9 verified
Raw
History Blame Contribute Delete
5.47 kB
import React, { useEffect, useRef } from 'react';
import { X } from 'lucide-react';
import { Button } from '@/components/ui/button';
interface CustomDialogProps {
isOpen: boolean;
onClose: () => void;
title: string;
description?: string;
children: React.ReactNode;
size?: 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl' | '5xl' | '6xl' | '7xl' | 'full';
showCloseButton?: boolean;
closeOnOverlayClick?: boolean;
className?: string;
}
const CustomDialog: React.FC<CustomDialogProps> = ({
isOpen,
onClose,
title,
description,
children,
size = 'md',
showCloseButton = true,
closeOnOverlayClick = true,
className = '',
}) => {
const dialogRef = useRef<HTMLDivElement>(null);
const titleRef = useRef<HTMLHeadingElement>(null);
// Focus management
useEffect(() => {
if (isOpen) {
// Focus the title for screen readers
titleRef.current?.focus();
// Trap focus within the dialog
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
onClose();
}
if (e.key === 'Tab') {
const focusableElements = dialogRef.current?.querySelectorAll(
'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
);
if (focusableElements && focusableElements.length > 0) {
const firstElement = focusableElements[0] as HTMLElement;
const lastElement = focusableElements[focusableElements.length - 1] as HTMLElement;
if (e.shiftKey) {
if (document.activeElement === firstElement) {
e.preventDefault();
lastElement.focus();
}
} else {
if (document.activeElement === lastElement) {
e.preventDefault();
firstElement.focus();
}
}
}
}
};
document.addEventListener('keydown', handleKeyDown);
// Prevent body scroll
document.body.style.overflow = 'hidden';
return () => {
document.removeEventListener('keydown', handleKeyDown);
document.body.style.overflow = 'unset';
};
}
}, [isOpen, onClose]);
if (!isOpen) return null;
const handleOverlayClick = (e: React.MouseEvent) => {
if (closeOnOverlayClick && e.target === e.currentTarget) {
onClose();
}
};
const getSizeClasses = () => {
switch (size) {
case 'sm':
return 'max-w-sm';
case 'md':
return 'max-w-md';
case 'lg':
return 'max-w-lg';
case 'xl':
return 'max-w-xl';
case '2xl':
return 'max-w-2xl';
case '3xl':
return 'max-w-3xl';
case '4xl':
return 'max-w-4xl';
case '5xl':
return 'max-w-5xl';
case '6xl':
return 'max-w-6xl';
case '7xl':
return 'max-w-7xl w-[95vw]';
case 'full':
return 'max-w-full w-full h-full md:max-w-2xl md:w-auto md:h-auto md:mx-4';
default:
return 'max-w-md';
}
};
return (
<div
className={`fixed inset-0 z-[999] flex items-center justify-center overflow-y-auto ${size === 'full' ? 'p-0 md:p-4' : 'p-4'}`}
role="dialog"
aria-modal="true"
aria-labelledby="dialog-title"
aria-describedby={description ? "dialog-description" : undefined}
>
{/* Overlay */}
<div
className="fixed inset-0 bg-black/80 backdrop-blur-sm transition-opacity"
onClick={handleOverlayClick}
aria-hidden="true"
/>
{/* Modal Content */}
<div
ref={dialogRef}
className={`relative z-[1000] w-full ${getSizeClasses()} ${size === 'full' ? 'md:rounded-lg' : 'rounded-lg'} border bg-background shadow-lg animate-in fade-in-0 zoom-in-95 duration-200 ${className}`}
>
{/* Header */}
<div className={`flex items-center justify-between ${size === 'full' ? 'p-4 md:p-6' : 'p-4 sm:p-6'} border-b`}>
<div className="flex-1 min-w-0">
<h2
ref={titleRef}
id="dialog-title"
className="text-base sm:text-lg md:text-xl font-semibold text-foreground truncate"
tabIndex={-1}
>
{title}
</h2>
{description && (
<p id="dialog-description" className="text-xs sm:text-sm text-muted-foreground mt-1">
{description}
</p>
)}
</div>
{showCloseButton && (
<Button
variant="ghost"
size="sm"
onClick={onClose}
className="ml-4 h-9 w-9 md:h-8 md:w-8 p-0 hover:bg-accent touch-manipulation"
aria-label="Close dialog"
>
<X className="h-5 w-5 md:h-4 md:w-4" />
</Button>
)}
</div>
{/* Content */}
<div className={`${size === 'full' ? 'p-4 md:p-6 overflow-y-auto max-h-[calc(100vh-80px)] md:max-h-[calc(100vh-200px)]' : 'p-4 sm:p-6'}`}>
{children}
</div>
</div>
</div>
);
};
export default CustomDialog;