You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
47 lines
1.6 KiB
TypeScript
47 lines
1.6 KiB
TypeScript
import { createContext, useCallback, useContext, useMemo, useState, type ReactNode } from 'react'
|
|
import { dismissToast, initialToasts, pushToast, type Toast, type ToastState } from './toast-store'
|
|
|
|
const AUTO_DISMISS_MS = 4000
|
|
|
|
interface ToastApi { ok: (msg: string) => void; err: (msg: string) => void }
|
|
const Ctx = createContext<ToastApi | null>(null)
|
|
|
|
/** Mount once at app root; renders the bottom-right stack. */
|
|
export function ToastProvider(props: { children: ReactNode }) {
|
|
const [state, setState] = useState<ToastState>(initialToasts)
|
|
|
|
const push = useCallback((tone: Toast['tone'], message: string) => {
|
|
setState((s) => {
|
|
const next = pushToast(s, tone, message)
|
|
const id = next.toasts[next.toasts.length - 1]!.id
|
|
window.setTimeout(() => setState((s2) => dismissToast(s2, id)), AUTO_DISMISS_MS)
|
|
return next
|
|
})
|
|
}, [])
|
|
|
|
const api = useMemo<ToastApi>(() => ({
|
|
ok: (msg) => push('ok', msg),
|
|
err: (msg) => push('err', msg),
|
|
}), [push])
|
|
|
|
return (
|
|
<Ctx.Provider value={api}>
|
|
{props.children}
|
|
<div className="wf-toasts" aria-live="polite">
|
|
{state.toasts.map((t) => (
|
|
<div key={t.id} className={`wf-toast ${t.tone}`}>
|
|
<span className="msg">{t.message}</span>
|
|
<button type="button" aria-label="Dismiss" onClick={() => setState((s) => dismissToast(s, t.id))}>✕</button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</Ctx.Provider>
|
|
)
|
|
}
|
|
|
|
export function useToast(): ToastApi {
|
|
const api = useContext(Ctx)
|
|
if (api === null) throw new Error('useToast requires <ToastProvider> at the app root')
|
|
return api
|
|
}
|