UNPKG

sonner-native

Version:

An opinionated toast component for React Native. A port of @emilkowalski's sonner.

175 lines (150 loc) 4.04 kB
import { useReducedMotion, withTiming } from 'react-native-reanimated'; import type { EntryExitAnimationFunction } from 'react-native-reanimated'; import { getEnteringTranslateY, getExitingTranslateY } from './animation-utils'; import { toastDefaultValues } from './constants'; import { useToastContext } from './context'; import { easeInOutCubic, easeOutQuartFn } from './easings'; import type { ToastAnimation, ToastEntryExitAnimation, ToastPosition } from './types'; export const ENTERING_ANIMATION_DURATION = 300; export const STACKING_ANIMATION_DURATION = 600; type ResolvedAnimation = Exclude<ToastEntryExitAnimation, 'default'>; export const resolveAnimationField = ( toastValue: ToastEntryExitAnimation | undefined, toasterValue: ToastEntryExitAnimation | undefined, defaultValue: EntryExitAnimationFunction ): ResolvedAnimation => { const resolved = toastValue !== undefined ? toastValue : toasterValue; if (resolved === undefined || resolved === 'default') { return defaultValue; } return resolved; }; export const useToastLayoutAnimations = ( positionProp: ToastPosition | undefined, animationProp: ToastAnimation | undefined, isHiddenByLimit?: boolean, numberOfToasts?: number ) => { const { position: positionCtx, gap, animation: animationCtx } = useToastContext(); const position = positionProp || positionCtx; const stackGap = gap ?? toastDefaultValues.stackGap; const reducedMotion = useReducedMotion(); if (reducedMotion) { return { entering: undefined, exiting: undefined }; } const defaultEntering: EntryExitAnimationFunction = () => { 'worklet'; return getToastEntering({ position }); }; const defaultExiting: EntryExitAnimationFunction = () => { 'worklet'; return getToastExiting({ position, isHiddenByLimit, numberOfToasts, stackGap, }); }; const entering = resolveAnimationField( animationProp?.enter, animationCtx?.enter, defaultEntering ); // Overflow-cull (isHiddenByLimit) always uses the library fade exit. // User-supplied custom exits would look wrong on a buried toast. const exiting = isHiddenByLimit ? defaultExiting : resolveAnimationField( animationProp?.exit, animationCtx?.exit, defaultExiting ); return { entering, exiting }; }; type GetToastAnimationParams = { position: ToastPosition; isHiddenByLimit?: boolean; numberOfToasts?: number; stackGap?: number; }; export const getToastEntering = ({ position }: GetToastAnimationParams) => { 'worklet'; const animations = { opacity: withTiming(1, { easing: easeOutQuartFn, duration: ENTERING_ANIMATION_DURATION, }), transform: [ { translateY: withTiming(0, { easing: easeOutQuartFn, duration: ENTERING_ANIMATION_DURATION, }), }, ], }; const translateY = getEnteringTranslateY(position); const initialValues = { opacity: 0, transform: [ { translateY, }, ], }; return { initialValues, animations, }; }; export const getToastExiting = ({ position, isHiddenByLimit, numberOfToasts, stackGap = 8, }: GetToastAnimationParams) => { 'worklet'; if (isHiddenByLimit) { const animations = { opacity: withTiming(0, { easing: easeInOutCubic, duration: ENTERING_ANIMATION_DURATION, }), }; const initialValues = { opacity: 1, }; return { initialValues, animations, }; } const translateY = getExitingTranslateY({ position, isHiddenByLimit, numberOfToasts, stackGap, }); const animations = { opacity: withTiming(0, { easing: easeInOutCubic }), transform: [ { translateY: withTiming(translateY, { easing: easeInOutCubic, }), }, ], }; const initialValues = { opacity: 1, transform: [ { translateY: 0, }, ], }; return { initialValues, animations, }; };