From c7b54a07268ef5a8cee08e8d26ce34602453f915 Mon Sep 17 00:00:00 2001 From: Nitzan Yizhar Date: Thu, 23 May 2024 09:40:50 +0300 Subject: [PATCH] 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 --- .../SegmentedControlScreen.tsx | 54 ++++--- src/components/segmentedControl/index.tsx | 149 ++++++++++++------ src/components/segmentedControl/segment.tsx | 11 +- .../useSegmentedControlPreset.ts | 54 +++++++ 4 files changed, 202 insertions(+), 66 deletions(-) create mode 100644 src/components/segmentedControl/useSegmentedControlPreset.ts diff --git a/demo/src/screens/componentScreens/SegmentedControlScreen.tsx b/demo/src/screens/componentScreens/SegmentedControlScreen.tsx index 2e060658ac..1b28a2d2e0 100644 --- a/demo/src/screens/componentScreens/SegmentedControlScreen.tsx +++ b/demo/src/screens/componentScreens/SegmentedControlScreen.tsx @@ -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> = { + first: [{label: 'Default'}, {label: 'Form'}], second: [{label: '1'}, {label: '2'}, {label: '3'}, {label: Assets.emojis.airplane}, {label: '5'}], third: [ { @@ -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 ( @@ -32,32 +33,34 @@ const SegmentedControlScreen = () => { - + + Preset: + + setScreenPreset(index === 0 ? SegmentedControl.presets.DEFAULT : SegmentedControl.presets.FORM) + } + initialIndex={screenPreset === SegmentedControl.presets.DEFAULT ? 0 : 1} + /> + - - - + + Custom Typography @@ -65,6 +68,21 @@ const SegmentedControlScreen = () => { containerStyle={styles.container} segments={segments.seventh} segmentLabelStyle={styles.customTypography} + preset={screenPreset} + /> + + Custom Styling + + diff --git a/src/components/segmentedControl/index.tsx b/src/components/segmentedControl/index.tsx index 07dc120262..f2e5ccd36f 100644 --- a/src/components/segmentedControl/index.tsx +++ b/src/components/segmentedControl/index.tsx @@ -1,7 +1,7 @@ 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, @@ -9,17 +9,23 @@ import Reanimated, { 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 = { /** @@ -84,8 +90,25 @@ export type SegmentedControlProps = { containerStyle?: StyleProp; style?: StyleProp; testID?: string; + /** + * Preset type + */ + preset?: Presets | `${Presets}`; +}; + +const nonAreUndefined = (array: Array): array is Array => { + 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 @@ -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; @@ -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 @@ -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 ( - + + + {!isLastSegment && shouldRenderDividers && ( + + )} + ); }); - return ( - {renderSegments()} + {shouldRenderDividers && ( + + )} ); @@ -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(SegmentedControl); +SegmentedControl.presets = Presets; +export default asBaseComponent(SegmentedControl); diff --git a/src/components/segmentedControl/segment.tsx b/src/components/segmentedControl/segment.tsx index 8ae73bb56b..7c39b18130 100644 --- a/src/components/segmentedControl/segment.tsx +++ b/src/components/segmentedControl/segment.tsx @@ -23,6 +23,10 @@ export type SegmentedControlItemProps = Pick { iconOnRight, style, segmentLabelStyle, - testID + testID, + iconTintColor } = props; const animatedTextStyle = useAnimatedStyle(() => { @@ -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}; }); @@ -130,5 +135,5 @@ const Segment = React.memo((props: SegmentProps) => { ); }); - +Segment.displayName = 'SegmentedControl.Segment'; export default asBaseComponent(Segment); diff --git a/src/components/segmentedControl/useSegmentedControlPreset.ts b/src/components/segmentedControl/useSegmentedControlPreset.ts new file mode 100644 index 0000000000..2a1310a7b1 --- /dev/null +++ b/src/components/segmentedControl/useSegmentedControlPreset.ts @@ -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 { + 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;