@@ -6,11 +6,14 @@ import {
66 FlatList ,
77 ViewabilityConfigCallbackPairs ,
88 ViewToken ,
9+ NativeSyntheticEvent ,
10+ NativeScrollEvent ,
911} from "react-native" ;
1012
1113import { generateNumbers } from "../../utils/generateNumbers" ;
1214import { colorToRgba } from "../../utils/colorToRgba" ;
1315import { generateStyles } from "./TimerPicker.styles" ;
16+ import { getAdjustedLimit } from "../../utils/getAdjustedLimit" ;
1417
1518type LinearGradientPoint = {
1619 x : number ;
@@ -24,13 +27,19 @@ export type LinearGradientProps = React.ComponentProps<typeof View> & {
2427 end ?: LinearGradientPoint | null ;
2528} ;
2629
30+ export type LimitType = {
31+ max ?: number ;
32+ min ?: number ;
33+ } ;
34+
2735interface DurationScrollProps {
2836 numberOfItems : number ;
29- label ?: string ;
37+ label ?: string | React . ReactElement ;
3038 initialIndex ?: number ;
3139 onDurationChange : ( duration : number ) => void ;
3240 padNumbersWithZero ?: boolean ;
3341 disableInfiniteScroll ?: boolean ;
42+ limit ?: LimitType ;
3443 padWithNItems : number ;
3544 pickerGradientOverlayProps ?: LinearGradientProps ;
3645 // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -39,13 +48,17 @@ interface DurationScrollProps {
3948 styles : ReturnType < typeof generateStyles > ;
4049}
4150
51+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
52+ const KEY_EXTRACTOR = ( _ : any , index : number ) => index . toString ( ) ;
53+
4254const DurationScroll = ( {
4355 numberOfItems,
4456 label,
4557 initialIndex = 0 ,
4658 onDurationChange,
4759 padNumbersWithZero = false ,
4860 disableInfiniteScroll = false ,
61+ limit,
4962 padWithNItems,
5063 pickerGradientOverlayProps,
5164 LinearGradient,
@@ -63,13 +76,88 @@ const DurationScroll = ({
6376
6477 const numberOfItemsToShow = 1 + padWithNItems * 2 ;
6578
66- const renderItem = ( { item } : { item : string } ) => (
67- < View
68- key = { item }
69- style = { styles . pickerItemContainer }
70- testID = "picker-item" >
71- < Text style = { styles . pickerItem } > { item } </ Text >
72- </ View >
79+ const adjustedLimited = getAdjustedLimit ( limit , numberOfItems ) ;
80+
81+ const renderItem = useCallback (
82+ ( { item } : { item : string } ) => {
83+ const intItem = parseInt ( item ) ;
84+
85+ return (
86+ < View
87+ key = { item }
88+ style = { styles . pickerItemContainer }
89+ testID = "picker-item" >
90+ < Text
91+ style = { [
92+ styles . pickerItem ,
93+ intItem > adjustedLimited . max ||
94+ intItem < adjustedLimited . min
95+ ? styles . disabledPickerItem
96+ : { } ,
97+ ] } >
98+ { item }
99+ </ Text >
100+ </ View >
101+ ) ;
102+ } ,
103+ [
104+ adjustedLimited . max ,
105+ adjustedLimited . min ,
106+ styles . disabledPickerItem ,
107+ styles . pickerItem ,
108+ styles . pickerItemContainer ,
109+ ]
110+ ) ;
111+
112+ const onMomentumScrollEnd = useCallback (
113+ ( e : NativeSyntheticEvent < NativeScrollEvent > ) => {
114+ const newIndex = Math . round (
115+ e . nativeEvent . contentOffset . y /
116+ styles . pickerItemContainer . height
117+ ) ;
118+ let newDuration =
119+ ( disableInfiniteScroll ? newIndex : newIndex + padWithNItems ) %
120+ ( numberOfItems + 1 ) ;
121+
122+ // check limits
123+ if ( newDuration > adjustedLimited . max ) {
124+ const targetScrollIndex =
125+ newIndex - ( newDuration - adjustedLimited . max ) ;
126+ flatListRef . current ?. scrollToIndex ( {
127+ animated : true ,
128+ index :
129+ // guard against scrolling beyond end of list
130+ targetScrollIndex >= 0
131+ ? targetScrollIndex
132+ : adjustedLimited . max - 1 ,
133+ } ) ; // scroll down to max
134+ newDuration = adjustedLimited . max ;
135+ } else if ( newDuration < adjustedLimited . min ) {
136+ const targetScrollIndex =
137+ newIndex + ( adjustedLimited . min - newDuration ) ;
138+ flatListRef . current ?. scrollToIndex ( {
139+ animated : true ,
140+ index :
141+ // guard against scrolling beyond end of list
142+ targetScrollIndex <= data . length - 1
143+ ? targetScrollIndex
144+ : adjustedLimited . min ,
145+ } ) ; // scroll up to min
146+ newDuration = adjustedLimited . min ;
147+ }
148+
149+ onDurationChange ( newDuration ) ;
150+ } ,
151+ [
152+ adjustedLimited . max ,
153+ adjustedLimited . min ,
154+ data . length ,
155+ disableInfiniteScroll ,
156+ numberOfItems ,
157+ onDurationChange ,
158+ padWithNItems ,
159+ styles . pickerItemContainer . height ,
160+ ]
73161 ) ;
74162
75163 const onViewableItemsChanged = useCallback (
@@ -95,6 +183,15 @@ const DurationScroll = ({
95183 [ numberOfItems ]
96184 ) ;
97185
186+ const getItemLayout = useCallback (
187+ ( _ : ArrayLike < string > | null | undefined , index : number ) => ( {
188+ length : styles . pickerItemContainer . height ,
189+ offset : styles . pickerItemContainer . height * index ,
190+ index,
191+ } ) ,
192+ [ styles . pickerItemContainer . height ]
193+ ) ;
194+
98195 const viewabilityConfigCallbackPairs =
99196 useRef < ViewabilityConfigCallbackPairs > ( [
100197 {
@@ -113,11 +210,7 @@ const DurationScroll = ({
113210 < FlatList
114211 ref = { flatListRef }
115212 data = { data }
116- getItemLayout = { ( _ , index ) => ( {
117- length : styles . pickerItemContainer . height ,
118- offset : styles . pickerItemContainer . height * index ,
119- index,
120- } ) }
213+ getItemLayout = { getItemLayout }
121214 initialScrollIndex = {
122215 ( initialIndex % numberOfItems ) +
123216 numberOfItems +
@@ -126,7 +219,7 @@ const DurationScroll = ({
126219 }
127220 windowSize = { numberOfItemsToShow }
128221 renderItem = { renderItem }
129- keyExtractor = { ( _ , index ) => index . toString ( ) }
222+ keyExtractor = { KEY_EXTRACTOR }
130223 showsVerticalScrollIndicator = { false }
131224 decelerationRate = "fast"
132225 scrollEventThrottle = { 16 }
@@ -140,20 +233,15 @@ const DurationScroll = ({
140233 ? viewabilityConfigCallbackPairs ?. current
141234 : undefined
142235 }
143- onMomentumScrollEnd = { ( e ) => {
144- const newIndex = Math . round (
145- e . nativeEvent . contentOffset . y /
146- styles . pickerItemContainer . height
147- ) ;
148- onDurationChange (
149- ( disableInfiniteScroll ? newIndex : newIndex + padWithNItems ) %
150- ( numberOfItems + 1 )
151- ) ;
152- } }
236+ onMomentumScrollEnd = { onMomentumScrollEnd }
153237 testID = "duration-scroll-flatlist"
154238 />
155239 < View style = { styles . pickerLabelContainer } >
156- < Text style = { styles . pickerLabel } > { label } </ Text >
240+ { typeof label === "string" ? (
241+ < Text style = { styles . pickerLabel } > { label } </ Text >
242+ ) : (
243+ label ?? null
244+ ) }
157245 </ View >
158246 { LinearGradient ? (
159247 < >
@@ -193,4 +281,4 @@ const DurationScroll = ({
193281 ) ;
194282} ;
195283
196- export default DurationScroll ;
284+ export default React . memo ( DurationScroll ) ;
0 commit comments