Skip to content

Commit

Permalink
CP-9492: PageControl component (#2110)
Browse files Browse the repository at this point in the history
  • Loading branch information
onghwan authored Nov 25, 2024
1 parent d1f7f7f commit ff8c0f0
Show file tree
Hide file tree
Showing 3 changed files with 216 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import React, { useState } from 'react'
import { ScrollView, Text, View } from '../Primitives'
import { Button, useTheme } from '../..'
import { PageControl } from './PageControl'

export default {
title: 'PageControl'
}

export const All = (): JSX.Element => {
const { theme } = useTheme()

const NUMBER_OF_PAGES = [3, 5, 20]

return (
<ScrollView
style={{
width: '100%',
backgroundColor: theme.colors.$surfacePrimary
}}
contentContainerStyle={{ padding: 16 }}>
<View style={{ marginTop: 32, gap: 100 }}>
{NUMBER_OF_PAGES.map((numberOfPage, index) => (
<View key={index}>
<PageControlStory numberOfPage={numberOfPage} />
</View>
))}
</View>
</ScrollView>
)
}

const PageControlStory = ({
numberOfPage
}: {
numberOfPage: number
}): JSX.Element => {
const [currentPage, setCurrentPage] = useState(0)

const handlePressPrevious = (): void => {
setCurrentPage(prev => Math.max(prev - 1, 0))
}

const handlePressNext = (): void => {
setCurrentPage(prev => Math.min(prev + 1, numberOfPage - 1))
}

return (
<>
<Text sx={{ alignSelf: 'center' }}>Page Size: {numberOfPage}</Text>
<PageControl
style={{
marginTop: 20,
marginBottom: 20,
alignSelf: 'center'
}}
numberOfPage={numberOfPage}
currentPage={currentPage}
/>
<View sx={{ flexDirection: 'row', gap: 12, alignSelf: 'center' }}>
<Button type="primary" size="small" onPress={handlePressPrevious}>
{'<'}
</Button>
<Button type="primary" size="small" onPress={handlePressNext}>
{'>'}
</Button>
</View>
</>
)
}
145 changes: 145 additions & 0 deletions packages/k2-alpine/src/components/PageControl/PageControl.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import React, { useEffect, useState } from 'react'
import { ViewStyle, View } from 'react-native'
import Animated, {
runOnJS,
useAnimatedStyle,
useSharedValue,
withDelay,
withTiming
} from 'react-native-reanimated'
import { useTheme } from '../..'

export const PageControl = ({
numberOfPage,
currentPage,
style
}: {
numberOfPage: number
currentPage: number
style?: ViewStyle
}): JSX.Element => {
const translationAnimation = useSharedValue(0)
const viewPortWidth =
(Configuration.dot.width + Configuration.gap) *
(Configuration.maxDotsInViewPort - 1) +
Configuration.dot.selectedWidth
const [translatedX, setTranslatedX] = useState(0)

useEffect(() => {
const currentOffset =
currentPage * (Configuration.dot.width + Configuration.gap)
const shouldTranslateLeft =
currentOffset + Configuration.dot.selectedWidth >
viewPortWidth - translatedX
const shouldTranslateRight = currentOffset < -translatedX
const targetTranslation = shouldTranslateLeft
? translatedX - Configuration.dot.width - Configuration.gap
: shouldTranslateRight
? translatedX + Configuration.dot.width + Configuration.gap
: translatedX

translationAnimation.value =
shouldTranslateLeft || shouldTranslateRight
? withTiming(
targetTranslation,
{ duration: Configuration.translationAnimation.duration },
() => {
runOnJS(setTranslatedX)(targetTranslation)
}
)
: targetTranslation
}, [currentPage, translationAnimation, viewPortWidth, translatedX])

const translationAnimatedStyle = useAnimatedStyle(() => {
return {
transform: [
{
translateX: translationAnimation.value
}
]
}
})

return (
<View
style={[
{
padding: 0,
overflow: 'hidden'
},
style
]}>
<View
style={[
{
marginHorizontal: Configuration.gap + Configuration.dot.width,
maxWidth: viewPortWidth,
justifyContent: 'center'
}
]}>
<Animated.View
style={[
{
gap: Configuration.gap,
flexDirection: 'row'
},
translationAnimatedStyle
]}>
{Array.from({ length: numberOfPage }).map((_, index) => {
return <AnimatedDot key={index} selected={index === currentPage} />
})}
</Animated.View>
</View>
</View>
)
}

const AnimatedDot = ({ selected }: { selected: boolean }): JSX.Element => {
const { theme } = useTheme()

const animatedStyle = useAnimatedStyle(() => {
return {
width: withDelay(
Configuration.dot.animation.delay,
withTiming(
selected ? Configuration.dot.selectedWidth : Configuration.dot.width,
{ duration: Configuration.dot.animation.duration }
)
),
opacity: withTiming(selected ? 1 : 0.4, {
duration: Configuration.dot.animation.duration
})
}
})

return (
<Animated.View
style={[
{
width: Configuration.dot.width,
height: Configuration.dot.height,
borderRadius: 8,
backgroundColor: theme.colors.$textPrimary
},
animatedStyle
]}
/>
)
}

const Configuration = {
gap: 5,
dot: {
width: 7,
height: 7,
selectedWidth: 17,
animation: {
delay: 0,
duration: 300
}
},
maxDotsInViewPort: 5,
translationAnimation: {
duration: 200
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export const All = (): JSX.Element => {
marginBottom: 20,
justifyContent: 'space-between'
}}>
<View sx={{}}>
<View>
<Text>Your Pincode: {PIN_CODE}</Text>
</View>
<View
Expand Down

0 comments on commit ff8c0f0

Please sign in to comment.