@backpackapp-io/react-native-toast
Version:
A toasting library for React Native. Built in features such as swipe to dismiss, multiple toasts, & no context power this library.
233 lines (231 loc) • 9.37 kB
JavaScript
"use strict";
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { Platform, Pressable, Text, useColorScheme, useWindowDimensions, View } from 'react-native';
import Animated, { Easing, ReduceMotion, runOnJS, useAnimatedStyle, useSharedValue, withSpring, withTiming } from 'react-native-reanimated';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { Directions, Gesture, GestureDetector } from 'react-native-gesture-handler';
import { DismissReason } from '../core/types';
import { resolveValue, ToastPosition } from '../core/types';
import { colors, ConstructShadow, useVisibilityChange } from '../utils';
import { toast as toasting } from '../headless';
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
const AnimatedPressable = Animated.createAnimatedComponent(Pressable);
const DEFAULT_TOAST_HEIGHT = 50;
const MAX_WIDTH = 360;
export const Toast = ({
toast,
updateHeight,
offset,
startPause,
endPause,
overrideDarkMode,
onToastHide,
onToastPress,
onToastShow,
extraInsets,
defaultStyle,
keyboardVisible,
keyboardHeight
}) => {
const insets = useSafeAreaInsets();
const {
width,
height
} = useWindowDimensions();
const isSystemDarkMode = useColorScheme() === 'dark';
const [toastHeight, setToastHeight] = useState(toast?.height ? toast.height : DEFAULT_TOAST_HEIGHT);
const maxWidth = toast?.maxWidth ? toast.maxWidth : MAX_WIDTH;
const [toastWidth, setToastWidth] = useState(toast?.width ? toast.width : width - 32 > maxWidth ? maxWidth : width - 32);
const isDarkMode = overrideDarkMode !== undefined ? overrideDarkMode : isSystemDarkMode;
const getStartingPosition = useMemo(() => {
let leftPosition = (width - toastWidth) / 2; // Default to center
if (toast.position === ToastPosition.TOP_LEFT || toast.position === ToastPosition.BOTTOM_LEFT) {
leftPosition = insets.left + 16 + (extraInsets?.left ?? 0);
}
if (toast.position === ToastPosition.TOP_RIGHT || toast.position === ToastPosition.BOTTOM_RIGHT) {
leftPosition = width - toastWidth - insets.right - 16 - (extraInsets?.right ?? 0);
}
let startY = 0;
if (toast.position === ToastPosition.TOP || toast.position === ToastPosition.TOP_LEFT || toast.position === ToastPosition.TOP_RIGHT) {
startY = -(toast.height || DEFAULT_TOAST_HEIGHT) - insets.top - 50;
} else {
startY = height - insets.bottom - Platform.select({
ios: 16,
default: 32
});
}
return {
startY,
leftPosition
};
}, [height, width, toastWidth, toast.position, toast.height, insets, extraInsets]);
const {
startY,
leftPosition
} = getStartingPosition;
const opacity = useSharedValue(0);
const position = useSharedValue(startY);
const offsetY = useSharedValue(startY);
const onPress = useCallback(() => {
if (toast.onPress) {
toast.onPress(toast);
}
if (onToastPress) {
onToastPress(toast);
}
}, [toast, onToastPress]);
const dismiss = useCallback((id, reason) => {
toasting.dismiss(id, reason);
}, []);
const getSwipeDirection = useCallback(() => {
if (toast.position === ToastPosition.TOP || toast.position === ToastPosition.TOP_LEFT || toast.position === ToastPosition.TOP_RIGHT) {
return Directions.UP;
} else {
return Directions.DOWN;
}
}, [toast.position]);
const setPosition = useCallback(() => {
let timingConfig = {
duration: 300
};
let springConfig = {
stiffness: 80
};
if (toast.animationConfig) {
const {
duration = 300,
easing = Easing.inOut(Easing.quad),
reduceMotion = ReduceMotion.System,
...spring
} = toast.animationConfig;
timingConfig = {
duration,
easing,
reduceMotion
};
springConfig = spring;
}
const useSpringAnimation = toast.animationType === 'spring';
const animation = useSpringAnimation ? withSpring : withTiming;
if (toast.position === ToastPosition.TOP || toast.position === ToastPosition.TOP_LEFT || toast.position === ToastPosition.TOP_RIGHT) {
offsetY.value = animation(toast.visible ? offset : startY, useSpringAnimation ? springConfig : timingConfig);
position.value = animation(toast.visible ? offset : startY, useSpringAnimation ? springConfig : timingConfig);
} else {
let kbHeight = keyboardVisible ? keyboardHeight : 0;
const val = toast.visible ? startY - toastHeight - offset - kbHeight - insets.bottom - (extraInsets?.bottom ?? 0) - Platform.select({
ios: 32,
default: 24
}) : startY;
offsetY.value = animation(val, useSpringAnimation ? springConfig : timingConfig);
position.value = animation(val, useSpringAnimation ? springConfig : timingConfig);
}
}, [offset, toast.visible, keyboardVisible, keyboardHeight, toastHeight, insets.bottom, position, startY, toast.position, offsetY, extraInsets, toast.animationConfig, toast.animationType]);
const composedGesture = useMemo(() => {
const panGesture = Gesture.Pan().onUpdate(e => {
offsetY.value = e.translationY / 4 + position.value;
}).onEnd(() => {
runOnJS(setPosition)();
});
const flingGesture = Gesture.Fling().direction(getSwipeDirection()).onEnd(() => {
offsetY.value = withTiming(startY, {
duration: toast?.animationConfig?.flingPositionReturnDuration ?? 40
});
runOnJS(dismiss)(toast.id, DismissReason.SWIPE);
});
return toast.isSwipeable ? Gesture.Simultaneous(flingGesture, panGesture) : panGesture;
}, [offsetY, startY, position, setPosition, toast.id, dismiss, toast.isSwipeable, toast.animationConfig, getSwipeDirection]);
useVisibilityChange(() => {
if (toast.onShow) {
toast.onShow(toast);
}
onToastShow?.(toast);
}, reason => {
if (toast.onHide) {
toast.onHide(toast, reason || DismissReason.PROGRAMMATIC);
}
onToastHide?.(toast, reason || DismissReason.PROGRAMMATIC);
}, toast.visible, toast.dismissReason);
useEffect(() => {
setToastHeight(toast?.height ? toast.height : DEFAULT_TOAST_HEIGHT);
}, [toast.height]);
useEffect(() => {
setToastWidth(toast?.width ? toast.width : width - 32 > maxWidth ? maxWidth : width - 32);
}, [toast.width, maxWidth, width]);
useEffect(() => {
opacity.value = withTiming(toast.visible ? 1 : 0, {
duration: toast?.animationConfig?.duration ?? 300
});
}, [toast.visible, opacity, toast.animationConfig]);
useEffect(() => {
setPosition();
}, [offset, toast.visible, keyboardVisible, keyboardHeight, toastHeight, setPosition]);
const style = useAnimatedStyle(() => {
//Control opacity and translation of toast
return {
opacity: opacity.value,
transform: [{
translateY: offsetY.value
}]
};
});
const resolvedValue = resolveValue(toast.message, toast);
return /*#__PURE__*/_jsx(GestureDetector, {
gesture: composedGesture,
children: /*#__PURE__*/_jsx(AnimatedPressable, {
testID: "toast-pressable",
onPressIn: startPause,
onPressOut: () => {
endPause();
},
onPress: onPress,
style: [{
backgroundColor: !toast.customToast ? isDarkMode ? colors.backgroundDark : colors.backgroundLight : undefined,
borderRadius: 8,
position: 'absolute',
left: leftPosition,
zIndex: toast.visible ? 9999 : undefined,
alignItems: 'center',
justifyContent: 'center'
}, style, !toast.disableShadow && ConstructShadow('#181821', 0.15, false), defaultStyle?.pressable, toast.styles?.pressable],
children: toast.customToast ? /*#__PURE__*/_jsx(View, {
testID: "toast-view",
onLayout: event => updateHeight(toast.id, event.nativeEvent.layout.height),
children: toast.customToast({
...toast,
height: toastHeight,
width: toastWidth
})
}, toast.id) : /*#__PURE__*/_jsxs(View, {
testID: "toast-view",
onLayout: event => updateHeight(toast.id, event.nativeEvent.layout.height),
style: [{
minHeight: toastHeight,
width: toastWidth,
flexDirection: 'row',
alignItems: 'center',
paddingVertical: 12,
paddingHorizontal: 16
}, defaultStyle?.view, toast.styles?.view],
children: [(toast.type === 'error' || toast.type === 'success') && /*#__PURE__*/_jsx(View, {
style: [{
backgroundColor: toast.type === 'error' ? colors.error : toast.type === 'success' ? colors.success : isDarkMode ? colors.backgroundDark : colors.backgroundLight,
width: 3,
height: '100%',
borderRadius: 12,
marginRight: 12
}, defaultStyle?.indicator, toast?.styles?.indicator]
}), typeof toast.icon === 'string' ? /*#__PURE__*/_jsx(Text, {
children: toast.icon
}) : toast.icon, typeof resolvedValue === 'string' ? /*#__PURE__*/_jsx(Text, {
style: [{
color: isDarkMode ? colors.textLight : colors.textDark,
padding: 4,
flex: 1
}, defaultStyle?.text, toast?.styles?.text],
children: resolvedValue
}) : resolvedValue]
}, toast.id)
})
}, toast.id);
};
//# sourceMappingURL=Toast.js.map