about summary refs log tree commit diff
path: root/src/components/Toast/index.web.tsx
blob: f2517e28dacd89bb499ea48b6115c2c2f79977e7 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
/*
 * Note: relies on styles in #/styles.css
 */

import {useEffect, useState} from 'react'
import {AccessibilityInfo, Pressable, View} from 'react-native'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'

import {atoms as a, useBreakpoints} from '#/alf'
import {DEFAULT_TOAST_DURATION} from '#/components/Toast/const'
import {Toast} from '#/components/Toast/Toast'
import {type ToastApi, type ToastType} from '#/components/Toast/types'

const TOAST_ANIMATION_STYLES = {
  entering: {
    animation: 'toastFadeIn 0.3s ease-out forwards',
  },
  exiting: {
    animation: 'toastFadeOut 0.2s ease-in forwards',
  },
}

interface ActiveToast {
  type: ToastType
  content: React.ReactNode
  a11yLabel: string
}
type GlobalSetActiveToast = (_activeToast: ActiveToast | undefined) => void
let globalSetActiveToast: GlobalSetActiveToast | undefined
let toastTimeout: NodeJS.Timeout | undefined
type ToastContainerProps = {}

export const ToastContainer: React.FC<ToastContainerProps> = ({}) => {
  const {_} = useLingui()
  const {gtPhone} = useBreakpoints()
  const [activeToast, setActiveToast] = useState<ActiveToast | undefined>()
  const [isExiting, setIsExiting] = useState(false)

  useEffect(() => {
    globalSetActiveToast = (t: ActiveToast | undefined) => {
      if (!t && activeToast) {
        setIsExiting(true)
        setTimeout(() => {
          setActiveToast(t)
          setIsExiting(false)
        }, 200)
      } else {
        if (t) {
          AccessibilityInfo.announceForAccessibility(t.a11yLabel)
        }
        setActiveToast(t)
        setIsExiting(false)
      }
    }
  }, [activeToast])

  return (
    <>
      {activeToast && (
        <View
          style={[
            a.fixed,
            {
              left: a.px_xl.paddingLeft,
              right: a.px_xl.paddingLeft,
              bottom: a.px_xl.paddingLeft,
              ...(isExiting
                ? TOAST_ANIMATION_STYLES.exiting
                : TOAST_ANIMATION_STYLES.entering),
            },
            gtPhone && [
              {
                maxWidth: 380,
              },
            ],
          ]}>
          <Toast content={activeToast.content} type={activeToast.type} />
          <Pressable
            style={[a.absolute, a.inset_0]}
            accessibilityLabel={_(
              msg({
                message: `Dismiss message`,
                comment: `Accessibility label for dismissing a toast notification`,
              }),
            )}
            accessibilityHint=""
            onPress={() => setActiveToast(undefined)}
          />
        </View>
      )}
    </>
  )
}

export const toast: ToastApi = {
  show(props) {
    if (toastTimeout) {
      clearTimeout(toastTimeout)
    }

    globalSetActiveToast?.({
      type: props.type,
      content: props.content,
      a11yLabel: props.a11yLabel,
    })

    toastTimeout = setTimeout(() => {
      globalSetActiveToast?.(undefined)
    }, props.duration || DEFAULT_TOAST_DURATION)
  },
}