Skip to content

Instantly share code, notes, and snippets.

@vilindberg
Created November 22, 2019 10:15
Show Gist options
  • Save vilindberg/66f7483744cf99007475d560886116ab to your computer and use it in GitHub Desktop.
Save vilindberg/66f7483744cf99007475d560886116ab to your computer and use it in GitHub Desktop.
import React, { forwardRef, useEffect, useRef, useState } from 'react'
import { Animated, View } from 'react-native'
import styled from 'styled-components/native'
import { almostBlack, offWhite } from '../../colors'
import { baseText } from '../../styles'
import { measure } from '../../utils/view.util'
import { Touchable } from '../touchable'
type ItemProps = {
text: string
onSelect: (text: string) => void
}
const MultiToggleItem = forwardRef<View, ItemProps>(({ onSelect, text }: ItemProps, itemRef) => {
return (
<Touchable style={{ zIndex: 1, justifyContent: 'center' }} onPress={() => onSelect(text)}>
<ItemWrapper ref={itemRef}>
<Label>{text}</Label>
</ItemWrapper>
</Touchable>
)
})
type Props = {
style?: object
}
const items = ['3 mån', 'YTD', 'Start']
export const MultiToggle = ({ ...props }: Props) => {
const [selected, setSelected] = useState('3 mån')
const [startedMounting, setStartedMounting] = useState(false)
const [isMounted, setIsMounted] = useState(false)
const widthRef = useRef(new Animated.Value(0))
const translateRef = useRef(new Animated.Value(4))
const opacityRef = useRef(new Animated.Value(0))
const references = useRef<React.RefObject<View>>([] as any)
useEffect(() => {
setTimeout(() => {
setStartedMounting(true)
}, 300)
setTimeout(() => {
setIsMounted(true)
}, 400)
}, [])
useEffect(() => {
if (!startedMounting) return
Animated.timing(opacityRef.current, {
toValue: 1,
duration: 100
}).start()
}, [startedMounting])
useEffect(() => {
if (!isMounted) return
const asyncFn = async () => {
const selectedIndex = items.findIndex(i => i === selected)
const itemsBefore = items.slice(0, selectedIndex)
const selectedItemRef = references.current[selectedIndex]
const selectedItemMeasurements = await measure(selectedItemRef.current)
const selectedItemWidth = selectedItemMeasurements.width
const startPosition = await itemsBefore.reduce(async (tutti, _, idx) => {
const itemMeasurements = await measure(references.current[idx].current)
const itemWidth = itemMeasurements.width
const previous = await tutti
return Promise.resolve(previous + itemWidth + 4)
}, Promise.resolve(4))
Animated.timing(widthRef.current, {
toValue: selectedItemWidth - 4,
duration: 200
}).start()
Animated.timing(translateRef.current, {
toValue: startPosition,
duration: 200
}).start()
}
asyncFn()
}, [selected, isMounted])
function getOrCreateRef(id: number): React.RefObject<View> {
if (!references.current.hasOwnProperty(id)) {
references.current[id] = React.createRef<View>()
}
return references.current[id]
}
return (
<Wrapper style={{ opacity: opacityRef.current }} {...props}>
{items.map((item, index) => (
<MultiToggleItem ref={getOrCreateRef(index)} onSelect={() => setSelected(item)} text={item} />
))}
<Overlay style={{ width: widthRef.current, transform: [{ translateX: translateRef.current }] }} />
</Wrapper>
)
}
const Overlay = styled(Animated.View)`
position: absolute;
background-color: ${offWhite};
border-radius: 4px;
height: 24px;
width: 40px;
top: 4px;
shadow-opacity: 0.25;
shadow-radius: 3.84px;
shadow-color: ${almostBlack};
shadow-offset: 0px 2px;
elevation: 5;
`
const Wrapper = styled(Animated.View)`
background-color: rgba(17, 21, 28, 0.05);
height: 32px;
flex-direction: row;
border-radius: 4px;
`
const ItemWrapper = styled.View`
border-radius: 4px;
padding: 4px 8px;
margin: 0 2px;
`
const Label = styled.Text`
${baseText};
text-align: center;
`
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment