29ccbc22e9
Modal component: portal, ESC to close, Tab focus-trap, focus-on-open
(prefers [data-modal-autofocus]), focus restore, overlay click, scroll lock.
ConfirmProvider exposes useConfirm(): async confirm({...}) as a drop-in for
window.confirm; Enter confirms, ESC cancels. Task/account deletes now use it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMHQTtnQtQqL8muAXHr9kd
74 lines
2.2 KiB
TypeScript
74 lines
2.2 KiB
TypeScript
import { createContext, useCallback, useContext, useRef, useState, type ReactNode } from 'react'
|
|
import { Modal } from './Modal'
|
|
|
|
export type ConfirmOptions = {
|
|
title?: string
|
|
message: string
|
|
confirmLabel?: string
|
|
cancelLabel?: string
|
|
danger?: boolean
|
|
}
|
|
|
|
type ConfirmFn = (opts: ConfirmOptions) => Promise<boolean>
|
|
|
|
const ConfirmContext = createContext<ConfirmFn | null>(null)
|
|
|
|
// Drop-in async replacement for window.confirm(): `await confirm({ message })`.
|
|
export function useConfirm(): ConfirmFn {
|
|
const ctx = useContext(ConfirmContext)
|
|
if (!ctx) throw new Error('useConfirm must be used within <ConfirmProvider>')
|
|
return ctx
|
|
}
|
|
|
|
export function ConfirmProvider({ children }: { children: ReactNode }) {
|
|
const [opts, setOpts] = useState<ConfirmOptions | null>(null)
|
|
const resolver = useRef<(v: boolean) => void>(() => {})
|
|
|
|
const confirm = useCallback<ConfirmFn>((o) => {
|
|
setOpts(o)
|
|
return new Promise<boolean>((resolve) => {
|
|
resolver.current = resolve
|
|
})
|
|
}, [])
|
|
|
|
const finish = useCallback((result: boolean) => {
|
|
resolver.current(result)
|
|
resolver.current = () => {}
|
|
setOpts(null)
|
|
}, [])
|
|
|
|
return (
|
|
<ConfirmContext.Provider value={confirm}>
|
|
{children}
|
|
<Modal open={opts !== null} title={opts?.title ?? 'Confirm'} onClose={() => finish(false)}>
|
|
{opts && (
|
|
<div
|
|
className="confirm-body"
|
|
onKeyDown={(e) => {
|
|
if (e.key === 'Enter') {
|
|
e.preventDefault()
|
|
finish(true)
|
|
}
|
|
}}
|
|
>
|
|
<p className="confirm-message">{opts.message}</p>
|
|
<div className="modal-actions">
|
|
<button type="button" className="btn" onClick={() => finish(false)}>
|
|
{opts.cancelLabel ?? 'Cancel'}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className={opts.danger ? 'btn btn-danger' : 'btn btn-primary'}
|
|
data-modal-autofocus
|
|
onClick={() => finish(true)}
|
|
>
|
|
{opts.confirmLabel ?? 'Confirm'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</Modal>
|
|
</ConfirmContext.Provider>
|
|
)
|
|
}
|