diff options
Diffstat (limited to 'src/components/ProgressGuide/Toast.tsx')
-rw-r--r-- | src/components/ProgressGuide/Toast.tsx | 169 |
1 files changed, 169 insertions, 0 deletions
diff --git a/src/components/ProgressGuide/Toast.tsx b/src/components/ProgressGuide/Toast.tsx new file mode 100644 index 000000000..346312af5 --- /dev/null +++ b/src/components/ProgressGuide/Toast.tsx @@ -0,0 +1,169 @@ +import React, {useImperativeHandle} from 'react' +import {Pressable, useWindowDimensions, View} from 'react-native' +import Animated, { + Easing, + runOnJS, + useAnimatedStyle, + useSharedValue, + withTiming, +} from 'react-native-reanimated' +import {useSafeAreaInsets} from 'react-native-safe-area-context' +import {msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {isWeb} from '#/platform/detection' +import {atoms as a, useTheme} from '#/alf' +import {Portal} from '#/components/Portal' +import {AnimatedCheck, AnimatedCheckRef} from '../anim/AnimatedCheck' +import {Text} from '../Typography' + +export interface ProgressGuideToastRef { + open(): void + close(): void +} + +export interface ProgressGuideToastProps { + title: string + subtitle?: string + visibleDuration?: number // default 5s +} + +export const ProgressGuideToast = React.forwardRef< + ProgressGuideToastRef, + ProgressGuideToastProps +>(function ProgressGuideToast({title, subtitle, visibleDuration}, ref) { + const t = useTheme() + const {_} = useLingui() + const insets = useSafeAreaInsets() + const [isOpen, setIsOpen] = React.useState(false) + const translateY = useSharedValue(0) + const opacity = useSharedValue(0) + const animatedCheckRef = React.useRef<AnimatedCheckRef | null>(null) + const timeoutRef = React.useRef<NodeJS.Timeout | undefined>() + const winDim = useWindowDimensions() + + /** + * Methods + */ + + const close = React.useCallback(() => { + // clear the timeout, in case this was called imperatively + if (timeoutRef.current) { + clearTimeout(timeoutRef.current) + timeoutRef.current = undefined + } + + // animate the opacity then set isOpen to false when done + const setIsntOpen = () => setIsOpen(false) + opacity.value = withTiming( + 0, + { + duration: 400, + easing: Easing.out(Easing.cubic), + }, + () => runOnJS(setIsntOpen)(), + ) + }, [setIsOpen, opacity]) + + const open = React.useCallback(() => { + // set isOpen=true to render + setIsOpen(true) + + // animate the vertical translation, the opacity, and the checkmark + const playCheckmark = () => animatedCheckRef.current?.play() + opacity.value = 0 + opacity.value = withTiming( + 1, + { + duration: 100, + easing: Easing.out(Easing.cubic), + }, + () => runOnJS(playCheckmark)(), + ) + translateY.value = 0 + translateY.value = withTiming(insets.top + 10, { + duration: 500, + easing: Easing.out(Easing.cubic), + }) + + // start the countdown timer to autoclose + timeoutRef.current = setTimeout(close, visibleDuration || 5e3) + }, [setIsOpen, translateY, opacity, insets, close, visibleDuration]) + + useImperativeHandle( + ref, + () => ({ + open, + close, + }), + [open, close], + ) + + const containerStyle = React.useMemo(() => { + let left = 10 + let right = 10 + if (isWeb && winDim.width > 400) { + left = right = (winDim.width - 380) / 2 + } + return { + position: isWeb ? 'fixed' : 'absolute', + top: 0, + left, + right, + } + }, [winDim.width]) + + const animatedStyle = useAnimatedStyle(() => ({ + transform: [{translateY: translateY.value}], + opacity: opacity.value, + })) + + return ( + isOpen && ( + <Portal> + <Animated.View + style={[ + // @ts-ignore position: fixed is web only + containerStyle, + animatedStyle, + ]}> + <Pressable + style={[ + t.atoms.bg, + a.flex_row, + a.align_center, + a.gap_md, + a.border, + t.atoms.border_contrast_high, + a.rounded_md, + a.px_lg, + a.py_md, + a.shadow_sm, + { + shadowRadius: 8, + shadowOpacity: 0.1, + shadowOffset: {width: 0, height: 2}, + elevation: 8, + }, + ]} + onPress={close} + accessibilityLabel={_(msg`Tap to dismiss`)} + accessibilityHint=""> + <AnimatedCheck + fill={t.palette.primary_500} + ref={animatedCheckRef} + /> + <View> + <Text style={[a.text_md, a.font_semibold]}>{title}</Text> + {subtitle && ( + <Text style={[a.text_sm, t.atoms.text_contrast_medium]}> + {subtitle} + </Text> + )} + </View> + </Pressable> + </Animated.View> + </Portal> + ) + ) +}) |