UNPKG

@codebayu/react-native-toast

Version:

The Package for creating dynamic and reusable styles in React Native App

243 lines (242 loc) 9.84 kB
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";