Files
support_frontend/src/components/ui/dialog.tsx
T
2026-08-21 10:41:09 +05:30

190 lines
5.1 KiB
TypeScript

import * as React from 'react';
import { X } from 'lucide-react';
import { cn } from '@/lib/utils';
interface DialogContextValue {
open: boolean;
onOpenChange: (open: boolean) => void;
}
const DialogContext = React.createContext<DialogContextValue | null>(null);
export interface DialogProps {
open?: boolean;
onOpenChange?: (open: boolean) => void;
defaultOpen?: boolean;
children: React.ReactNode;
}
const Dialog: React.FC<DialogProps> = ({
open: controlledOpen,
onOpenChange,
defaultOpen = false,
children,
}) => {
const [uncontrolledOpen, setUncontrolledOpen] = React.useState(defaultOpen);
const isOpen = controlledOpen !== undefined ? controlledOpen : uncontrolledOpen;
const handleOpenChange = React.useCallback(
(newOpen: boolean) => {
if (controlledOpen === undefined) {
setUncontrolledOpen(newOpen);
}
onOpenChange?.(newOpen);
},
[controlledOpen, onOpenChange]
);
return (
<DialogContext.Provider value={{ open: isOpen, onOpenChange: handleOpenChange }}>
{children}
</DialogContext.Provider>
);
};
export interface DialogTriggerProps {
asChild?: boolean;
children: React.ReactElement;
}
const DialogTrigger: React.FC<DialogTriggerProps> = ({ children }) => {
const context = React.useContext(DialogContext);
if (!context) throw new Error('DialogTrigger must be used within Dialog');
return React.cloneElement(children, {
onClick: (e: React.MouseEvent) => {
children.props.onClick?.(e);
context.onOpenChange(true);
},
});
};
export interface DialogContentProps extends React.HTMLAttributes<HTMLDivElement> {
showClose?: boolean;
}
const DialogContent = React.forwardRef<HTMLDivElement, DialogContentProps>(
({ className, children, showClose = true, ...props }, ref) => {
const context = React.useContext(DialogContext);
if (!context) throw new Error('DialogContent must be used within Dialog');
React.useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape' && context.open) {
context.onOpenChange(false);
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [context]);
if (!context.open) return null;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
{/* Backdrop */}
<div
className="fixed inset-0 bg-black/50 backdrop-blur-sm animate-fade-in"
onClick={() => context.onOpenChange(false)}
/>
{/* Modal Panel */}
<div
ref={ref}
role="dialog"
aria-modal="true"
className={cn(
'relative z-50 w-full max-w-lg rounded-xl border border-border bg-background p-6 shadow-dialog animate-scale-in flex flex-col gap-4',
className
)}
{...props}
>
{children}
{showClose && (
<button
type="button"
onClick={() => context.onOpenChange(false)}
className="absolute right-4 top-4 rounded-md p-1.5 text-muted-foreground hover:bg-accent hover:text-foreground transition-colors"
aria-label="Close modal"
>
<X className="h-4 w-4" />
</button>
)}
</div>
</div>
);
}
);
DialogContent.displayName = 'DialogContent';
const DialogHeader: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({
className,
...props
}) => (
<div
className={cn('flex flex-col space-y-1.5 text-center sm:text-left', className)}
{...props}
/>
);
DialogHeader.displayName = 'DialogHeader';
const DialogTitle = React.forwardRef<
HTMLHeadingElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h2
ref={ref}
className={cn('text-lg font-semibold leading-none tracking-tight text-foreground', className)}
{...props}
/>
));
DialogTitle.displayName = 'DialogTitle';
const DialogDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<p
ref={ref}
className={cn('text-sm text-muted-foreground leading-relaxed', className)}
{...props}
/>
));
DialogDescription.displayName = 'DialogDescription';
const DialogFooter: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({
className,
...props
}) => (
<div
className={cn(
'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2 gap-2 mt-4',
className
)}
{...props}
/>
);
DialogFooter.displayName = 'DialogFooter';
const DialogClose: React.FC<{ children: React.ReactElement }> = ({ children }) => {
const context = React.useContext(DialogContext);
if (!context) throw new Error('DialogClose must be used within Dialog');
return React.cloneElement(children, {
onClick: (e: React.MouseEvent) => {
children.props.onClick?.(e);
context.onOpenChange(false);
},
});
};
export {
Dialog,
DialogTrigger,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogFooter,
DialogClose,
};