Skip to content

Commit c7b54a0

Browse files
authored
Feat/segemented control new UI preset (#3061)
* added new segmented control ui preset * fixed icon tint coloring * changed display name and removed extra imports * fixed outline offsets with the divider * changed inset and width calculation. added key to dividers * exposed iconTintColor. added example in example screen * removed icon from example * added key to fregment * changed const to enum * added static members * removed enum usage in useSegmentedControl * added preset segmented control to screen * fixed labels * remnoved another reanimated view on default preset * changed borderWidth checking * changed displayName * moved custom styling to bottom of screen * fixed displayName * fix to preset change * removed extra segmentes. removed key from view * formattings
1 parent f31986a commit c7b54a0

File tree

4 files changed

+202
-66
lines changed

4 files changed

+202
-66
lines changed

demo/src/screens/componentScreens/SegmentedControlScreen.tsx

Lines changed: 36 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import React, {useCallback} from 'react';
1+
import React, {useCallback, useState} from 'react';
22
import {StyleSheet} from 'react-native';
3-
import {Text, View, Colors, SegmentedControl, Assets, Spacings, BorderRadiuses, Typography} from 'react-native-ui-lib';
3+
import {Text, View, Colors, SegmentedControl, Assets, Spacings, BorderRadiuses, Typography, SegmentedControlItemProps} from 'react-native-ui-lib';
44

5-
const segments = {
6-
first: [{label: 'Left'}, {label: 'Right'}],
5+
const segments: Record<string, Array<SegmentedControlItemProps>> = {
6+
first: [{label: 'Default'}, {label: 'Form'}],
77
second: [{label: '1'}, {label: '2'}, {label: '3'}, {label: Assets.emojis.airplane}, {label: '5'}],
88
third: [
99
{
@@ -24,6 +24,7 @@ const SegmentedControlScreen = () => {
2424
const onChangeIndex = useCallback((index: number) => {
2525
console.warn('Index ' + index + ' of the second segmentedControl was pressed');
2626
}, []);
27+
const [screenPreset, setScreenPreset] = useState(SegmentedControl.presets.DEFAULT);
2728

2829
return (
2930
<View flex bottom padding-page>
@@ -32,39 +33,56 @@ const SegmentedControlScreen = () => {
3233
</Text>
3334
<View flex marginT-s8>
3435
<View center>
35-
<SegmentedControl segments={segments.first}/>
36+
<View row gap-s10 bottom>
37+
<Text text70>Preset:</Text>
38+
<SegmentedControl
39+
segments={segments.first}
40+
preset={screenPreset}
41+
onChangeIndex={index =>
42+
setScreenPreset(index === 0 ? SegmentedControl.presets.DEFAULT : SegmentedControl.presets.FORM)
43+
}
44+
initialIndex={screenPreset === SegmentedControl.presets.DEFAULT ? 0 : 1}
45+
/>
46+
</View>
3647
<SegmentedControl
3748
onChangeIndex={onChangeIndex}
3849
containerStyle={styles.container}
3950
segments={segments.second}
4051
initialIndex={2}
52+
preset={screenPreset}
4153
/>
4254
<SegmentedControl
4355
containerStyle={styles.container}
4456
activeColor={Colors.$textDangerLight}
57+
outlineColor={Colors.$textDangerLight}
4558
segments={segments.third}
46-
/>
47-
<SegmentedControl
48-
containerStyle={styles.container}
49-
segments={segments.forth}
50-
activeColor={Colors.$textDefault}
51-
borderRadius={BorderRadiuses.br20}
52-
backgroundColor={Colors.$backgroundInverted}
53-
activeBackgroundColor={Colors.$backgroundNeutralIdle}
54-
inactiveColor={Colors.$textDisabled}
55-
style={styles.customStyle}
56-
segmentsStyle={styles.customSegmentsStyle}
59+
preset={screenPreset}
5760
/>
5861
</View>
59-
<SegmentedControl containerStyle={styles.container} segments={segments.fifth}/>
60-
<SegmentedControl containerStyle={styles.container} segments={segments.sixth}/>
62+
<SegmentedControl containerStyle={styles.container} segments={segments.fifth} preset={screenPreset}/>
63+
<SegmentedControl containerStyle={styles.container} segments={segments.sixth} preset={screenPreset}/>
6164
<Text marginT-s4 center>
6265
Custom Typography
6366
</Text>
6467
<SegmentedControl
6568
containerStyle={styles.container}
6669
segments={segments.seventh}
6770
segmentLabelStyle={styles.customTypography}
71+
preset={screenPreset}
72+
/>
73+
<Text marginT-s4 center>
74+
Custom Styling
75+
</Text>
76+
<SegmentedControl
77+
containerStyle={styles.container}
78+
segments={segments.forth}
79+
activeColor={Colors.$textDefault}
80+
borderRadius={BorderRadiuses.br20}
81+
backgroundColor={Colors.$backgroundInverted}
82+
activeBackgroundColor={Colors.$backgroundNeutralIdle}
83+
inactiveColor={Colors.$textDisabled}
84+
style={styles.customStyle}
85+
segmentsStyle={styles.customSegmentsStyle}
6886
/>
6987
</View>
7088
</View>

src/components/segmentedControl/index.tsx

Lines changed: 104 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,31 @@
11
import _ from 'lodash';
22
import React, {useRef, useCallback, useEffect} from 'react';
33
import {StyleSheet, StyleProp, ViewStyle, TextStyle, LayoutChangeEvent} from 'react-native';
4-
import Reanimated, {
4+
import {
55
Easing,
66
useAnimatedReaction,
77
useAnimatedStyle,
88
useSharedValue,
99
withTiming,
1010
runOnJS
1111
} from 'react-native-reanimated';
12-
import {Colors, BorderRadiuses, Spacings} from '../../style';
12+
import {Colors} from '../../style';
1313
import {Constants, asBaseComponent} from '../../commons/new';
1414
import View from '../view';
1515
import Segment, {SegmentedControlItemProps} from './segment';
16+
import useSegmentedControlPreset from './useSegmentedControlPreset';
1617

17-
const BORDER_WIDTH = 1;
18+
const CONTAINER_BORDER_WIDTH = 1;
1819
const TIMING_CONFIG = {
1920
duration: 300,
2021
easing: Easing.bezier(0.33, 1, 0.68, 1)
2122
};
2223

24+
export enum Presets {
25+
DEFAULT = 'default',
26+
FORM = 'form'
27+
}
28+
2329
export {SegmentedControlItemProps};
2430
export type SegmentedControlProps = {
2531
/**
@@ -84,8 +90,25 @@ export type SegmentedControlProps = {
8490
containerStyle?: StyleProp<ViewStyle>;
8591
style?: StyleProp<ViewStyle>;
8692
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;
87106
};
88107

108+
function getInitialSegmentsDimensionsArray(length: number) {
109+
return Array<{x: number; width: number} | undefined>(length).fill(undefined);
110+
}
111+
89112
/**
90113
* @description: SegmentedControl component for toggling two values or more
91114
* @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) => {
97120
containerStyle,
98121
style,
99122
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,
107130
throttleTime = 0,
108131
segmentsStyle: segmentsStyleProp,
109132
segmentLabelStyle,
110-
testID
111-
} = props;
133+
testID,
134+
iconTintColor,
135+
segmentDividerWidth,
136+
segmentDividerColor
137+
} = useSegmentedControlPreset(props);
112138
const animatedSelectedIndex = useSharedValue(initialIndex);
113139
const segmentsStyle = useSharedValue([] as {x: number; width: number}[]);
114140
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));
116143

117144
useEffect(() => {
118145
animatedSelectedIndex.value = initialIndex;
@@ -142,14 +169,17 @@ const SegmentedControl = (props: SegmentedControlProps) => {
142169
}, []);
143170

144171
const onLayout = useCallback((index: number, event: LayoutChangeEvent) => {
172+
// if (shouldResetOnDimensionsOnNextLayout.current) {
173+
// shouldResetOnDimensionsOnNextLayout.current = false;
174+
// // segmentsDimensions.current = getInitialSegmentsDimensionsArray(segments?.length || 0);
175+
// }
145176
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;
149179

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.)
153183
}
154184
},
155185
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -159,50 +189,79 @@ const SegmentedControl = (props: SegmentedControlProps) => {
159189
if (segmentsStyle.value.length !== 0) {
160190
const isFirstElementSelected = animatedSelectedIndex.value === 0;
161191
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);
165197
const height = segmentedControlHeight.value;
166198
return Constants.isRTL ? {width, right: inset, height} : {width, left: inset, height};
167199
}
168200
return {};
169201
});
202+
const shouldRenderDividers = segmentDividerWidth !== 0;
170203

171204
const renderSegments = () =>
172205
_.map(segments, (_value, index) => {
206+
const isLastSegment = index + 1 === segments?.length;
173207
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>
187232
);
188233
});
189-
190234
return (
191235
<View style={containerStyle} testID={testID}>
192236
<View row center style={[styles.container, style, {borderRadius, backgroundColor}]}>
193-
<Reanimated.View
237+
<View
238+
reanimated
194239
style={[
195240
styles.selectedSegment,
196241
{
197-
borderColor: outlineColor,
198242
borderRadius,
199243
backgroundColor: activeBackgroundColor,
200-
borderWidth: outlineWidth
244+
borderWidth: shouldRenderDividers ? undefined : outlineWidth,
245+
borderColor: shouldRenderDividers ? undefined : outlineColor
201246
},
202247
animatedStyle
203248
]}
204249
/>
205250
{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+
)}
206265
</View>
207266
</View>
208267
);
@@ -212,16 +271,16 @@ const styles = StyleSheet.create({
212271
container: {
213272
backgroundColor: Colors.$backgroundNeutralLight,
214273
borderColor: Colors.$outlineDefault,
215-
borderWidth: BORDER_WIDTH
274+
borderWidth: CONTAINER_BORDER_WIDTH
216275
},
217276
selectedSegment: {
218277
position: 'absolute'
219-
},
220-
segment: {
221-
paddingHorizontal: Spacings.s3
222278
}
223279
});
280+
interface StaticMembers {
281+
presets: typeof Presets;
282+
}
224283

225284
SegmentedControl.displayName = 'SegmentedControl';
226-
227-
export default asBaseComponent<SegmentedControlProps>(SegmentedControl);
285+
SegmentedControl.presets = Presets;
286+
export default asBaseComponent<SegmentedControlProps, StaticMembers>(SegmentedControl);

src/components/segmentedControl/segment.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ export type SegmentedControlItemProps = Pick<SegmentedControlProps, 'segmentLabe
2323
* Should the icon be on right of the label
2424
*/
2525
iconOnRight?: boolean;
26+
/**
27+
* Icon tint color
28+
*/
29+
iconTintColor?: string;
2630
};
2731

2832
export type SegmentProps = SegmentedControlItemProps & {
@@ -74,7 +78,8 @@ const Segment = React.memo((props: SegmentProps) => {
7478
iconOnRight,
7579
style,
7680
segmentLabelStyle,
77-
testID
81+
testID,
82+
iconTintColor
7883
} = props;
7984

8085
const animatedTextStyle = useAnimatedStyle(() => {
@@ -83,7 +88,7 @@ const Segment = React.memo((props: SegmentProps) => {
8388
});
8489

8590
const animatedIconStyle = useAnimatedStyle(() => {
86-
const tintColor = selectedIndex?.value === index ? activeColor : inactiveColor;
91+
const tintColor = selectedIndex?.value === index ? activeColor : (iconTintColor || inactiveColor);
8792
return {tintColor};
8893
});
8994

@@ -130,5 +135,5 @@ const Segment = React.memo((props: SegmentProps) => {
130135
</TouchableOpacity>
131136
);
132137
});
133-
138+
Segment.displayName = 'SegmentedControl.Segment';
134139
export default asBaseComponent<SegmentProps>(Segment);

0 commit comments

Comments
 (0)