UNPKG

rn-toastify

Version:

A professional, production-ready toast notification library for React Native. Featuring smooth spring animations, swipe-to-dismiss gestures, progress bars, queue management, and a beautiful design system with light/dark themes.

224 lines (205 loc) 7.73 kB
import React, { useEffect, useState, useCallback, useMemo } from 'react'; import { View, StyleSheet, Appearance, Platform, Keyboard } from 'react-native'; import Toast from '../Toast'; import toastManagerInstance from './ToastManager'; import { heightPercentageToDP as hp, widthPercentageToDP as wp, } from '../utils/Pixel/Index'; const ToastContainer = ({ theme: forcedTheme, maxVisible = 3, defaultPosition = 'top', topOffset = 0, bottomOffset = 0, swipeable = true, } = {}) => { const [toasts, setToasts] = useState([]); const [theme, setTheme] = useState(forcedTheme ?? (Appearance?.getColorScheme?.() || 'light')); const [keyboardHeight, setKeyboardHeight] = useState(0); useEffect(() => { // Configure the manager with our settings toastManagerInstance.configure({ maxVisible }); }, [maxVisible]); useEffect(() => { const handleShow = (toast) => { setToasts((prev) => { // Prevent duplicate IDs const filtered = prev.filter((t) => t.id !== toast.id); return [...filtered, toast]; }); }; const handleRemove = (id) => { setToasts((prev) => prev.filter((toast) => toast.id !== id)); }; const handleUpdate = (updatedToast) => { setToasts((prev) => prev.map((t) => (t.id === updatedToast.id ? updatedToast : t)) ); }; toastManagerInstance.on('show', handleShow); toastManagerInstance.on('remove', handleRemove); toastManagerInstance.on('update', handleUpdate); // Theme listener let appearanceSub; if (!forcedTheme && Appearance?.addChangeListener) { appearanceSub = Appearance.addChangeListener(({ colorScheme }) => { setTheme(colorScheme ?? 'light'); }); } // Keyboard listener for bottom toasts const keyboardShowSub = Keyboard.addListener( Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow', (e) => setKeyboardHeight(e.endCoordinates.height) ); const keyboardHideSub = Keyboard.addListener( Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide', () => setKeyboardHeight(0) ); return () => { toastManagerInstance.off('show', handleShow); toastManagerInstance.off('remove', handleRemove); toastManagerInstance.off('update', handleUpdate); if (appearanceSub?.remove) appearanceSub.remove(); keyboardShowSub.remove(); keyboardHideSub.remove(); }; }, [forcedTheme]); // Calculate safe top margin const getTopMargin = useCallback(() => { if (Platform.OS === 'android') { // Use StatusBar height if available try { const { StatusBar } = require('react-native'); return (StatusBar.currentHeight || 0) + topOffset; } catch { return topOffset; } } // iOS: default safe area return 50 + topOffset; }, [topOffset]); // Separate toasts by position const { topToasts, centerToasts, bottomToasts } = useMemo(() => { const top = []; const center = []; const bottom = []; toasts.forEach((toast) => { const pos = toast?.options?.position || defaultPosition; if (pos === 'top') top.push(toast); else if (pos === 'center') center.push(toast); else bottom.push(toast); }); return { topToasts: top, centerToasts: center, bottomToasts: bottom }; }, [toasts, defaultPosition]); const topMargin = getTopMargin(); const toastSpacing = hp(1.2); const toastHeight = hp(8.5); return ( <> {/* Top positioned toasts */} {topToasts.length > 0 && ( <View style={[styles.topContainer, { top: topMargin }]} pointerEvents="box-none" > {topToasts.map((toast, index) => ( <Toast key={toast.id} visible={true} duration={toast?.options?.duration} position="top" theme={theme} style={[ toast?.options?.style || {}, { marginTop: index * (toastHeight + toastSpacing) }, ]} onHide={() => toastManagerInstance.remove(toast.id)} > {toast.content} </Toast> ))} </View> )} {/* Center positioned toasts */} {centerToasts.length > 0 && ( <View style={styles.centerContainer} pointerEvents="box-none"> {centerToasts.map((toast, index) => ( <Toast key={toast.id} visible={true} duration={toast?.options?.duration} position="center" theme={theme} style={[ toast?.options?.style || {}, { marginTop: index * (toastHeight + toastSpacing) }, ]} onHide={() => toastManagerInstance.remove(toast.id)} > {toast.content} </Toast> ))} </View> )} {/* Bottom positioned toasts */} {bottomToasts.length > 0 && ( <View style={[ styles.bottomContainer, { bottom: bottomOffset + keyboardHeight }, ]} pointerEvents="box-none" > {bottomToasts.map((toast, index) => ( <Toast key={toast.id} visible={true} duration={toast?.options?.duration} position="bottom" theme={theme} style={[ toast?.options?.style || {}, { bottom: hp(2) + index * (toastHeight + toastSpacing) }, ]} onHide={() => toastManagerInstance.remove(toast.id)} > {toast.content} </Toast> ))} </View> )} </> ); }; const styles = StyleSheet.create({ topContainer: { position: 'absolute', left: 0, right: 0, zIndex: 9999, pointerEvents: 'box-none', alignItems: 'center', }, centerContainer: { position: 'absolute', top: 0, bottom: 0, left: 0, right: 0, zIndex: 9999, pointerEvents: 'box-none', alignItems: 'center', justifyContent: 'center', }, bottomContainer: { position: 'absolute', bottom: 0, left: 0, right: 0, zIndex: 9999, pointerEvents: 'box-none', alignItems: 'center', }, }); export default React.memo(ToastContainer);