All files / src/hooks useConfirmDialog.tsx

100% Statements 22/22
100% Branches 6/6
100% Functions 7/7
100% Lines 22/22

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                                            51x                                                 362x 362x     362x 138x 138x 138x       362x   10x 10x 10x 10x                     362x 2x 2x 2x     362x 2x 2x 2x       362x                       362x    
import { useState, useRef, useCallback, useEffect } from "react";
import { ConfirmDialog } from "../components/ui/confirm-dialog";
 
export interface ConfirmOptions {
	title: string;
	description: string;
	confirmLabel?: string;
	cancelLabel?: string;
	variant?: "default" | "destructive";
}
 
export type ConfirmFn = (options: ConfirmOptions) => Promise<boolean>;
 
interface DialogState {
	open: boolean;
	title: string;
	description: string;
	confirmLabel: string;
	cancelLabel: string;
	variant: "default" | "destructive";
}
 
const CLOSED_STATE: DialogState = {
	open: false,
	title: "",
	description: "",
	confirmLabel: "Confirm",
	cancelLabel: "Cancel",
	variant: "default",
};
 
/**
 * Promise-based confirm dialog hook. Returns a `confirm()` function that opens
 * a dialog and resolves to `true` (confirmed) or `false` (cancelled), plus a
 * `dialog` element to render in JSX.
 *
 * Usage:
 * ```tsx
 * const { confirm, dialog } = useConfirmDialog();
 * const handleDelete = async () => {
 *   if (!(await confirm({ title: "Delete?", description: "Cannot undo." }))) return;
 *   // proceed with delete
 * };
 * return <>{dialog}</>;
 * ```
 */
export function useConfirmDialog() {
	const [state, setState] = useState(CLOSED_STATE);
	const resolveRef = useRef<((value: boolean) => void) | null>(null);
 
	// Resolve any pending promise as false on unmount to prevent leaks
	useEffect(() => {
		return () => {
			resolveRef.current?.(false);
			resolveRef.current = null;
		};
	}, []);
 
	const confirm = useCallback((options: ConfirmOptions): Promise<boolean> => {
		// Resolve any pending promise as cancelled before opening a new dialog
		resolveRef.current?.(false);
		return new Promise<boolean>((resolve) => {
			resolveRef.current = resolve;
			setState({
				open: true,
				title: options.title,
				description: options.description,
				confirmLabel: options.confirmLabel ?? "Confirm",
				cancelLabel: options.cancelLabel ?? "Cancel",
				variant: options.variant ?? "default",
			});
		});
	}, []);
 
	const handleConfirm = useCallback(() => {
		resolveRef.current?.(true);
		resolveRef.current = null;
		setState(CLOSED_STATE);
	}, []);
 
	const handleCancel = useCallback(() => {
		resolveRef.current?.(false);
		resolveRef.current = null;
		setState(CLOSED_STATE);
	}, []);
 
	const dialog = (
		<ConfirmDialog
			open={state.open}
			title={state.title}
			description={state.description}
			confirmLabel={state.confirmLabel}
			cancelLabel={state.cancelLabel}
			variant={state.variant}
			onConfirm={handleConfirm}
			onCancel={handleCancel}
		/>
	);
 
	return { confirm, dialog };
}