forked from CredenceOrg/Credence-Frontend
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathToastProvider.tsx
More file actions
119 lines (102 loc) · 4.04 KB
/
Copy pathToastProvider.tsx
File metadata and controls
119 lines (102 loc) · 4.04 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
import { createContext, useContext, useState, useCallback, useRef, type ReactNode } from 'react'
import { useSettings } from '../context/SettingsContext'
import Toast, { type ToastData, type ToastSeverity } from './Toast'
import './Toast.css'
const TIMEOUTS: Record<ToastSeverity, number> = {
info: 5000,
success: 5000,
warning: 8000,
danger: 0,
}
interface ToastContextValue {
addToast: (severity: ToastSeverity, message: string) => void
removeToast: (id: string) => void
removeAllToasts: () => void
}
const ToastContext = createContext<ToastContextValue | null>(null)
export function useToast() {
const ctx = useContext(ToastContext)
if (!ctx) throw new Error('useToast must be used within ToastProvider')
return ctx
}
export default function ToastProvider({ children }: { children: ReactNode }) {
const { toastsEnabled, autoDismiss } = useSettings()
/**
* We use a ref to track the current settings to avoid recreating `addToast`
* on every setting change, which would cause unnecessary re-renders of consumers.
*/
const settingsRef = useRef({ toastsEnabled, autoDismiss })
settingsRef.current = { toastsEnabled, autoDismiss }
const [toasts, setToasts] = useState<ToastData[]>([])
const idCounter = useRef(0)
const timeoutsMap = useRef<Map<string, ReturnType<typeof setTimeout>>>(new Map())
const removeToast = useCallback((id: string) => {
setToasts((prev: ToastData[]) => prev.filter((t: ToastData) => t.id !== id))
const timerId = timeoutsMap.current.get(id)
if (timerId) {
clearTimeout(timerId)
timeoutsMap.current.delete(id)
}
}, [])
const removeAllToasts = useCallback(() => {
setToasts([])
timeoutsMap.current.forEach((timerId) => clearTimeout(timerId))
timeoutsMap.current.clear()
}, [])
const addToast = useCallback(
(severity: ToastSeverity, message: string) => {
const { toastsEnabled, autoDismiss } = settingsRef.current
// respect global toast enable setting
if (!toastsEnabled) return
const id = String(++idCounter.current)
const newToast: ToastData = { id, severity, message }
setToasts((prev: ToastData[]) => [...prev, newToast])
// compute timeout: settings `autoDismiss` can override default TIMEOUTS
let timeout = TIMEOUTS[severity]
if (timeout > 0) {
try {
if (autoDismiss === 'off') {
timeout = 0
} else if (typeof autoDismiss === 'string' && autoDismiss.endsWith('s')) {
const seconds = Number(autoDismiss.replace('s', ''))
if (!Number.isNaN(seconds)) timeout = seconds * 1000
}
} catch {
// fallback to default
}
}
if (timeout > 0) {
const timerId = setTimeout(() => removeToast(id), timeout)
timeoutsMap.current.set(id, timerId)
}
},
[removeToast]
)
/** Toasts split by politeness: danger → assertive; all others → polite. */
const politeToasts = toasts.filter((t: ToastData) => t.severity !== 'danger')
const assertiveToasts = toasts.filter((t: ToastData) => t.severity === 'danger')
return (
<ToastContext.Provider value={{ addToast, removeToast, removeAllToasts }}>
{children}
<div className="toast-container">
{toasts.length > 1 && (
<button type="button" className="toast-dismiss-all" onClick={removeAllToasts}>
Dismiss All
</button>
)}
{/* Polite region: info, success, warning — announced when the screen reader is idle */}
<div role="region" aria-live="polite" aria-label="Notifications">
{politeToasts.map((t: ToastData) => (
<Toast key={t.id} toast={t} onDismiss={removeToast} />
))}
</div>
{/* Assertive region: danger — interrupts and announces immediately */}
<div role="region" aria-live="assertive" aria-label="Error notifications">
{assertiveToasts.map((t: ToastData) => (
<Toast key={t.id} toast={t} onDismiss={removeToast} />
))}
</div>
</div>
</ToastContext.Provider>
)
}