Files
hackerspace.sh/components/ui/confirm.tsx
T
Moheeb Zara 1db9434930 feat(ux): accessible confirm dialog replaces window.confirm
- new components/ui/confirm.tsx: ConfirmProvider + useConfirm() built
  on the existing Radix alert-dialog (focus trap, ESC, aria for free)
- wired once into the app layout
- all 13 native window.confirm() delete/remove guards now use the
  styled, accessible confirm with a destructive action button
2026-05-15 22:18:15 -07:00

92 lines
2.7 KiB
TypeScript

'use client'
import * as React from 'react'
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog'
import { buttonVariants } from '@/components/ui/button'
// An accessible replacement for window.confirm(). Wrap the app once in
// <ConfirmProvider> and call `const confirm = useConfirm()` then
// `if (!(await confirm({ title, ... }))) return` at the call site, so the
// imperative shape stays close to the native confirm() it replaces.
type ConfirmOptions = {
title: string
description?: string
confirmText?: string
cancelText?: string
destructive?: boolean
}
type ConfirmFn = (opts: ConfirmOptions) => Promise<boolean>
const ConfirmContext = React.createContext<ConfirmFn | null>(null)
export function useConfirm(): ConfirmFn {
const ctx = React.useContext(ConfirmContext)
if (!ctx) throw new Error('useConfirm must be used within ConfirmProvider')
return ctx
}
export function ConfirmProvider({ children }: { children: React.ReactNode }) {
const [opts, setOpts] = React.useState<ConfirmOptions | null>(null)
const resolverRef = React.useRef<((v: boolean) => void) | null>(null)
const confirm = React.useCallback<ConfirmFn>((next) => {
setOpts(next)
return new Promise<boolean>((resolve) => {
resolverRef.current = resolve
})
}, [])
const settle = React.useCallback((value: boolean) => {
resolverRef.current?.(value)
resolverRef.current = null
setOpts(null)
}, [])
return (
<ConfirmContext.Provider value={confirm}>
{children}
<AlertDialog
open={opts !== null}
onOpenChange={(open) => {
if (!open) settle(false)
}}
>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{opts?.title}</AlertDialogTitle>
{opts?.description ? (
<AlertDialogDescription>{opts.description}</AlertDialogDescription>
) : null}
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel onClick={() => settle(false)}>
{opts?.cancelText ?? 'Cancel'}
</AlertDialogCancel>
<AlertDialogAction
onClick={() => settle(true)}
className={
opts?.destructive
? buttonVariants({ variant: 'destructive' })
: undefined
}
>
{opts?.confirmText ?? 'Confirm'}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</ConfirmContext.Provider>
)
}