import React, { forwardRef, useCallback, useEffect, useId, useRef, useState } from 'react'; import { Surface, cn, toneMap, surfaceClasses, useEffectiveSurface, } from '../common'; import { PixelPortal } from '../overlay-foundation/PixelPortal'; import { OverlayBackdrop } from './_internal/OverlayBackdrop'; import { useFocusTrap } from '../hooks/useFocusTrap'; import { useEscape } from '../hooks/useEscape'; import { useScrollLock } from '../hooks/useScrollLock'; import { useReducedMotion } from '../hooks/useReducedMotion'; export interface PixelAlertDialogProps { open: boolean; onOpenChange: (open: boolean) => void; title: string; description?: string; cancelLabel?: string; actionLabel?: string; onAction: () => void | Promise; /** * Called when `onAction` throws / rejects. Receives the thrown value. * When set, the dialog stays OPEN on failure so the consumer can show * an inline error. When unset, errors are silently swallowed (back-compat). */ onError?: (error: unknown) => void; destructive?: boolean; surface?: Surface; } export const PixelAlertDialog = forwardRef(function PixelAlertDialog( { open, onOpenChange, title, description, cancelLabel = 'Cancel', actionLabel = 'Confirm', onAction, onError, destructive = false, surface: surfaceProp, }, ref, ) { const surface = useEffectiveSurface(surfaceProp); const s = surfaceClasses(surface); const titleId = useId(); const descId = useId(); const containerRef = useRef(null); const cancelBtnRef = useRef(null); const [pending, setPending] = useState(false); const reducedMotion = useReducedMotion(); const spinClass = reducedMotion ? '' : 'animate-spin'; const setRefs = (node: HTMLDivElement | null) => { (containerRef as React.MutableRefObject).current = node; if (typeof ref === 'function') ref(node); else if (ref) (ref as React.MutableRefObject).current = node; }; useScrollLock(open); useFocusTrap(open, containerRef); useEscape(() => { if (pending) return; onOpenChange(false); }, open); // Pin focus to the Cancel button on open (safer default for destructive flows). useEffect(() => { if (!open) return; const id = setTimeout(() => cancelBtnRef.current?.focus(), 0); return () => clearTimeout(id); }, [open]); // Reset pending when the dialog is closed externally. useEffect(() => { if (!open) setPending(false); }, [open]); const handleCancel = useCallback(() => { if (pending) return; onOpenChange(false); }, [pending, onOpenChange]); const handleAction = useCallback(async () => { if (pending) return; let result: void | Promise; try { result = onAction(); } catch (err) { // Sync throw: do not auto-close so the consumer can show the error. if (onError) onError(err); else throw err; return; } const isPromise = result && typeof (result as Promise).then === 'function'; if (!isPromise) { onOpenChange(false); return; } setPending(true); try { await result; onOpenChange(false); } catch (err) { // Async reject: keep open + surface error. if (onError) onError(err); else if (typeof console !== 'undefined') { console.error('[PixelAlertDialog] onAction rejected:', err); } } finally { setPending(false); } }, [pending, onAction, onOpenChange, onError]); if (!open) return null; const accentTone = destructive ? 'red' : 'cyan'; const t = toneMap[accentTone]; return (
{surface === 'pixel' ? ( <>

{title}

{description && (

{description}

)}
) : ( <>

{title}

{description && (

{description}

)}
)}
); }); PixelAlertDialog.displayName = 'PixelAlertDialog';