sonner-native
Version:
An opinionated toast component for React Native. A port of @emilkowalski's sonner.
175 lines (150 loc) • 4.04 kB
text/typescript
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,
};
};