rn-toastify 1.0.12 → 2.0.0
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/LICENSE +21 -21
- package/README.MD +221 -190
- package/babel.config.js +5 -5
- package/docs/demo.gif +0 -0
- package/example/App.js +193 -0
- package/index.js +60 -34
- package/jest.config.js +14 -14
- package/jest.setup.js +1 -1
- package/package.json +86 -74
- package/src/Toast.js +194 -114
- package/src/__tests__/Toast.test.js +54 -54
- package/src/components/BaseToast.js +163 -0
- package/src/components/CustomToast.js +58 -0
- package/src/components/CustomeToast.js +4 -40
- package/src/components/EmojiToast.js +142 -57
- package/src/components/ErrorToast.js +23 -65
- package/src/components/InfoToast.js +23 -0
- package/src/components/LoadingToast.js +24 -55
- package/src/components/ProgressBar.js +67 -0
- package/src/components/SuccessToast.js +23 -65
- package/src/components/WarningToast.js +23 -0
- package/src/components/icons/CheckIcon.js +98 -0
- package/src/components/icons/CrossIcon.js +84 -0
- package/src/components/icons/InfoIcon.js +71 -0
- package/src/components/icons/LoadingSpinner.js +78 -0
- package/src/components/icons/WarningIcon.js +84 -0
- package/src/components/icons/index.js +5 -0
- package/src/context/ToastContainer.js +223 -144
- package/src/context/ToastManager.js +150 -36
- package/src/hooks/useToast.js +123 -49
- package/src/types.d.ts +172 -0
- package/src/utils/Pixel/Index.js +28 -28
- package/src/utils/theme.js +81 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import React, { useEffect } from 'react';
|
|
2
|
+
import { View, StyleSheet } from 'react-native';
|
|
3
|
+
import Animated, {
|
|
4
|
+
useSharedValue,
|
|
5
|
+
useAnimatedStyle,
|
|
6
|
+
withTiming,
|
|
7
|
+
withDelay,
|
|
8
|
+
withSequence,
|
|
9
|
+
Easing,
|
|
10
|
+
} from 'react-native-reanimated';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* WarningIcon — Animated exclamation mark with subtle shake.
|
|
14
|
+
* Bar + dot that fade in, followed by a quick shake.
|
|
15
|
+
*/
|
|
16
|
+
const WarningIcon = ({ size = 22, color = '#D97706', animated = true }) => {
|
|
17
|
+
const opacity = useSharedValue(animated ? 0 : 1);
|
|
18
|
+
const shake = useSharedValue(0);
|
|
19
|
+
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
if (animated) {
|
|
22
|
+
opacity.value = withDelay(
|
|
23
|
+
100,
|
|
24
|
+
withTiming(1, { duration: 350, easing: Easing.out(Easing.cubic) })
|
|
25
|
+
);
|
|
26
|
+
shake.value = withDelay(
|
|
27
|
+
350,
|
|
28
|
+
withSequence(
|
|
29
|
+
withTiming(-2.5, { duration: 40 }),
|
|
30
|
+
withTiming(2.5, { duration: 40 }),
|
|
31
|
+
withTiming(-1.5, { duration: 40 }),
|
|
32
|
+
withTiming(1.5, { duration: 40 }),
|
|
33
|
+
withTiming(0, { duration: 40 })
|
|
34
|
+
)
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
}, [animated]);
|
|
38
|
+
|
|
39
|
+
const contentStyle = useAnimatedStyle(() => ({
|
|
40
|
+
opacity: opacity.value,
|
|
41
|
+
transform: [{ translateX: shake.value }],
|
|
42
|
+
}));
|
|
43
|
+
|
|
44
|
+
const barW = Math.max(2, size * 0.12);
|
|
45
|
+
const barH = size * 0.35;
|
|
46
|
+
const dotSize = Math.max(3, size * 0.16);
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<View style={[styles.container, { width: size, height: size }]}>
|
|
50
|
+
<Animated.View style={[styles.content, contentStyle]}>
|
|
51
|
+
<View
|
|
52
|
+
style={{
|
|
53
|
+
width: barW,
|
|
54
|
+
height: barH,
|
|
55
|
+
backgroundColor: color,
|
|
56
|
+
borderRadius: barW,
|
|
57
|
+
marginBottom: size * 0.08,
|
|
58
|
+
}}
|
|
59
|
+
/>
|
|
60
|
+
<View
|
|
61
|
+
style={{
|
|
62
|
+
width: dotSize,
|
|
63
|
+
height: dotSize,
|
|
64
|
+
borderRadius: dotSize / 2,
|
|
65
|
+
backgroundColor: color,
|
|
66
|
+
}}
|
|
67
|
+
/>
|
|
68
|
+
</Animated.View>
|
|
69
|
+
</View>
|
|
70
|
+
);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const styles = StyleSheet.create({
|
|
74
|
+
container: {
|
|
75
|
+
alignItems: 'center',
|
|
76
|
+
justifyContent: 'center',
|
|
77
|
+
},
|
|
78
|
+
content: {
|
|
79
|
+
alignItems: 'center',
|
|
80
|
+
justifyContent: 'center',
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
export default React.memo(WarningIcon);
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { default as CheckIcon } from './CheckIcon';
|
|
2
|
+
export { default as CrossIcon } from './CrossIcon';
|
|
3
|
+
export { default as InfoIcon } from './InfoIcon';
|
|
4
|
+
export { default as WarningIcon } from './WarningIcon';
|
|
5
|
+
export { default as LoadingSpinner } from './LoadingSpinner';
|
|
@@ -1,145 +1,224 @@
|
|
|
1
|
-
import React, { useEffect, useState } from 'react';
|
|
2
|
-
import { View, StyleSheet, Appearance,
|
|
3
|
-
import Toast from '../Toast';
|
|
4
|
-
import toastManagerInstance from './ToastManager';
|
|
5
|
-
import {
|
|
6
|
-
heightPercentageToDP as hp,
|
|
7
|
-
widthPercentageToDP as wp,
|
|
8
|
-
} from '../utils/Pixel/Index';
|
|
9
|
-
|
|
10
|
-
const ToastContainer = ({
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
toastManagerInstance.
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
1
|
+
import React, { useEffect, useState, useCallback, useMemo } from 'react';
|
|
2
|
+
import { View, StyleSheet, Appearance, Platform, Keyboard } from 'react-native';
|
|
3
|
+
import Toast from '../Toast';
|
|
4
|
+
import toastManagerInstance from './ToastManager';
|
|
5
|
+
import {
|
|
6
|
+
heightPercentageToDP as hp,
|
|
7
|
+
widthPercentageToDP as wp,
|
|
8
|
+
} from '../utils/Pixel/Index';
|
|
9
|
+
|
|
10
|
+
const ToastContainer = ({
|
|
11
|
+
theme: forcedTheme,
|
|
12
|
+
maxVisible = 3,
|
|
13
|
+
defaultPosition = 'top',
|
|
14
|
+
topOffset = 0,
|
|
15
|
+
bottomOffset = 0,
|
|
16
|
+
swipeable = true,
|
|
17
|
+
} = {}) => {
|
|
18
|
+
const [toasts, setToasts] = useState([]);
|
|
19
|
+
const [theme, setTheme] = useState(forcedTheme ?? (Appearance?.getColorScheme?.() || 'light'));
|
|
20
|
+
const [keyboardHeight, setKeyboardHeight] = useState(0);
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
// Configure the manager with our settings
|
|
24
|
+
toastManagerInstance.configure({ maxVisible });
|
|
25
|
+
}, [maxVisible]);
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
const handleShow = (toast) => {
|
|
29
|
+
setToasts((prev) => {
|
|
30
|
+
// Prevent duplicate IDs
|
|
31
|
+
const filtered = prev.filter((t) => t.id !== toast.id);
|
|
32
|
+
return [...filtered, toast];
|
|
33
|
+
});
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const handleRemove = (id) => {
|
|
37
|
+
setToasts((prev) => prev.filter((toast) => toast.id !== id));
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const handleUpdate = (updatedToast) => {
|
|
41
|
+
setToasts((prev) =>
|
|
42
|
+
prev.map((t) => (t.id === updatedToast.id ? updatedToast : t))
|
|
43
|
+
);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
toastManagerInstance.on('show', handleShow);
|
|
47
|
+
toastManagerInstance.on('remove', handleRemove);
|
|
48
|
+
toastManagerInstance.on('update', handleUpdate);
|
|
49
|
+
|
|
50
|
+
// Theme listener
|
|
51
|
+
let appearanceSub;
|
|
52
|
+
if (!forcedTheme && Appearance?.addChangeListener) {
|
|
53
|
+
appearanceSub = Appearance.addChangeListener(({ colorScheme }) => {
|
|
54
|
+
setTheme(colorScheme ?? 'light');
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Keyboard listener for bottom toasts
|
|
59
|
+
const keyboardShowSub = Keyboard.addListener(
|
|
60
|
+
Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow',
|
|
61
|
+
(e) => setKeyboardHeight(e.endCoordinates.height)
|
|
62
|
+
);
|
|
63
|
+
const keyboardHideSub = Keyboard.addListener(
|
|
64
|
+
Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide',
|
|
65
|
+
() => setKeyboardHeight(0)
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
return () => {
|
|
69
|
+
toastManagerInstance.off('show', handleShow);
|
|
70
|
+
toastManagerInstance.off('remove', handleRemove);
|
|
71
|
+
toastManagerInstance.off('update', handleUpdate);
|
|
72
|
+
if (appearanceSub?.remove) appearanceSub.remove();
|
|
73
|
+
keyboardShowSub.remove();
|
|
74
|
+
keyboardHideSub.remove();
|
|
75
|
+
};
|
|
76
|
+
}, [forcedTheme]);
|
|
77
|
+
|
|
78
|
+
// Calculate safe top margin
|
|
79
|
+
const getTopMargin = useCallback(() => {
|
|
80
|
+
if (Platform.OS === 'android') {
|
|
81
|
+
// Use StatusBar height if available
|
|
82
|
+
try {
|
|
83
|
+
const { StatusBar } = require('react-native');
|
|
84
|
+
return (StatusBar.currentHeight || 0) + topOffset;
|
|
85
|
+
} catch {
|
|
86
|
+
return topOffset;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// iOS: default safe area
|
|
90
|
+
return 50 + topOffset;
|
|
91
|
+
}, [topOffset]);
|
|
92
|
+
|
|
93
|
+
// Separate toasts by position
|
|
94
|
+
const { topToasts, centerToasts, bottomToasts } = useMemo(() => {
|
|
95
|
+
const top = [];
|
|
96
|
+
const center = [];
|
|
97
|
+
const bottom = [];
|
|
98
|
+
|
|
99
|
+
toasts.forEach((toast) => {
|
|
100
|
+
const pos = toast?.options?.position || defaultPosition;
|
|
101
|
+
if (pos === 'top') top.push(toast);
|
|
102
|
+
else if (pos === 'center') center.push(toast);
|
|
103
|
+
else bottom.push(toast);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
return { topToasts: top, centerToasts: center, bottomToasts: bottom };
|
|
107
|
+
}, [toasts, defaultPosition]);
|
|
108
|
+
|
|
109
|
+
const topMargin = getTopMargin();
|
|
110
|
+
const toastSpacing = hp(1.2);
|
|
111
|
+
const toastHeight = hp(8.5);
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<>
|
|
115
|
+
{/* Top positioned toasts */}
|
|
116
|
+
{topToasts.length > 0 && (
|
|
117
|
+
<View
|
|
118
|
+
style={[styles.topContainer, { top: topMargin }]}
|
|
119
|
+
pointerEvents="box-none"
|
|
120
|
+
>
|
|
121
|
+
{topToasts.map((toast, index) => (
|
|
122
|
+
<Toast
|
|
123
|
+
key={toast.id}
|
|
124
|
+
visible={true}
|
|
125
|
+
duration={toast?.options?.duration}
|
|
126
|
+
position="top"
|
|
127
|
+
theme={theme}
|
|
128
|
+
style={[
|
|
129
|
+
toast?.options?.style || {},
|
|
130
|
+
{ marginTop: index * (toastHeight + toastSpacing) },
|
|
131
|
+
]}
|
|
132
|
+
onHide={() => toastManagerInstance.remove(toast.id)}
|
|
133
|
+
>
|
|
134
|
+
{toast.content}
|
|
135
|
+
</Toast>
|
|
136
|
+
))}
|
|
137
|
+
</View>
|
|
138
|
+
)}
|
|
139
|
+
|
|
140
|
+
{/* Center positioned toasts */}
|
|
141
|
+
{centerToasts.length > 0 && (
|
|
142
|
+
<View style={styles.centerContainer} pointerEvents="box-none">
|
|
143
|
+
{centerToasts.map((toast, index) => (
|
|
144
|
+
<Toast
|
|
145
|
+
key={toast.id}
|
|
146
|
+
visible={true}
|
|
147
|
+
duration={toast?.options?.duration}
|
|
148
|
+
position="center"
|
|
149
|
+
theme={theme}
|
|
150
|
+
style={[
|
|
151
|
+
toast?.options?.style || {},
|
|
152
|
+
{ marginTop: index * (toastHeight + toastSpacing) },
|
|
153
|
+
]}
|
|
154
|
+
onHide={() => toastManagerInstance.remove(toast.id)}
|
|
155
|
+
>
|
|
156
|
+
{toast.content}
|
|
157
|
+
</Toast>
|
|
158
|
+
))}
|
|
159
|
+
</View>
|
|
160
|
+
)}
|
|
161
|
+
|
|
162
|
+
{/* Bottom positioned toasts */}
|
|
163
|
+
{bottomToasts.length > 0 && (
|
|
164
|
+
<View
|
|
165
|
+
style={[
|
|
166
|
+
styles.bottomContainer,
|
|
167
|
+
{ bottom: bottomOffset + keyboardHeight },
|
|
168
|
+
]}
|
|
169
|
+
pointerEvents="box-none"
|
|
170
|
+
>
|
|
171
|
+
{bottomToasts.map((toast, index) => (
|
|
172
|
+
<Toast
|
|
173
|
+
key={toast.id}
|
|
174
|
+
visible={true}
|
|
175
|
+
duration={toast?.options?.duration}
|
|
176
|
+
position="bottom"
|
|
177
|
+
theme={theme}
|
|
178
|
+
style={[
|
|
179
|
+
toast?.options?.style || {},
|
|
180
|
+
{ bottom: hp(2) + index * (toastHeight + toastSpacing) },
|
|
181
|
+
]}
|
|
182
|
+
onHide={() => toastManagerInstance.remove(toast.id)}
|
|
183
|
+
>
|
|
184
|
+
{toast.content}
|
|
185
|
+
</Toast>
|
|
186
|
+
))}
|
|
187
|
+
</View>
|
|
188
|
+
)}
|
|
189
|
+
</>
|
|
190
|
+
);
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const styles = StyleSheet.create({
|
|
194
|
+
topContainer: {
|
|
195
|
+
position: 'absolute',
|
|
196
|
+
left: 0,
|
|
197
|
+
right: 0,
|
|
198
|
+
zIndex: 9999,
|
|
199
|
+
pointerEvents: 'box-none',
|
|
200
|
+
alignItems: 'center',
|
|
201
|
+
},
|
|
202
|
+
centerContainer: {
|
|
203
|
+
position: 'absolute',
|
|
204
|
+
top: 0,
|
|
205
|
+
bottom: 0,
|
|
206
|
+
left: 0,
|
|
207
|
+
right: 0,
|
|
208
|
+
zIndex: 9999,
|
|
209
|
+
pointerEvents: 'box-none',
|
|
210
|
+
alignItems: 'center',
|
|
211
|
+
justifyContent: 'center',
|
|
212
|
+
},
|
|
213
|
+
bottomContainer: {
|
|
214
|
+
position: 'absolute',
|
|
215
|
+
bottom: 0,
|
|
216
|
+
left: 0,
|
|
217
|
+
right: 0,
|
|
218
|
+
zIndex: 9999,
|
|
219
|
+
pointerEvents: 'box-none',
|
|
220
|
+
alignItems: 'center',
|
|
221
|
+
},
|
|
222
|
+
});
|
|
223
|
+
|
|
145
224
|
export default React.memo(ToastContainer);
|
|
@@ -1,36 +1,150 @@
|
|
|
1
|
-
import { EventEmitter } from 'events';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
this.
|
|
13
|
-
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const
|
|
36
|
-
|
|
1
|
+
import { EventEmitter } from 'events';
|
|
2
|
+
import { TOAST_DEFAULTS } from '../utils/theme';
|
|
3
|
+
|
|
4
|
+
let _idCounter = 0;
|
|
5
|
+
const generateId = () => `toast_${++_idCounter}_${Date.now()}`;
|
|
6
|
+
|
|
7
|
+
class ToastManager extends EventEmitter {
|
|
8
|
+
constructor() {
|
|
9
|
+
super();
|
|
10
|
+
this.setMaxListeners(20);
|
|
11
|
+
this._activeToasts = new Map();
|
|
12
|
+
this._queue = [];
|
|
13
|
+
this._maxVisible = TOAST_DEFAULTS.maxVisible;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Configure the toast manager.
|
|
18
|
+
* @param {{ maxVisible?: number }} config
|
|
19
|
+
*/
|
|
20
|
+
configure(config = {}) {
|
|
21
|
+
if (config.maxVisible !== undefined) {
|
|
22
|
+
this._maxVisible = config.maxVisible;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Show a toast notification.
|
|
28
|
+
* @param {React.ReactNode} content - The toast content to render
|
|
29
|
+
* @param {Object} options - Toast options (duration, position, style)
|
|
30
|
+
* @param {string|null} id - Optional custom ID for the toast
|
|
31
|
+
* @returns {string} The toast ID
|
|
32
|
+
*/
|
|
33
|
+
show(content, options = {}, id = null) {
|
|
34
|
+
const _id = id ?? generateId();
|
|
35
|
+
const defaultOptions = {
|
|
36
|
+
duration: TOAST_DEFAULTS.duration,
|
|
37
|
+
position: TOAST_DEFAULTS.position,
|
|
38
|
+
style: {},
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const toast = {
|
|
42
|
+
id: _id,
|
|
43
|
+
content,
|
|
44
|
+
options: { ...defaultOptions, ...options },
|
|
45
|
+
createdAt: Date.now(),
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// If we've hit the max, queue the toast or dismiss the oldest
|
|
49
|
+
if (this._activeToasts.size >= this._maxVisible) {
|
|
50
|
+
// Remove the oldest toast to make room
|
|
51
|
+
const oldest = this._activeToasts.keys().next().value;
|
|
52
|
+
if (oldest) {
|
|
53
|
+
this.remove(oldest);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
this._activeToasts.set(_id, toast);
|
|
58
|
+
this.emit('show', toast);
|
|
59
|
+
return _id;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Remove a toast by ID.
|
|
64
|
+
* @param {string} id
|
|
65
|
+
*/
|
|
66
|
+
remove(id) {
|
|
67
|
+
this._activeToasts.delete(id);
|
|
68
|
+
this.emit('remove', id);
|
|
69
|
+
|
|
70
|
+
// Process queue if there are waiting toasts
|
|
71
|
+
if (this._queue.length > 0 && this._activeToasts.size < this._maxVisible) {
|
|
72
|
+
const next = this._queue.shift();
|
|
73
|
+
if (next) {
|
|
74
|
+
this.show(next.content, next.options, next.id);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Dismiss all active toasts.
|
|
81
|
+
*/
|
|
82
|
+
dismissAll() {
|
|
83
|
+
const ids = Array.from(this._activeToasts.keys());
|
|
84
|
+
ids.forEach((id) => this.remove(id));
|
|
85
|
+
this._queue = [];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Update an existing toast's content.
|
|
90
|
+
* @param {string} id - The toast ID to update
|
|
91
|
+
* @param {React.ReactNode} content - New content
|
|
92
|
+
* @param {Object} options - New options (merged with existing)
|
|
93
|
+
*/
|
|
94
|
+
update(id, content, options = {}) {
|
|
95
|
+
if (this._activeToasts.has(id)) {
|
|
96
|
+
const existing = this._activeToasts.get(id);
|
|
97
|
+
const updated = {
|
|
98
|
+
...existing,
|
|
99
|
+
content: content ?? existing.content,
|
|
100
|
+
options: { ...existing.options, ...options },
|
|
101
|
+
};
|
|
102
|
+
this._activeToasts.set(id, updated);
|
|
103
|
+
this.emit('update', updated);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Check if a toast is currently active.
|
|
109
|
+
* @param {string} id
|
|
110
|
+
* @returns {boolean}
|
|
111
|
+
*/
|
|
112
|
+
isActive(id) {
|
|
113
|
+
return this._activeToasts.has(id);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Handle promise states with loading → success/error toasts.
|
|
118
|
+
* @param {Promise} promise
|
|
119
|
+
* @param {{ loading: React.ReactNode, success: React.ReactNode|Function, error: React.ReactNode|Function }} messages
|
|
120
|
+
* @param {Object} options
|
|
121
|
+
*/
|
|
122
|
+
async promise(promise, { loading, success, error }, options = {}) {
|
|
123
|
+
const id = generateId();
|
|
124
|
+
// Show the loading toast
|
|
125
|
+
this.show(loading, { ...options, duration: Infinity, position: options.position || 'top' }, id);
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
const result = await promise;
|
|
129
|
+
this.remove(id);
|
|
130
|
+
// Support function callbacks for dynamic messages
|
|
131
|
+
const successContent = typeof success === 'function' ? success(result) : success;
|
|
132
|
+
this.show(successContent, {
|
|
133
|
+
duration: options.successDuration || TOAST_DEFAULTS.duration,
|
|
134
|
+
position: options.position || 'top',
|
|
135
|
+
});
|
|
136
|
+
return result;
|
|
137
|
+
} catch (err) {
|
|
138
|
+
this.remove(id);
|
|
139
|
+
const errorContent = typeof error === 'function' ? error(err) : error;
|
|
140
|
+
this.show(errorContent, {
|
|
141
|
+
duration: options.errorDuration || TOAST_DEFAULTS.duration,
|
|
142
|
+
position: options.position || 'top',
|
|
143
|
+
});
|
|
144
|
+
throw err;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const toastManagerInstance = new ToastManager();
|
|
150
|
+
export default toastManagerInstance;
|