1
1
import _ from 'lodash' ;
2
2
import React , { useRef , useCallback , useEffect } from 'react' ;
3
3
import { StyleSheet , StyleProp , ViewStyle , TextStyle , LayoutChangeEvent } from 'react-native' ;
4
- import Reanimated , {
4
+ import {
5
5
Easing ,
6
6
useAnimatedReaction ,
7
7
useAnimatedStyle ,
8
8
useSharedValue ,
9
9
withTiming ,
10
10
runOnJS
11
11
} from 'react-native-reanimated' ;
12
- import { Colors , BorderRadiuses , Spacings } from '../../style' ;
12
+ import { Colors } from '../../style' ;
13
13
import { Constants , asBaseComponent } from '../../commons/new' ;
14
14
import View from '../view' ;
15
15
import Segment , { SegmentedControlItemProps } from './segment' ;
16
+ import useSegmentedControlPreset from './useSegmentedControlPreset' ;
16
17
17
- const BORDER_WIDTH = 1 ;
18
+ const CONTAINER_BORDER_WIDTH = 1 ;
18
19
const TIMING_CONFIG = {
19
20
duration : 300 ,
20
21
easing : Easing . bezier ( 0.33 , 1 , 0.68 , 1 )
21
22
} ;
22
23
24
+ export enum Presets {
25
+ DEFAULT = 'default' ,
26
+ FORM = 'form'
27
+ }
28
+
23
29
export { SegmentedControlItemProps } ;
24
30
export type SegmentedControlProps = {
25
31
/**
@@ -84,8 +90,25 @@ export type SegmentedControlProps = {
84
90
containerStyle ?: StyleProp < ViewStyle > ;
85
91
style ?: StyleProp < ViewStyle > ;
86
92
testID ?: string ;
93
+ /**
94
+ * Preset type
95
+ */
96
+ preset ?: Presets | `${Presets } `;
97
+ } ;
98
+
99
+ const nonAreUndefined = < T , > ( array : Array < T | undefined > ) : array is Array < T > => {
100
+ for ( const item of array ) {
101
+ if ( item === undefined ) {
102
+ return false ;
103
+ }
104
+ }
105
+ return true ;
87
106
} ;
88
107
108
+ function getInitialSegmentsDimensionsArray ( length : number ) {
109
+ return Array < { x : number ; width : number } | undefined > ( length ) . fill ( undefined ) ;
110
+ }
111
+
89
112
/**
90
113
* @description : SegmentedControl component for toggling two values or more
91
114
* @example : https://github.com/wix/react-native-ui-lib/blob/master/demo/src/screens/componentScreens/SegmentedControlScreen.tsx
@@ -97,22 +120,26 @@ const SegmentedControl = (props: SegmentedControlProps) => {
97
120
containerStyle,
98
121
style,
99
122
segments,
100
- activeColor = Colors . $textPrimary ,
101
- borderRadius = BorderRadiuses . br100 ,
102
- backgroundColor = Colors . $backgroundNeutralLight ,
103
- activeBackgroundColor = Colors . $backgroundDefault ,
104
- inactiveColor = Colors . $textNeutralHeavy ,
105
- outlineColor = activeColor ,
106
- outlineWidth = BORDER_WIDTH ,
123
+ activeColor,
124
+ borderRadius,
125
+ backgroundColor,
126
+ activeBackgroundColor,
127
+ inactiveColor,
128
+ outlineColor,
129
+ outlineWidth,
107
130
throttleTime = 0 ,
108
131
segmentsStyle : segmentsStyleProp ,
109
132
segmentLabelStyle,
110
- testID
111
- } = props ;
133
+ testID,
134
+ iconTintColor,
135
+ segmentDividerWidth,
136
+ segmentDividerColor
137
+ } = useSegmentedControlPreset ( props ) ;
112
138
const animatedSelectedIndex = useSharedValue ( initialIndex ) ;
113
139
const segmentsStyle = useSharedValue ( [ ] as { x : number ; width : number } [ ] ) ;
114
140
const segmentedControlHeight = useSharedValue ( 0 ) ;
115
- const segmentsCounter = useRef ( 0 ) ;
141
+ // const shouldResetOnDimensionsOnNextLayout = useRef(false); // use this flag if there bugs with onLayout being called more than once.
142
+ const segmentsDimensions = useRef ( getInitialSegmentsDimensionsArray ( segments ?. length || 0 ) ) ;
116
143
117
144
useEffect ( ( ) => {
118
145
animatedSelectedIndex . value = initialIndex ;
@@ -142,14 +169,17 @@ const SegmentedControl = (props: SegmentedControlProps) => {
142
169
} , [ ] ) ;
143
170
144
171
const onLayout = useCallback ( ( index : number , event : LayoutChangeEvent ) => {
172
+ // if (shouldResetOnDimensionsOnNextLayout.current) {
173
+ // shouldResetOnDimensionsOnNextLayout.current = false;
174
+ // // segmentsDimensions.current = getInitialSegmentsDimensionsArray(segments?.length || 0);
175
+ // }
145
176
const { x, width, height} = event . nativeEvent . layout ;
146
- segmentsStyle . value [ index ] = { x, width} ;
147
- segmentedControlHeight . value = height + 2 * BORDER_WIDTH ;
148
- segmentsCounter . current ++ ;
177
+ segmentsDimensions . current [ index ] = { x, width} ;
178
+ segmentedControlHeight . value = height + 2 * CONTAINER_BORDER_WIDTH ;
149
179
150
- if ( segmentsCounter . current === segments ?. length ) {
151
- segmentsStyle . value = [ ...segmentsStyle . value ] ;
152
- segmentsCounter . current = 0 ; // in case onLayout will be called again (orientation change etc.)
180
+ if ( nonAreUndefined ( segmentsDimensions . current ) ) {
181
+ segmentsStyle . value = [ ...segmentsDimensions . current ] ;
182
+ // shouldResetOnDimensionsOnNextLayout .current = true; // in case onLayout will be called again (orientation change etc.)
153
183
}
154
184
} ,
155
185
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -159,50 +189,79 @@ const SegmentedControl = (props: SegmentedControlProps) => {
159
189
if ( segmentsStyle . value . length !== 0 ) {
160
190
const isFirstElementSelected = animatedSelectedIndex . value === 0 ;
161
191
const isLastElementSelected = animatedSelectedIndex . value === segmentsStyle . value . length - 1 ;
162
- const xOffset = isFirstElementSelected ? - 2 : isLastElementSelected ? 2 : 0 ;
163
- const inset = withTiming ( segmentsStyle . value [ animatedSelectedIndex . value ] . x + xOffset , TIMING_CONFIG ) ;
164
- const width = withTiming ( segmentsStyle . value [ animatedSelectedIndex . value ] . width * BORDER_WIDTH , TIMING_CONFIG ) ;
192
+ const isMiddleSelected = ! isFirstElementSelected && ! isLastElementSelected ;
193
+ const insetFix = - CONTAINER_BORDER_WIDTH - ( ! isFirstElementSelected ? segmentDividerWidth : 1 ) ;
194
+ const widthFix = isMiddleSelected ? 2 * segmentDividerWidth : CONTAINER_BORDER_WIDTH + segmentDividerWidth ;
195
+ const inset = withTiming ( segmentsStyle . value [ animatedSelectedIndex . value ] . x + insetFix , TIMING_CONFIG ) ;
196
+ const width = withTiming ( segmentsStyle . value [ animatedSelectedIndex . value ] . width + widthFix , TIMING_CONFIG ) ;
165
197
const height = segmentedControlHeight . value ;
166
198
return Constants . isRTL ? { width, right : inset , height} : { width, left : inset , height} ;
167
199
}
168
200
return { } ;
169
201
} ) ;
202
+ const shouldRenderDividers = segmentDividerWidth !== 0 ;
170
203
171
204
const renderSegments = ( ) =>
172
205
_ . map ( segments , ( _value , index ) => {
206
+ const isLastSegment = index + 1 === segments ?. length ;
173
207
return (
174
- < Segment
175
- key = { index }
176
- onLayout = { onLayout }
177
- index = { index }
178
- onPress = { onSegmentPress }
179
- selectedIndex = { animatedSelectedIndex }
180
- activeColor = { activeColor }
181
- inactiveColor = { inactiveColor }
182
- style = { segmentsStyleProp }
183
- segmentLabelStyle = { segmentLabelStyle }
184
- { ...segments ?. [ index ] }
185
- testID = { testID }
186
- />
208
+ < React . Fragment key = { `segment-fragment-${ index } ` } >
209
+ < Segment
210
+ key = { `segment-${ index } ` }
211
+ onLayout = { onLayout }
212
+ index = { index }
213
+ onPress = { onSegmentPress }
214
+ selectedIndex = { animatedSelectedIndex }
215
+ activeColor = { activeColor }
216
+ inactiveColor = { inactiveColor }
217
+ style = { [ segmentsStyleProp ] }
218
+ segmentLabelStyle = { segmentLabelStyle }
219
+ iconTintColor = { iconTintColor }
220
+ { ...segments ?. [ index ] }
221
+ testID = { testID }
222
+ />
223
+ { ! isLastSegment && shouldRenderDividers && (
224
+ < View
225
+ key = { `segment.divider-${ index } ` }
226
+ width = { segmentDividerWidth }
227
+ height = { '100%' }
228
+ style = { { backgroundColor : segmentDividerColor } }
229
+ />
230
+ ) }
231
+ </ React . Fragment >
187
232
) ;
188
233
} ) ;
189
-
190
234
return (
191
235
< View style = { containerStyle } testID = { testID } >
192
236
< View row center style = { [ styles . container , style , { borderRadius, backgroundColor} ] } >
193
- < Reanimated . View
237
+ < View
238
+ reanimated
194
239
style = { [
195
240
styles . selectedSegment ,
196
241
{
197
- borderColor : outlineColor ,
198
242
borderRadius,
199
243
backgroundColor : activeBackgroundColor ,
200
- borderWidth : outlineWidth
244
+ borderWidth : shouldRenderDividers ? undefined : outlineWidth ,
245
+ borderColor : shouldRenderDividers ? undefined : outlineColor
201
246
} ,
202
247
animatedStyle
203
248
] }
204
249
/>
205
250
{ renderSegments ( ) }
251
+ { shouldRenderDividers && (
252
+ < View
253
+ reanimated
254
+ style = { [
255
+ styles . selectedSegment ,
256
+ {
257
+ borderColor : outlineColor ,
258
+ borderRadius,
259
+ borderWidth : outlineWidth
260
+ } ,
261
+ animatedStyle
262
+ ] }
263
+ />
264
+ ) }
206
265
</ View >
207
266
</ View >
208
267
) ;
@@ -212,16 +271,16 @@ const styles = StyleSheet.create({
212
271
container : {
213
272
backgroundColor : Colors . $backgroundNeutralLight ,
214
273
borderColor : Colors . $outlineDefault ,
215
- borderWidth : BORDER_WIDTH
274
+ borderWidth : CONTAINER_BORDER_WIDTH
216
275
} ,
217
276
selectedSegment : {
218
277
position : 'absolute'
219
- } ,
220
- segment : {
221
- paddingHorizontal : Spacings . s3
222
278
}
223
279
} ) ;
280
+ interface StaticMembers {
281
+ presets : typeof Presets ;
282
+ }
224
283
225
284
SegmentedControl . displayName = 'SegmentedControl' ;
226
-
227
- export default asBaseComponent < SegmentedControlProps > ( SegmentedControl ) ;
285
+ SegmentedControl . presets = Presets ;
286
+ export default asBaseComponent < SegmentedControlProps , StaticMembers > ( SegmentedControl ) ;
0 commit comments