pmtool / src /components /asset-management /shared /ConfirmDialog.tsx
devarshia5's picture
Upload 487 files
d97b8f9 verified
Raw
History Blame Contribute Delete
6.88 kB
import React, { useEffect, useRef } from 'react';
import { X, AlertCircle, Trash2 } from 'lucide-react';
import { Button } from '@/components/ui/button';
interface ConfirmDialogProps {
isOpen: boolean;
onClose: () => void;
onConfirm: () => void;
title: string;
description?: string | React.ReactNode;
confirmText?: string;
cancelText?: string;
variant?: 'destructive' | 'default' | 'outline' | 'secondary' | 'ghost' | 'link';
isLoading?: boolean;
type?: 'delete' | 'warning' | 'info';
}
const ConfirmDialog: React.FC<ConfirmDialogProps> = ({
isOpen,
onClose,
onConfirm,
title,
description,
confirmText = 'Confirm',
cancelText = 'Cancel',
variant = 'destructive',
isLoading = false,
type = 'warning'
}) => {
const dialogRef = useRef<HTMLDivElement>(null);
const cancelButtonRef = useRef<HTMLButtonElement>(null);
const confirmButtonRef = useRef<HTMLButtonElement>(null);
// Focus management
useEffect(() => {
if (isOpen) {
// Focus the cancel button by default for safety
cancelButtonRef.current?.focus();
// Trap focus within the dialog
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape' && !isLoading) {
onClose();
}
if (e.key === 'Tab') {
const focusableElements = dialogRef.current?.querySelectorAll(
'button:not([disabled]), [href], input, select, textarea, [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);
return () => document.removeEventListener('keydown', handleKeyDown);
}
}, [isOpen, isLoading, onClose]);
if (!isOpen) return null;
const handleOverlayClick = (e: React.MouseEvent) => {
// Only allow closing if not in loading state
if (!isLoading && e.target === e.currentTarget) {
onClose();
}
};
const getIcon = () => {
switch (type) {
case 'delete':
return <Trash2 className="h-5 w-5" />;
case 'warning':
case 'info':
default:
return <AlertCircle className="h-5 w-5" />;
}
};
const getIconBgColor = () => {
switch (type) {
case 'delete':
return 'bg-destructive/20 text-destructive';
case 'warning':
return 'bg-yellow-500/20 text-yellow-600';
case 'info':
return 'bg-blue-500/20 text-blue-600';
default:
return 'bg-destructive/20 text-destructive';
}
};
return (
<div
className="fixed inset-0 z-[999] flex items-center justify-center overflow-y-auto p-4"
role="dialog"
aria-modal="true"
aria-labelledby="dialog-title"
aria-describedby="dialog-description"
>
{/* Overlay */}
<div
className="fixed inset-0 bg-black/80 backdrop-blur-sm"
onClick={handleOverlayClick}
aria-hidden="true"
/>
{/* Modal Content */}
<div
ref={dialogRef}
className="relative z-[1000] w-full max-w-md rounded-lg border bg-background p-4 sm:p-6 shadow-lg"
>
<div className="flex gap-3 sm:gap-4 items-start mb-4">
<div className={`flex h-8 w-8 sm:h-10 sm:w-10 items-center justify-center rounded-full ${getIconBgColor()}`} aria-hidden="true">
{getIcon()}
</div>
<div className="flex-1 min-w-0">
<h2 id="dialog-title" className="text-base sm:text-lg font-semibold">{title}</h2>
{description && (
typeof description === 'string'
? <p id="dialog-description" className="text-sm text-muted-foreground mt-1 break-words" aria-live="polite">{description}</p>
: <div id="dialog-description" className="mt-2" aria-live="polite">{description}</div>
)}
</div>
</div>
<div className="flex flex-col-reverse sm:flex-row justify-end gap-2 sm:gap-2 mt-6">
<Button
ref={cancelButtonRef}
variant="outline"
onClick={onClose}
disabled={isLoading}
className="w-full sm:w-auto min-h-10"
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
if (!isLoading) {
onClose();
}
}
}}
tabIndex={0}
role="button"
aria-label={`${cancelText} and close dialog`}
>
{cancelText}
</Button>
<Button
ref={confirmButtonRef}
variant={variant}
onClick={onConfirm}
disabled={isLoading}
className="w-full sm:w-auto min-h-10"
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
if (!isLoading) {
onConfirm();
}
}
}}
tabIndex={0}
role="button"
aria-label={`${confirmText} action`}
aria-describedby={isLoading ? 'loading-status' : undefined}
>
{isLoading ? (
<>
Processing...
<span id="loading-status" className="sr-only">Please wait, processing your request</span>
</>
) : (
confirmText
)}
</Button>
</div>
{/* Close button */}
{!isLoading && (
<button
onClick={onClose}
className="absolute right-2 top-2 sm:right-4 sm:top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none p-1"
aria-label="Close dialog"
>
<X className="h-4 w-4" />
<span className="sr-only">Close dialog</span>
</button>
)}
</div>
</div>
);
};
export default ConfirmDialog;