-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
CP-9492: PageControl component (#2110)
- Loading branch information
Showing
3 changed files
with
216 additions
and
1 deletion.
There are no files selected for viewing
70 changes: 70 additions & 0 deletions
70
packages/k2-alpine/src/components/PageControl/PageControl.stories.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
145
packages/k2-alpine/src/components/PageControl/PageControl.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters