Add web animations
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
import {useCallback, useEffect, useMemo, useState} from 'react'
|
||||
import {AccessibilityInfo, Pressable, View} from 'react-native'
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||
import {Motion} from '@legendapp/motion'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {nanoid} from 'nanoid/non-secure'
|
||||
@@ -23,6 +24,9 @@ import {
|
||||
export {useToast} from '#/components/Toast/Context'
|
||||
export {Outlet} from '#/components/Toast/Portal'
|
||||
|
||||
const ANIMATION_DURATION = 300
|
||||
const TOAST_GAP = 8 // Gap between toasts
|
||||
|
||||
const TOAST_ANIMATION_STYLES = {
|
||||
entering: {
|
||||
animation: 'toastFadeIn 0.3s ease-out forwards',
|
||||
@@ -48,22 +52,54 @@ export function ToastProvider({children}: {children: React.ReactNode}) {
|
||||
const [toasts, setToasts] = useState<
|
||||
{
|
||||
id: string
|
||||
toast: ToastProps
|
||||
timeout: NodeJS.Timeout
|
||||
props: ToastProps
|
||||
dimensions: {
|
||||
width: number | undefined
|
||||
height: number | undefined
|
||||
}
|
||||
setDimensions: (width: number, height: number) => void
|
||||
exiting: boolean
|
||||
timers: NodeJS.Timeout[]
|
||||
}[]
|
||||
>([])
|
||||
|
||||
const show = useCallback<ToastApi['show']>(props => {
|
||||
setToasts(prevToasts => {
|
||||
const id = nanoid()
|
||||
const duration = props.duration || DEFAULT_TOAST_DURATION
|
||||
return [
|
||||
...prevToasts,
|
||||
{
|
||||
id,
|
||||
toast: props,
|
||||
timeout: setTimeout(() => {
|
||||
setToasts(currentToasts => currentToasts.filter(t => t.id !== id))
|
||||
}, props.duration || DEFAULT_TOAST_DURATION),
|
||||
props,
|
||||
dimensions: {width: undefined, height: undefined},
|
||||
exiting: false,
|
||||
setDimensions: (width: number, height: number) => {
|
||||
setToasts(currentToasts =>
|
||||
currentToasts.map(t => {
|
||||
if (t.id === id) {
|
||||
t.dimensions.width = width
|
||||
t.dimensions.height = height
|
||||
}
|
||||
return t
|
||||
}),
|
||||
)
|
||||
},
|
||||
timers: [
|
||||
setTimeout(() => {
|
||||
setToasts(currentToasts =>
|
||||
currentToasts.map(t => {
|
||||
if (t.id === id) {
|
||||
t.exiting = true
|
||||
}
|
||||
return t
|
||||
}),
|
||||
)
|
||||
}, duration),
|
||||
setTimeout(() => {
|
||||
setToasts(currentToasts => currentToasts.filter(t => t.id !== id))
|
||||
}, duration + ANIMATION_DURATION),
|
||||
],
|
||||
},
|
||||
]
|
||||
})
|
||||
@@ -72,8 +108,8 @@ export function ToastProvider({children}: {children: React.ReactNode}) {
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
setToasts(toasts => {
|
||||
toasts.map(({timeout}) => {
|
||||
clearTimeout(timeout)
|
||||
toasts.map(({timers}) => {
|
||||
timers.map(clearTimeout)
|
||||
})
|
||||
return toasts
|
||||
})
|
||||
@@ -97,7 +133,6 @@ export function ToastProvider({children}: {children: React.ReactNode}) {
|
||||
<View
|
||||
style={[
|
||||
a.fixed,
|
||||
a.gap_sm,
|
||||
{
|
||||
flexDirection: 'column-reverse',
|
||||
left: a.px_xl.paddingLeft,
|
||||
@@ -110,9 +145,68 @@ export function ToastProvider({children}: {children: React.ReactNode}) {
|
||||
},
|
||||
],
|
||||
]}>
|
||||
{toasts.map(({id, toast}) => (
|
||||
<Toast key={id} content={toast.content} type={toast.type} />
|
||||
))}
|
||||
{toasts.map(toast => {
|
||||
const isReady = !!toast.dimensions.width
|
||||
const toastComponent = (
|
||||
<Toast
|
||||
content={toast.props.content}
|
||||
type={toast.props.type}
|
||||
/>
|
||||
)
|
||||
|
||||
if (!isReady) {
|
||||
return (
|
||||
<View
|
||||
key={toast.id}
|
||||
style={[a.absolute, a.inset_0, {opacity: 0}]}>
|
||||
<View
|
||||
style={[a.justify_end, {paddingTop: TOAST_GAP}]}
|
||||
onLayout={e => {
|
||||
if (toast.dimensions.width) return
|
||||
toast.setDimensions(
|
||||
e.nativeEvent.layout.width,
|
||||
e.nativeEvent.layout.height,
|
||||
)
|
||||
}}>
|
||||
{toastComponent}
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Motion.View
|
||||
key={toast.id}
|
||||
initial={{opacity: 0, y: 20}}
|
||||
animate={{
|
||||
y: 0,
|
||||
opacity: toast.exiting ? 0 : 1,
|
||||
height: toast.exiting ? 0 : toast.dimensions.height,
|
||||
}}
|
||||
transition={{
|
||||
type: 'timing',
|
||||
easing: 'circOut',
|
||||
duration: ANIMATION_DURATION,
|
||||
}}>
|
||||
<View
|
||||
key={toast.id}
|
||||
style={[
|
||||
a.justify_end,
|
||||
{
|
||||
position: toast.exiting ? 'absolute' : 'relative',
|
||||
width: toast.dimensions.width,
|
||||
height: toast.dimensions.height,
|
||||
paddingTop: TOAST_GAP,
|
||||
},
|
||||
]}>
|
||||
<Toast
|
||||
content={toast.props.content}
|
||||
type={toast.props.type}
|
||||
/>
|
||||
</View>
|
||||
</Motion.View>
|
||||
)
|
||||
})}
|
||||
</View>
|
||||
</Portal.Portal>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user