@codebayu/react-native-toast
Version:
The Package for creating dynamic and reusable styles in React Native App
243 lines (242 loc) • 9.84 kB
JavaScript
import { useRef, useState, useImperativeHandle, forwardRef, useEffect } from "react";
import { Image, View, Text, Platform, } from "react-native";
import Animated, { runOnJS, useAnimatedStyle, useSharedValue, withDelay, withTiming, cancelAnimation, } from "react-native-reanimated";
import { ASSETS, CONTAINER_SIZE, PADDING, SCREEN_WIDTH } from "./constant";
import { ToastManager } from "./state";
import { styles } from "./styles";
export const Toast = forwardRef((props, ref) => {
const { defaultDuration = 4000, defaultAnimationDuration = 400, defaultPosition = Platform.OS === "ios" ? 90 : 100, // Add default position
customIcons, customColors, } = props;
const [config, setConfig] = useState({
text: undefined,
type: undefined,
visible: false,
duration: defaultDuration,
animationDuration: defaultAnimationDuration,
position: defaultPosition, // Add position to config
});
const [textWidth, setTextWidth] = useState(0);
const timer = useRef(null);
const isAnimating = useRef(false);
// Animated values
const transY = useSharedValue(-100);
const toastWidth = useSharedValue(CONTAINER_SIZE);
const textOpacity = useSharedValue(0);
// Function to update animation status safely
const updateAnimationStatus = (status) => {
isAnimating.current = status;
ToastManager.setAnimating(status);
};
useImperativeHandle(ref, () => ({
show: (message, type, options) => {
// Skip empty messages
if (!message || message.trim() === "") {
updateAnimationStatus(false);
return;
}
const actualType = type || "warning"; // Default to 'warning' if type is undefined
const animationDuration = options?.animationDuration || defaultAnimationDuration;
const duration = options?.duration || defaultDuration;
const position = options?.position || defaultPosition; // Get position from options
showToast(message, actualType, animationDuration, duration, position);
},
hide: (callback) => {
hideToast(callback);
},
}));
// Register with the global ToastManager
useEffect(() => {
const refObject = {
show: (message, type, options) => {
// Skip empty messages
if (!message || message.trim() === "") {
updateAnimationStatus(false);
return;
}
const actualType = type || "warning"; // Default to 'warning' if type is undefined
const animationDuration = options?.animationDuration || defaultAnimationDuration;
const duration = options?.duration || defaultDuration;
const position = options?.position || defaultPosition; // Get position from options
showToast(message, actualType, animationDuration, duration, position);
},
hide: hideToast,
};
ToastManager.setToastRef(refObject);
return () => {
// Clean up on unmount
ToastManager.setToastRef(null);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// Animation when text width changes
useEffect(() => {
if (textWidth > 0 && config.text && config.visible) {
const totalWidth = Math.min(CONTAINER_SIZE + textWidth + PADDING, SCREEN_WIDTH - 32);
// Cancel any ongoing animations
cancelAnimation(toastWidth);
cancelAnimation(textOpacity);
toastWidth.value = withDelay(config.animationDuration, withTiming(totalWidth, { duration: config.animationDuration }));
textOpacity.value = withDelay(config.animationDuration * 2, withTiming(1, { duration: config.animationDuration }));
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [textWidth, config.text, config.visible, config.animationDuration]);
// Animated styles
const animatedContainerStyle = useAnimatedStyle(() => ({
transform: [{ translateY: transY.value }],
}));
const animatedInnerStyle = useAnimatedStyle(() => ({
width: toastWidth.value,
}));
const animatedTextStyle = useAnimatedStyle(() => ({
opacity: textOpacity.value,
}));
// Measure text width
const onTextLayout = (e) => {
const { width } = e.nativeEvent.layout;
if (width && width !== textWidth) {
setTextWidth(width);
}
};
function showToast(text, type, animationDuration, displayDuration, position // Add position parameter
) {
// Validate text content
if (!text || text.trim() === "") {
updateAnimationStatus(false);
return;
}
// Clear any existing timers
if (timer.current) {
clearTimeout(timer.current);
}
// Cancel any ongoing animations
cancelAnimation(transY);
cancelAnimation(toastWidth);
cancelAnimation(textOpacity);
// Set animation status
updateAnimationStatus(true);
// Reset animation
toastWidth.value = CONTAINER_SIZE;
textOpacity.value = 0;
// Update config
setConfig({
text,
type,
visible: true,
duration: displayDuration,
animationDuration,
position, // Add position to config
});
// Ensure text width is properly measured on next cycle
setTimeout(() => {
// Show toast with custom position
transY.value = withTiming(position, { duration: animationDuration });
// Auto-hide after duration
timer.current = setTimeout(() => {
hideToast();
}, displayDuration);
}, 0);
}
function hideToast(callback) {
if (!isAnimating.current && transY.value === -100) {
// Toast is already hidden
if (callback)
callback();
return;
}
if (timer.current) {
clearTimeout(timer.current);
timer.current = null;
}
const animDuration = config.animationDuration;
// Cancel any ongoing animations
cancelAnimation(transY);
cancelAnimation(toastWidth);
cancelAnimation(textOpacity);
// Reverse animation
textOpacity.value = withTiming(0, { duration: animDuration / 2 });
toastWidth.value = withDelay(animDuration / 2, withTiming(CONTAINER_SIZE, { duration: animDuration / 2 }));
transY.value = withDelay(animDuration, withTiming(-100, { duration: animDuration }, () => {
runOnJS(completeHideAnimation)(callback);
}));
}
// Function to handle completion of hide animation
const completeHideAnimation = (callback) => {
updateAnimationStatus(false);
resetConfig();
if (callback)
callback();
};
function resetConfig() {
setConfig((prev) => ({
...prev,
text: undefined,
type: undefined,
visible: false,
}));
}
// Toast styling helpers
function getIconSource() {
if (customIcons && config.type && customIcons[config.type]) {
return customIcons[config.type];
}
switch (config.type) {
case "success":
return ASSETS.success;
case "error":
return ASSETS.error;
default:
return ASSETS.warning;
}
}
function getBackgroundColor() {
if (customColors && config.type && customColors[config.type]) {
return customColors[config.type].background;
}
return "#102128";
}
function getTextColor() {
if (customColors && config.type && customColors[config.type]) {
return customColors[config.type].text;
}
switch (config.type) {
case "success":
return "#15d1ae";
case "error":
return "#f47068";
default:
return "#f69d51";
}
}
// Only render if we have actual text content
if (!config.text || config.text.trim() === "") {
return null;
}
return (React.createElement(React.Fragment, null,
config.text && (React.createElement(View, { style: styles.hiddenTextContainer, pointerEvents: "none" },
React.createElement(Text, { style: {
color: getTextColor(),
fontWeight: "500",
fontSize: 16,
}, onLayout: onTextLayout }, config.text))),
React.createElement(Animated.View, { style: [styles.container, animatedContainerStyle] },
React.createElement(Animated.View, { style: [
styles.innerContainer,
animatedInnerStyle,
{
backgroundColor: getBackgroundColor(),
borderWidth: 1,
borderColor: getTextColor(),
},
] },
React.createElement(Image, { source: getIconSource(), style: styles.image }),
React.createElement(View, { style: styles.textContainer },
React.createElement(Animated.Text, { numberOfLines: 1, style: [
{
color: getTextColor(),
fontWeight: "500",
fontSize: 16,
},
animatedTextStyle,
] }, config.text))))));
});
Toast.displayName = "Toast";