react-native-earl-toastify 1.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/CHANGELOG.md +71 -0
- package/LICENSE +21 -0
- package/README.md +513 -0
- package/dist/Toast.d.ts +12 -0
- package/dist/Toast.d.ts.map +1 -0
- package/dist/Toast.js +143 -0
- package/dist/Toast.js.map +1 -0
- package/dist/ToastContainer.d.ts +12 -0
- package/dist/ToastContainer.d.ts.map +1 -0
- package/dist/ToastContainer.js +46 -0
- package/dist/ToastContainer.js.map +1 -0
- package/dist/ToastProvider.d.ts +22 -0
- package/dist/ToastProvider.d.ts.map +1 -0
- package/dist/ToastProvider.js +163 -0
- package/dist/ToastProvider.js.map +1 -0
- package/dist/animations.d.ts +54 -0
- package/dist/animations.d.ts.map +1 -0
- package/dist/animations.js +118 -0
- package/dist/animations.js.map +1 -0
- package/dist/icons.d.ts +12 -0
- package/dist/icons.d.ts.map +1 -0
- package/dist/icons.js +110 -0
- package/dist/icons.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/styles.d.ts +63 -0
- package/dist/styles.d.ts.map +1 -0
- package/dist/styles.js +182 -0
- package/dist/styles.js.map +1 -0
- package/dist/types.d.ts +100 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/useToast.d.ts +40 -0
- package/dist/useToast.d.ts.map +1 -0
- package/dist/useToast.js +48 -0
- package/dist/useToast.js.map +1 -0
- package/dist/useToastAnimation.d.ts +24 -0
- package/dist/useToastAnimation.d.ts.map +1 -0
- package/dist/useToastAnimation.js +59 -0
- package/dist/useToastAnimation.js.map +1 -0
- package/package.json +58 -0
- package/src/Toast.tsx +250 -0
- package/src/ToastContainer.tsx +70 -0
- package/src/ToastProvider.tsx +271 -0
- package/src/animations.ts +161 -0
- package/src/icons.tsx +152 -0
- package/src/index.ts +41 -0
- package/src/styles.ts +208 -0
- package/src/types.ts +157 -0
- package/src/useToast.ts +53 -0
- package/src/useToastAnimation.ts +108 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { useRef, useEffect, useCallback } from "react";
|
|
2
|
+
import { Animated } from "react-native";
|
|
3
|
+
import { getInitialAnimatedValues, createEnterAnimation, createExitAnimation, DEFAULT_ANIMATION_CONFIG, } from "./animations";
|
|
4
|
+
/**
|
|
5
|
+
* Custom hook for managing toast animations
|
|
6
|
+
*/
|
|
7
|
+
export const useToastAnimation = (animationIn, animationOut, position, animationDuration = DEFAULT_ANIMATION_CONFIG.duration) => {
|
|
8
|
+
// Get initial values based on enter animation
|
|
9
|
+
const initialValues = getInitialAnimatedValues(animationIn, position);
|
|
10
|
+
// Create animated values
|
|
11
|
+
const opacity = useRef(new Animated.Value(initialValues.opacity)).current;
|
|
12
|
+
const translateX = useRef(new Animated.Value(initialValues.translateX)).current;
|
|
13
|
+
const translateY = useRef(new Animated.Value(initialValues.translateY)).current;
|
|
14
|
+
// Animation config
|
|
15
|
+
const config = {
|
|
16
|
+
duration: animationDuration,
|
|
17
|
+
useNativeDriver: true,
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Start enter animation
|
|
21
|
+
*/
|
|
22
|
+
const startEnterAnimation = useCallback(() => {
|
|
23
|
+
const animation = createEnterAnimation({ opacity, translateX, translateY }, config);
|
|
24
|
+
animation.start();
|
|
25
|
+
}, [opacity, translateX, translateY, config.duration]);
|
|
26
|
+
/**
|
|
27
|
+
* Start exit animation with optional callback
|
|
28
|
+
*/
|
|
29
|
+
const startExitAnimation = useCallback((callback) => {
|
|
30
|
+
const animation = createExitAnimation({ opacity, translateX, translateY }, animationOut, position, config);
|
|
31
|
+
animation.start(({ finished }) => {
|
|
32
|
+
if (finished && callback) {
|
|
33
|
+
callback();
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
}, [
|
|
37
|
+
opacity,
|
|
38
|
+
translateX,
|
|
39
|
+
translateY,
|
|
40
|
+
animationOut,
|
|
41
|
+
position,
|
|
42
|
+
config.duration,
|
|
43
|
+
]);
|
|
44
|
+
// Start enter animation on mount
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
startEnterAnimation();
|
|
47
|
+
}, []);
|
|
48
|
+
// Animated style object
|
|
49
|
+
const animatedStyle = {
|
|
50
|
+
opacity,
|
|
51
|
+
transform: [{ translateX }, { translateY }],
|
|
52
|
+
};
|
|
53
|
+
return {
|
|
54
|
+
animatedStyle,
|
|
55
|
+
startEnterAnimation,
|
|
56
|
+
startExitAnimation,
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
//# sourceMappingURL=useToastAnimation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useToastAnimation.js","sourceRoot":"","sources":["../src/useToastAnimation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,OAAO,CAAC;AACvD,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAExC,OAAO,EACN,wBAAwB,EACxB,oBAAoB,EACpB,mBAAmB,EACnB,wBAAwB,GACxB,MAAM,cAAc,CAAC;AAkBtB;;GAEG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAChC,WAA2B,EAC3B,YAA4B,EAC5B,QAAuB,EACvB,oBAA4B,wBAAwB,CAAC,QAAQ,EACnC,EAAE;IAC5B,8CAA8C;IAC9C,MAAM,aAAa,GAAG,wBAAwB,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC;IAEtE,yBAAyB;IACzB,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,QAAQ,CAAC,KAAK,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC;IAC1E,MAAM,UAAU,GAAG,MAAM,CACxB,IAAI,QAAQ,CAAC,KAAK,CAAC,aAAa,CAAC,UAAU,CAAC,CAC5C,CAAC,OAAO,CAAC;IACV,MAAM,UAAU,GAAG,MAAM,CACxB,IAAI,QAAQ,CAAC,KAAK,CAAC,aAAa,CAAC,UAAU,CAAC,CAC5C,CAAC,OAAO,CAAC;IAEV,mBAAmB;IACnB,MAAM,MAAM,GAAG;QACd,QAAQ,EAAE,iBAAiB;QAC3B,eAAe,EAAE,IAAI;KACrB,CAAC;IAEF;;OAEG;IACH,MAAM,mBAAmB,GAAG,WAAW,CAAC,GAAG,EAAE;QAC5C,MAAM,SAAS,GAAG,oBAAoB,CACrC,EAAE,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,EACnC,MAAM,CACN,CAAC;QACF,SAAS,CAAC,KAAK,EAAE,CAAC;IACnB,CAAC,EAAE,CAAC,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC;IAEvD;;OAEG;IACH,MAAM,kBAAkB,GAAG,WAAW,CACrC,CAAC,QAAqB,EAAE,EAAE;QACzB,MAAM,SAAS,GAAG,mBAAmB,CACpC,EAAE,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,EACnC,YAAY,EACZ,QAAQ,EACR,MAAM,CACN,CAAC;QACF,SAAS,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,EAAE,EAAE,EAAE;YAChC,IAAI,QAAQ,IAAI,QAAQ,EAAE,CAAC;gBAC1B,QAAQ,EAAE,CAAC;YACZ,CAAC;QACF,CAAC,CAAC,CAAC;IACJ,CAAC,EACD;QACC,OAAO;QACP,UAAU;QACV,UAAU;QACV,YAAY;QACZ,QAAQ;QACR,MAAM,CAAC,QAAQ;KACf,CACD,CAAC;IAEF,iCAAiC;IACjC,SAAS,CAAC,GAAG,EAAE;QACd,mBAAmB,EAAE,CAAC;IACvB,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,wBAAwB;IACxB,MAAM,aAAa,GAA6C;QAC/D,OAAO;QACP,SAAS,EAAE,CAAC,EAAE,UAAU,EAAE,EAAE,EAAE,UAAU,EAAE,CAAqB;KAC/D,CAAC;IAEF,OAAO;QACN,aAAa;QACb,mBAAmB;QACnB,kBAAkB;KAClB,CAAC;AACH,CAAC,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "react-native-earl-toastify",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A beautiful, customizable toast notification library for React Native with animations, multiple toast types, and accessibility support",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"module": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"react-native": "src/index.ts",
|
|
9
|
+
"source": "src/index.ts",
|
|
10
|
+
"files": [
|
|
11
|
+
"src",
|
|
12
|
+
"dist",
|
|
13
|
+
"README.md",
|
|
14
|
+
"CHANGELOG.md"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsc",
|
|
18
|
+
"prepublishOnly": "npm run build",
|
|
19
|
+
"test": "echo \"No tests specified\" && exit 0"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"react-native",
|
|
23
|
+
"toast",
|
|
24
|
+
"toastify",
|
|
25
|
+
"notification",
|
|
26
|
+
"alert",
|
|
27
|
+
"message",
|
|
28
|
+
"snackbar",
|
|
29
|
+
"popup",
|
|
30
|
+
"animated",
|
|
31
|
+
"accessible"
|
|
32
|
+
],
|
|
33
|
+
"author": "Ordovez, Earl Romeo",
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "https://github.com/Swif7ify/react-native-earl-toastify.git"
|
|
38
|
+
},
|
|
39
|
+
"bugs": {
|
|
40
|
+
"url": "https://github.com/Swif7ify/react-native-earl-toastify/issues"
|
|
41
|
+
},
|
|
42
|
+
"homepage": "https://github.com/Swif7ify/react-native-earl-toastify#readme",
|
|
43
|
+
"funding": {
|
|
44
|
+
"type": "github",
|
|
45
|
+
"url": "https://github.com/sponsors/Swif7ify"
|
|
46
|
+
},
|
|
47
|
+
"peerDependencies": {
|
|
48
|
+
"react": ">=17.0.0",
|
|
49
|
+
"react-native": ">=0.64.0"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@types/react": "^18.2.0",
|
|
53
|
+
"@types/react-native": "^0.72.0",
|
|
54
|
+
"react": "^18.2.0",
|
|
55
|
+
"react-native": "^0.72.0",
|
|
56
|
+
"typescript": "^5.0.0"
|
|
57
|
+
}
|
|
58
|
+
}
|
package/src/Toast.tsx
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import React, { useEffect, useRef, useCallback } from "react";
|
|
2
|
+
import {
|
|
3
|
+
Animated,
|
|
4
|
+
Text,
|
|
5
|
+
TouchableOpacity,
|
|
6
|
+
View,
|
|
7
|
+
StyleSheet,
|
|
8
|
+
AccessibilityInfo,
|
|
9
|
+
} from "react-native";
|
|
10
|
+
import { Toast as ToastData, ToastPosition, ToastAnimation } from "./types";
|
|
11
|
+
import { useToastAnimation } from "./useToastAnimation";
|
|
12
|
+
import {
|
|
13
|
+
getToastColors,
|
|
14
|
+
baseToastStyle,
|
|
15
|
+
getPositionalStyle,
|
|
16
|
+
titleStyle,
|
|
17
|
+
descriptionStyle,
|
|
18
|
+
messageStyle,
|
|
19
|
+
textContainerStyle,
|
|
20
|
+
iconContainerStyle,
|
|
21
|
+
closeButtonStyle,
|
|
22
|
+
} from "./styles";
|
|
23
|
+
import { DefaultIcons } from "./icons";
|
|
24
|
+
|
|
25
|
+
export interface ToastProps {
|
|
26
|
+
toast: ToastData;
|
|
27
|
+
position: ToastPosition;
|
|
28
|
+
onHide: (id: string) => void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Individual Toast component with animations
|
|
33
|
+
*/
|
|
34
|
+
export const Toast: React.FC<ToastProps> = ({ toast, position, onHide }) => {
|
|
35
|
+
const {
|
|
36
|
+
id,
|
|
37
|
+
title,
|
|
38
|
+
message,
|
|
39
|
+
type = "default",
|
|
40
|
+
duration = 3000,
|
|
41
|
+
animationIn = "fade",
|
|
42
|
+
animationOut = "fade",
|
|
43
|
+
animationDuration = 300,
|
|
44
|
+
dismissable = true,
|
|
45
|
+
icon,
|
|
46
|
+
hideIcon = false,
|
|
47
|
+
backgroundColor,
|
|
48
|
+
textColor,
|
|
49
|
+
borderColor,
|
|
50
|
+
style,
|
|
51
|
+
textStyle,
|
|
52
|
+
onShow,
|
|
53
|
+
onHide: onHideCallback,
|
|
54
|
+
onPress,
|
|
55
|
+
} = toast;
|
|
56
|
+
|
|
57
|
+
const timerRef = useRef<NodeJS.Timeout | null>(null);
|
|
58
|
+
const isHidingRef = useRef(false);
|
|
59
|
+
|
|
60
|
+
// Get colors based on type
|
|
61
|
+
const colors = getToastColors(type);
|
|
62
|
+
const finalBackgroundColor = backgroundColor || colors.background;
|
|
63
|
+
const finalTextColor = textColor || colors.text;
|
|
64
|
+
const finalBorderColor = borderColor || colors.border;
|
|
65
|
+
|
|
66
|
+
// Animation hook
|
|
67
|
+
const { animatedStyle, startExitAnimation } = useToastAnimation(
|
|
68
|
+
animationIn,
|
|
69
|
+
animationOut,
|
|
70
|
+
position,
|
|
71
|
+
animationDuration,
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Handle hiding the toast
|
|
76
|
+
*/
|
|
77
|
+
const handleHide = useCallback(() => {
|
|
78
|
+
if (isHidingRef.current) return;
|
|
79
|
+
isHidingRef.current = true;
|
|
80
|
+
|
|
81
|
+
// Clear timer
|
|
82
|
+
if (timerRef.current) {
|
|
83
|
+
clearTimeout(timerRef.current);
|
|
84
|
+
timerRef.current = null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Start exit animation
|
|
88
|
+
startExitAnimation(() => {
|
|
89
|
+
onHideCallback?.();
|
|
90
|
+
onHide(id);
|
|
91
|
+
});
|
|
92
|
+
}, [id, onHide, onHideCallback, startExitAnimation]);
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Handle press on toast
|
|
96
|
+
*/
|
|
97
|
+
const handlePress = useCallback(() => {
|
|
98
|
+
onPress?.();
|
|
99
|
+
if (dismissable) {
|
|
100
|
+
handleHide();
|
|
101
|
+
}
|
|
102
|
+
}, [onPress, dismissable, handleHide]);
|
|
103
|
+
|
|
104
|
+
// Setup auto-dismiss timer
|
|
105
|
+
useEffect(() => {
|
|
106
|
+
// Call onShow callback
|
|
107
|
+
onShow?.();
|
|
108
|
+
|
|
109
|
+
// Announce to accessibility
|
|
110
|
+
AccessibilityInfo.announceForAccessibility(message);
|
|
111
|
+
|
|
112
|
+
// Setup auto-dismiss if duration > 0
|
|
113
|
+
if (duration > 0) {
|
|
114
|
+
timerRef.current = setTimeout(() => {
|
|
115
|
+
handleHide();
|
|
116
|
+
}, duration);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return () => {
|
|
120
|
+
if (timerRef.current) {
|
|
121
|
+
clearTimeout(timerRef.current);
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
}, [duration, message, onShow, handleHide]);
|
|
125
|
+
|
|
126
|
+
// Render icon
|
|
127
|
+
const renderIcon = () => {
|
|
128
|
+
if (hideIcon) return null;
|
|
129
|
+
|
|
130
|
+
if (icon) {
|
|
131
|
+
return <View style={iconContainerStyle}>{icon}</View>;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Use default icon based on type
|
|
135
|
+
const DefaultIcon = DefaultIcons[type];
|
|
136
|
+
if (DefaultIcon) {
|
|
137
|
+
return (
|
|
138
|
+
<View style={iconContainerStyle}>
|
|
139
|
+
<DefaultIcon color={colors.icon} size={20} />
|
|
140
|
+
</View>
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return null;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
// Render close button
|
|
148
|
+
const renderCloseButton = () => {
|
|
149
|
+
if (!dismissable) return null;
|
|
150
|
+
|
|
151
|
+
return (
|
|
152
|
+
<TouchableOpacity
|
|
153
|
+
style={closeButtonStyle}
|
|
154
|
+
onPress={handleHide}
|
|
155
|
+
accessibilityRole="button"
|
|
156
|
+
accessibilityLabel="Dismiss notification"
|
|
157
|
+
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
|
|
158
|
+
>
|
|
159
|
+
<DefaultIcons.close color={colors.icon} size={16} />
|
|
160
|
+
</TouchableOpacity>
|
|
161
|
+
);
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const containerStyles = [
|
|
165
|
+
baseToastStyle,
|
|
166
|
+
getPositionalStyle(position),
|
|
167
|
+
{
|
|
168
|
+
backgroundColor: finalBackgroundColor,
|
|
169
|
+
borderLeftColor: finalBorderColor,
|
|
170
|
+
},
|
|
171
|
+
style,
|
|
172
|
+
];
|
|
173
|
+
|
|
174
|
+
// Accessibility label combines title and message
|
|
175
|
+
const accessibilityLabel = title ? `${title}. ${message}` : message;
|
|
176
|
+
|
|
177
|
+
// Render text content - title + description or just message
|
|
178
|
+
const renderTextContent = () => {
|
|
179
|
+
if (title) {
|
|
180
|
+
// Has title: render title (larger) + message as description (smaller)
|
|
181
|
+
return (
|
|
182
|
+
<View style={textContainerStyle}>
|
|
183
|
+
<Text
|
|
184
|
+
style={[
|
|
185
|
+
titleStyle,
|
|
186
|
+
{ color: finalTextColor },
|
|
187
|
+
textStyle,
|
|
188
|
+
]}
|
|
189
|
+
numberOfLines={2}
|
|
190
|
+
>
|
|
191
|
+
{title}
|
|
192
|
+
</Text>
|
|
193
|
+
<Text
|
|
194
|
+
style={[descriptionStyle, { color: finalTextColor }]}
|
|
195
|
+
numberOfLines={2}
|
|
196
|
+
>
|
|
197
|
+
{message}
|
|
198
|
+
</Text>
|
|
199
|
+
</View>
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
// No title: render message as main text
|
|
203
|
+
return (
|
|
204
|
+
<Text
|
|
205
|
+
style={[messageStyle, { color: finalTextColor }, textStyle]}
|
|
206
|
+
numberOfLines={3}
|
|
207
|
+
>
|
|
208
|
+
{message}
|
|
209
|
+
</Text>
|
|
210
|
+
);
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
return (
|
|
214
|
+
<Animated.View
|
|
215
|
+
style={[
|
|
216
|
+
containerStyles,
|
|
217
|
+
{
|
|
218
|
+
opacity: animatedStyle.opacity,
|
|
219
|
+
transform: animatedStyle.transform,
|
|
220
|
+
},
|
|
221
|
+
]}
|
|
222
|
+
accessibilityRole="alert"
|
|
223
|
+
accessibilityLiveRegion="polite"
|
|
224
|
+
>
|
|
225
|
+
<TouchableOpacity
|
|
226
|
+
style={styles.content}
|
|
227
|
+
onPress={handlePress}
|
|
228
|
+
activeOpacity={dismissable ? 0.8 : 1}
|
|
229
|
+
disabled={!dismissable && !onPress}
|
|
230
|
+
accessibilityRole="button"
|
|
231
|
+
accessibilityLabel={accessibilityLabel}
|
|
232
|
+
accessibilityHint={
|
|
233
|
+
dismissable ? "Double tap to dismiss" : undefined
|
|
234
|
+
}
|
|
235
|
+
>
|
|
236
|
+
{renderIcon()}
|
|
237
|
+
{renderTextContent()}
|
|
238
|
+
{renderCloseButton()}
|
|
239
|
+
</TouchableOpacity>
|
|
240
|
+
</Animated.View>
|
|
241
|
+
);
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const styles = StyleSheet.create({
|
|
245
|
+
content: {
|
|
246
|
+
flex: 1,
|
|
247
|
+
flexDirection: "row",
|
|
248
|
+
alignItems: "center",
|
|
249
|
+
},
|
|
250
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import React, { useMemo } from "react";
|
|
2
|
+
import { View, StyleSheet } from "react-native";
|
|
3
|
+
import { Toast as ToastData, ToastPosition } from "./types";
|
|
4
|
+
import { Toast } from "./Toast";
|
|
5
|
+
|
|
6
|
+
export interface ToastContainerProps {
|
|
7
|
+
toasts: ToastData[];
|
|
8
|
+
position: ToastPosition;
|
|
9
|
+
onHide: (id: string) => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Container component that positions and renders toasts
|
|
14
|
+
*/
|
|
15
|
+
export const ToastContainer: React.FC<ToastContainerProps> = ({
|
|
16
|
+
toasts,
|
|
17
|
+
position,
|
|
18
|
+
onHide,
|
|
19
|
+
}) => {
|
|
20
|
+
// Filter toasts for this position
|
|
21
|
+
const positionedToasts = useMemo(() => {
|
|
22
|
+
return toasts.filter((toast) => (toast.position || "top") === position);
|
|
23
|
+
}, [toasts, position]);
|
|
24
|
+
|
|
25
|
+
if (positionedToasts.length === 0) return null;
|
|
26
|
+
|
|
27
|
+
// Get container positioning based on position
|
|
28
|
+
const getContainerPositionStyle = () => {
|
|
29
|
+
switch (position) {
|
|
30
|
+
case "top":
|
|
31
|
+
return { top: 0, paddingTop: 50 }; // 50px for status bar
|
|
32
|
+
case "bottom":
|
|
33
|
+
return { bottom: 0, paddingBottom: 34 }; // 34px for home indicator
|
|
34
|
+
case "center":
|
|
35
|
+
return { top: 0, bottom: 0, justifyContent: "center" as const };
|
|
36
|
+
default:
|
|
37
|
+
return { top: 0, paddingTop: 50 };
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<View
|
|
43
|
+
style={[styles.container, getContainerPositionStyle()]}
|
|
44
|
+
pointerEvents="box-none"
|
|
45
|
+
>
|
|
46
|
+
{positionedToasts.map((toast, index) => (
|
|
47
|
+
<View
|
|
48
|
+
key={toast.id}
|
|
49
|
+
style={index > 0 ? styles.toastGap : undefined}
|
|
50
|
+
>
|
|
51
|
+
<Toast toast={toast} position={position} onHide={onHide} />
|
|
52
|
+
</View>
|
|
53
|
+
))}
|
|
54
|
+
</View>
|
|
55
|
+
);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const styles = StyleSheet.create({
|
|
59
|
+
container: {
|
|
60
|
+
position: "absolute",
|
|
61
|
+
left: 0,
|
|
62
|
+
right: 0,
|
|
63
|
+
zIndex: 9999,
|
|
64
|
+
elevation: 9999,
|
|
65
|
+
pointerEvents: "box-none",
|
|
66
|
+
},
|
|
67
|
+
toastGap: {
|
|
68
|
+
marginTop: 8,
|
|
69
|
+
},
|
|
70
|
+
});
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import React, {
|
|
2
|
+
createContext,
|
|
3
|
+
useState,
|
|
4
|
+
useCallback,
|
|
5
|
+
useMemo,
|
|
6
|
+
ReactNode,
|
|
7
|
+
} from "react";
|
|
8
|
+
import { View, StyleSheet } from "react-native";
|
|
9
|
+
import {
|
|
10
|
+
Toast as ToastData,
|
|
11
|
+
ToastConfig,
|
|
12
|
+
ToastContextValue,
|
|
13
|
+
ToastProviderConfig,
|
|
14
|
+
ToastPosition,
|
|
15
|
+
} from "./types";
|
|
16
|
+
import { ToastContainer } from "./ToastContainer";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Toast context for accessing toast functions
|
|
20
|
+
*/
|
|
21
|
+
export const ToastContext = createContext<ToastContextValue | null>(null);
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Generate unique ID for toasts
|
|
25
|
+
*/
|
|
26
|
+
const generateId = (): string => {
|
|
27
|
+
return `toast-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Default configuration values
|
|
32
|
+
*/
|
|
33
|
+
const DEFAULT_CONFIG: Required<ToastProviderConfig> = {
|
|
34
|
+
defaultPosition: "top",
|
|
35
|
+
defaultDuration: 3000,
|
|
36
|
+
defaultAnimationIn: "fade",
|
|
37
|
+
defaultAnimationOut: "fade",
|
|
38
|
+
defaultAnimationDuration: 300,
|
|
39
|
+
maxToasts: 5,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export interface ToastProviderProps {
|
|
43
|
+
children: ReactNode;
|
|
44
|
+
config?: ToastProviderConfig;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* ToastProvider - Wrap your app with this component to enable toasts
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* ```tsx
|
|
52
|
+
* <ToastProvider config={{ defaultPosition: 'bottom' }}>
|
|
53
|
+
* <App />
|
|
54
|
+
* </ToastProvider>
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
export const ToastProvider: React.FC<ToastProviderProps> = ({
|
|
58
|
+
children,
|
|
59
|
+
config = {},
|
|
60
|
+
}) => {
|
|
61
|
+
const [toasts, setToasts] = useState<ToastData[]>([]);
|
|
62
|
+
|
|
63
|
+
// Merge config with defaults
|
|
64
|
+
const mergedConfig = useMemo(
|
|
65
|
+
() => ({ ...DEFAULT_CONFIG, ...config }),
|
|
66
|
+
[config],
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Show a toast notification
|
|
71
|
+
*/
|
|
72
|
+
const show = useCallback(
|
|
73
|
+
(toastConfig: ToastConfig): string => {
|
|
74
|
+
const id = generateId();
|
|
75
|
+
|
|
76
|
+
const newToast: ToastData = {
|
|
77
|
+
id,
|
|
78
|
+
title: toastConfig.title,
|
|
79
|
+
message: toastConfig.message,
|
|
80
|
+
type: toastConfig.type || "default",
|
|
81
|
+
position: toastConfig.position || mergedConfig.defaultPosition,
|
|
82
|
+
duration: toastConfig.duration ?? mergedConfig.defaultDuration,
|
|
83
|
+
animationIn:
|
|
84
|
+
toastConfig.animationIn || mergedConfig.defaultAnimationIn,
|
|
85
|
+
animationOut:
|
|
86
|
+
toastConfig.animationOut ||
|
|
87
|
+
mergedConfig.defaultAnimationOut,
|
|
88
|
+
animationDuration:
|
|
89
|
+
toastConfig.animationDuration ||
|
|
90
|
+
mergedConfig.defaultAnimationDuration,
|
|
91
|
+
dismissable: toastConfig.dismissable ?? true,
|
|
92
|
+
icon: toastConfig.icon,
|
|
93
|
+
hideIcon: toastConfig.hideIcon,
|
|
94
|
+
backgroundColor: toastConfig.backgroundColor,
|
|
95
|
+
textColor: toastConfig.textColor,
|
|
96
|
+
borderColor: toastConfig.borderColor,
|
|
97
|
+
style: toastConfig.style,
|
|
98
|
+
textStyle: toastConfig.textStyle,
|
|
99
|
+
onShow: toastConfig.onShow,
|
|
100
|
+
onHide: toastConfig.onHide,
|
|
101
|
+
onPress: toastConfig.onPress,
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
setToasts((prev) => {
|
|
105
|
+
// Limit to maxToasts
|
|
106
|
+
const updated = [...prev, newToast];
|
|
107
|
+
if (updated.length > mergedConfig.maxToasts) {
|
|
108
|
+
return updated.slice(-mergedConfig.maxToasts);
|
|
109
|
+
}
|
|
110
|
+
return updated;
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
return id;
|
|
114
|
+
},
|
|
115
|
+
[mergedConfig],
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Helper to parse flexible arguments: (message) or (title, description)
|
|
120
|
+
*/
|
|
121
|
+
const parseArgs = (
|
|
122
|
+
titleOrMessage: string,
|
|
123
|
+
descriptionOrConfig?: string | Partial<ToastConfig>,
|
|
124
|
+
config?: Partial<ToastConfig>,
|
|
125
|
+
): { title?: string; message: string; config?: Partial<ToastConfig> } => {
|
|
126
|
+
if (typeof descriptionOrConfig === "string") {
|
|
127
|
+
// Called as (title, description, config?)
|
|
128
|
+
return {
|
|
129
|
+
title: titleOrMessage,
|
|
130
|
+
message: descriptionOrConfig,
|
|
131
|
+
config,
|
|
132
|
+
};
|
|
133
|
+
} else {
|
|
134
|
+
// Called as (message, config?)
|
|
135
|
+
return {
|
|
136
|
+
message: titleOrMessage,
|
|
137
|
+
config: descriptionOrConfig,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Show a success toast
|
|
144
|
+
*/
|
|
145
|
+
const success = useCallback(
|
|
146
|
+
(
|
|
147
|
+
titleOrMessage: string,
|
|
148
|
+
descriptionOrConfig?: string | Partial<ToastConfig>,
|
|
149
|
+
config?: Partial<ToastConfig>,
|
|
150
|
+
): string => {
|
|
151
|
+
const {
|
|
152
|
+
title,
|
|
153
|
+
message,
|
|
154
|
+
config: cfg,
|
|
155
|
+
} = parseArgs(titleOrMessage, descriptionOrConfig, config);
|
|
156
|
+
return show({ ...cfg, title, message, type: "success" });
|
|
157
|
+
},
|
|
158
|
+
[show],
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Show a warning toast
|
|
163
|
+
*/
|
|
164
|
+
const warning = useCallback(
|
|
165
|
+
(
|
|
166
|
+
titleOrMessage: string,
|
|
167
|
+
descriptionOrConfig?: string | Partial<ToastConfig>,
|
|
168
|
+
config?: Partial<ToastConfig>,
|
|
169
|
+
): string => {
|
|
170
|
+
const {
|
|
171
|
+
title,
|
|
172
|
+
message,
|
|
173
|
+
config: cfg,
|
|
174
|
+
} = parseArgs(titleOrMessage, descriptionOrConfig, config);
|
|
175
|
+
return show({ ...cfg, title, message, type: "warning" });
|
|
176
|
+
},
|
|
177
|
+
[show],
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Show an error toast
|
|
182
|
+
*/
|
|
183
|
+
const error = useCallback(
|
|
184
|
+
(
|
|
185
|
+
titleOrMessage: string,
|
|
186
|
+
descriptionOrConfig?: string | Partial<ToastConfig>,
|
|
187
|
+
config?: Partial<ToastConfig>,
|
|
188
|
+
): string => {
|
|
189
|
+
const {
|
|
190
|
+
title,
|
|
191
|
+
message,
|
|
192
|
+
config: cfg,
|
|
193
|
+
} = parseArgs(titleOrMessage, descriptionOrConfig, config);
|
|
194
|
+
return show({ ...cfg, title, message, type: "error" });
|
|
195
|
+
},
|
|
196
|
+
[show],
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Show an info toast
|
|
201
|
+
*/
|
|
202
|
+
const info = useCallback(
|
|
203
|
+
(
|
|
204
|
+
titleOrMessage: string,
|
|
205
|
+
descriptionOrConfig?: string | Partial<ToastConfig>,
|
|
206
|
+
config?: Partial<ToastConfig>,
|
|
207
|
+
): string => {
|
|
208
|
+
const {
|
|
209
|
+
title,
|
|
210
|
+
message,
|
|
211
|
+
config: cfg,
|
|
212
|
+
} = parseArgs(titleOrMessage, descriptionOrConfig, config);
|
|
213
|
+
return show({ ...cfg, title, message, type: "info" });
|
|
214
|
+
},
|
|
215
|
+
[show],
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Hide a specific toast by ID
|
|
220
|
+
*/
|
|
221
|
+
const hide = useCallback((id: string): void => {
|
|
222
|
+
setToasts((prev) => prev.filter((toast) => toast.id !== id));
|
|
223
|
+
}, []);
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Hide all toasts
|
|
227
|
+
*/
|
|
228
|
+
const hideAll = useCallback((): void => {
|
|
229
|
+
setToasts([]);
|
|
230
|
+
}, []);
|
|
231
|
+
|
|
232
|
+
// Context value
|
|
233
|
+
const contextValue = useMemo<ToastContextValue>(
|
|
234
|
+
() => ({
|
|
235
|
+
show,
|
|
236
|
+
success,
|
|
237
|
+
warning,
|
|
238
|
+
error,
|
|
239
|
+
info,
|
|
240
|
+
hide,
|
|
241
|
+
hideAll,
|
|
242
|
+
}),
|
|
243
|
+
[show, success, warning, error, info, hide, hideAll],
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
// Group toasts by position
|
|
247
|
+
const positions: ToastPosition[] = ["top", "center", "bottom"];
|
|
248
|
+
|
|
249
|
+
return (
|
|
250
|
+
<ToastContext.Provider value={contextValue}>
|
|
251
|
+
<View style={styles.container}>
|
|
252
|
+
{children}
|
|
253
|
+
{/* Render toast containers for each position */}
|
|
254
|
+
{positions.map((position) => (
|
|
255
|
+
<ToastContainer
|
|
256
|
+
key={position}
|
|
257
|
+
toasts={toasts}
|
|
258
|
+
position={position}
|
|
259
|
+
onHide={hide}
|
|
260
|
+
/>
|
|
261
|
+
))}
|
|
262
|
+
</View>
|
|
263
|
+
</ToastContext.Provider>
|
|
264
|
+
);
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
const styles = StyleSheet.create({
|
|
268
|
+
container: {
|
|
269
|
+
flex: 1,
|
|
270
|
+
},
|
|
271
|
+
});
|