digitalfabrik/integreat-app

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

Summary

Maintainability
A
0 mins
Test Coverage
F
0%
import React, { JSXElementConstructor, ReactElement, useRef, useState } from 'react'
import { Dimensions, NativeScrollEvent, NativeSyntheticEvent, ScrollView } from 'react-native'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
import { SvgProps } from 'react-native-svg'
import styled from 'styled-components/native'

import { TileModel } from 'shared'

import { ArrowBackIcon } from '../assets'
import HighlightBox from './HighlightBox'
import NavigationTile from './NavigationTile'
import Icon from './base/Icon'
import Pressable from './base/Pressable'

const widthBreakPoint = 400
const anchorWidth = 30
const wideScreenItemsCount = 4
const smallScreenItemsCount = 3
const scrolledToEndThreshold = 0.95

const TilesRow = styled(HighlightBox)`
  display: flex;
  flex-direction: row;
  align-items: center;
  justify-content: space-around;
  shadow-offset: 1px;
`

const StyledPressable = styled(Pressable)`
  padding: 0 4px;
`

const StyledIcon = styled(Icon)<{ disabled: boolean }>`
  color: ${props => (props.disabled ? props.theme.colors.textDecorationColor : props.theme.colors.textColor)};
`

type NavigationTilesProps = {
  tiles: TileModel<JSXElementConstructor<SvgProps>>[]
}

const NavigationTiles = ({ tiles }: NavigationTilesProps): ReactElement => {
  const { left, right } = useSafeAreaInsets()
  const { width } = Dimensions.get('screen')
  const layoutWidth = left && right ? width - (left + right) : width
  const isWideScreen = layoutWidth >= widthBreakPoint
  const scrollViewWidth: number = layoutWidth - anchorWidth * 2
  const navigationItemWidth = isWideScreen
    ? scrollViewWidth / wideScreenItemsCount
    : scrollViewWidth / smallScreenItemsCount
  const allTilesWidth = tiles.length * navigationItemWidth
  const isScrollable = allTilesWidth > layoutWidth

  const scrollViewRef = useRef<ScrollView>(null)
  const [percentageScrolled, setPercentageScrolled] = useState<number>(0)

  const handleScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
    const { contentSize, contentOffset, layoutMeasurement } = event.nativeEvent
    setPercentageScrolled(contentOffset.x / (contentSize.width - layoutMeasurement.width))
  }

  const scrolledToStart = percentageScrolled === 0
  const scrolledToEnd = percentageScrolled >= scrolledToEndThreshold

  const scrollToStart = () => scrollViewRef.current?.scrollTo({ x: 0, y: 0, animated: true })
  const scrollToEnd = () => scrollViewRef.current?.scrollToEnd({ animated: true })

  return (
    <TilesRow>
      {isScrollable && (
        <StyledPressable role='button' onPress={scrollToStart} aria-hidden>
          <StyledIcon Icon={ArrowBackIcon} disabled={scrolledToStart} directionDependent />
        </StyledPressable>
      )}
      <ScrollView
        horizontal
        ref={scrollViewRef}
        contentContainerStyle={{
          flexGrow: 1,
          justifyContent: 'space-around',
        }}
        style={{
          width: scrollViewWidth,
        }}
        showsHorizontalScrollIndicator={false}
        pagingEnabled
        scrollEnabled
        snapToInterval={navigationItemWidth}
        decelerationRate='fast'
        bounces={false}
        onScroll={handleScroll}
        scrollEventThrottle={16}>
        {tiles.map(tile => (
          <NavigationTile key={tile.path} tile={tile} width={navigationItemWidth} />
        ))}
      </ScrollView>
      {isScrollable && (
        <StyledPressable role='button' onPress={scrollToEnd} aria-hidden>
          <StyledIcon Icon={ArrowBackIcon} disabled={scrolledToEnd} directionDependent reverse />
        </StyledPressable>
      )}
    </TilesRow>
  )
}

export default NavigationTiles