From ff8c0f01c5053debf84fd82a1f0bf27ece2b6239 Mon Sep 17 00:00:00 2001 From: JungHwan Jang Date: Mon, 25 Nov 2024 14:04:17 -0500 Subject: [PATCH] CP-9492: PageControl component (#2110) --- .../PageControl/PageControl.stories.tsx | 70 +++++++++ .../components/PageControl/PageControl.tsx | 145 ++++++++++++++++++ .../components/PinInput/PinInput.stories.tsx | 2 +- 3 files changed, 216 insertions(+), 1 deletion(-) create mode 100644 packages/k2-alpine/src/components/PageControl/PageControl.stories.tsx create mode 100644 packages/k2-alpine/src/components/PageControl/PageControl.tsx diff --git a/packages/k2-alpine/src/components/PageControl/PageControl.stories.tsx b/packages/k2-alpine/src/components/PageControl/PageControl.stories.tsx new file mode 100644 index 000000000..87beffe81 --- /dev/null +++ b/packages/k2-alpine/src/components/PageControl/PageControl.stories.tsx @@ -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 ( + + + {NUMBER_OF_PAGES.map((numberOfPage, index) => ( + + + + ))} + + + ) +} + +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 ( + <> + Page Size: {numberOfPage} + + + + + + + ) +} diff --git a/packages/k2-alpine/src/components/PageControl/PageControl.tsx b/packages/k2-alpine/src/components/PageControl/PageControl.tsx new file mode 100644 index 000000000..77b3e8d45 --- /dev/null +++ b/packages/k2-alpine/src/components/PageControl/PageControl.tsx @@ -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 ( + + + + {Array.from({ length: numberOfPage }).map((_, index) => { + return + })} + + + + ) +} + +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 ( + + ) +} + +const Configuration = { + gap: 5, + dot: { + width: 7, + height: 7, + selectedWidth: 17, + animation: { + delay: 0, + duration: 300 + } + }, + maxDotsInViewPort: 5, + translationAnimation: { + duration: 200 + } +} diff --git a/packages/k2-alpine/src/components/PinInput/PinInput.stories.tsx b/packages/k2-alpine/src/components/PinInput/PinInput.stories.tsx index d8a47b2cb..f54ecaa45 100644 --- a/packages/k2-alpine/src/components/PinInput/PinInput.stories.tsx +++ b/packages/k2-alpine/src/components/PinInput/PinInput.stories.tsx @@ -69,7 +69,7 @@ export const All = (): JSX.Element => { marginBottom: 20, justifyContent: 'space-between' }}> - + Your Pincode: {PIN_CODE}