"use client"; import { useId, useLayoutEffect, useRef, type ReactNode, type RefObject } from "react"; import { Button } from "@kandev/ui/button"; import { Popover, PopoverAnchor, PopoverContent, PopoverDescription, PopoverHeader, PopoverTitle, } from "@kandev/ui/popover"; export type ActionConfirmPopoverProps = { open: boolean; disabled?: boolean; anchorRef: RefObject; focusBoundaryRef?: RefObject; title: ReactNode; description?: ReactNode; cancelLabel: ReactNode; confirmLabel: ReactNode; confirmAriaLabel?: string; confirmTestId?: string; testId?: string; onOpenChange: (open: boolean) => void; onCancel?: () => void; onConfirm: () => void | Promise; }; /** * A non-modal confirmation surface for one anchored action. * * The component owns only the confirmation shell. Consumers retain mutation, * pending, error, and success state, and the shell closes before confirmation * invokes the consumer callback. */ export function ActionConfirmPopover({ open, disabled = false, anchorRef, focusBoundaryRef, title, description, cancelLabel, confirmLabel, confirmAriaLabel, confirmTestId, testId = "action-confirm-popover", onOpenChange, onCancel, onConfirm, }: ActionConfirmPopoverProps) { const titleId = useId(); const descriptionId = useId(); const cancelRef = useRef(null); const confirmedRef = useRef(false); // Intentionally runs on every render: an anchor can disappear through live // data without changing the confirmation's open state, so each render must // re-check the guard before the shell can invoke a stale action. useLayoutEffect(() => { if (!open) { confirmedRef.current = false; return; } if (isConnected(anchorRef.current)) return; onCancel?.(); onOpenChange(false); }); const handleOpenChange = (nextOpen: boolean) => { if (nextOpen) { confirmedRef.current = false; onOpenChange(true); return; } closeActionConfirm(confirmedRef, onCancel, onOpenChange); }; const handleConfirm = () => { if (disabled) return; if (!isConnected(anchorRef.current)) { handleOpenChange(false); return; } confirmedRef.current = true; handleOpenChange(false); queueMicrotask(() => { void Promise.resolve() .then(onConfirm) .catch(() => undefined); }); }; return ( {/* Radix accepts a null current value at runtime while its public type omits it. */} } /> handleOpenChange(false)} onConfirm={handleConfirm} /> ); } type ActionConfirmPopoverContentProps = { titleId: string; descriptionId: string; title: ReactNode; description?: ReactNode; cancelLabel: ReactNode; confirmLabel: ReactNode; confirmAriaLabel?: string; confirmTestId?: string; testId: string; disabled: boolean; cancelRef: RefObject; focusBoundaryRef?: RefObject; confirmedRef: { current: boolean }; anchorRef: RefObject; onCancel: () => void; onConfirm: () => void; }; function ActionConfirmPopoverContent({ titleId, descriptionId, title, description, cancelLabel, confirmLabel, confirmAriaLabel, confirmTestId, testId, disabled, cancelRef, focusBoundaryRef, confirmedRef, anchorRef, onCancel, onConfirm, }: ActionConfirmPopoverContentProps) { return ( { event.preventDefault(); cancelRef.current?.focus(); }} onFocusOutside={(event) => { if (focusBoundaryRef?.current?.contains(event.target as Node)) event.preventDefault(); }} onCloseAutoFocus={(event) => { event.preventDefault(); if (!confirmedRef.current && isConnected(anchorRef.current)) anchorRef.current.focus(); confirmedRef.current = false; }} > {title} {description ? ( {description} ) : null}
); } function closeActionConfirm( confirmedRef: { current: boolean }, onCancel: (() => void) | undefined, onOpenChange: (open: boolean) => void, ) { if (!confirmedRef.current) onCancel?.(); onOpenChange(false); } function isConnected(element: HTMLElement | null): element is HTMLElement { return element !== null && element.isConnected; }