Add ProgressGuide Toast component
This commit is contained in:
@@ -0,0 +1,150 @@
|
|||||||
|
import React, {useImperativeHandle} from 'react'
|
||||||
|
import {Keyboard, Pressable, 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 {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>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 animatedStyle = useAnimatedStyle(() => ({
|
||||||
|
transform: [{translateY: translateY.value}],
|
||||||
|
opacity: opacity.value,
|
||||||
|
}))
|
||||||
|
|
||||||
|
return (
|
||||||
|
isOpen && (
|
||||||
|
<Portal>
|
||||||
|
<View
|
||||||
|
// iOS
|
||||||
|
accessibilityViewIsModal
|
||||||
|
// Android
|
||||||
|
importantForAccessibility="yes"
|
||||||
|
style={[a.absolute, a.inset_0]}
|
||||||
|
onTouchMove={() => Keyboard.dismiss()}>
|
||||||
|
<Animated.View
|
||||||
|
style={[a.absolute, {top: 0, left: 10, right: 10}, 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,
|
||||||
|
]}
|
||||||
|
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>
|
||||||
|
</View>
|
||||||
|
</Portal>
|
||||||
|
)
|
||||||
|
)
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user