Skip to content

Commit

Permalink
Feat/segemented control new UI preset (#3061)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
nitzanyiz committed May 23, 2024
1 parent f31986a commit c7b54a0
Show file tree
Hide file tree
Showing 4 changed files with 202 additions and 66 deletions.
54 changes: 36 additions & 18 deletions demo/src/screens/componentScreens/SegmentedControlScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import React, {useCallback} from 'react';
import React, {useCallback, useState} from 'react';
import {StyleSheet} from 'react-native';
import {Text, View, Colors, SegmentedControl, Assets, Spacings, BorderRadiuses, Typography} from 'react-native-ui-lib';
import {Text, View, Colors, SegmentedControl, Assets, Spacings, BorderRadiuses, Typography, SegmentedControlItemProps} from 'react-native-ui-lib';

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

return (
<View flex bottom padding-page>
Expand All @@ -32,39 +33,56 @@ const SegmentedControlScreen = () => {
</Text>
<View flex marginT-s8>
<View center>
<SegmentedControl segments={segments.first}/>
<View row gap-s10 bottom>
<Text text70>Preset:</Text>
<SegmentedControl
segments={segments.first}
preset={screenPreset}
onChangeIndex={index =>
setScreenPreset(index === 0 ? SegmentedControl.presets.DEFAULT : SegmentedControl.presets.FORM)
}
initialIndex={screenPreset === SegmentedControl.presets.DEFAULT ? 0 : 1}
/>
</View>
<SegmentedControl
onChangeIndex={onChangeIndex}
containerStyle={styles.container}
segments={segments.second}
initialIndex={2}
preset={screenPreset}
/>
<SegmentedControl
containerStyle={styles.container}
activeColor={Colors.$textDangerLight}
outlineColor={Colors.$textDangerLight}
segments={segments.third}
/>
<SegmentedControl
containerStyle={styles.container}
segments={segments.forth}
activeColor={Colors.$textDefault}
borderRadius={BorderRadiuses.br20}
backgroundColor={Colors.$backgroundInverted}
activeBackgroundColor={Colors.$backgroundNeutralIdle}
inactiveColor={Colors.$textDisabled}
style={styles.customStyle}
segmentsStyle={styles.customSegmentsStyle}
preset={screenPreset}
/>
</View>
<SegmentedControl containerStyle={styles.container} segments={segments.fifth}/>
<SegmentedControl containerStyle={styles.container} segments={segments.sixth}/>
<SegmentedControl containerStyle={styles.container} segments={segments.fifth} preset={screenPreset}/>
<SegmentedControl containerStyle={styles.container} segments={segments.sixth} preset={screenPreset}/>
<Text marginT-s4 center>
Custom Typography
</Text>
<SegmentedControl
containerStyle={styles.container}
segments={segments.seventh}
segmentLabelStyle={styles.customTypography}
preset={screenPreset}
/>
<Text marginT-s4 center>
Custom Styling
</Text>
<SegmentedControl
containerStyle={styles.container}
segments={segments.forth}
activeColor={Colors.$textDefault}
borderRadius={BorderRadiuses.br20}
backgroundColor={Colors.$backgroundInverted}
activeBackgroundColor={Colors.$backgroundNeutralIdle}
inactiveColor={Colors.$textDisabled}
style={styles.customStyle}
segmentsStyle={styles.customSegmentsStyle}
/>
</View>
</View>
Expand Down
149 changes: 104 additions & 45 deletions src/components/segmentedControl/index.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,31 @@
import _ from 'lodash';
import React, {useRef, useCallback, useEffect} from 'react';
import {StyleSheet, StyleProp, ViewStyle, TextStyle, LayoutChangeEvent} from 'react-native';
import Reanimated, {
import {
Easing,
useAnimatedReaction,
useAnimatedStyle,
useSharedValue,
withTiming,
runOnJS
} from 'react-native-reanimated';
import {Colors, BorderRadiuses, Spacings} from '../../style';
import {Colors} from '../../style';
import {Constants, asBaseComponent} from '../../commons/new';
import View from '../view';
import Segment, {SegmentedControlItemProps} from './segment';
import useSegmentedControlPreset from './useSegmentedControlPreset';

const BORDER_WIDTH = 1;
const CONTAINER_BORDER_WIDTH = 1;
const TIMING_CONFIG = {
duration: 300,
easing: Easing.bezier(0.33, 1, 0.68, 1)
};

export enum Presets {
DEFAULT = 'default',
FORM = 'form'
}

export {SegmentedControlItemProps};
export type SegmentedControlProps = {
/**
Expand Down Expand Up @@ -84,8 +90,25 @@ export type SegmentedControlProps = {
containerStyle?: StyleProp<ViewStyle>;
style?: StyleProp<ViewStyle>;
testID?: string;
/**
* Preset type
*/
preset?: Presets | `${Presets}`;
};

const nonAreUndefined = <T, >(array: Array<T | undefined>): array is Array<T> => {
for (const item of array) {
if (item === undefined) {
return false;
}
}
return true;
};

function getInitialSegmentsDimensionsArray(length: number) {
return Array<{x: number; width: number} | undefined>(length).fill(undefined);
}

/**
* @description: SegmentedControl component for toggling two values or more
* @example: https://github.com/wix/react-native-ui-lib/blob/master/demo/src/screens/componentScreens/SegmentedControlScreen.tsx
Expand All @@ -97,22 +120,26 @@ const SegmentedControl = (props: SegmentedControlProps) => {
containerStyle,
style,
segments,
activeColor = Colors.$textPrimary,
borderRadius = BorderRadiuses.br100,
backgroundColor = Colors.$backgroundNeutralLight,
activeBackgroundColor = Colors.$backgroundDefault,
inactiveColor = Colors.$textNeutralHeavy,
outlineColor = activeColor,
outlineWidth = BORDER_WIDTH,
activeColor,
borderRadius,
backgroundColor,
activeBackgroundColor,
inactiveColor,
outlineColor,
outlineWidth,
throttleTime = 0,
segmentsStyle: segmentsStyleProp,
segmentLabelStyle,
testID
} = props;
testID,
iconTintColor,
segmentDividerWidth,
segmentDividerColor
} = useSegmentedControlPreset(props);
const animatedSelectedIndex = useSharedValue(initialIndex);
const segmentsStyle = useSharedValue([] as {x: number; width: number}[]);
const segmentedControlHeight = useSharedValue(0);
const segmentsCounter = useRef(0);
// const shouldResetOnDimensionsOnNextLayout = useRef(false); // use this flag if there bugs with onLayout being called more than once.
const segmentsDimensions = useRef(getInitialSegmentsDimensionsArray(segments?.length || 0));

useEffect(() => {
animatedSelectedIndex.value = initialIndex;
Expand Down Expand Up @@ -142,14 +169,17 @@ const SegmentedControl = (props: SegmentedControlProps) => {
}, []);

const onLayout = useCallback((index: number, event: LayoutChangeEvent) => {
// if (shouldResetOnDimensionsOnNextLayout.current) {
// shouldResetOnDimensionsOnNextLayout.current = false;
// // segmentsDimensions.current = getInitialSegmentsDimensionsArray(segments?.length || 0);
// }
const {x, width, height} = event.nativeEvent.layout;
segmentsStyle.value[index] = {x, width};
segmentedControlHeight.value = height + 2 * BORDER_WIDTH;
segmentsCounter.current++;
segmentsDimensions.current[index] = {x, width};
segmentedControlHeight.value = height + 2 * CONTAINER_BORDER_WIDTH;

if (segmentsCounter.current === segments?.length) {
segmentsStyle.value = [...segmentsStyle.value];
segmentsCounter.current = 0; // in case onLayout will be called again (orientation change etc.)
if (nonAreUndefined(segmentsDimensions.current)) {
segmentsStyle.value = [...segmentsDimensions.current];
// shouldResetOnDimensionsOnNextLayout.current = true;// in case onLayout will be called again (orientation change etc.)
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
Expand All @@ -159,50 +189,79 @@ const SegmentedControl = (props: SegmentedControlProps) => {
if (segmentsStyle.value.length !== 0) {
const isFirstElementSelected = animatedSelectedIndex.value === 0;
const isLastElementSelected = animatedSelectedIndex.value === segmentsStyle.value.length - 1;
const xOffset = isFirstElementSelected ? -2 : isLastElementSelected ? 2 : 0;
const inset = withTiming(segmentsStyle.value[animatedSelectedIndex.value].x + xOffset, TIMING_CONFIG);
const width = withTiming(segmentsStyle.value[animatedSelectedIndex.value].width * BORDER_WIDTH, TIMING_CONFIG);
const isMiddleSelected = !isFirstElementSelected && !isLastElementSelected;
const insetFix = -CONTAINER_BORDER_WIDTH - (!isFirstElementSelected ? segmentDividerWidth : 1);
const widthFix = isMiddleSelected ? 2 * segmentDividerWidth : CONTAINER_BORDER_WIDTH + segmentDividerWidth;
const inset = withTiming(segmentsStyle.value[animatedSelectedIndex.value].x + insetFix, TIMING_CONFIG);
const width = withTiming(segmentsStyle.value[animatedSelectedIndex.value].width + widthFix, TIMING_CONFIG);
const height = segmentedControlHeight.value;
return Constants.isRTL ? {width, right: inset, height} : {width, left: inset, height};
}
return {};
});
const shouldRenderDividers = segmentDividerWidth !== 0;

const renderSegments = () =>
_.map(segments, (_value, index) => {
const isLastSegment = index + 1 === segments?.length;
return (
<Segment
key={index}
onLayout={onLayout}
index={index}
onPress={onSegmentPress}
selectedIndex={animatedSelectedIndex}
activeColor={activeColor}
inactiveColor={inactiveColor}
style={segmentsStyleProp}
segmentLabelStyle={segmentLabelStyle}
{...segments?.[index]}
testID={testID}
/>
<React.Fragment key={`segment-fragment-${index}`}>
<Segment
key={`segment-${index}`}
onLayout={onLayout}
index={index}
onPress={onSegmentPress}
selectedIndex={animatedSelectedIndex}
activeColor={activeColor}
inactiveColor={inactiveColor}
style={[segmentsStyleProp]}
segmentLabelStyle={segmentLabelStyle}
iconTintColor={iconTintColor}
{...segments?.[index]}
testID={testID}
/>
{!isLastSegment && shouldRenderDividers && (
<View
key={`segment.divider-${index}`}
width={segmentDividerWidth}
height={'100%'}
style={{backgroundColor: segmentDividerColor}}
/>
)}
</React.Fragment>
);
});

return (
<View style={containerStyle} testID={testID}>
<View row center style={[styles.container, style, {borderRadius, backgroundColor}]}>
<Reanimated.View
<View
reanimated
style={[
styles.selectedSegment,
{
borderColor: outlineColor,
borderRadius,
backgroundColor: activeBackgroundColor,
borderWidth: outlineWidth
borderWidth: shouldRenderDividers ? undefined : outlineWidth,
borderColor: shouldRenderDividers ? undefined : outlineColor
},
animatedStyle
]}
/>
{renderSegments()}
{shouldRenderDividers && (
<View
reanimated
style={[
styles.selectedSegment,
{
borderColor: outlineColor,
borderRadius,
borderWidth: outlineWidth
},
animatedStyle
]}
/>
)}
</View>
</View>
);
Expand All @@ -212,16 +271,16 @@ const styles = StyleSheet.create({
container: {
backgroundColor: Colors.$backgroundNeutralLight,
borderColor: Colors.$outlineDefault,
borderWidth: BORDER_WIDTH
borderWidth: CONTAINER_BORDER_WIDTH
},
selectedSegment: {
position: 'absolute'
},
segment: {
paddingHorizontal: Spacings.s3
}
});
interface StaticMembers {
presets: typeof Presets;
}

SegmentedControl.displayName = 'SegmentedControl';

export default asBaseComponent<SegmentedControlProps>(SegmentedControl);
SegmentedControl.presets = Presets;
export default asBaseComponent<SegmentedControlProps, StaticMembers>(SegmentedControl);
11 changes: 8 additions & 3 deletions src/components/segmentedControl/segment.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ export type SegmentedControlItemProps = Pick<SegmentedControlProps, 'segmentLabe
* Should the icon be on right of the label
*/
iconOnRight?: boolean;
/**
* Icon tint color
*/
iconTintColor?: string;
};

export type SegmentProps = SegmentedControlItemProps & {
Expand Down Expand Up @@ -74,7 +78,8 @@ const Segment = React.memo((props: SegmentProps) => {
iconOnRight,
style,
segmentLabelStyle,
testID
testID,
iconTintColor
} = props;

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

const animatedIconStyle = useAnimatedStyle(() => {
const tintColor = selectedIndex?.value === index ? activeColor : inactiveColor;
const tintColor = selectedIndex?.value === index ? activeColor : (iconTintColor || inactiveColor);
return {tintColor};
});

Expand Down Expand Up @@ -130,5 +135,5 @@ const Segment = React.memo((props: SegmentProps) => {
</TouchableOpacity>
);
});

Segment.displayName = 'SegmentedControl.Segment';
export default asBaseComponent<SegmentProps>(Segment);

0 comments on commit c7b54a0

Please sign in to comment.