funda-ui 4.7.101 → 4.7.105
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Chatbox/index.js +6 -1
- package/Checkbox/index.js +10 -1
- package/Date/index.js +12 -2
- package/Input/index.js +6 -1
- package/LiveSearch/index.js +5 -0
- package/MultipleCheckboxes/index.js +27 -1
- package/NumberInput/index.js +6 -1
- package/Radio/index.js +22 -1
- package/RangeSlider/index.js +6 -1
- package/Stepper/index.css +109 -34
- package/Stepper/index.d.ts +1 -1
- package/Stepper/index.js +55 -2
- package/TagInput/index.js +10 -1
- package/Textarea/index.js +6 -1
- package/Toast/index.css +23 -75
- package/Toast/index.d.ts +3 -34
- package/Toast/index.js +652 -175
- package/lib/cjs/Chatbox/index.js +6 -1
- package/lib/cjs/Checkbox/index.js +10 -1
- package/lib/cjs/Date/index.js +12 -2
- package/lib/cjs/Input/index.js +6 -1
- package/lib/cjs/LiveSearch/index.js +5 -0
- package/lib/cjs/MultipleCheckboxes/index.js +27 -1
- package/lib/cjs/NumberInput/index.js +6 -1
- package/lib/cjs/Radio/index.js +22 -1
- package/lib/cjs/RangeSlider/index.js +6 -1
- package/lib/cjs/Stepper/index.d.ts +1 -1
- package/lib/cjs/Stepper/index.js +55 -2
- package/lib/cjs/TagInput/index.js +10 -1
- package/lib/cjs/Textarea/index.js +6 -1
- package/lib/cjs/Toast/index.d.ts +3 -34
- package/lib/cjs/Toast/index.js +652 -175
- package/lib/css/Stepper/index.css +109 -34
- package/lib/css/Toast/index.css +23 -75
- package/lib/esm/Checkbox/index.tsx +12 -1
- package/lib/esm/Date/index.tsx +8 -1
- package/lib/esm/Input/index.tsx +8 -1
- package/lib/esm/LiveSearch/index.tsx +7 -0
- package/lib/esm/MultipleCheckboxes/index.tsx +19 -1
- package/lib/esm/NumberInput/index.tsx +8 -1
- package/lib/esm/Radio/index.tsx +17 -1
- package/lib/esm/Stepper/index.scss +135 -36
- package/lib/esm/Stepper/index.tsx +51 -3
- package/lib/esm/TagInput/index.tsx +8 -1
- package/lib/esm/Textarea/index.tsx +8 -1
- package/lib/esm/Toast/Item.tsx +52 -11
- package/lib/esm/Toast/Toast.tsx +391 -0
- package/lib/esm/Toast/ToastContext.tsx +104 -0
- package/lib/esm/Toast/__toast.vanilla.js +422 -0
- package/lib/esm/Toast/index.scss +24 -96
- package/lib/esm/Toast/index.tsx +3 -374
- package/lib/esm/Toast/types.ts +60 -0
- package/lib/esm/Toast/useToast.tsx +72 -0
- package/package.json +1 -1
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
import React, { useRef, useEffect, useState, useCallback } from 'react';
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
import RootPortal from 'funda-root-portal';
|
|
5
|
+
import useComId from 'funda-utils/dist/cjs/useComId';
|
|
6
|
+
import { clsWrite, combinedCls } from 'funda-utils/dist/cjs/cls';
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
import Item from './Item';
|
|
10
|
+
|
|
11
|
+
import type { ToastOptions } from './types';
|
|
12
|
+
|
|
13
|
+
export interface ToastProps {
|
|
14
|
+
data: Array<ToastOptions>;
|
|
15
|
+
|
|
16
|
+
// default props
|
|
17
|
+
defaultWrapperClassName?: string;
|
|
18
|
+
defaultOnlyShowOne?: boolean;
|
|
19
|
+
defaultDirection?: ToastOptions['direction'];
|
|
20
|
+
defaultCascading?: boolean;
|
|
21
|
+
defaultReverseDisplay?: boolean;
|
|
22
|
+
|
|
23
|
+
onUpdate?: (updatedData: Array<ToastOptions>) => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
export const Toast: React.FC<ToastProps> = ({
|
|
28
|
+
data,
|
|
29
|
+
|
|
30
|
+
// default props
|
|
31
|
+
defaultWrapperClassName,
|
|
32
|
+
defaultOnlyShowOne,
|
|
33
|
+
defaultDirection,
|
|
34
|
+
defaultCascading,
|
|
35
|
+
defaultReverseDisplay,
|
|
36
|
+
|
|
37
|
+
onUpdate,
|
|
38
|
+
}) => {
|
|
39
|
+
const ANIM_SPEED = 300;
|
|
40
|
+
const DEFAULT_AUTO_CLOSE_TIME = 3000;
|
|
41
|
+
const uniqueID = useComId();
|
|
42
|
+
const rootRef = useRef<any>(null);
|
|
43
|
+
|
|
44
|
+
// action id
|
|
45
|
+
const [currentActionId, setCurrentActionId] = useState<string | number | null | undefined>(undefined);
|
|
46
|
+
|
|
47
|
+
// 追踪每个 toast 的动画状态
|
|
48
|
+
const [animatedToasts, setAnimatedToasts] = useState<Set<string>>(new Set());
|
|
49
|
+
|
|
50
|
+
// force display
|
|
51
|
+
const [initPopRoot, setInitPopRoot] = useState<boolean>(false);
|
|
52
|
+
|
|
53
|
+
// Get the global configuration from the first toast item (if it exists)
|
|
54
|
+
const firstToast = data[0] || {};
|
|
55
|
+
|
|
56
|
+
// Use default values but allow individual toast overrides
|
|
57
|
+
const wrapperClassName = firstToast.wrapperClassName || defaultWrapperClassName;
|
|
58
|
+
const direction = firstToast.direction || defaultDirection;
|
|
59
|
+
const cascadingEnabled = typeof firstToast.cascading !== 'undefined' ? firstToast.cascading : defaultCascading;
|
|
60
|
+
const onlyShowOne = typeof firstToast.onlyShowOne !== 'undefined' ? firstToast.onlyShowOne : defaultOnlyShowOne;
|
|
61
|
+
const reverseDisplay = typeof firstToast.reverseDisplay !== 'undefined' ? firstToast.reverseDisplay : defaultReverseDisplay;
|
|
62
|
+
|
|
63
|
+
const depth: number = data.length + 1;
|
|
64
|
+
|
|
65
|
+
// Processes the order of data based on the "direction" and "reverseDisplay" parameters
|
|
66
|
+
const getProcessedData = useCallback(() => {
|
|
67
|
+
let processedData = [...data];
|
|
68
|
+
|
|
69
|
+
if (
|
|
70
|
+
(direction?.startsWith('top-') || direction?.startsWith('vertical-')) &&
|
|
71
|
+
reverseDisplay
|
|
72
|
+
) {
|
|
73
|
+
processedData.reverse();
|
|
74
|
+
} else if (
|
|
75
|
+
direction?.startsWith('bottom-') &&
|
|
76
|
+
!reverseDisplay
|
|
77
|
+
) {
|
|
78
|
+
processedData.reverse();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// If only one is displayed, only the last one is returned
|
|
82
|
+
return onlyShowOne ? [processedData[processedData.length - 1]] : processedData;
|
|
83
|
+
}, [data, direction, reverseDisplay, onlyShowOne]);
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
// Store the status of each toast
|
|
88
|
+
const progressPausedRef = useRef<Map<string, boolean>>(new Map());
|
|
89
|
+
const progressObjRef = useRef<Map<string, any>>(new Map());
|
|
90
|
+
const progressIntervalRef = useRef<Map<string, NodeJS.Timeout | null>>(new Map());
|
|
91
|
+
|
|
92
|
+
const startProgressTimer = useCallback((el: any, toastId: string, i: number) => {
|
|
93
|
+
// If the toast already has a timer running, do not add it again
|
|
94
|
+
if (progressIntervalRef.current.has(toastId)) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const currentToast = data[i];
|
|
98
|
+
|
|
99
|
+
// progress animation
|
|
100
|
+
const PROGRESS_TRANSITION_TIME: any = typeof (currentToast.autoCloseTime) === 'undefined' || currentToast.autoCloseTime === false ? DEFAULT_AUTO_CLOSE_TIME as number : currentToast.autoCloseTime;
|
|
101
|
+
|
|
102
|
+
// init progress
|
|
103
|
+
let progressCurrentChunk = 100 / (PROGRESS_TRANSITION_TIME / 100);
|
|
104
|
+
el.firstChild.style.width = 100 + '%';
|
|
105
|
+
el.firstChild.ariaValueNow = 100;
|
|
106
|
+
|
|
107
|
+
// animation
|
|
108
|
+
const intervalId = setInterval(() => {
|
|
109
|
+
// console.log('toast setInterval');
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
if (!progressPausedRef.current.get(toastId)) {
|
|
113
|
+
const progPercent = 100 - progressCurrentChunk;
|
|
114
|
+
|
|
115
|
+
el.firstChild.style.width = progPercent + '%';
|
|
116
|
+
el.firstChild.ariaValueNow = progPercent;
|
|
117
|
+
progressCurrentChunk++;
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
//
|
|
121
|
+
if (progPercent === 0 || progPercent < 1) { // may be 0.xxx
|
|
122
|
+
el.classList.add('complete');
|
|
123
|
+
|
|
124
|
+
// stop current animation
|
|
125
|
+
stopProgressTimer(toastId);
|
|
126
|
+
|
|
127
|
+
// hide toast item
|
|
128
|
+
const currentItem = el.closest('.toast-container');
|
|
129
|
+
handleClose(null, i, currentItem);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}, PROGRESS_TRANSITION_TIME / 100);
|
|
133
|
+
|
|
134
|
+
// Save the timer ID
|
|
135
|
+
progressIntervalRef.current.set(toastId, intervalId);
|
|
136
|
+
|
|
137
|
+
}, [data]);
|
|
138
|
+
|
|
139
|
+
const clearAllProgressTimer = useCallback(() => {
|
|
140
|
+
progressIntervalRef.current.forEach((timer, id) => {
|
|
141
|
+
if (timer) {
|
|
142
|
+
clearInterval(timer);
|
|
143
|
+
progressIntervalRef.current.set(id, null);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
progressIntervalRef.current.clear();
|
|
147
|
+
}, []);
|
|
148
|
+
|
|
149
|
+
const stopProgressTimer = useCallback((toastId: string) => {
|
|
150
|
+
const timer = progressIntervalRef.current.get(toastId);
|
|
151
|
+
if (timer) {
|
|
152
|
+
clearInterval(timer);
|
|
153
|
+
progressIntervalRef.current.delete(toastId);
|
|
154
|
+
}
|
|
155
|
+
}, []);
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
function handleProgressPaused(e: any) {
|
|
159
|
+
const toastId = e.currentTarget.dataset.toastId;
|
|
160
|
+
progressPausedRef.current.set(toastId, true);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function handleProgressStart(e: any) {
|
|
164
|
+
const toastId = e.currentTarget.dataset.toastId;
|
|
165
|
+
progressPausedRef.current.set(toastId, false);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function handleClose(e: any, index: number, currentItem: HTMLDivElement) {
|
|
169
|
+
if (typeof e !== 'undefined' && e !== null) e.preventDefault();
|
|
170
|
+
if (rootRef.current === null) return;
|
|
171
|
+
|
|
172
|
+
const curIndex = Number(index);
|
|
173
|
+
const currentToast = data[curIndex];
|
|
174
|
+
const toastId = currentToast.id as string;
|
|
175
|
+
const _list: HTMLDivElement[] = [].slice.call(rootRef.current.querySelectorAll('.toast-container'));
|
|
176
|
+
currentItem.classList.add('hide-start');
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
//Let the removed animation show
|
|
180
|
+
setTimeout(() => {
|
|
181
|
+
_list.forEach((node: any) => {
|
|
182
|
+
node.classList.remove('hide-start');
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// remove current
|
|
186
|
+
currentItem.classList.add('hide-end');
|
|
187
|
+
|
|
188
|
+
// rearrange
|
|
189
|
+
if (cascadingEnabled) {
|
|
190
|
+
_list.filter((node: any) => !node.classList.contains('hide-end'))
|
|
191
|
+
.forEach((node: any, k: number) => {
|
|
192
|
+
node.style.transform = `perspective(100px) translateZ(-${2 * k}px) translateY(${35 * k}px)`;
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
// stop all animations or current animation
|
|
198
|
+
stopProgressTimer(toastId);
|
|
199
|
+
|
|
200
|
+
// close callback
|
|
201
|
+
const currentToast = data[curIndex];
|
|
202
|
+
if (currentToast.onClose) {
|
|
203
|
+
currentToast.onClose(
|
|
204
|
+
rootRef.current,
|
|
205
|
+
curIndex,
|
|
206
|
+
_list.filter((node: HTMLDivElement) => !node.classList.contains('hide-end'))
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// After the animation ends, remove the item from the data source
|
|
211
|
+
if (onUpdate) {
|
|
212
|
+
const newData = [...data];
|
|
213
|
+
newData.splice(curIndex, 1);
|
|
214
|
+
onUpdate(newData);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
}, ANIM_SPEED);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
useEffect(() => {
|
|
223
|
+
if (initPopRoot) {
|
|
224
|
+
const $toast = rootRef.current;
|
|
225
|
+
if (!$toast) return;
|
|
226
|
+
|
|
227
|
+
// When "onlyShowOne" is true, only the latest toast is kept
|
|
228
|
+
//--------------
|
|
229
|
+
if (onlyShowOne && data.length > 1) {
|
|
230
|
+
// Clear all old timers
|
|
231
|
+
data.slice(0, -1).forEach((toast, index) => {
|
|
232
|
+
stopProgressTimer(toast.id as string);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
//Only keep the latest toast
|
|
236
|
+
if (onUpdate) {
|
|
237
|
+
onUpdate([data[data.length - 1]]);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
// Auto hide
|
|
245
|
+
//--------------
|
|
246
|
+
data.forEach((toast, i) => {
|
|
247
|
+
// auto close
|
|
248
|
+
const AUTO_CLOSE_TIME: any = typeof (toast.autoCloseTime) === 'undefined' || toast.autoCloseTime === false ? false : toast.autoCloseTime;
|
|
249
|
+
|
|
250
|
+
if (AUTO_CLOSE_TIME !== false) {
|
|
251
|
+
const el = progressObjRef.current.get(toast.id as string);
|
|
252
|
+
if (el) {
|
|
253
|
+
startProgressTimer(el, toast.id as string, i);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
}, [data, initPopRoot, onlyShowOne]);
|
|
261
|
+
|
|
262
|
+
// Handling animation when data changes
|
|
263
|
+
useEffect(() => {
|
|
264
|
+
if (initPopRoot && data.length > 0) {
|
|
265
|
+
// Add animation to the new toast
|
|
266
|
+
const newToasts = data.filter(toast => !animatedToasts.has(toast.id as string));
|
|
267
|
+
|
|
268
|
+
if (newToasts.length > 0) {
|
|
269
|
+
// Keep the new toast hidden first
|
|
270
|
+
newToasts.forEach(toast => {
|
|
271
|
+
const progressEl = progressObjRef.current.get(toast.id as string);
|
|
272
|
+
if (progressEl) {
|
|
273
|
+
const currentItem = progressEl.closest('.toast-container');
|
|
274
|
+
currentItem.classList.add('animate-ready');
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
// Use "requestAnimationFrame" to ensure the DOM is updated before adding animation classes
|
|
279
|
+
requestAnimationFrame(() => {
|
|
280
|
+
setTimeout(() => {
|
|
281
|
+
newToasts.forEach(toast => {
|
|
282
|
+
const progressEl = progressObjRef.current.get(toast.id as string);
|
|
283
|
+
if (progressEl) {
|
|
284
|
+
const currentItem = progressEl.closest('.toast-container');
|
|
285
|
+
currentItem.classList.remove('animate-ready');
|
|
286
|
+
currentItem.classList.add('animate-in');
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
// Update the animated toast list
|
|
291
|
+
setAnimatedToasts(prev => {
|
|
292
|
+
const newSet = new Set(prev);
|
|
293
|
+
newToasts.forEach(toast => newSet.add(toast.id as string));
|
|
294
|
+
return newSet;
|
|
295
|
+
});
|
|
296
|
+
}, 50); // A small delay ensures that animate-ready styles are applied
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}, [data, initPopRoot]);
|
|
301
|
+
|
|
302
|
+
// Monitor the currently displayed toast id
|
|
303
|
+
useEffect(() => {
|
|
304
|
+
if (onlyShowOne && data.length > 0) {
|
|
305
|
+
const latestToast = data[data.length - 1];
|
|
306
|
+
setCurrentActionId(latestToast.actionId);
|
|
307
|
+
} else if (data.length > 0) {
|
|
308
|
+
const currentIds = data.map(toast => toast.actionId);
|
|
309
|
+
setCurrentActionId(currentIds[currentIds.length - 1]);
|
|
310
|
+
} else {
|
|
311
|
+
setCurrentActionId(null);
|
|
312
|
+
}
|
|
313
|
+
}, [data, onlyShowOne]);
|
|
314
|
+
|
|
315
|
+
// The timer and data are emptied each time "useToast().show()" is executed
|
|
316
|
+
useEffect(() => {
|
|
317
|
+
if (currentActionId === null || typeof currentActionId === 'undefined') return;
|
|
318
|
+
if (onUpdate) {
|
|
319
|
+
onUpdate([data[data.length - 1]]);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Remove the global list of events, especially as scroll and interval.
|
|
323
|
+
//--------------
|
|
324
|
+
return () => {
|
|
325
|
+
clearAllProgressTimer();
|
|
326
|
+
};
|
|
327
|
+
}, [currentActionId]);
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
// init
|
|
331
|
+
useEffect(() => {
|
|
332
|
+
setInitPopRoot(true);
|
|
333
|
+
|
|
334
|
+
// Remove the global list of events, especially as scroll and interval.
|
|
335
|
+
//--------------
|
|
336
|
+
return () => {
|
|
337
|
+
clearAllProgressTimer();
|
|
338
|
+
};
|
|
339
|
+
}, []);
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
return (
|
|
345
|
+
<RootPortal show={initPopRoot} containerClassName="Toast">
|
|
346
|
+
<div
|
|
347
|
+
id={`toasts__wrapper-${uniqueID}`}
|
|
348
|
+
className={combinedCls(
|
|
349
|
+
'toasts__wrapper',
|
|
350
|
+
`toasts__wrapper--${direction}`,
|
|
351
|
+
clsWrite(wrapperClassName, ''),
|
|
352
|
+
{
|
|
353
|
+
'toasts__wrapper--cascading': cascadingEnabled,
|
|
354
|
+
'toasts__wrapper--only-one"': onlyShowOne
|
|
355
|
+
}
|
|
356
|
+
)}
|
|
357
|
+
ref={rootRef}
|
|
358
|
+
>
|
|
359
|
+
<div className="toasts">
|
|
360
|
+
{getProcessedData().map((item, i) => (
|
|
361
|
+
<Item
|
|
362
|
+
ref={el => progressObjRef.current.set(item.id as string, el)}
|
|
363
|
+
key={item.id}
|
|
364
|
+
uniqueID={item.id}
|
|
365
|
+
isNew={!progressObjRef.current.has(item.id as string)} // Mark the new toast
|
|
366
|
+
onlyOne={data.length === 1}
|
|
367
|
+
depth={depth - i}
|
|
368
|
+
index={i}
|
|
369
|
+
title={item.title}
|
|
370
|
+
note={item.note}
|
|
371
|
+
theme={item.theme}
|
|
372
|
+
lock={item.lock}
|
|
373
|
+
cascading={cascadingEnabled}
|
|
374
|
+
schemeBody={item.schemeBody}
|
|
375
|
+
schemeHeader={item.schemeHeader}
|
|
376
|
+
closeBtnColor={item.closeBtnColor}
|
|
377
|
+
closeDisabled={item.closeDisabled}
|
|
378
|
+
message={item.message}
|
|
379
|
+
autoCloseTime={item.autoCloseTime}
|
|
380
|
+
evStart={handleProgressStart}
|
|
381
|
+
evPause={handleProgressPaused}
|
|
382
|
+
evClose={handleClose}
|
|
383
|
+
/>
|
|
384
|
+
))}
|
|
385
|
+
</div>
|
|
386
|
+
</div>
|
|
387
|
+
</RootPortal>
|
|
388
|
+
);
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
export default Toast;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import React, { createContext, useContext, useReducer } from 'react';
|
|
2
|
+
import { Toast } from './Toast';
|
|
3
|
+
import type { ToastOptions, ToastGlobalConfig } from './types';
|
|
4
|
+
|
|
5
|
+
interface ToastItem extends ToastOptions {
|
|
6
|
+
id: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface ToastContextState {
|
|
10
|
+
toasts: ToastItem[];
|
|
11
|
+
config: ToastGlobalConfig;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
type ToastAction =
|
|
15
|
+
| { type: 'ADD_TOAST'; payload: ToastItem }
|
|
16
|
+
| { type: 'REMOVE_TOAST'; payload: string }
|
|
17
|
+
| { type: 'REMOVE_ALL' }
|
|
18
|
+
| { type: 'UPDATE_CONFIG'; payload: ToastGlobalConfig };
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
const defaultConfig: ToastGlobalConfig = {
|
|
22
|
+
defaultWrapperClassName: '',
|
|
23
|
+
defaultOnlyShowOne: false,
|
|
24
|
+
defaultDirection: 'bottom-center',
|
|
25
|
+
defaultCascading: false,
|
|
26
|
+
defaultReverseDisplay: false
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
const ToastContext = createContext<{
|
|
31
|
+
state: ToastContextState;
|
|
32
|
+
dispatch: React.Dispatch<ToastAction>;
|
|
33
|
+
} | undefined>(undefined);
|
|
34
|
+
|
|
35
|
+
const toastReducer = (state: ToastContextState, action: ToastAction): ToastContextState => {
|
|
36
|
+
switch (action.type) {
|
|
37
|
+
case 'ADD_TOAST':
|
|
38
|
+
return {
|
|
39
|
+
...state,
|
|
40
|
+
toasts: [...state.toasts, action.payload],
|
|
41
|
+
};
|
|
42
|
+
case 'REMOVE_TOAST':
|
|
43
|
+
return {
|
|
44
|
+
...state,
|
|
45
|
+
toasts: state.toasts.filter((toast) => toast.id !== action.payload),
|
|
46
|
+
};
|
|
47
|
+
case 'REMOVE_ALL':
|
|
48
|
+
return {
|
|
49
|
+
...state,
|
|
50
|
+
toasts: [],
|
|
51
|
+
};
|
|
52
|
+
case 'UPDATE_CONFIG':
|
|
53
|
+
return {
|
|
54
|
+
...state,
|
|
55
|
+
config: { ...state.config, ...action.payload }
|
|
56
|
+
};
|
|
57
|
+
default:
|
|
58
|
+
return state;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const ToastProvider: React.FC<{
|
|
63
|
+
children: React.ReactNode;
|
|
64
|
+
config?: ToastGlobalConfig;
|
|
65
|
+
}> = ({ children, config = {} }) => {
|
|
66
|
+
const [state, dispatch] = useReducer(toastReducer, {
|
|
67
|
+
toasts: [],
|
|
68
|
+
config: { ...defaultConfig, ...config }
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<ToastContext.Provider value={{ state, dispatch }}>
|
|
73
|
+
{children}
|
|
74
|
+
<Toast
|
|
75
|
+
data={state.toasts}
|
|
76
|
+
|
|
77
|
+
// default props
|
|
78
|
+
defaultWrapperClassName={state.config.defaultWrapperClassName}
|
|
79
|
+
defaultDirection={state.config.defaultDirection}
|
|
80
|
+
defaultOnlyShowOne={state.config.defaultOnlyShowOne}
|
|
81
|
+
defaultCascading={state.config.defaultCascading}
|
|
82
|
+
defaultReverseDisplay={state.config.defaultReverseDisplay}
|
|
83
|
+
|
|
84
|
+
//
|
|
85
|
+
onUpdate={(updatedData) => {
|
|
86
|
+
// Iterate through the current toasts and remove the toasts that are not in the "updatedData"
|
|
87
|
+
state.toasts.forEach(toast => {
|
|
88
|
+
if (!updatedData.find(item => item.id === toast.id)) {
|
|
89
|
+
dispatch({ type: 'REMOVE_TOAST', payload: toast.id });
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
}}
|
|
93
|
+
/>
|
|
94
|
+
</ToastContext.Provider>
|
|
95
|
+
);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export const useToastContext = () => {
|
|
99
|
+
const context = useContext(ToastContext);
|
|
100
|
+
if (!context) {
|
|
101
|
+
throw new Error('useToastContext must be used within a ToastProvider');
|
|
102
|
+
}
|
|
103
|
+
return context;
|
|
104
|
+
};
|