Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/segemented control new UI preset #3061

Merged
merged 23 commits into from
May 23, 2024
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
093dfd6
added new segmented control ui preset
nitzanyiz May 7, 2024
356c534
fixed icon tint coloring
nitzanyiz May 7, 2024
1b20dd7
changed display name and removed extra imports
nitzanyiz May 7, 2024
6666f8c
fixed outline offsets with the divider
nitzanyiz May 9, 2024
ca6bd35
Merge remote-tracking branch 'origin' into feat/SegementedControlNewU…
nitzanyiz May 9, 2024
7b4a79a
changed inset and width calculation. added key to dividers
nitzanyiz May 9, 2024
b2614f5
exposed iconTintColor. added example in example screen
nitzanyiz May 12, 2024
e5aa8c7
removed icon from example
nitzanyiz May 12, 2024
1a73286
Merge remote-tracking branch 'origin' into feat/SegementedControlNewU…
nitzanyiz May 12, 2024
fe74337
added key to fregment
nitzanyiz May 12, 2024
6d06d0b
changed const to enum
nitzanyiz May 13, 2024
8762bb0
added static members
nitzanyiz May 13, 2024
eeca220
removed enum usage in useSegmentedControl
nitzanyiz May 13, 2024
6c15bce
added preset segmented control to screen
nitzanyiz May 13, 2024
5ecb31f
fixed labels
nitzanyiz May 13, 2024
04ac68d
remnoved another reanimated view on default preset
nitzanyiz May 13, 2024
a58aeda
changed borderWidth checking
nitzanyiz May 20, 2024
c44252c
changed displayName
nitzanyiz May 20, 2024
00020ca
moved custom styling to bottom of screen
nitzanyiz May 20, 2024
25f498d
fixed displayName
nitzanyiz May 20, 2024
39b81a3
fix to preset change
nitzanyiz May 22, 2024
7acbb2d
removed extra segmentes. removed key from view
nitzanyiz May 22, 2024
3eb4c8f
formattings
nitzanyiz May 22, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
44 changes: 35 additions & 9 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 @@ -17,32 +17,56 @@ const segments = {
forth: [{label: 'With'}, {label: 'Custom'}, {label: 'Style'}],
fifth: [{label: 'Full'}, {label: 'Width'}],
sixth: [{label: 'Full'}, {label: 'Width'}, {label: 'With'}, {label: 'A'}, {label: 'Very Long Segment'}],
seventh: [{label: '$'}, {label: '%'}]
seventh: [{label: '$'}, {label: '%'}],
eighth: [
{label: `${Assets.emojis.arrow_left} Left`},
{label: `${Assets.emojis.arrow_up} Up`},
{label: `${Assets.emojis.arrow_down} Down`},
{label: `${Assets.emojis.arrow_right} Right`}
]
};

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>
<Text center text40 $textDefault>
Segmented Control
</Text>
<View flex marginT-s8>
<View
flex
marginT-s8
key={screenPreset /* added here because changing the preset based on state was causing wrong calculations */}
Inbal-Tish marked this conversation as resolved.
Show resolved Hide resolved
>
<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}
segments={segments.third}
preset={screenPreset}
/>
<SegmentedControl
containerStyle={styles.container}
Expand All @@ -54,17 +78,19 @@ const SegmentedControlScreen = () => {
inactiveColor={Colors.$textDisabled}
style={styles.customStyle}
segmentsStyle={styles.customSegmentsStyle}
preset={screenPreset}
Inbal-Tish marked this conversation as resolved.
Show resolved Hide resolved
/>
</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}
/>
</View>
</View>
Expand Down
119 changes: 80 additions & 39 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,6 +90,10 @@ export type SegmentedControlProps = {
containerStyle?: StyleProp<ViewStyle>;
style?: StyleProp<ViewStyle>;
testID?: string;
/**
* Preset type
*/
preset?: Presets | `${Presets}`;
};

/**
Expand All @@ -97,18 +107,21 @@ 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);
Expand Down Expand Up @@ -144,7 +157,7 @@ const SegmentedControl = (props: SegmentedControlProps) => {
const onLayout = useCallback((index: number, event: LayoutChangeEvent) => {
const {x, width, height} = event.nativeEvent.layout;
segmentsStyle.value[index] = {x, width};
segmentedControlHeight.value = height + 2 * BORDER_WIDTH;
segmentedControlHeight.value = height + 2 * CONTAINER_BORDER_WIDTH;
segmentsCounter.current++;

if (segmentsCounter.current === segments?.length) {
Expand All @@ -159,9 +172,11 @@ 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};
}
Expand All @@ -170,39 +185,65 @@ const SegmentedControl = (props: SegmentedControlProps) => {

const renderSegments = () =>
_.map(segments, (_value, index) => {
const isLastSegment = index + 1 === segments?.length;
Inbal-Tish marked this conversation as resolved.
Show resolved Hide resolved
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 && segmentDividerWidth !== 0 && (
<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: segmentDividerWidth !== 0 ? undefined : outlineWidth,
Inbal-Tish marked this conversation as resolved.
Show resolved Hide resolved
borderColor: segmentDividerWidth !== 0 ? undefined : outlineColor
},
animatedStyle
]}
/>
{renderSegments()}
{segmentDividerWidth !== 0 && (
<View
reanimated
style={[
styles.selectedSegment,
{
borderColor: outlineColor,
borderRadius,
borderWidth: outlineWidth
},
animatedStyle
]}
/>
)}
</View>
</View>
);
Expand All @@ -212,16 +253,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';
Inbal-Tish marked this conversation as resolved.
Show resolved Hide resolved
export default asBaseComponent<SegmentProps>(Segment);
54 changes: 54 additions & 0 deletions src/components/segmentedControl/useSegmentedControlPreset.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import type {SegmentedControlProps, Presets} from './';
import {BorderRadiuses, Colors} from '../../style/';
import {SegmentProps} from './segment';
import type {ColorValue} from 'react-native';

interface useSegmentedControlPresetProps extends SegmentedControlProps, Partial<SegmentProps> {
Inbal-Tish marked this conversation as resolved.
Show resolved Hide resolved
segmentDividerWidth: number;
segmentDividerColor: ColorValue;
iconTintColor?: string;
}
const DEFAULT_ACTIVE_COLOR = Colors.$textPrimary;
const FORM_TEXT_COLOR = Colors.$textDefault;

const useSegmentedControlPreset = (props: SegmentedControlProps): useSegmentedControlPresetProps => {
const {preset = 'default', activeColor, inactiveColor, outlineColor} = props;
const presetProps = {
...defaultsPresetsProps[preset],
...props
};
if (activeColor && !outlineColor && preset === 'default') {
presetProps.outlineColor = activeColor;
}
if (activeColor || inactiveColor) {
delete presetProps.iconTintColor;
}
return presetProps;
};

const defaultsPresetsProps: Record<`${Presets}`, useSegmentedControlPresetProps> = {
default: {
activeColor: DEFAULT_ACTIVE_COLOR,
borderRadius: BorderRadiuses.br100,
backgroundColor: Colors.$backgroundNeutralLight,
activeBackgroundColor: Colors.$backgroundDefault,
inactiveColor: Colors.$textNeutralHeavy,
outlineColor: DEFAULT_ACTIVE_COLOR,
outlineWidth: 1,
segmentDividerWidth: 0,
segmentDividerColor: ''
},
form: {
activeColor: FORM_TEXT_COLOR,
inactiveColor: FORM_TEXT_COLOR,
backgroundColor: Colors.$backgroundDefault,
activeBackgroundColor: Colors.$backgroundElevated,
outlineColor: Colors.$outlinePrimary,
borderRadius: BorderRadiuses.br20,
outlineWidth: 2,
iconTintColor: Colors.$iconDefault,
segmentDividerWidth: 1,
segmentDividerColor: Colors.$outlineDefault
}
};
export default useSegmentedControlPreset;