digitalfabrik/integreat-app

View on GitHub
native/src/components/SnackbarContainer.tsx

Summary

Maintainability
A
0 mins
Test Coverage
A
100%
import React, { createContext, ReactElement, useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Animated, View, LayoutChangeEvent } from 'react-native'
import styled from 'styled-components/native'

import Snackbar, { SnackbarActionType } from '../components/Snackbar'

const Container = styled(View)`
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
`

// https://github.com/styled-components/styled-components/issues/892
const AnimatedContainer = Animated.createAnimatedComponent(Container)
const ANIMATION_DURATION = 300
const DEFAULT_SHOW_DURATION = 5000
const MAX_HEIGHT = 9999
const translate = new Animated.Value(1)

export type SnackbarType = {
  text: string
  positiveAction?: SnackbarActionType
  negativeAction?: SnackbarActionType
  showDuration?: number
}

type SnackbarContextType = (snackbar: SnackbarType) => void
export const SnackbarContext = createContext<SnackbarContextType>(() => undefined)

type SnackbarContainerProps = {
  children: ReactElement
}

const SnackbarContainer = ({ children }: SnackbarContainerProps): ReactElement | null => {
  const [height, setHeight] = useState<number | null>(null)
  const [enqueuedSnackbars, setEnqueuedSnackbars] = useState<SnackbarType[]>([])
  const displayedSnackbar = enqueuedSnackbars[0]
  const { t } = useTranslation('error')

  const enqueueSnackbar = useCallback((snackbar: SnackbarType) => {
    // Don't show same snackbar multiple times
    setEnqueuedSnackbars(snackbars => (snackbars[0]?.text !== snackbar.text ? [...snackbars, snackbar] : snackbars))
  }, [])

  const show = useCallback(() => {
    Animated.timing(translate, {
      toValue: 0,
      duration: ANIMATION_DURATION,
      useNativeDriver: true,
    }).start()
  }, [])

  const hide = useCallback(() => {
    Animated.timing(translate, {
      toValue: 1,
      duration: ANIMATION_DURATION,
      useNativeDriver: true,
    }).start(() => setEnqueuedSnackbars(snackbars => snackbars.slice(1)))
  }, [])

  useEffect(() => {
    if (displayedSnackbar) {
      show()
      const timeout = setTimeout(hide, displayedSnackbar.showDuration ?? DEFAULT_SHOW_DURATION)
      return () => clearTimeout(timeout)
    }
    return () => undefined
  }, [displayedSnackbar, hide, show])

  const onLayout = (event: LayoutChangeEvent) => setHeight(event.nativeEvent.layout.height)

  const outputRange: number[] = [0, height ?? MAX_HEIGHT]
  const interpolated = translate.interpolate({
    inputRange: [0, 1],
    outputRange,
  })

  return (
    <SnackbarContext.Provider value={enqueueSnackbar}>
      {children}
      {displayedSnackbar ? (
        <AnimatedContainer
          onLayout={onLayout}
          style={{
            transform: [
              {
                translateY: interpolated,
              },
            ],
          }}>
          <Snackbar
            text={t(displayedSnackbar.text)}
            positiveAction={displayedSnackbar.positiveAction}
            negativeAction={displayedSnackbar.negativeAction}
          />
        </AnimatedContainer>
      ) : null}
    </SnackbarContext.Provider>
  )
}

export default SnackbarContainer