Skip to content

Commit 99ee150

Browse files
committed
feat(toast): handle animation file
1 parent fd0b58f commit 99ee150

File tree

9 files changed

+368
-75
lines changed

9 files changed

+368
-75
lines changed

example/src/app/(home)/components/toast.tsx

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
type ToastComponentProps,
77
} from 'heroui-native';
88
import { View } from 'react-native';
9+
import { ZoomIn, ZoomOut } from 'react-native-reanimated';
910
import { toast as sonnerToast } from 'sonner-native';
1011
import type { UsageVariant } from '../../../components/component-presentation/types';
1112
import { UsageVariantFlatList } from '../../../components/component-presentation/usage-variant-flatlist';
@@ -83,6 +84,10 @@ const MyToast = (props: ToastComponentProps) => {
8384
variant="accent"
8485
placement="bottom"
8586
className="flex-row items-center gap-3"
87+
animation={{
88+
entering: { top: ZoomIn, bottom: ZoomIn },
89+
exiting: { top: ZoomOut, bottom: ZoomOut },
90+
}}
8691
{...props}
8792
>
8893
<View className="flex-1">
@@ -97,7 +102,7 @@ const MyToast = (props: ToastComponentProps) => {
97102
};
98103

99104
const InteractiveDemoContent = () => {
100-
const toast = useToast();
105+
const { toast } = useToast();
101106

102107
const _renderToast = (props: ToastComponentProps) => <MyToast {...props} />;
103108

@@ -124,9 +129,9 @@ const InteractiveDemoContent = () => {
124129
Hide All Toasts
125130
</Button>
126131

127-
{/* <Button onPress={() => sonnerToast('Hello, World!')}>
132+
<Button onPress={() => sonnerToast('Hello, World!')}>
128133
Sonner Toast
129-
</Button> */}
134+
</Button>
130135
</View>
131136
</View>
132137
);
@@ -135,7 +140,7 @@ const InteractiveDemoContent = () => {
135140
// ------------------------------------------------------------------------------
136141

137142
const MultipleToastsContent = () => {
138-
const toast = useToast();
143+
const { toast, isToastVisible } = useToast();
139144

140145
return (
141146
<View className="flex-1 px-5">

example/src/components/component-presentation/usage-variant-flatlist.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { BlurView } from 'expo-blur';
22
import * as Haptics from 'expo-haptics';
3+
import { useToast } from 'heroui-native';
34
import { memo, useCallback, useRef, useState } from 'react';
45
import {
56
FlatList,
@@ -18,6 +19,7 @@ import Animated, {
1819
type SharedValue,
1920
} from 'react-native-reanimated';
2021
import { useSafeAreaInsets } from 'react-native-safe-area-context';
22+
import { scheduleOnRN } from 'react-native-worklets';
2123
import { useAppTheme } from '../../contexts/app-theme-context';
2224
import { useAccessibilityInfo } from '../../helpers/hooks/use-accessability-info';
2325
import { PaginationIndicator } from './pagination-indicator';
@@ -87,6 +89,8 @@ export const UsageVariantFlatList = ({
8789

8890
const { isDark } = useAppTheme();
8991

92+
const { toast, isToastVisible } = useToast();
93+
9094
const insets = useSafeAreaInsets();
9195
const { width, height } = useWindowDimensions();
9296
const itemHeight = height;
@@ -118,6 +122,9 @@ export const UsageVariantFlatList = ({
118122
const scrollHandler = useAnimatedScrollHandler({
119123
onScroll: (event) => {
120124
scrollY.set(event.contentOffset.y);
125+
if (isToastVisible) {
126+
scheduleOnRN(toast.hide);
127+
}
121128
},
122129
});
123130

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
import type { ViewStyle } from 'react-native';
2+
import {
3+
Easing,
4+
FadeInDown,
5+
FadeInUp,
6+
interpolate,
7+
Keyframe,
8+
useAnimatedStyle,
9+
withTiming,
10+
} from 'react-native-reanimated';
11+
import {
12+
getAnimationState,
13+
getAnimationValueMergedConfig,
14+
getAnimationValueProperty,
15+
getStyleProperties,
16+
getStyleTransform,
17+
} from '../../helpers/utils/animation';
18+
import type {
19+
ToastEnteringExitingAnimation,
20+
ToastPlacement,
21+
ToastRootAnimation,
22+
} from './toast.types';
23+
24+
// --------------------------------------------------
25+
26+
export const enteringTop = FadeInUp.springify()
27+
.withInitialValues({
28+
opacity: 1,
29+
transform: [{ translateY: -100 }],
30+
})
31+
.mass(3);
32+
33+
export const exitingTop = new Keyframe({
34+
0: {
35+
opacity: 1,
36+
transform: [{ translateY: 0 }, { scale: 1 }],
37+
},
38+
100: {
39+
opacity: 0.5,
40+
transform: [{ translateY: -100 }, { scale: 0.97 }],
41+
easing: Easing.in(Easing.ease),
42+
},
43+
}).duration(200);
44+
45+
export const enteringBottom = FadeInDown.springify()
46+
.withInitialValues({
47+
opacity: 1,
48+
transform: [{ translateY: 100 }],
49+
})
50+
.mass(3);
51+
52+
export const exitingBottom = new Keyframe({
53+
0: {
54+
opacity: 1,
55+
transform: [{ translateY: 0 }, { scale: 1 }],
56+
},
57+
100: {
58+
opacity: 0.5,
59+
transform: [{ translateY: 100 }, { scale: 0.97 }],
60+
easing: Easing.in(Easing.ease),
61+
},
62+
}).duration(200);
63+
64+
// --------------------------------------------------
65+
66+
/**
67+
* Animation hook for Toast root component
68+
* Handles opacity, translateY, and scale animations based on toast index and placement
69+
*/
70+
export function useToastRootAnimation(options: {
71+
animation: ToastRootAnimation | undefined;
72+
style: ViewStyle | undefined;
73+
index: number;
74+
total: number;
75+
placement: ToastPlacement;
76+
}) {
77+
const { animation, style, index, total, placement } = options;
78+
79+
const { animationConfig, isAnimationDisabled } = getAnimationState(animation);
80+
81+
// Entering animation
82+
const enteringTopValue = getAnimationValueProperty({
83+
animationValue: animationConfig?.entering,
84+
property: 'top',
85+
defaultValue: enteringTop as ToastEnteringExitingAnimation,
86+
});
87+
88+
const enteringBottomValue = getAnimationValueProperty({
89+
animationValue: animationConfig?.entering,
90+
property: 'bottom',
91+
defaultValue: enteringBottom as ToastEnteringExitingAnimation,
92+
});
93+
94+
// Exiting animation
95+
const exitingTopValue = getAnimationValueProperty({
96+
animationValue: animationConfig?.exiting,
97+
property: 'top',
98+
defaultValue: exitingTop as ToastEnteringExitingAnimation,
99+
});
100+
101+
const exitingBottomValue = getAnimationValueProperty({
102+
animationValue: animationConfig?.exiting,
103+
property: 'bottom',
104+
defaultValue: exitingBottom as ToastEnteringExitingAnimation,
105+
});
106+
107+
// Opacity animation
108+
const opacityValue = getAnimationValueProperty({
109+
animationValue: animationConfig?.opacity,
110+
property: 'value',
111+
defaultValue: [1, 0] as [number, number],
112+
});
113+
114+
const opacityTimingConfig = getAnimationValueMergedConfig({
115+
animationValue: animationConfig?.opacity,
116+
property: 'timingConfig',
117+
defaultValue: { duration: 300 },
118+
});
119+
120+
// TranslateY animation
121+
const translateYValue = getAnimationValueProperty({
122+
animationValue: animationConfig?.translateY,
123+
property: 'value',
124+
defaultValue: [0, 10] as [number, number],
125+
});
126+
127+
const translateYTimingConfig = getAnimationValueMergedConfig({
128+
animationValue: animationConfig?.translateY,
129+
property: 'timingConfig',
130+
defaultValue: { duration: 300 },
131+
});
132+
133+
// Scale animation
134+
const scaleValue = getAnimationValueProperty({
135+
animationValue: animationConfig?.scale,
136+
property: 'value',
137+
defaultValue: [1, 0.97] as [number, number],
138+
});
139+
140+
const scaleTimingConfig = getAnimationValueMergedConfig({
141+
animationValue: animationConfig?.scale,
142+
property: 'timingConfig',
143+
defaultValue: { duration: 300 },
144+
});
145+
146+
// Extract style overrides OUTSIDE useAnimatedStyle
147+
const styleProps = getStyleProperties(style, ['opacity']);
148+
const styleTransform = getStyleTransform(style);
149+
150+
const rContainerStyle = useAnimatedStyle(() => {
151+
const sign = placement === 'top' ? 1 : -1;
152+
153+
const inputRange = [total - 1, total - 2];
154+
const opacityInputRange = [total - 3, total - 4];
155+
156+
const opacity = interpolate(index, opacityInputRange, opacityValue);
157+
const translateY = interpolate(index, inputRange, [
158+
translateYValue[0],
159+
translateYValue[1] * sign,
160+
]);
161+
const scale = interpolate(index, inputRange, scaleValue);
162+
163+
if (isAnimationDisabled) {
164+
return {
165+
pointerEvents: opacity === 0 ? 'none' : 'auto',
166+
opacity,
167+
transform: [
168+
{
169+
translateY,
170+
},
171+
{
172+
scale,
173+
},
174+
...styleTransform,
175+
],
176+
...styleProps,
177+
};
178+
}
179+
180+
return {
181+
pointerEvents: opacity === 0 ? 'none' : 'auto',
182+
opacity: withTiming(opacity, opacityTimingConfig),
183+
transform: [
184+
{
185+
translateY: withTiming(translateY, translateYTimingConfig),
186+
},
187+
{
188+
scale: withTiming(scale, scaleTimingConfig),
189+
},
190+
...styleTransform,
191+
],
192+
...styleProps,
193+
};
194+
});
195+
196+
// Determine entering and exiting animations based on placement
197+
const enteringAnimation =
198+
placement === 'top' ? enteringTopValue : enteringBottomValue;
199+
const exitingAnimation =
200+
placement === 'top' ? exitingTopValue : exitingBottomValue;
201+
202+
return {
203+
rContainerStyle,
204+
isAnimationDisabled,
205+
entering: isAnimationDisabled ? undefined : enteringAnimation,
206+
exiting: isAnimationDisabled ? undefined : exitingAnimation,
207+
};
208+
}

src/components/toast/toast.styles.ts

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,7 @@ import { tv } from 'tailwind-variants';
33
import { combineStyles } from '../../helpers/theme/utils/combine-styles';
44

55
const root = tv({
6-
base: 'absolute left-0 right-0 rounded-3xl p-4 bg-surface border border-muted/10 shadow-2xl shadow-black/5',
7-
variants: {
8-
placement: {
9-
top: 'top-0',
10-
bottom: 'bottom-0',
11-
},
12-
},
13-
defaultVariants: {
14-
placement: 'top',
15-
},
6+
base: 'rounded-3xl p-4 bg-surface border border-muted/10 shadow-2xl shadow-black/5',
167
});
178

189
const label = tv({

0 commit comments

Comments
 (0)