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
This commit is contained in:
@@ -8,6 +8,7 @@ import remarkGfm from 'remark-gfm'
|
||||
import { addComment, deleteComment } from '@/lib/actions'
|
||||
import { Trash2, MessageCircle } from 'lucide-react'
|
||||
import { Empty, EmptyHeader, EmptyMedia, EmptyTitle } from '@/components/ui/empty'
|
||||
import { useConfirm } from '@/components/ui/confirm'
|
||||
|
||||
export type CommentEntityType = 'forum_thread' | 'proposal' | 'incident' | 'policy'
|
||||
|
||||
@@ -31,6 +32,7 @@ interface Props {
|
||||
|
||||
export function CommentThread({ entityType, entityId, comments, currentMemberId, canModerate, locked }: Props) {
|
||||
const router = useRouter()
|
||||
const confirm = useConfirm()
|
||||
const [body, setBody] = useState('')
|
||||
const [saving, setSaving] = useState(false)
|
||||
|
||||
@@ -46,7 +48,7 @@ export function CommentThread({ entityType, entityId, comments, currentMemberId,
|
||||
}
|
||||
|
||||
async function handleDelete(commentId: string) {
|
||||
if (!confirm('Delete this comment?')) return
|
||||
if (!(await confirm({ title: 'Delete comment', description: 'This comment will be permanently removed.', confirmText: 'Delete', destructive: true }))) return
|
||||
const result = await deleteComment(commentId)
|
||||
if (result.error) { toast.error(result.error); return }
|
||||
toast.success('Comment deleted')
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
'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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user