UNPKG

react-native-toast-notifications

Version:

[![Version][version-badge]][package] [![MIT License][license-badge]][license]

372 lines (330 loc) 9.39 kB
import React, { FC, useRef, useEffect, useState } from "react"; import { View, StyleSheet, Animated, StyleProp, ViewStyle, TextStyle, Text, TouchableWithoutFeedback, PanResponder, PanResponderInstance, PanResponderGestureState, Platform, } from "react-native"; import { useDimensions } from "./utils/useDimensions"; export interface ToastOptions { /** * Id is optional, you may provide an id only if you want to update toast later using toast.update() */ id?: string; /** * Customize toast icon */ icon?: JSX.Element; /** * Toast types, You can implement your custom types with JSX using renderType method on ToastContainer. */ type?: "normal" | "success" | "danger" | "warning" | string; /** * In ms, How long toast will stay before it go away */ duration?: number; /** * Customize when toast should be placed */ placement?: "top" | "bottom" | "center"; /** * Customize style of toast */ style?: StyleProp<ViewStyle>; /** * Customize style of toast text */ textStyle?: StyleProp<TextStyle>; /** * Customize how fast toast will show and hide */ animationDuration?: number; /** * Customize how toast is animated when added or removed */ animationType?: "slide-in" | "zoom-in"; /** * Customize success type icon */ successIcon?: JSX.Element; /** * Customize danger type icon */ dangerIcon?: JSX.Element; /** * Customize warning type icon */ warningIcon?: JSX.Element; /** * Customize success type color. changes toast background color */ successColor?: string; /** * Customize danger type color. changes toast background color */ dangerColor?: string; /** * Customize warning type color. changes toast background color */ warningColor?: string; /** * Customize normal type color. changes toast background color */ normalColor?: string; /** * Register event for when toast is pressed. If you're using a custom toast you have to pass this to a Touchable. */ onPress?(id: string): void; /** * Execute event after toast is closed */ onClose?(): void; /** * Payload data for custom toasts. You can pass whatever you want */ data?: any; swipeEnabled?: boolean; } export interface ToastProps extends ToastOptions { id: string; onDestroy(): void; message: string | JSX.Element; open: boolean; renderToast?(toast: ToastProps): JSX.Element; renderType?: { [type: string]: (toast: ToastProps) => JSX.Element }; onHide(): void; } const Toast: FC<ToastProps> = (props) => { let { id, onDestroy, icon, type = "normal", message, duration = 5000, style, textStyle, animationDuration = 250, animationType = "slide-in", successIcon, dangerIcon, warningIcon, successColor, dangerColor, warningColor, normalColor, placement, swipeEnabled, onPress, } = props; const containerRef = useRef<View>(null); const [animation] = useState(new Animated.Value(0)); const panResponderRef = useRef<PanResponderInstance>(); const panResponderAnimRef = useRef<Animated.ValueXY>(); const closeTimeoutRef = useRef<NodeJS.Timeout | null>(null); const dims = useDimensions(); useEffect(() => { Animated.timing(animation, { toValue: 1, useNativeDriver: Platform.OS !== "web", duration: animationDuration, }).start(); if (duration !== 0 && typeof duration === "number") { closeTimeoutRef.current = setTimeout(() => { handleClose(); }, duration); } return () => { closeTimeoutRef.current && clearTimeout(closeTimeoutRef.current); }; }, [duration]); // Handles hide & hideAll useEffect(() => { if (!props.open) { // Unregister close timeout closeTimeoutRef.current && clearTimeout(closeTimeoutRef.current); // Close animation them remove from stack. handleClose(); } }, [props.open]); const handleClose = () => { Animated.timing(animation, { toValue: 0, useNativeDriver: Platform.OS !== "web", duration: animationDuration, }).start(() => onDestroy()); }; const panReleaseToLeft = (gestureState: PanResponderGestureState) => { Animated.timing(getPanResponderAnim(), { toValue: { x: (-dims.width / 10) * 9, y: gestureState.dy }, useNativeDriver: Platform.OS !== "web", duration: 250, }).start(() => onDestroy()); }; const panReleaseToRight = (gestureState: PanResponderGestureState) => { Animated.timing(getPanResponderAnim(), { toValue: { x: (dims.width / 10) * 9, y: gestureState.dy }, useNativeDriver: Platform.OS !== "web", duration: 250, }).start(() => onDestroy()); }; const getPanResponder = () => { if (panResponderRef.current) return panResponderRef.current; const swipeThreshold = Platform.OS === "android" ? 10 : 0; panResponderRef.current = PanResponder.create({ onMoveShouldSetPanResponder: (_, gestureState) => { //return true if user is swiping, return false if it's a single click return ( Math.abs(gestureState.dx) > swipeThreshold || Math.abs(gestureState.dy) > swipeThreshold ); }, onPanResponderMove: (_, gestureState) => { getPanResponderAnim()?.setValue({ x: gestureState.dx, y: gestureState.dy, }); }, onPanResponderRelease: (_, gestureState) => { if (gestureState.dx > 50) { panReleaseToRight(gestureState); } else if (gestureState.dx < -50) { panReleaseToLeft(gestureState); } else { Animated.spring(getPanResponderAnim(), { toValue: { x: 0, y: 0 }, useNativeDriver: Platform.OS !== "web", }).start(); } }, }); return panResponderRef.current; }; const getPanResponderAnim = () => { if (panResponderAnimRef.current) return panResponderAnimRef.current; panResponderAnimRef.current = new Animated.ValueXY({ x: 0, y: 0 }); return panResponderAnimRef.current; }; if (icon === undefined) { switch (type) { case "success": { if (successIcon) { icon = successIcon; } break; } case "danger": { if (dangerIcon) { icon = dangerIcon; } break; } case "warning": { if (warningIcon) { icon = warningIcon; } break; } } } let backgroundColor = ""; switch (type) { case "success": backgroundColor = successColor || "rgb(46, 125, 50)"; break; case "danger": backgroundColor = dangerColor || "rgb(211, 47, 47)"; break; case "warning": backgroundColor = warningColor || "rgb(237, 108, 2)"; break; default: backgroundColor = normalColor || "#333"; } const animationStyle: Animated.WithAnimatedObject<ViewStyle> = { opacity: animation, transform: [ { translateY: animation.interpolate({ inputRange: [0, 1], outputRange: placement === "bottom" ? [20, 0] : [-20, 0], // 0 : 150, 0.5 : 75, 1 : 0 }), }, ], }; if (swipeEnabled) { animationStyle.transform?.push( getPanResponderAnim().getTranslateTransform()[0] ); } if (animationType === "zoom-in") { animationStyle.transform?.push({ scale: animation.interpolate({ inputRange: [0, 1], outputRange: [0.7, 1], }), }); } return ( <Animated.View pointerEvents={"box-none"} ref={containerRef} {...(swipeEnabled ? getPanResponder().panHandlers : null)} style={[styles.container, animationStyle]} > {props.renderType && props.renderType[type] ? ( props.renderType[type](props) ) : props.renderToast ? ( props.renderToast(props) ) : ( <TouchableWithoutFeedback disabled={!onPress} onPress={() => onPress && onPress(id)} > <View style={[ styles.toastContainer, { maxWidth: (dims.width / 10) * 9, backgroundColor }, style, ]} > {icon ? <View style={styles.iconContainer}>{icon}</View> : null} {React.isValidElement(message) ? ( message ) : ( <Text style={[styles.message, textStyle]}>{message}</Text> )} </View> </TouchableWithoutFeedback> )} </Animated.View> ); }; const styles = StyleSheet.create({ container: { width: "100%", alignItems: "center" }, toastContainer: { paddingHorizontal: 12, paddingVertical: 12, borderRadius: 5, marginVertical: 5, flexDirection: "row", alignItems: "center", overflow: "hidden", }, message: { color: "#fff", fontWeight: "500", }, iconContainer: { marginRight: 5, }, }); export default Toast;