From 25585c26f4bdf2be406ef2a63d9f1b435cebde1e Mon Sep 17 00:00:00 2001 From: M-i-k-e-l Date: Wed, 21 Jun 2023 15:58:29 +0300 Subject: [PATCH 01/26] Fix prReleaseNotesCommon file type --- .npmignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.npmignore b/.npmignore index f6ff1ccbe8..178a5650a5 100644 --- a/.npmignore +++ b/.npmignore @@ -241,7 +241,7 @@ markdowns/ # typings/ # eslint-rules/ scripts/* -!scripts/prReleaseNotesCommon.ts +!scripts/prReleaseNotesCommon.js demo-app.component.js index.android.js index.ios.js From 45a768e81598934e5323671b8cfe3f1a751a681e Mon Sep 17 00:00:00 2001 From: Adi Mordo Date: Thu, 22 Jun 2023 11:15:51 +0300 Subject: [PATCH 02/26] Fixed TabBar scroll when there is selected index (#2630) --- src/components/tabController/TabBar.tsx | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/components/tabController/TabBar.tsx b/src/components/tabController/TabBar.tsx index 658bfdeb50..00df2bdae3 100644 --- a/src/components/tabController/TabBar.tsx +++ b/src/components/tabController/TabBar.tsx @@ -165,13 +165,7 @@ const TabBar = (props: Props) => { const tabBar = useRef(); const [key, setKey] = useState(Constants.orientation); const context = useContext(TabBarContext); - const { - items: contextItems, - currentPage, - targetPage, - initialIndex, - containerWidth: contextContainerWidth - } = context; + const {items: contextItems, currentPage, targetPage, containerWidth: contextContainerWidth} = context; const containerWidth: number = useMemo(() => { return propsContainerWidth || contextContainerWidth; }, [propsContainerWidth, contextContainerWidth]); @@ -194,7 +188,7 @@ const TabBar = (props: Props) => { // @ts-expect-error TODO: typing bug scrollViewRef: tabBar, itemsCount, - selectedIndex: initialIndex, + selectedIndex: currentPage.value, containerWidth, offsetType: centerSelected ? useScrollToItem.offsetType.CENTER : useScrollToItem.offsetType.DYNAMIC }); From 5f0ade6b3fbacda26cf595e13730a7da2153a5cb Mon Sep 17 00:00:00 2001 From: Miki Leib <38354019+M-i-k-e-l@users.noreply.github.com> Date: Thu, 22 Jun 2023 12:18:41 +0300 Subject: [PATCH 03/26] Incubator.TextField - expose retainValidationSpace (#2631) --- src/incubator/TextField/ValidationMessage.tsx | 4 ++-- src/incubator/TextField/index.tsx | 5 +++-- src/incubator/TextField/textField.api.json | 6 ++++++ src/incubator/TextField/types.ts | 5 ++++- 4 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/incubator/TextField/ValidationMessage.tsx b/src/incubator/TextField/ValidationMessage.tsx index bc12d93705..3541785d76 100644 --- a/src/incubator/TextField/ValidationMessage.tsx +++ b/src/incubator/TextField/ValidationMessage.tsx @@ -9,7 +9,7 @@ const ValidationMessage = ({ validationMessage, enableErrors, validationMessageStyle, - retainSpace, + retainValidationSpace, validate, testID }: ValidationMessageProps) => { @@ -17,7 +17,7 @@ const ValidationMessage = ({ const style = useMemo(() => [styles.validationMessage, validationMessageStyle], [validationMessageStyle]); - if (!enableErrors || (!retainSpace && context.isValid)) { + if (!enableErrors || (!retainValidationSpace && context.isValid)) { return null; } diff --git a/src/incubator/TextField/index.tsx b/src/incubator/TextField/index.tsx index 2b46656204..7611287d55 100644 --- a/src/incubator/TextField/index.tsx +++ b/src/incubator/TextField/index.tsx @@ -69,6 +69,7 @@ const TextField = (props: InternalTextFieldProps) => { enableErrors, // TODO: rename to enableValidation validationMessageStyle, validationMessagePosition = ValidationMessagePosition.BOTTOM, + retainValidationSpace = true, // Char Counter showCharCounter, charCounterStyle, @@ -140,7 +141,7 @@ const TextField = (props: InternalTextFieldProps) => { validate={others.validate} validationMessage={others.validationMessage} validationMessageStyle={_validationMessageStyle} - retainSpace={retainTopMessageSpace} + retainValidationSpace={retainValidationSpace && retainTopMessageSpace} testID={`${props.testID}.validationMessage`} /> )} @@ -195,7 +196,7 @@ const TextField = (props: InternalTextFieldProps) => { validate={others.validate} validationMessage={others.validationMessage} validationMessageStyle={_validationMessageStyle} - retainSpace + retainValidationSpace={retainValidationSpace} testID={`${props.testID}.validationMessage`} /> )} diff --git a/src/incubator/TextField/textField.api.json b/src/incubator/TextField/textField.api.json index 0380f225d0..4d6dfaa36c 100644 --- a/src/incubator/TextField/textField.api.json +++ b/src/incubator/TextField/textField.api.json @@ -43,6 +43,12 @@ "description": "The position of the validation message (top/bottom)" }, {"name": "validationMessageStyle", "type": "TextStyle", "description": "Custom style for the validation message"}, + { + "name": "retainValidationSpace", + "type": "boolean", + "description": "Keep the validation space even if there is no validation message", + "default": "true" + }, {"name": "validateOnStart", "type": "boolean", "description": "Should validate when the TextField mounts"}, {"name": "validateOnChange", "type": "boolean", "description": "Should validate when the TextField value changes"}, {"name": "validateOnBlur", "type": "boolean", "description": "Should validate when losing focus of TextField"}, diff --git a/src/incubator/TextField/types.ts b/src/incubator/TextField/types.ts index 304b1d1855..36970f81eb 100644 --- a/src/incubator/TextField/types.ts +++ b/src/incubator/TextField/types.ts @@ -112,7 +112,10 @@ export interface ValidationMessageProps { * Custom style for the validation message */ validationMessageStyle?: StyleProp; - retainSpace?: boolean; + /** + * Keep the validation space even if there is no validation message + */ + retainValidationSpace?: boolean; validate?: FieldStateProps['validate']; testID?: string; } From 4969b9cf9b6543dfcd10eff75e20c67c159f425e Mon Sep 17 00:00:00 2001 From: M-i-k-e-l Date: Thu, 22 Jun 2023 13:00:06 +0300 Subject: [PATCH 04/26] Add "light-date" to demo's dependencies --- demo/package.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/demo/package.json b/demo/package.json index f653187ddd..9d67ce93a9 100644 --- a/demo/package.json +++ b/demo/package.json @@ -15,6 +15,9 @@ "release": "node ./scripts/release.js", "demoRelease": "node ./scripts/demoRelease.js" }, + "dependencies": { + "light-date": "^1.2.0" + }, "devDependencies": { "react-native-ui-lib": "7.1.0", "shell-utils": "^1.0.10" From 388274b30e05cec60ea05ef62ca384116aec3c49 Mon Sep 17 00:00:00 2001 From: Inbal Tish <33805983+Inbal-Tish@users.noreply.github.com> Date: Sun, 25 Jun 2023 16:39:03 +0300 Subject: [PATCH 05/26] Incubator.Slider - fix gap not equal on both max and min ends (#2634) --- demo/src/screens/incubatorScreens/IncubatorSliderScreen.tsx | 2 +- src/incubator/Slider/Thumb.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/demo/src/screens/incubatorScreens/IncubatorSliderScreen.tsx b/demo/src/screens/incubatorScreens/IncubatorSliderScreen.tsx index c8656f915b..6e466b85e9 100644 --- a/demo/src/screens/incubatorScreens/IncubatorSliderScreen.tsx +++ b/demo/src/screens/incubatorScreens/IncubatorSliderScreen.tsx @@ -6,7 +6,7 @@ import {renderBooleanOption} from '../ExampleScreenPresenter'; const VALUE = 20; const NEGATIVE_VALUE = -30; const MIN = 0; -const MAX = Constants.screenWidth - 40; // horizontal margins 20 +const MAX = 350; const INITIAL_MIN = 30; const INITIAL_MAX = 270; const COLOR = Colors.blue30; diff --git a/src/incubator/Slider/Thumb.tsx b/src/incubator/Slider/Thumb.tsx index e5fe7f59bb..227a2e0e16 100644 --- a/src/incubator/Slider/Thumb.tsx +++ b/src/incubator/Slider/Thumb.tsx @@ -68,8 +68,8 @@ const Thumb = (props: ThumbProps) => { // adjust end edge newX = end.value; } - if (!secondary && newX < gap || - secondary && newX > end.value - gap || + if (!secondary && newX <= gap || + secondary && newX >= end.value - gap || newX < end.value - gap && newX > start.value + gap) { offset.value = newX; } From 00ecaeabdd2ccd1c2a1f37533fa2a81149df22f3 Mon Sep 17 00:00:00 2001 From: Miki Leib <38354019+M-i-k-e-l@users.noreply.github.com> Date: Tue, 27 Jun 2023 14:32:46 +0300 Subject: [PATCH 06/26] SegmentedControl - onChangeIndex should react to changes (#2637) --- src/components/segmentedControl/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/segmentedControl/index.tsx b/src/components/segmentedControl/index.tsx index 580aba1559..0322e0f5af 100644 --- a/src/components/segmentedControl/index.tsx +++ b/src/components/segmentedControl/index.tsx @@ -116,7 +116,7 @@ const SegmentedControl = (props: SegmentedControlProps) => { }, throttleTime, {trailing: true, leading: false}), - [throttleTime]); + [throttleTime, onChangeIndex]); useAnimatedReaction(() => { return animatedSelectedIndex.value; @@ -126,7 +126,7 @@ const SegmentedControl = (props: SegmentedControlProps) => { onChangeIndex && runOnJS(changeIndex)(); } }, - []); + [changeIndex]); const onSegmentPress = useCallback((index: number) => { animatedSelectedIndex.value = index; From 9bf2be2aa24e54f423a3a62df3af106dd94db093 Mon Sep 17 00:00:00 2001 From: Miki Leib <38354019+M-i-k-e-l@users.noreply.github.com> Date: Tue, 27 Jun 2023 14:39:57 +0300 Subject: [PATCH 07/26] ExpandableOverlay - add migrateDialog (#2635) --- src/incubator/expandableOverlay/index.tsx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/incubator/expandableOverlay/index.tsx b/src/incubator/expandableOverlay/index.tsx index e661c3ab18..7ae146e7cd 100644 --- a/src/incubator/expandableOverlay/index.tsx +++ b/src/incubator/expandableOverlay/index.tsx @@ -3,7 +3,8 @@ import React, {useCallback, useState, forwardRef, PropsWithChildren, useImperati import TouchableOpacity, {TouchableOpacityProps} from '../../components/touchableOpacity'; import View from '../../components/view'; import Modal, {ModalProps, ModalTopBarProps} from '../../components/modal'; -import Dialog, {DialogProps} from '../../components/dialog'; +import DialogOld, {DialogProps as DialogPropsOld} from '../../components/dialog'; +import DialogNew, {DialogProps as DialogPropsNew} from '../Dialog'; import {Colors} from 'style'; export interface ExpandableOverlayMethods { @@ -33,7 +34,11 @@ export type ExpandableOverlayProps = TouchableOpacityProps & /** * The props to pass to the dialog expandable container */ - dialogProps?: DialogProps; + dialogProps?: DialogPropsOld | DialogPropsNew; + /** + * Migrate the Dialog to DialogNew (make sure you use only new props in dialogProps) + */ + migrateDialog?: boolean; /** * Whether to render a modal top bar (relevant only for modal) */ @@ -59,6 +64,7 @@ const ExpandableOverlay = (props: ExpandableOverlayProps, ref: any) => { useDialog, modalProps, dialogProps, + migrateDialog, showTopBar, topBarProps, renderCustomOverlay, @@ -103,7 +109,9 @@ const ExpandableOverlay = (props: ExpandableOverlayProps, ref: any) => { }; const renderDialog = () => { + const Dialog = migrateDialog ? DialogNew : DialogOld; return ( + // @ts-expect-error {expandableContent} From 7b3230d2a8e649377351b669082e96e9d1d5bfb2 Mon Sep 17 00:00:00 2001 From: Adi Date: Wed, 28 Jun 2023 12:56:56 +0300 Subject: [PATCH 08/26] textField centerd style dependes on fieldState.value --- src/incubator/TextField/index.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/incubator/TextField/index.tsx b/src/incubator/TextField/index.tsx index 7611287d55..c751880ac6 100644 --- a/src/incubator/TextField/index.tsx +++ b/src/incubator/TextField/index.tsx @@ -116,9 +116,12 @@ const TextField = (props: InternalTextFieldProps) => { const _validationMessageStyle = useMemo(() => { return centered ? [validationMessageStyle, styles.centeredValidationMessage] : validationMessageStyle; }, [validationMessageStyle, centered]); + const hasValue = useMemo(() => { + return fieldState.value !== undefined; + }, [fieldState.value]); const inputStyle = useMemo(() => { - return [typographyStyle, colorStyle, others.style, fieldState.value && centered && styles.centeredInput]; - }, [typographyStyle, colorStyle, others.style, centered]); + return [typographyStyle, colorStyle, others.style, hasValue && centered && styles.centeredInput]; + }, [typographyStyle, colorStyle, others.style, centered, hasValue]); const dummyPlaceholderStyle = useMemo(() => { return [inputStyle, styles.dummyPlaceholder]; }, [inputStyle]); From 50d702e0d0f07054582e19fe29ddabcfcac01884 Mon Sep 17 00:00:00 2001 From: Adi Date: Wed, 28 Jun 2023 13:06:24 +0300 Subject: [PATCH 09/26] TextField inputStyle hasValue refactor --- src/incubator/TextField/index.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/incubator/TextField/index.tsx b/src/incubator/TextField/index.tsx index c751880ac6..36a1ec6613 100644 --- a/src/incubator/TextField/index.tsx +++ b/src/incubator/TextField/index.tsx @@ -116,9 +116,7 @@ const TextField = (props: InternalTextFieldProps) => { const _validationMessageStyle = useMemo(() => { return centered ? [validationMessageStyle, styles.centeredValidationMessage] : validationMessageStyle; }, [validationMessageStyle, centered]); - const hasValue = useMemo(() => { - return fieldState.value !== undefined; - }, [fieldState.value]); + const hasValue = fieldState.value !== undefined; const inputStyle = useMemo(() => { return [typographyStyle, colorStyle, others.style, hasValue && centered && styles.centeredInput]; }, [typographyStyle, colorStyle, others.style, centered, hasValue]); From 47d2ab11ad6f193da057bd85023f94ca39ec15ab Mon Sep 17 00:00:00 2001 From: Adi Mordo Date: Thu, 29 Jun 2023 16:24:29 +0300 Subject: [PATCH 10/26] webDemo react-native-gesture-handler version upgrade (#2647) --- webDemo/package.json | 2 +- webDemo/src/App.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/webDemo/package.json b/webDemo/package.json index 9c93ffdab5..e3a2deb813 100644 --- a/webDemo/package.json +++ b/webDemo/package.json @@ -19,7 +19,7 @@ "@react-native-community/netinfo": "^9.3.0", "react": "^17.0.2", "react-dom": "^17.0.2", - "react-native-gesture-handler": "^1.10.3", + "react-native-gesture-handler": "2.9.0", "react-native-haptic-feedback": "^1.14.0", "react-native-linear-gradient": "^2.6.2", "react-native-reanimated": "2.4.1", diff --git a/webDemo/src/App.tsx b/webDemo/src/App.tsx index 17784d2e33..5d0e72fa67 100644 --- a/webDemo/src/App.tsx +++ b/webDemo/src/App.tsx @@ -267,8 +267,8 @@ const itemsToRender: ItemToRender[] = [ onValueChange={(value: any) => { console.log('setSliderValue: ', value); setSliderValue(value); - }} + migrate value={sliderValue} minimumValue={0} maximumValue={10} From 630cf4633f008b309cf0ca379f79694ef79a2286 Mon Sep 17 00:00:00 2001 From: Miki Leib <38354019+M-i-k-e-l@users.noreply.github.com> Date: Sun, 2 Jul 2023 14:46:37 +0300 Subject: [PATCH 11/26] ColorPicker - small fixes (#2650) --- .../componentScreens/ColorPickerScreen.tsx | 4 ++-- .../colorPicker/ColorPickerDialog.tsx | 19 +++++++++---------- src/components/colorPicker/index.tsx | 17 ++++------------- 3 files changed, 15 insertions(+), 25 deletions(-) diff --git a/demo/src/screens/componentScreens/ColorPickerScreen.tsx b/demo/src/screens/componentScreens/ColorPickerScreen.tsx index b5025b0047..c3cb27cc69 100644 --- a/demo/src/screens/componentScreens/ColorPickerScreen.tsx +++ b/demo/src/screens/componentScreens/ColorPickerScreen.tsx @@ -4,7 +4,6 @@ import {StyleSheet, ScrollView} from 'react-native'; import {Colors, View, Text, ColorPicker, ColorPalette, ColorName} from 'react-native-ui-lib'; import {renderMultipleSegmentOptions} from '../ExampleScreenPresenter'; -interface Props {} interface State { color: string; textColor?: string; @@ -14,6 +13,7 @@ interface State { } const INITIAL_COLOR = Colors.$backgroundPrimaryHeavy; +// prettier-ignore const colors = [ '#20303C', '#43515C', '#66737C', '#858F96', '#A3ABB0', '#C2C7CB', '#E0E3E5', '#F2F4F5', '#3182C8', '#4196E0', '#459FED', '#57a8ef', '#8fc5f4', '#b5d9f8', '#daecfb', '#ecf5fd', @@ -25,7 +25,7 @@ const colors = [ '#8B1079', '#A0138E', '#B13DAC', '#C164BD', '#D08BCD', '#E0B1DE', '#EFD8EE', '#F7EBF7' ]; -export default class ColorPickerScreen extends Component { +export default class ColorPickerScreen extends Component<{}, State> { state: State = { color: INITIAL_COLOR, textColor: Colors.$textDefaultLight, diff --git a/src/components/colorPicker/ColorPickerDialog.tsx b/src/components/colorPicker/ColorPickerDialog.tsx index e2c709c077..27d96bcd6b 100644 --- a/src/components/colorPicker/ColorPickerDialog.tsx +++ b/src/components/colorPicker/ColorPickerDialog.tsx @@ -45,13 +45,13 @@ interface Props extends DialogProps { /** * Ok (v) button color */ - doneButtonColor?: string, + doneButtonColor?: string; accessibilityLabels?: { - dismissButton?: string, - doneButton?: string, - input?: string + dismissButton?: string; + doneButton?: string; + input?: string; }; - /** + /** * Whether to use the new Slider implementation using Reanimated */ migrate?: boolean; @@ -59,10 +59,10 @@ interface Props extends DialogProps { export type ColorPickerDialogProps = Props; interface State { - keyboardHeight: number, - color: any, - text?: string, - valid: boolean + keyboardHeight: number; + color: any; + text?: string; + valid: boolean; } const KEYBOARD_HEIGHT = 216; @@ -357,7 +357,6 @@ class ColorPickerDialog extends PureComponent { export default asBaseComponent(ColorPickerDialog); - const BORDER_RADIUS = 12; const styles = StyleSheet.create({ diff --git a/src/components/colorPicker/index.tsx b/src/components/colorPicker/index.tsx index 454ddd3348..304890b6e6 100644 --- a/src/components/colorPicker/index.tsx +++ b/src/components/colorPicker/index.tsx @@ -4,11 +4,11 @@ import Assets from '../../assets'; import {Colors} from '../../style'; import View from '../view'; import Button from '../button'; -import ColorPalette from '../colorPalette'; +import ColorPalette, {ColorPaletteProps} from '../colorPalette'; import {SWATCH_MARGIN, SWATCH_SIZE} from '../colorSwatch'; import ColorPickerDialog, {ColorPickerDialogProps} from './ColorPickerDialog'; -interface Props extends ColorPickerDialogProps { +interface Props extends ColorPickerDialogProps, Pick { /** * Array of colors for the picker's color palette (hex values) */ @@ -21,10 +21,6 @@ interface Props extends ColorPickerDialogProps { * The index of the item to animate at first render (default is last) */ animatedIndex?: number; - /** - * onValueChange callback for the picker's color palette change - */ - onValueChange?: (value: string, options: object) => void; /** * Accessibility labels as an object of strings, ex. * { @@ -90,7 +86,7 @@ class ColorPicker extends PureComponent { }; render() { - const {initialColor, colors, value, testID, accessibilityLabels, backgroundColor} = this.props; + const {initialColor, colors, value, testID, accessibilityLabels, backgroundColor, onValueChange} = this.props; const {show} = this.state; return ( @@ -100,7 +96,7 @@ class ColorPicker extends PureComponent { style={styles.palette} usePagination={false} animatedIndex={this.animatedIndex} - onValueChange={this.onValueChange} + onValueChange={onValueChange} testID={`${testID}-palette`} backgroundColor={backgroundColor} /> @@ -131,11 +127,6 @@ class ColorPicker extends PureComponent { ); } - - // ColorPalette - onValueChange = (value: string, options: object) => { - this.props.onValueChange?.(value, options); - }; } export default ColorPicker; From f4473b84e66d6256fe571bdf926045a4c71310c7 Mon Sep 17 00:00:00 2001 From: Adi Mordo Date: Tue, 4 Jul 2023 10:20:37 +0300 Subject: [PATCH 12/26] webDemo reanimated version upgrade to 3 (#2649) --- webDemo/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webDemo/package.json b/webDemo/package.json index e3a2deb813..e76f60c945 100644 --- a/webDemo/package.json +++ b/webDemo/package.json @@ -22,7 +22,7 @@ "react-native-gesture-handler": "2.9.0", "react-native-haptic-feedback": "^1.14.0", "react-native-linear-gradient": "^2.6.2", - "react-native-reanimated": "2.4.1", + "react-native-reanimated": "3.1.0", "react-native-shimmer-placeholder": "^2.0.8", "react-native-svg": "^12.1.0", "react-native-svg-transformer": "^0.14.3", From e7541328558a3ffdf859ed10d795acc5224ce77c Mon Sep 17 00:00:00 2001 From: Miki Leib <38354019+M-i-k-e-l@users.noreply.github.com> Date: Thu, 6 Jul 2023 15:05:36 +0300 Subject: [PATCH 13/26] NumberInput - fix design (MaskedInput look) (#2645) * NumberInput - fix design (MaskedInput look) * Fix some types and the API * Remove getInitialData and parsePastedData * Remove Clipboard dependency * Remove decimal separator from input (not from initial) * Fix (most) tests and package.json * Remove unused prop * Support validateOnBlur * Add textStyle * Add factor * Fix --- .../componentScreens/NumberInputScreen.tsx | 53 ++--- src/components/maskedInput/new.tsx | 5 +- .../numberInput/NumberInput.driver.ts | 23 ++ src/components/numberInput/Presenter.ts | 49 +++-- .../numberInput/__tests__/Presenter.spec.ts | 200 +++++++++++------- .../numberInput/__tests__/index.spec.tsx | 28 ++- src/components/numberInput/index.tsx | 125 +++++++---- .../numberInput/numberInput.api.json | 16 +- src/incubator/TextField/TextField.driver.ts | 5 + 9 files changed, 312 insertions(+), 192 deletions(-) create mode 100644 src/components/numberInput/NumberInput.driver.ts diff --git a/demo/src/screens/componentScreens/NumberInputScreen.tsx b/demo/src/screens/componentScreens/NumberInputScreen.tsx index a857b04a39..96b3c0b11c 100644 --- a/demo/src/screens/componentScreens/NumberInputScreen.tsx +++ b/demo/src/screens/componentScreens/NumberInputScreen.tsx @@ -41,9 +41,6 @@ const NumberInputScreen = () => { case 'valid': newText = currentData.current.formattedNumber; break; - case 'empty': - newText = 'Empty'; - break; case 'error': newText = `Error: value '${currentData.current.userInput}' is invalid`; break; @@ -73,18 +70,6 @@ const NumberInputScreen = () => { } }, [showLabel, exampleType]); - const placeholder = useMemo(() => { - switch (exampleType) { - case 'price': - default: - return 'Price'; - case 'percentage': - return 'Discount'; - case 'number': - return 'Any number'; - } - }, [exampleType]); - const fractionDigits = useMemo(() => { switch (exampleType) { case 'price': @@ -158,9 +143,26 @@ const NumberInputScreen = () => { } }, [exampleType]); + const textStyle = useMemo(() => { + return [styles.mainText, !leadingText && {marginLeft: Spacings.s4}, !trailingText && {marginRight: Spacings.s4}]; + }, [leadingText, trailingText]); + + const textFieldProps = useMemo(() => { + return { + label, + labelStyle: styles.label, + style: textStyle, + validate, + validationMessage, + validationMessageStyle: Typography.text80M, + validateOnChange: true, + centered: true + }; + }, [label, textStyle, validate, validationMessage]); + return ( - + Number Input @@ -174,30 +176,19 @@ const NumberInputScreen = () => { ], {state: exampleType, setState: setExampleType})} - + {text} diff --git a/src/components/maskedInput/new.tsx b/src/components/maskedInput/new.tsx index 0f7f80432c..41cf51cbcd 100644 --- a/src/components/maskedInput/new.tsx +++ b/src/components/maskedInput/new.tsx @@ -5,7 +5,6 @@ import React, { useState, useImperativeHandle, forwardRef, - ReactElement, ForwardedRef } from 'react'; import _ from 'lodash'; @@ -22,7 +21,7 @@ export interface MaskedInputProps extends Omit { /** * callback for rendering the custom input out of the value returns from the actual input */ - renderMaskedText?: ReactElement; + renderMaskedText?: (value?: string) => JSX.Element | undefined; /** * Custom formatter for the input value */ @@ -30,7 +29,7 @@ export interface MaskedInputProps extends Omit { /** * container style for the masked input container */ - containerStyle: StyleProp; + containerStyle?: StyleProp; } /** diff --git a/src/components/numberInput/NumberInput.driver.ts b/src/components/numberInput/NumberInput.driver.ts new file mode 100644 index 0000000000..96800ab63d --- /dev/null +++ b/src/components/numberInput/NumberInput.driver.ts @@ -0,0 +1,23 @@ +import {NumberInputProps} from './index'; +import {ComponentDriver, ComponentDriverArgs} from '../../testkit/Component.driver'; +import {TextFieldDriver} from '../../incubator/TextField/TextField.driver'; + +export class NumberInputDriver extends ComponentDriver { + private readonly maskedInputDriver: TextFieldDriver; + private readonly visualTextFieldDriver: TextFieldDriver; + + constructor(componentDriverArgs: ComponentDriverArgs) { + super(componentDriverArgs); + + this.maskedInputDriver = new TextFieldDriver({...componentDriverArgs, testID: `${this.testID}`}); + this.visualTextFieldDriver = new TextFieldDriver({...componentDriverArgs, testID: `${this.testID}.visual`}); + } + + changeText = async (text: string) => { + await this.maskedInputDriver.changeText(text); + }; + + getText = async () => { + return await this.visualTextFieldDriver.getText(); + }; +} diff --git a/src/components/numberInput/Presenter.ts b/src/components/numberInput/Presenter.ts index d58f71be00..2139e96f8c 100644 --- a/src/components/numberInput/Presenter.ts +++ b/src/components/numberInput/Presenter.ts @@ -1,12 +1,7 @@ -import {isEmpty} from 'lodash'; - export type NumberInputData = | {type: 'valid'; userInput: string; number: number; formattedNumber: string} - | {type: 'empty'} | {type: 'error'; userInput: string}; -export const EMPTY: NumberInputData = {type: 'empty'}; - export interface LocaleOptions { locale: string; decimalSeparator: string; @@ -19,20 +14,25 @@ export interface Options { } function formatNumber(value: number, options: Options) { - return value.toLocaleString(options.localeOptions.locale, {maximumFractionDigits: options.fractionDigits}); + return value.toLocaleString(options.localeOptions.locale, { + maximumFractionDigits: options.fractionDigits, + minimumFractionDigits: options.fractionDigits + }); } function generateLocaleOptions(locale: string) { - const options: Options = { + const options: Omit = { localeOptions: { locale, decimalSeparator: '', // fake decimalSeparator, we're creating it now thousandSeparator: '' // fake thousandSeparator, we're creating it now - }, - fractionDigits: 1 + } }; - const decimalSeparator = formatNumber(1.1, options).replace(/1/g, ''); - const thousandSeparator = formatNumber(1111, options).replace(/1/g, ''); + const decimalOptions: Options = {...options, fractionDigits: 1}; + const thousandOptions: Options = {...options, fractionDigits: 0}; + + const decimalSeparator = formatNumber(1.1, decimalOptions).replace(/1/g, ''); + const thousandSeparator = formatNumber(1111, thousandOptions).replace(/1/g, ''); return { locale, @@ -45,28 +45,27 @@ export function generateOptions(locale: string, fractionDigits: number): Options return {localeOptions: generateLocaleOptions(locale), fractionDigits}; } -export function parseInput(text: string, options: Options): NumberInputData { - if (isEmpty(text)) { - return EMPTY; - } +function factor(options: Options): number { + return Math.pow(10, options.fractionDigits); +} + +export function getInitialNumber(propsInitialNumber = 0, options: Options) { + return propsInitialNumber * factor(options); +} +export function parseInput(text: string, options: Options, initialNumber?: number): NumberInputData { let cleanInput: string = text.replaceAll(options.localeOptions.thousandSeparator, ''); - cleanInput = cleanInput.replaceAll(options.localeOptions.decimalSeparator, '.'); + cleanInput = cleanInput.replaceAll(options.localeOptions.decimalSeparator, initialNumber ? '.' : ''); let number = Number(cleanInput); if (isNaN(number)) { return {type: 'error', userInput: text}; } number = Number(number.toFixed(options.fractionDigits)); - const formattedNumber = formatNumber(number, options); - - return {type: 'valid', userInput: text, number, formattedNumber}; -} - -export function getInitialData(options: Options, initialValue?: number): NumberInputData { - if (initialValue === undefined) { - return EMPTY; + if (options.fractionDigits > 0) { + number /= factor(options); } - return parseInput(formatNumber(initialValue, options), options); + const formattedNumber = formatNumber(number, options); + return {type: 'valid', userInput: initialNumber ? `${initialNumber}` : cleanInput, number, formattedNumber}; } diff --git a/src/components/numberInput/__tests__/Presenter.spec.ts b/src/components/numberInput/__tests__/Presenter.spec.ts index 1084d546bf..854a456202 100644 --- a/src/components/numberInput/__tests__/Presenter.spec.ts +++ b/src/components/numberInput/__tests__/Presenter.spec.ts @@ -1,4 +1,4 @@ -import {getInitialData, parseInput, EMPTY, Options} from '../Presenter'; +import {getInitialNumber, parseInput, Options} from '../Presenter'; const EN_OPTIONS: Options = { localeOptions: { @@ -19,16 +19,47 @@ const DE_OPTIONS: Options = { }; describe('NumberInput', () => { + describe('getInitialNumber', () => { + it('should return 0 for undefined', () => { + expect(getInitialNumber(undefined, EN_OPTIONS)).toEqual(0); + }); + + it('should return 0 for 0', () => { + expect(getInitialNumber(0, EN_OPTIONS)).toEqual(0); + }); + + it('should return 100 for 1', () => { + expect(getInitialNumber(1, EN_OPTIONS)).toEqual(100); + }); + + it('should return 1 for 1 if fractionDigits is 0', () => { + expect(getInitialNumber(1, {...EN_OPTIONS, fractionDigits: 0})).toEqual(1); + }); + + it('should return 10 for 1 if fractionDigits is 1', () => { + expect(getInitialNumber(1, {...EN_OPTIONS, fractionDigits: 1})).toEqual(10); + }); + }); + describe('getInitialData', () => { + const getInitialData = (options: Options, initialNumber: number | undefined) => { + return parseInput(`${getInitialNumber(initialNumber, options)}`, options); + }; + it('should return undefined for undefined', () => { - expect(getInitialData(EN_OPTIONS, undefined)).toEqual(EMPTY); + expect(getInitialData(EN_OPTIONS, undefined)).toEqual({ + formattedNumber: '0.00', + userInput: '0', + number: 0, + type: 'valid' + }); }); it('should return one decimal point and not two', () => { expect(getInitialData(EN_OPTIONS, 12.1)).toEqual({ type: 'valid', - userInput: '12.1', - formattedNumber: '12.1', + userInput: '1210', + formattedNumber: '12.10', number: 12.1 }); }); @@ -36,8 +67,8 @@ describe('NumberInput', () => { it('should return string that ends without a dot', () => { expect(getInitialData(EN_OPTIONS, 12)).toEqual({ type: 'valid', - userInput: '12', - formattedNumber: '12', + userInput: '1200', + formattedNumber: '12.00', number: 12 }); }); @@ -46,8 +77,8 @@ describe('NumberInput', () => { it('should return one decimal point and not two', () => { expect(getInitialData(DE_OPTIONS, 12.1)).toEqual({ type: 'valid', - userInput: '12,1', - formattedNumber: '12,1', + userInput: '1210', + formattedNumber: '12,10', number: 12.1 }); }); @@ -55,8 +86,8 @@ describe('NumberInput', () => { it('should return string that ends without a dot', () => { expect(getInitialData(DE_OPTIONS, 12)).toEqual({ type: 'valid', - userInput: '12', - formattedNumber: '12', + userInput: '1200', + formattedNumber: '12,00', number: 12 }); }); @@ -69,8 +100,8 @@ describe('NumberInput', () => { expect(parseInput('1', EN_OPTIONS)).toEqual({ type: 'valid', userInput: '1', - formattedNumber: '1', - number: 1 + formattedNumber: '0.01', + number: 0.01 }); }); @@ -78,44 +109,53 @@ describe('NumberInput', () => { expect(parseInput('12', EN_OPTIONS)).toEqual({ type: 'valid', userInput: '12', - formattedNumber: '12', - number: 12 + formattedNumber: '0.12', + number: 0.12 }); }); it('decimal separator', () => { expect(parseInput('12.', EN_OPTIONS)).toEqual({ type: 'valid', - userInput: '12.', - formattedNumber: '12', - number: 12 + userInput: '12', + formattedNumber: '0.12', + number: 0.12 }); }); it('digit after decimal separator', () => { - expect(parseInput('12.3', EN_OPTIONS)).toEqual({ + expect(parseInput('1.23', EN_OPTIONS)).toEqual({ type: 'valid', - userInput: '12.3', - formattedNumber: '12.3', - number: 12.3 + userInput: '123', + formattedNumber: '1.23', + number: 1.23 + }); + }); + + it('3rd digit', () => { + expect(parseInput('123', EN_OPTIONS)).toEqual({ + type: 'valid', + userInput: '123', + formattedNumber: '1.23', + number: 1.23 }); }); it('3rd digit after decimal separator', () => { expect(parseInput('12.345', EN_OPTIONS)).toEqual({ type: 'valid', - userInput: '12.345', - formattedNumber: '12.35', - number: 12.35 + userInput: '12345', + formattedNumber: '123.45', + number: 123.45 }); }); it('thousand separator', () => { - expect(parseInput('1234', EN_OPTIONS)).toEqual({ + expect(parseInput('123456', EN_OPTIONS)).toEqual({ type: 'valid', - userInput: '1234', - formattedNumber: '1,234', - number: 1234 + userInput: '123456', + formattedNumber: '1,234.56', + number: 1234.56 }); }); @@ -125,17 +165,19 @@ describe('NumberInput', () => { it('decimal separator first', () => { expect(parseInput('.', EN_OPTIONS)).toEqual({ - type: 'error', - userInput: '.' + type: 'valid', + userInput: '', + formattedNumber: '0.00', + number: 0 }); }); it('decimal separator first and then a digit', () => { expect(parseInput('.1', EN_OPTIONS)).toEqual({ type: 'valid', - userInput: '.1', - formattedNumber: '0.1', - number: 0.1 + userInput: '1', + formattedNumber: '0.01', + number: 0.01 }); }); @@ -143,16 +185,16 @@ describe('NumberInput', () => { it('fractionDigits=0 decimal separator first', () => { expect(parseInput('.1', {...EN_OPTIONS, fractionDigits: 0})).toEqual({ type: 'valid', - userInput: '.1', - formattedNumber: '0', - number: 0 + userInput: '1', + formattedNumber: '1', + number: 1 }); }); it('fractionDigits=0 after a few digits', () => { expect(parseInput('123.', {...EN_OPTIONS, fractionDigits: 0})).toEqual({ type: 'valid', - userInput: '123.', + userInput: '123', formattedNumber: '123', number: 123 }); @@ -161,7 +203,7 @@ describe('NumberInput', () => { it('fractionDigits=3 3rd digit after decimal separator', () => { expect(parseInput('12.345', {...EN_OPTIONS, fractionDigits: 3})).toEqual({ type: 'valid', - userInput: '12.345', + userInput: '12345', formattedNumber: '12.345', number: 12.345 }); @@ -170,9 +212,9 @@ describe('NumberInput', () => { it('fractionDigits=3 4th digit after decimal separator', () => { expect(parseInput('12.3454', {...EN_OPTIONS, fractionDigits: 3})).toEqual({ type: 'valid', - userInput: '12.3454', - formattedNumber: '12.345', - number: 12.345 + userInput: '123454', + formattedNumber: '123.454', + number: 123.454 }); }); }); @@ -182,7 +224,7 @@ describe('NumberInput', () => { expect(parseInput('0', EN_OPTIONS)).toEqual({ type: 'valid', userInput: '0', - formattedNumber: '0', + formattedNumber: '0.00', number: 0 }); }); @@ -191,7 +233,7 @@ describe('NumberInput', () => { expect(parseInput('00', EN_OPTIONS)).toEqual({ type: 'valid', userInput: '00', - formattedNumber: '0', + formattedNumber: '0.00', number: 0 }); }); @@ -200,17 +242,17 @@ describe('NumberInput', () => { expect(parseInput('007', EN_OPTIONS)).toEqual({ type: 'valid', userInput: '007', - formattedNumber: '7', - number: 7 + formattedNumber: '0.07', + number: 0.07 }); }); it('zero prefix and fraction: 0123.456', () => { expect(parseInput('0123.456', EN_OPTIONS)).toEqual({ type: 'valid', - userInput: '0123.456', - formattedNumber: '123.46', - number: 123.46 + userInput: '0123456', + formattedNumber: '1,234.56', + number: 1234.56 }); }); }); @@ -221,8 +263,8 @@ describe('NumberInput', () => { expect(parseInput('1', DE_OPTIONS)).toEqual({ type: 'valid', userInput: '1', - formattedNumber: '1', - number: 1 + formattedNumber: '0,01', + number: 0.01 }); }); @@ -230,44 +272,44 @@ describe('NumberInput', () => { expect(parseInput('12', DE_OPTIONS)).toEqual({ type: 'valid', userInput: '12', - formattedNumber: '12', - number: 12 + formattedNumber: '0,12', + number: 0.12 }); }); it('decimal separator', () => { expect(parseInput('12,', DE_OPTIONS)).toEqual({ type: 'valid', - userInput: '12,', - formattedNumber: '12', - number: 12 + userInput: '12', + formattedNumber: '0,12', + number: 0.12 }); }); it('digit after decimal separator', () => { expect(parseInput('12,3', DE_OPTIONS)).toEqual({ type: 'valid', - userInput: '12,3', - formattedNumber: '12,3', - number: 12.3 + userInput: '123', + formattedNumber: '1,23', + number: 1.23 }); }); it('3rd digit after decimal separator', () => { expect(parseInput('12,345', DE_OPTIONS)).toEqual({ type: 'valid', - userInput: '12,345', - formattedNumber: '12,35', - number: 12.35 + userInput: '12345', + formattedNumber: '123,45', + number: 123.45 }); }); it('thousand separator', () => { - expect(parseInput('1234', DE_OPTIONS)).toEqual({ + expect(parseInput('123456', DE_OPTIONS)).toEqual({ type: 'valid', - userInput: '1234', - formattedNumber: '1.234', - number: 1234 + userInput: '123456', + formattedNumber: '1.234,56', + number: 1234.56 }); }); @@ -277,17 +319,19 @@ describe('NumberInput', () => { it('decimal separator first', () => { expect(parseInput(',', DE_OPTIONS)).toEqual({ - type: 'error', - userInput: ',' + type: 'valid', + userInput: '', + formattedNumber: '0,00', + number: 0 }); }); it('decimal separator first and then a digit', () => { expect(parseInput(',1', DE_OPTIONS)).toEqual({ type: 'valid', - userInput: ',1', - formattedNumber: '0,1', - number: 0.1 + userInput: '1', + formattedNumber: '0,01', + number: 0.01 }); }); @@ -295,16 +339,16 @@ describe('NumberInput', () => { it('fractionDigits=0 decimal separator first', () => { expect(parseInput(',1', {...DE_OPTIONS, fractionDigits: 0})).toEqual({ type: 'valid', - userInput: ',1', - formattedNumber: '0', - number: 0 + userInput: '1', + formattedNumber: '1', + number: 1 }); }); it('fractionDigits=0 after a few digits', () => { expect(parseInput('123,', {...DE_OPTIONS, fractionDigits: 0})).toEqual({ type: 'valid', - userInput: '123,', + userInput: '123', formattedNumber: '123', number: 123 }); @@ -313,7 +357,7 @@ describe('NumberInput', () => { it('fractionDigits=3 3rd digit after decimal separator', () => { expect(parseInput('12,345', {...DE_OPTIONS, fractionDigits: 3})).toEqual({ type: 'valid', - userInput: '12,345', + userInput: '12345', formattedNumber: '12,345', number: 12.345 }); @@ -322,9 +366,9 @@ describe('NumberInput', () => { it('fractionDigits=3 4th digit after decimal separator', () => { expect(parseInput('12,3454', {...DE_OPTIONS, fractionDigits: 3})).toEqual({ type: 'valid', - userInput: '12,3454', - formattedNumber: '12,345', - number: 12.345 + userInput: '123454', + formattedNumber: '123,454', + number: 123.454 }); }); }); diff --git a/src/components/numberInput/__tests__/index.spec.tsx b/src/components/numberInput/__tests__/index.spec.tsx index 87867be029..40da064145 100644 --- a/src/components/numberInput/__tests__/index.spec.tsx +++ b/src/components/numberInput/__tests__/index.spec.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import {fireEvent, render} from '@testing-library/react-native'; import NumberInput from '../index'; +import {NumberInputDriver} from '../NumberInput.driver'; const onChangeNumber = () => {}; @@ -16,14 +16,22 @@ const TestCase = props => { }; describe('NumberInput', () => { - it('Should update number when fractionDigits changes', () => { - const renderTree = render(); - const input = renderTree.getByTestId('field'); - fireEvent(input, 'focus'); - fireEvent.changeText(input, '123.4567'); - fireEvent(input, 'blur'); - renderTree.getByDisplayValue('123.46'); - renderTree.rerender(); - renderTree.getByDisplayValue('123.457'); + it('Should update number when fractionDigits changes', async () => { + const component = ; + const numberInputDriver = new NumberInputDriver({component, testID: 'field'}); + expect(await numberInputDriver.exists()).toBe(true); + numberInputDriver.changeText('1234567'); + expect(await numberInputDriver.getText()).toEqual('12,345.67'); + // TODO: add changing fractionDigits once we support rerender in our drivers + + + // const renderTree = render(); + // const input = renderTree.getByTestId('field'); + // fireEvent(input, 'focus'); + // fireEvent.changeText(input, '1234567'); + // fireEvent(input, 'blur'); + // renderTree.getByDisplayValue('1,234.67'); + // renderTree.rerender(); + // renderTree.getByDisplayValue('123.457'); }); }); diff --git a/src/components/numberInput/index.tsx b/src/components/numberInput/index.tsx index f885a119f2..ffd06be113 100644 --- a/src/components/numberInput/index.tsx +++ b/src/components/numberInput/index.tsx @@ -1,16 +1,34 @@ import {isEmpty} from 'lodash'; -import React, {useMemo, useCallback, useState, useEffect} from 'react'; -import {StyleSheet, StyleProp, ViewStyle} from 'react-native'; +import React, {useMemo, useCallback, useState, useRef} from 'react'; +import {StyleSheet, StyleProp, ViewStyle, TextStyle} from 'react-native'; import {useDidUpdate, useThemeProps} from 'hooks'; -import TextField, {TextFieldProps} from '../../incubator/TextField'; +import MaskedInput from '../maskedInput/new'; +import TextField, {TextFieldProps, TextFieldRef} from '../../incubator/TextField'; +import View from '../view'; import Text from '../text'; -import {getInitialData, parseInput, generateOptions, Options, NumberInputData} from './Presenter'; +import {parseInput, generateOptions, getInitialNumber, Options, NumberInputData} from './Presenter'; export {NumberInputData}; -export type NumberInputProps = React.PropsWithRef< - Omit & ThemeComponent -> & { +type _TextFieldProps = Omit< + TextFieldProps, + | 'leadingAccessory' + | 'trailingAccessory' + | 'value' + | 'onChangeText' + | 'placeholder' + | 'placeholderTextColor' + | 'floatingPlaceholder' + | 'floatingPlaceholderColor' + | 'floatingPlaceholderStyle' + | 'contextMenuHidden' +>; + +type _NumberInputProps = { + /** + * Pass additional props to the TextField + */ + textFieldProps?: _TextFieldProps; /** * Callback that is called when the number value has changed (undefined in both if the user has deleted the number). */ @@ -35,7 +53,7 @@ export type NumberInputProps = React.PropsWithRef< /** * The style of the leading text */ - leadingTextStyle?: StyleProp; + leadingTextStyle?: StyleProp; /** * A trailing text */ @@ -43,39 +61,54 @@ export type NumberInputProps = React.PropsWithRef< /** * The style of the trailing text */ - trailingTextStyle?: StyleProp; + trailingTextStyle?: StyleProp; + /** + * Container style of the whole component + */ + containerStyle?: StyleProp; + /** + * If true, context menu is hidden. The default value is true. + * Requires @react-native-community/clipboard to be installed. + */ + contextMenuHidden?: boolean; + testID?: string; }; +export type NumberInputProps = React.PropsWithRef<_NumberInputProps>; + function NumberInput(props: NumberInputProps, ref: any) { const themeProps = useThemeProps(props, 'NumberInput'); const { + textFieldProps, onChangeNumber, - initialNumber, + initialNumber: propsInitialNumber, fractionDigits = 2, // @ts-expect-error locale = 'en', containerStyle, + contextMenuHidden = true, leadingText, leadingTextStyle, trailingText, trailingTextStyle, - placeholder, - ...others + testID } = themeProps; const [options, setOptions] = useState(generateOptions(locale, fractionDigits)); - const [data, setData] = useState(); + const initialNumber = getInitialNumber(propsInitialNumber, options); + const [data, setData] = useState(parseInput(`${initialNumber}`, options, propsInitialNumber)); + const textField = useRef(); useDidUpdate(() => { setOptions(generateOptions(locale, fractionDigits)); }, [locale, fractionDigits]); const handleInitialValueChange = () => { - const newData = getInitialData(options, initialNumber); + const newData = parseInput(`${initialNumber}`, options, propsInitialNumber); onChangeNumber(newData); setData(newData); }; - useEffect(() => { + useDidUpdate(() => { handleInitialValueChange(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [initialNumber]); @@ -111,41 +144,53 @@ function NumberInput(props: NumberInputProps, ref: any) { } }, [hasText, trailingText, trailingTextStyle]); - const _containerStyle = useMemo(() => { - return [styles.containerStyle, containerStyle]; - }, [containerStyle]); - - const _onChangeText = useCallback((text: string) => { + const onChangeText = useCallback(async (text: string) => { processInput(text); }, [processInput]); - const value = useMemo(() => { - return data?.type === 'valid' || data?.type === 'error' ? data.userInput : ''; - }, [data]); - const formatter = useCallback(() => { return data?.type === 'valid' ? data.formattedNumber : data?.type === 'error' ? data.userInput : ''; }, [data]); - // Fixing RN bug in Android (placeholder + trailingText) - https://github.com/facebook/react-native/issues/35611 - const _placeholder = useMemo(() => { - return isEmpty(value) ? placeholder : undefined; - }, [placeholder, value]); + const onBlur = useCallback(() => { + if (textFieldProps?.validateOnBlur) { + textField.current?.validate(); + } + }, + [textFieldProps?.validateOnBlur]); + + const renderNumberInput = useCallback((value?: string) => { + return ( + + + + ); + }, + [containerStyle, formatter, leadingAccessory, textFieldProps, trailingAccessory, testID]); return ( - ); } @@ -153,8 +198,8 @@ function NumberInput(props: NumberInputProps, ref: any) { export default React.forwardRef(NumberInput); const styles = StyleSheet.create({ - containerStyle: { - overflow: 'hidden' + textFieldContainerStyle: { + flexShrink: 1 }, accessory: { flexGrow: 999 diff --git a/src/components/numberInput/numberInput.api.json b/src/components/numberInput/numberInput.api.json index a1ae5de26d..4247bda1ce 100644 --- a/src/components/numberInput/numberInput.api.json +++ b/src/components/numberInput/numberInput.api.json @@ -24,11 +24,17 @@ "default": "2" }, {"name": "leadingText", "type": "string", "description": "A leading text"}, - {"name": "leadingTextStyle", "type": "StyleProp", "description": "The style of the leading text"}, + {"name": "leadingTextStyle", "type": "TextStyle", "description": "The style of the leading text"}, {"name": "trailingText", "type": "string", "description": "A trailing text"}, - {"name": "trailingTextStyle", "type": "StyleProp", "description": "The style of the trailing text"} + {"name": "trailingTextStyle", "type": "TextStyle", "description": "The style of the trailing text"}, + {"name": "containerStyle", "type": "ViewStyle", "description": "Container style of the whole component"}, + { + "name": "contextMenuHidden", + "type": "boolean", + "description": "If true, context menu is hidden.", + "default": true, + "note": "Requires @react-native-community/clipboard to be installed." + } ], - "snippet": [ - "" - ] + "snippet": [""] } diff --git a/src/incubator/TextField/TextField.driver.ts b/src/incubator/TextField/TextField.driver.ts index c698f3d737..c95a1cffde 100644 --- a/src/incubator/TextField/TextField.driver.ts +++ b/src/incubator/TextField/TextField.driver.ts @@ -39,6 +39,11 @@ export class TextFieldDriver extends ComponentDriver { changeText = async (text: string) => (await this.uniDriver.selectorByTestId(this.testID)).typeText(text); + getText = async () => { + const props = await this.getPropsByTestId('field.visual'); + return props.value; + }; + getPlaceholderContent = async () => { if (await this.isPlaceholderVisible()) { return (await this.getElementProps()).placeholder; From 1bed591d39b2317980fbb7f9d4b2c2e35fb04c41 Mon Sep 17 00:00:00 2001 From: Miki Leib <38354019+M-i-k-e-l@users.noreply.github.com> Date: Sun, 9 Jul 2023 12:25:10 +0300 Subject: [PATCH 14/26] Add Incubator.Slider driver (#2629) --- jestSetup/jest-setup.js | 1 + src/incubator/Slider/Slider.driver.ts | 6 ++++ src/incubator/Slider/__tests__/index.spec.tsx | 36 +++++++++++++++++++ src/incubator/Slider/index.tsx | 4 +-- src/testkit/index.ts | 1 + 5 files changed, 46 insertions(+), 2 deletions(-) create mode 100644 src/incubator/Slider/Slider.driver.ts create mode 100644 src/incubator/Slider/__tests__/index.spec.tsx diff --git a/jestSetup/jest-setup.js b/jestSetup/jest-setup.js index 0da842324c..39dc7a15fb 100644 --- a/jestSetup/jest-setup.js +++ b/jestSetup/jest-setup.js @@ -72,6 +72,7 @@ jest.mock('react-native-gesture-handler', PanMock.type = 'pan'; PanMock.onStart = getDefaultMockedHandler('onStart'); + PanMock.onBegin = getDefaultMockedHandler('onBegin'); PanMock.onUpdate = getDefaultMockedHandler('onUpdate'); PanMock.onEnd = getDefaultMockedHandler('onEnd'); PanMock.onFinalize = getDefaultMockedHandler('onFinalize'); diff --git a/src/incubator/Slider/Slider.driver.ts b/src/incubator/Slider/Slider.driver.ts new file mode 100644 index 0000000000..098d972f6c --- /dev/null +++ b/src/incubator/Slider/Slider.driver.ts @@ -0,0 +1,6 @@ +import {SliderProps} from './index'; +import {ComponentDriver} from '../../testkit'; + +export class SliderDriver extends ComponentDriver { + isDisabled = async () => (await this.getElementProps()).accessibilityState?.disabled === true; +} diff --git a/src/incubator/Slider/__tests__/index.spec.tsx b/src/incubator/Slider/__tests__/index.spec.tsx new file mode 100644 index 0000000000..5ce58f52f2 --- /dev/null +++ b/src/incubator/Slider/__tests__/index.spec.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import Slider, {SliderProps} from '../index'; +import {SliderDriver} from '../Slider.driver'; + +describe('Slider', () => { + afterEach(() => { + SliderDriver.clear(); + }); + + const sliderDriver = (testID: string, props: Partial) => { + const defaultProps: Partial = { + testID, + onValueChange: jest.fn(), + disabled: false + }; + const component = (); + return new SliderDriver({ + component, + testID + }); + }; + + it('Should be disabled', async () => { + const testId = 'slider-comp'; + const driver = await sliderDriver(testId, {disabled: true}); + + expect(await driver.isDisabled()).toBe(true); + }); + + it('Should be disabled', async () => { + const testId = 'slider-comp'; + const driver = await sliderDriver(testId, {disabled: false}); + + expect(await driver.isDisabled()).toBe(false); + }); +}); diff --git a/src/incubator/Slider/index.tsx b/src/incubator/Slider/index.tsx index 873fe16471..dd1ea3c228 100644 --- a/src/incubator/Slider/index.tsx +++ b/src/incubator/Slider/index.tsx @@ -1,6 +1,6 @@ import _ from 'lodash'; import React, {useImperativeHandle, useCallback, useMemo, useEffect, ReactElement} from 'react'; -import {StyleSheet, AccessibilityRole, StyleProp, ViewStyle, GestureResponderEvent, LayoutChangeEvent, ViewProps} from 'react-native'; +import {StyleSheet, AccessibilityRole, StyleProp, ViewStyle, GestureResponderEvent, LayoutChangeEvent, ViewProps, AccessibilityProps} from 'react-native'; import {useSharedValue, useAnimatedStyle, runOnJS, useAnimatedReaction, withTiming} from 'react-native-reanimated'; import {forwardRef, ForwardRefInjectedProps, Constants} from '../../commons/new'; import {extractAccessibilityProps} from '../../commons/modifiers'; @@ -16,7 +16,7 @@ import { import Thumb from './Thumb'; import Track from './Track'; -export type SliderProps = { +export interface SliderProps extends AccessibilityProps { /** * Initial value */ diff --git a/src/testkit/index.ts b/src/testkit/index.ts index 434b3a0871..0c2428e0d1 100644 --- a/src/testkit/index.ts +++ b/src/testkit/index.ts @@ -12,3 +12,4 @@ export {RadioButtonDriver} from '../components/radioButton/RadioButton.driver'; export {RadioGroupDriver} from '../components/radioGroup/RadioGroup.driver'; export {SortableListItemDriver} from '../components/sortableList/SortableListItem.driver'; export {SliderDriver} from '../components/slider/slider.driver'; +export {SliderDriver as IncubatorSliderDriver} from '../incubator/Slider/Slider.driver'; From b5a790219efdec54ac6fc19d167c473e7ba0950d Mon Sep 17 00:00:00 2001 From: Miki Leib <38354019+M-i-k-e-l@users.noreply.github.com> Date: Tue, 11 Jul 2023 10:28:00 +0300 Subject: [PATCH 15/26] Feat/color picker add full color to result (#2651) * ColorPicker - small fixes * ColorSwatch - refactor result * Add hexString * Rename to value * Rename to value 2 * Rename to ColorInfo --- .../componentScreens/ColorPickerScreen.tsx | 10 +++++----- src/components/colorPalette/ColorPalette.api.json | 2 +- src/components/colorPalette/index.tsx | 8 ++++---- src/components/colorPicker/colorPicker.api.json | 2 +- src/components/colorSwatch/colorSwatch.api.json | 2 +- src/components/colorSwatch/index.tsx | 15 +++++++++++++-- src/index.ts | 2 +- 7 files changed, 26 insertions(+), 15 deletions(-) diff --git a/demo/src/screens/componentScreens/ColorPickerScreen.tsx b/demo/src/screens/componentScreens/ColorPickerScreen.tsx index c3cb27cc69..2912311fef 100644 --- a/demo/src/screens/componentScreens/ColorPickerScreen.tsx +++ b/demo/src/screens/componentScreens/ColorPickerScreen.tsx @@ -1,7 +1,7 @@ import _ from 'lodash'; import React, {Component} from 'react'; import {StyleSheet, ScrollView} from 'react-native'; -import {Colors, View, Text, ColorPicker, ColorPalette, ColorName} from 'react-native-ui-lib'; +import {Colors, View, Text, ColorPicker, ColorPalette, ColorName, ColorInfo} from 'react-native-ui-lib'; import {renderMultipleSegmentOptions} from '../ExampleScreenPresenter'; interface State { @@ -44,12 +44,12 @@ export default class ColorPickerScreen extends Component<{}, State> { this.setState({color, textColor, customColors: _.clone(customColors), paletteChange: false}); }; - onValueChange = (value: string, options: object) => { - this.setState({color: value, textColor: options ? _.get(options, 'tintColor') : undefined, paletteChange: false}); + onValueChange = (value: string, colorInfo: ColorInfo) => { + this.setState({color: value, textColor: colorInfo?.tintColor, paletteChange: false}); }; - onPaletteValueChange = (value: string, options: object) => { - this.setState({color: value, textColor: options ? _.get(options, 'tintColor') : undefined, paletteChange: true}); + onPaletteValueChange = (value: string, colorInfo: ColorInfo) => { + this.setState({color: value, textColor: colorInfo?.tintColor, paletteChange: true}); }; render() { diff --git a/src/components/colorPalette/ColorPalette.api.json b/src/components/colorPalette/ColorPalette.api.json index ffe59f90a3..63ffb0754d 100644 --- a/src/components/colorPalette/ColorPalette.api.json +++ b/src/components/colorPalette/ColorPalette.api.json @@ -31,7 +31,7 @@ }, { "name": "onValueChange", - "type": "(value: string, options: object) => void", + "type": "(value: string, colorInfo: ColorInfo) => void", "description": "Invoked once when value changes by selecting one of the swatches in the palette" }, {"name": "swatchStyle", "type": "ViewStyle", "description": "Style to pass all the ColorSwatches in the palette"}, diff --git a/src/components/colorPalette/index.tsx b/src/components/colorPalette/index.tsx index 9fee5a6734..a3693539c2 100644 --- a/src/components/colorPalette/index.tsx +++ b/src/components/colorPalette/index.tsx @@ -8,7 +8,7 @@ import View from '../view'; import Carousel from '../carousel'; import ScrollBar from '../scrollBar'; import PageControl from '../pageControl'; -import ColorSwatch, {SWATCH_SIZE, SWATCH_MARGIN} from '../colorSwatch'; +import ColorSwatch, {ColorSwatchProps, ColorInfo, SWATCH_SIZE, SWATCH_MARGIN} from '../colorSwatch'; interface Props { /** @@ -50,7 +50,7 @@ interface Props { /** * Invoked once when value changes by selecting one of the swatches in the palette */ - onValueChange?: (value: string, options: object) => void; + onValueChange?: ColorSwatchProps['onPress']; style?: StyleProp; testID?: string; /** @@ -261,8 +261,8 @@ class ColorPalette extends PureComponent { this.setState({currentPage: index}); }; - onValueChange = (value: string, options: object) => { - this.props.onValueChange?.(value, options); + onValueChange = (value: string, colorInfo: ColorInfo) => { + this.props.onValueChange?.(value, colorInfo); }; getHorizontalMargins = (index: number) => { diff --git a/src/components/colorPicker/colorPicker.api.json b/src/components/colorPicker/colorPicker.api.json index 8e3703dbce..206c3cfbce 100644 --- a/src/components/colorPicker/colorPicker.api.json +++ b/src/components/colorPicker/colorPicker.api.json @@ -22,7 +22,7 @@ }, { "name": "onValueChange", - "type": "(value: string, options: object) => void", + "type": "(value: string, colorInfo: ColorInfo) => void", "description": "Callback for the picker's color palette change" }, { diff --git a/src/components/colorSwatch/colorSwatch.api.json b/src/components/colorSwatch/colorSwatch.api.json index 7354014ac1..de2fd14b28 100644 --- a/src/components/colorSwatch/colorSwatch.api.json +++ b/src/components/colorSwatch/colorSwatch.api.json @@ -16,7 +16,7 @@ {"name": "color", "type": "string", "description": "The color of the ColorSwatch"}, {"name": "selected", "type": "boolean", "description": "Is the initial state is selected"}, {"name": "animated", "type": "boolean", "description": "Is first render should be animated"}, - {"name": "onPress", "type": "(value: string, options: object) => void", "description": "Callback from press event"}, + {"name": "onPress", "type": "(value: string, colorInfo: ColorInfo) => void", "description": "Callback from press event"}, {"name": "index", "type": "number", "description": "The index of the Swatch if in array"}, {"name": "style", "type": "ViewStyle", "description": "Component's style"}, {"name": "testID", "type": "string", "description": "The test id for e2e tests"}, diff --git a/src/components/colorSwatch/index.tsx b/src/components/colorSwatch/index.tsx index b9b9050da3..f71a479797 100644 --- a/src/components/colorSwatch/index.tsx +++ b/src/components/colorSwatch/index.tsx @@ -7,6 +7,15 @@ import View from '../view'; import TouchableOpacity from '../touchableOpacity'; import Image from '../image'; +export interface ColorInfo { + index?: number; + tintColor?: string; + /** + * The color result with 6 characters (#FFFFFF and never #FFF) + */ + hexString: string; +} + interface Props { /** * The identifier value of the ColorSwatch in a ColorSwatch palette. @@ -32,7 +41,7 @@ interface Props { /** * onPress callback */ - onPress?: (value: string, options: object) => void; + onPress?: (value: string, colorInfo: ColorInfo) => void; index?: number; style?: StyleProp; testID?: string; @@ -111,7 +120,9 @@ class ColorSwatch extends PureComponent { onPress = () => { const {color = '', value, index} = this.props; const tintColor = this.getTintColor(value); - this.props.onPress?.(value || color, {tintColor, index}); + const result = value || color; + const hexString = Colors.getHexString(result); + this.props.onPress?.(result, {tintColor, index, hexString}); }; getTintColor(color?: string) { diff --git a/src/index.ts b/src/index.ts index cfcd7905d2..c1b840e604 100644 --- a/src/index.ts +++ b/src/index.ts @@ -65,7 +65,7 @@ export {default as Chip, ChipProps} from './components/chip'; export {default as ColorPicker, ColorPickerProps} from './components/colorPicker'; export {default as ColorPalette, ColorPaletteProps} from './components/colorPalette'; export {default as ColorSliderGroup, ColorSliderGroupProps} from './components/slider/ColorSliderGroup'; -export {default as ColorSwatch, ColorSwatchProps} from './components/colorSwatch'; +export {default as ColorSwatch, ColorSwatchProps, ColorInfo} from './components/colorSwatch'; export {default as ConnectionStatusBar, ConnectionStatusBarProps} from './components/connectionStatusBar'; export {default as Dash, DashProps} from './components/dash'; export {default as DateTimePicker, DateTimePickerProps, DateTimePickerMode} from './components/dateTimePicker'; From ba7fa37fb3d52c474ee4f6c280df70376cca0b6d Mon Sep 17 00:00:00 2001 From: Lidor Dafna <66782556+lidord-wix@users.noreply.github.com> Date: Tue, 11 Jul 2023 13:09:49 +0300 Subject: [PATCH 16/26] Infra/ remove useCustomTheme leftovers (#2666) --- demo/src/screens/ExampleScreenPresenter.tsx | 3 --- src/components/dateTimePicker/index.tsx | 3 --- src/components/stepper/index.tsx | 7 +------ 3 files changed, 1 insertion(+), 12 deletions(-) diff --git a/demo/src/screens/ExampleScreenPresenter.tsx b/demo/src/screens/ExampleScreenPresenter.tsx index 2bbcf413da..638de2cf70 100644 --- a/demo/src/screens/ExampleScreenPresenter.tsx +++ b/demo/src/screens/ExampleScreenPresenter.tsx @@ -53,7 +53,6 @@ export function renderBooleanOption(title: string, {title} { return ( diff --git a/src/components/stepper/index.tsx b/src/components/stepper/index.tsx index b842d4c798..8c4fd94ff6 100644 --- a/src/components/stepper/index.tsx +++ b/src/components/stepper/index.tsx @@ -54,10 +54,6 @@ interface Props { * Test id for component */ testID?: string; - /** - * useCustomTheme for component. - */ - useCustomTheme?: boolean; } export type StepperProps = Props; @@ -173,7 +169,7 @@ class Stepper extends PureComponent { } renderButton(actionType: ActionType) { - const {disabled, small, testID, useCustomTheme} = this.props; + const {disabled, small, testID} = this.props; const allowStepChange = this.allowStepChange(actionType); const minusButton = small ? minusOutlineSmall : minusOutline; const plusButton = small ? plusOutlineSmall : plusOutline; @@ -186,7 +182,6 @@ class Stepper extends PureComponent { disabled={disabled || !allowStepChange} onPress={() => this.handleStepChange(actionType)} testID={actionType === ActionType.MINUS ? `${testID}.minusStep` : `${testID}.plusStep`} - useCustomTheme={useCustomTheme} /> ); } From d2d7e8547e8c6742fce0c3ad41f7facadf782836 Mon Sep 17 00:00:00 2001 From: Oren Zakay Date: Tue, 11 Jul 2023 15:47:42 +0300 Subject: [PATCH 17/26] feat: [MAW-257] svgImage support tintColor on web (#2667) * feat: [MAW-257] svgImage support tintColor on web * use StyleSheet.flatten --- src/components/svgImage/index.web.tsx | 42 +++++++++++++++------------ webDemo/src/App.tsx | 22 ++------------ 2 files changed, 26 insertions(+), 38 deletions(-) diff --git a/src/components/svgImage/index.web.tsx b/src/components/svgImage/index.web.tsx index 6acf2c956e..1883eb5091 100644 --- a/src/components/svgImage/index.web.tsx +++ b/src/components/svgImage/index.web.tsx @@ -1,8 +1,8 @@ import React, {useState} from 'react'; +import {StyleSheet} from 'react-native'; import Image from '../image'; import {isSvg, isSvgUri, isBase64ImageContent} from '../../utils/imageUtils'; -const EMPTY_STYLE = '{}'; const DEFAULT_SIZE = 16; export interface SvgImageProps { /** @@ -14,22 +14,23 @@ export interface SvgImageProps { } function SvgImage(props: SvgImageProps) { - const {data, style = {}, tintColor, ...others} = props; - const [svgStyleCss, setSvgStyleCss] = useState(EMPTY_STYLE); + const {data, style = [], tintColor, ...others} = props; + const [svgStyleCss, setSvgStyleCss] = useState(undefined); const [postCssStyleCalled, setPostCssStyleCalled] = useState(false); - const styleObj = JSON.parse(JSON.stringify(style)); - - const createStyleSvgCss = async (PostCssPackage: {postcss: any; cssjs: any}) => { + const createStyleSvgCss = async (PostCssPackage: {postcss: any; cssjs: any}, styleObj?: Record) => { setPostCssStyleCalled(true); const {postcss, cssjs} = PostCssPackage; postcss() .process(styleObj, {parser: cssjs}) - .then((style: {css: any}) => setSvgStyleCss(`{${style.css}}`)); + .then((style: {css: any}) => { + const svgPathCss = (styleObj?.tintColor) ? `svg path {fill: ${styleObj?.tintColor}}` : ''; + setSvgStyleCss(`svg {${style.css}} ${svgPathCss}}`); + }); }; if (isSvgUri(data)) { - return ; + return ; } else if (isBase64ImageContent(data)) { if (tintColor) { return ( @@ -42,23 +43,26 @@ function SvgImage(props: SvgImageProps) { /> ); } - return ; + return ; } else if (data) { + const PostCssPackage = require('../../optionalDependencies').PostCssPackage; if (PostCssPackage) { if (!postCssStyleCalled) { - createStyleSvgCss(PostCssPackage); + createStyleSvgCss(PostCssPackage, StyleSheet.flatten(style)); return null; } - const svgStyleTag = ``; - - return ( -
- ); + + if (svgStyleCss) { + const svgStyleTag = ``; + return ( +
+ ); + } } } return null; diff --git a/webDemo/src/App.tsx b/webDemo/src/App.tsx index 5d0e72fa67..aaf8581185 100644 --- a/webDemo/src/App.tsx +++ b/webDemo/src/App.tsx @@ -38,24 +38,7 @@ interface ItemToRender { title: string, FC: React.FC } -const svgData = ` - - - - - - - - - - - - - - - - -`; +const svgData = '\n \n \n \n\n'; const itemsToRender: ItemToRender[] = [ { @@ -118,7 +101,8 @@ const itemsToRender: ItemToRender[] = [ iconSource={svgData} iconStyle={{ width: 24, - height: 24 + height: 24, + tintColor: 'red' }} /> ) From 2c4e5e7fa5853e5befb991245a448644ac493087 Mon Sep 17 00:00:00 2001 From: Miki Leib <38354019+M-i-k-e-l@users.noreply.github.com> Date: Wed, 12 Jul 2023 17:39:35 +0300 Subject: [PATCH 18/26] Picker fix for validateForm (#2661) --- src/components/picker/helpers/useImperativePickerHandle.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/picker/helpers/useImperativePickerHandle.ts b/src/components/picker/helpers/useImperativePickerHandle.ts index 5ce3fb6e27..594a26c06d 100644 --- a/src/components/picker/helpers/useImperativePickerHandle.ts +++ b/src/components/picker/helpers/useImperativePickerHandle.ts @@ -6,7 +6,7 @@ const useImperativePickerHandle = (ref: React.Ref, expandableRef: React.MutableRefObject) => { const pickerRef = useRef(); useImperativeHandle(ref, () => { - const {isFocused, focus, blur, clear, validate} = pickerRef.current ?? {}; + const {isFocused, focus, blur, clear, validate, isValid} = pickerRef.current ?? {}; // @ts-expect-error useRef return type is possible null therefor it throw TS error const {openExpandable, closeExpandable, toggleExpandable} = expandableRef.current; @@ -16,6 +16,7 @@ const useImperativePickerHandle = (ref: React.Ref, blur, clear, validate, + isValid, openExpandable, closeExpandable, toggleExpandable From fd15874541435079b5597cfb7c1bc4b9bf06b848 Mon Sep 17 00:00:00 2001 From: Miki Leib <38354019+M-i-k-e-l@users.noreply.github.com> Date: Wed, 12 Jul 2023 19:08:29 +0300 Subject: [PATCH 19/26] NumberInput - fix editable (#2664) --- src/components/numberInput/index.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/numberInput/index.tsx b/src/components/numberInput/index.tsx index ffd06be113..5e80dc4994 100644 --- a/src/components/numberInput/index.tsx +++ b/src/components/numberInput/index.tsx @@ -191,6 +191,7 @@ function NumberInput(props: NumberInputProps, ref: any) { onChangeText={onChangeText} contextMenuHidden={contextMenuHidden} onBlur={onBlur} + editable={textFieldProps?.editable} /> ); } From 4b9f11b6fccf6efcd3db031f1158002cb930c6a5 Mon Sep 17 00:00:00 2001 From: Adi Mordo Date: Thu, 13 Jul 2023 08:54:44 +0300 Subject: [PATCH 20/26] Picker example for the webDemo (#2622) * Picker exmaple for the webDemo * new picker dialog example * Fix svg example * fixed review comments --------- Co-authored-by: lidord-wix --- webDemo/src/App.tsx | 12 ++-- webDemo/src/examples/Picker.tsx | 122 ++++++++++++++++++++++++++------ 2 files changed, 106 insertions(+), 28 deletions(-) diff --git a/webDemo/src/App.tsx b/webDemo/src/App.tsx index aaf8581185..d680cf7fd0 100644 --- a/webDemo/src/App.tsx +++ b/webDemo/src/App.tsx @@ -358,14 +358,14 @@ function App() { return ( - + Welcome to react-native-ui-lib for Web { itemsToRender.map(({title, FC}: ItemToRender) => ( - + {title} @@ -387,14 +387,9 @@ const styles = StyleSheet.create({ padding: 20 }, componentContainer: { - backgroundColor: '#F5FCFF', width: '100%', - alignItems: 'center', - justifyContent: 'center', borderColor: 'white', - borderBottomWidth: 5, - paddingBottom: 20, - paddingTop: 20 + borderBottomWidth: 5 }, compTitle: { fontWeight: 'bold', @@ -409,4 +404,5 @@ const styles = StyleSheet.create({ textAlign: 'center' } }); + export default App; diff --git a/webDemo/src/examples/Picker.tsx b/webDemo/src/examples/Picker.tsx index e0d6baf61c..6a79d1624c 100644 --- a/webDemo/src/examples/Picker.tsx +++ b/webDemo/src/examples/Picker.tsx @@ -1,6 +1,6 @@ import React, {useState} from 'react'; -import Picker from 'react-native-ui-lib/Picker'; -import {Colors} from 'react-native-ui-lib/style'; +import {ScrollView} from 'react-native-gesture-handler'; +import {Picker, Colors, View, Text, Incubator, PickerProps, PanningProvider} from 'react-native-ui-lib'; const options = [ {label: 'JavaScript', value: 'js'}, @@ -10,28 +10,110 @@ const options = [ {label: 'Perl', value: 'perl'} ]; -const PickerWrapper = () => { +const filters = [ + {value: 1, label: 'All'}, + {value: 2, label: 'Accessories'}, + {value: 3, label: 'Outwear'}, + {value: 4, label: 'Footwear'}, + {value: 5, label: 'Swimwear'}, + {value: 6, label: 'Tops'} +]; + +const schemes = [ + {label: 'Default', value: 1}, + {label: 'Light', value: 2}, + {label: 'Dark', value: 3} +]; +const PickerWrapper = () => { const [language, setLanguage] = useState(undefined); + const [filter, setFilter] = useState(undefined); + const [customModalValues, setCustomModalValues] = useState(undefined); + const renderDialog: PickerProps['renderCustomModal'] = (modalProps: any) => { + const {visible, children, toggleModal, onDone} = modalProps; + + return ( + { + onDone(); + toggleModal(false); + }} + width="40%" + height="45%" + bottom + useSafeArea + containerStyle={{backgroundColor: Colors.$backgroundDefault}} + direction={PanningProvider.Directions.DOWN} + headerProps={{title: 'Custom modal'}} + > + {children} + + ); + }; return ( - - {options.map(option => ( - - ))} - + + + Single Value Picker + + + + {options.map(option => ( + + ))} + + + + Multi Value Picker + + + + {filters.map(filter => ( + + ))} + + + + Dialog Picker + + + setCustomModalValues(items)} + mode={Picker.modes.MULTI} + renderCustomModal={renderDialog} + > + {schemes.map(option => ( + + ))} + + + ); }; From a95c781b45fc64538679115b18fa922ca277038a Mon Sep 17 00:00:00 2001 From: Miki Leib <38354019+M-i-k-e-l@users.noreply.github.com> Date: Sun, 16 Jul 2023 06:47:12 +0300 Subject: [PATCH 21/26] testkit - new iteration (#2655) * testkit - new iteration * Minor improvements * Add DEFAULT_LIST_ITEM_HEIGHT and SortableListDriver * Flatten actions --- jestSetup/GestureDetectorMock.tsx | 15 +++- .../sortableList/SortableList.driver.new.ts | 5 ++ .../SortableListItem.driver.new.ts | 39 +++++++++++ .../sortableList/SortableListItem.tsx | 2 + .../sortableList/__tests__/index.spec.tsx | 24 +++---- src/components/sortableList/index.tsx | 4 +- src/components/text/Text.driver.new.ts | 13 ++++ .../text/__tests__/index.driver.spec.tsx | 50 +++++++------- src/testkit/new/Component.driver.ts | 45 ++++++++++++ src/testkit/new/useDraggable.driver.ts | 42 +++++++++++ src/testkit/new/usePressable.driver.ts | 69 +++++++++++++++++++ 11 files changed, 264 insertions(+), 44 deletions(-) create mode 100644 src/components/sortableList/SortableList.driver.new.ts create mode 100644 src/components/sortableList/SortableListItem.driver.new.ts create mode 100644 src/components/text/Text.driver.new.ts create mode 100644 src/testkit/new/Component.driver.ts create mode 100644 src/testkit/new/useDraggable.driver.ts create mode 100644 src/testkit/new/usePressable.driver.ts diff --git a/jestSetup/GestureDetectorMock.tsx b/jestSetup/GestureDetectorMock.tsx index 57e2827a5c..4581c45066 100644 --- a/jestSetup/GestureDetectorMock.tsx +++ b/jestSetup/GestureDetectorMock.tsx @@ -34,8 +34,8 @@ export class GestureDetectorMock extends React.Component { ); case 'pan': return ( - { + { this.props.gesture._handlers.onStart?.(DEFAULT_DATA); if (Array.isArray(data)) { data.forEach(info => { @@ -49,10 +49,19 @@ export class GestureDetectorMock extends React.Component { }} > {this.props.children} - + ); default: throw new Error(`Unhandled gesture of type: ${this.props.gesture.type}`); } } } + +class CaptureEvent extends React.Component<{ + onPan: (event: typeof DEFAULT_DATA) => void; + children: JSX.Element; +}> { + render() { + return this.props.children; + } +} diff --git a/src/components/sortableList/SortableList.driver.new.ts b/src/components/sortableList/SortableList.driver.new.ts new file mode 100644 index 0000000000..c4c1b1bde2 --- /dev/null +++ b/src/components/sortableList/SortableList.driver.new.ts @@ -0,0 +1,5 @@ +import {useComponentDriver, ComponentProps} from '../../testkit/new/Component.driver'; + +export const SortableListDriver = (props: ComponentProps) => { + return useComponentDriver(props); +}; diff --git a/src/components/sortableList/SortableListItem.driver.new.ts b/src/components/sortableList/SortableListItem.driver.new.ts new file mode 100644 index 0000000000..b5654094a5 --- /dev/null +++ b/src/components/sortableList/SortableListItem.driver.new.ts @@ -0,0 +1,39 @@ +import _ from 'lodash'; +import {useComponentDriver, ComponentProps} from '../../testkit/new/Component.driver'; +import {useDraggableDriver} from '../../testkit/new/useDraggable.driver'; +import {SortableListItemProps} from './types'; +import {DEFAULT_LIST_ITEM_HEIGHT} from './SortableListItem'; + +export const SortableListItemDriver = (props: ComponentProps) => { + const driver = useDraggableDriver(useComponentDriver(props)); + + const dragUp = async (indices: number) => { + validateIndices(indices); + const data = _.times(indices, index => { + return { + translationY: -DEFAULT_LIST_ITEM_HEIGHT * (index + 1) + }; + }); + + driver.drag(data); + }; + + const dragDown = async (indices: number) => { + validateIndices(indices); + const data = _.times(indices, index => { + return { + translationY: DEFAULT_LIST_ITEM_HEIGHT * (index + 1) + }; + }); + + driver.drag(data); + }; + + const validateIndices = (indices: number) => { + if (indices <= 0 || !Number.isInteger(indices)) { + throw Error('indices must be a positive integer'); + } + }; + + return {...driver, dragUp, dragDown}; +}; diff --git a/src/components/sortableList/SortableListItem.tsx b/src/components/sortableList/SortableListItem.tsx index 1574f3b471..3e4b07ef3f 100644 --- a/src/components/sortableList/SortableListItem.tsx +++ b/src/components/sortableList/SortableListItem.tsx @@ -24,6 +24,8 @@ export interface InternalSortableListItemProps { type Props = PropsWithChildren; +export const DEFAULT_LIST_ITEM_HEIGHT = 52; + const animationConfig = { easing: Easing.inOut(Easing.ease), duration: 350 diff --git a/src/components/sortableList/__tests__/index.spec.tsx b/src/components/sortableList/__tests__/index.spec.tsx index 3c9a86607c..f7182dae0b 100644 --- a/src/components/sortableList/__tests__/index.spec.tsx +++ b/src/components/sortableList/__tests__/index.spec.tsx @@ -1,9 +1,11 @@ import _ from 'lodash'; import React, {useCallback} from 'react'; +import {render} from '@testing-library/react-native'; import Text from '../../text'; import View from '../../view'; import SortableList from '../index'; -import {ComponentDriver, SortableListItemDriver} from '../../../testkit'; +import {SortableListDriver} from '../SortableList.driver.new'; +import {SortableListItemDriver} from '../SortableListItem.driver.new'; const defaultProps = { testID: 'sortableList' @@ -35,21 +37,13 @@ const TestCase = props => { }; describe('SortableList', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - afterEach(() => { - ComponentDriver.clear(); - SortableListItemDriver.clear(); - }); - it('SortableList onOrderChange is called - down', async () => { const onOrderChange = jest.fn(); - const component = ; - const sortableListDriver = new ComponentDriver({component, testID: 'sortableList'}); + const renderTree = render(); + const sortableListDriver = SortableListDriver({renderTree, testID: 'sortableList'}); expect(await sortableListDriver.exists()).toBeTruthy(); expect(onOrderChange).toHaveBeenCalledTimes(0); - const item1Driver = new SortableListItemDriver({component, testID: 'item1'}); + const item1Driver = SortableListItemDriver({renderTree, testID: 'item1'}); expect(await item1Driver.exists()).toBeTruthy(); await item1Driver.dragDown(1); expect(onOrderChange).toHaveBeenCalledTimes(1); @@ -64,11 +58,11 @@ describe('SortableList', () => { it('SortableList onOrderChange is called - up', async () => { const onOrderChange = jest.fn(); - const component = ; - const sortableListDriver = new ComponentDriver({component, testID: 'sortableList'}); + const renderTree = render(); + const sortableListDriver = SortableListDriver({renderTree, testID: 'sortableList'}); expect(await sortableListDriver.exists()).toBeTruthy(); expect(onOrderChange).toHaveBeenCalledTimes(0); - const item4Driver = new SortableListItemDriver({component, testID: 'item4'}); + const item4Driver = SortableListItemDriver({renderTree, testID: 'item4'}); expect(await item4Driver.exists()).toBeTruthy(); await item4Driver.dragUp(3); expect(onOrderChange).toHaveBeenCalledTimes(1); diff --git a/src/components/sortableList/index.tsx b/src/components/sortableList/index.tsx index aa9cf79104..71549cdfd0 100644 --- a/src/components/sortableList/index.tsx +++ b/src/components/sortableList/index.tsx @@ -5,7 +5,7 @@ import {FlatList, LayoutChangeEvent} from 'react-native'; import {useSharedValue} from 'react-native-reanimated'; import {GestureHandlerRootView} from 'react-native-gesture-handler'; import SortableListContext from './SortableListContext'; -import SortableListItem from './SortableListItem'; +import SortableListItem, {DEFAULT_LIST_ITEM_HEIGHT} from './SortableListItem'; import {useDidUpdate, useThemeProps} from 'hooks'; import {SortableListProps, SortableListItemProps} from './types'; export {SortableListProps, SortableListItemProps}; @@ -26,7 +26,7 @@ const SortableList = (props: SortableListPr const itemsOrder = useSharedValue(generateItemsOrder(data)); const lockedIds = useSharedValue>(generateLockedIds(data)); - const itemHeight = useSharedValue(52); + const itemHeight = useSharedValue(DEFAULT_LIST_ITEM_HEIGHT); useDidUpdate(() => { itemsOrder.value = generateItemsOrder(data); diff --git a/src/components/text/Text.driver.new.ts b/src/components/text/Text.driver.new.ts new file mode 100644 index 0000000000..e8da152f1d --- /dev/null +++ b/src/components/text/Text.driver.new.ts @@ -0,0 +1,13 @@ +import {TextProps} from './index'; +import {useComponentDriver, ComponentProps} from '../../testkit/new/Component.driver'; +import {usePressableDriver} from '../../testkit/new/usePressable.driver'; + +export const TextDriver = (props: ComponentProps) => { + const driver = usePressableDriver(useComponentDriver(props)); + + const getText = () => { + return driver.getProps().children; + }; + + return {...driver, getText}; +}; diff --git a/src/components/text/__tests__/index.driver.spec.tsx b/src/components/text/__tests__/index.driver.spec.tsx index 8cdea62136..0d63a47a2e 100644 --- a/src/components/text/__tests__/index.driver.spec.tsx +++ b/src/components/text/__tests__/index.driver.spec.tsx @@ -1,44 +1,46 @@ import React from 'react'; +import {render} from '@testing-library/react-native'; import View from '../../view'; import Text from '../index'; -import {TextDriver} from '../Text.driver'; +import {TextDriver} from '../Text.driver.new'; const TEXT_ID = 'text_test_id'; const TEXT_CONTENT = 'text content'; -describe('Text', () => { - afterEach(() => { - TextDriver.clear(); - }); - it('should render Text Component', async () => { - const component = WrapperScreenWithText(); - const textDriver = new TextDriver({component, testID: TEXT_ID}); - const content = await textDriver.getTextContent(); +function WrapperScreenWithText(textProps: {onPress?: jest.Mock} = {}) { + const {onPress} = textProps; + return ( + + + {TEXT_CONTENT} + + + ); +} + +describe('Text', () => { + it('should render Text Component', () => { + const renderTree = render(); + const textDriver = TextDriver({renderTree, testID: TEXT_ID}); + const content = textDriver.getText(); expect(content).toEqual(TEXT_CONTENT); }); describe('onPress', () => { - it('should press the text, and run callback', async () => { + it('should press the text, and run callback', () => { const onPressCallback = jest.fn(); - const component = WrapperScreenWithText({onPress: onPressCallback}); - const textDriver = new TextDriver({component, testID: TEXT_ID}); + const renderTree = render(); + const textDriver = TextDriver({renderTree, testID: TEXT_ID}); - await textDriver.press(); + textDriver.press(); expect(onPressCallback).toHaveBeenCalledTimes(1); }); - it('should not be pressable if onPress prop is not supplied', async () => { - const component = WrapperScreenWithText(); - const textDriver = new TextDriver({component, testID: TEXT_ID}); - expect(await textDriver.isPressable()).toBeFalsy(); + it('should not be pressable if onPress prop is not supplied', () => { + const renderTree = render(); + const textDriver = TextDriver({renderTree, testID: TEXT_ID}); + expect(textDriver.hasOnPress()).toBeFalsy(); }); }); }); - -function WrapperScreenWithText(textProps: {onPress?: jest.Mock} = {}) { - const {onPress} = textProps; - return ( - {TEXT_CONTENT} - ); -} diff --git a/src/testkit/new/Component.driver.ts b/src/testkit/new/Component.driver.ts new file mode 100644 index 0000000000..bf534bcc02 --- /dev/null +++ b/src/testkit/new/Component.driver.ts @@ -0,0 +1,45 @@ +import {ReactTestInstance} from 'react-test-renderer'; +import {RenderResult} from '@testing-library/react-native'; + +export interface ComponentProps { + renderTree: RenderResult; + testID: string; +} + +export interface ComponentDriverResult { + getElement: () => ReactTestInstance; + exists: () => boolean; + getProps: () => Props; +} + +export const useComponentDriver = (props: ComponentProps): ComponentDriverResult => { + const {renderTree, testID} = props; + + const getElement = (): ReactTestInstance => { + const element = renderTree.queryByTestId(testID); + if (element) { + return element; + } else { + throw new Error(`Could not find element with testID: ${testID}`); + } + }; + + const exists = (): boolean => { + try { + getElement(); + return true; + } catch { + return false; + } + }; + + const getProps = (): Props => { + return getElement().props as Props; + }; + + return {getElement, exists, getProps}; +}; + +export const ComponentDriver = (props: ComponentProps): ComponentDriverResult => { + return useComponentDriver(props); +}; diff --git a/src/testkit/new/useDraggable.driver.ts b/src/testkit/new/useDraggable.driver.ts new file mode 100644 index 0000000000..1ff57c6d04 --- /dev/null +++ b/src/testkit/new/useDraggable.driver.ts @@ -0,0 +1,42 @@ +import {fireEvent} from '@testing-library/react-native'; +import {ComponentDriverResult} from './Component.driver'; + +export type DragEvent = { + absoluteX?: number; + absoluteY?: number; + translationX?: number; + translationY?: number; + velocityX?: number; + velocityY?: number; + x?: number; + y?: number; +}; + +export interface DraggableDriverResult extends ComponentDriverResult { + drag: (distanceOrEvent: DragEvent | DragEvent[] | number) => void; +} + +export const useDraggableDriver = < + Props, + DriverProps extends ComponentDriverResult = ComponentDriverResult // Allows for chaining multiple drivers +>( + driver: DriverProps + ): DraggableDriverResult & DriverProps => { + const drag = (distanceOrEvent: DragEvent | DragEvent[] | number) => { + let data: DragEvent | DragEvent[]; + if (typeof distanceOrEvent === 'number') { + const distance = distanceOrEvent; + data = [{translationY: distance}]; + } else { + const event = distanceOrEvent; + data = event; + } + + fireEvent(driver.getElement(), 'onPan', data); + }; + + return { + ...driver, + drag + }; +}; diff --git a/src/testkit/new/usePressable.driver.ts b/src/testkit/new/usePressable.driver.ts new file mode 100644 index 0000000000..6e67a7deaf --- /dev/null +++ b/src/testkit/new/usePressable.driver.ts @@ -0,0 +1,69 @@ +import {fireEvent} from '@testing-library/react-native'; +import {ComponentDriverResult} from './Component.driver'; +import {PressableProps} from 'react-native'; + +export interface PressableDriverResult extends ComponentDriverResult { + press: () => void; + hasOnPress: () => boolean; + onPressIn: () => void; + hasOnPressIn: () => boolean; + onPressOut: () => void; + hasOnPressOut: () => boolean; + onLongPress: () => void; + hasOnLongPress: () => boolean; +} + +export type PressableDriverProps = Partial< + Pick +>; + +export const usePressableDriver = < + Props extends PressableDriverProps, + DriverProps extends ComponentDriverResult = ComponentDriverResult // Allows for chaining multiple drivers +>( + driver: DriverProps + ): PressableDriverResult & DriverProps => { + const press = () => { + fireEvent.press(driver.getElement()); + }; + + const hasOnPress = () => { + return typeof driver.getProps().onPress === 'function'; + }; + + const onPressIn = () => { + fireEvent(driver.getElement(), 'onPressIn'); + }; + + const hasOnPressIn = () => { + return typeof driver.getProps().onPressIn === 'function'; + }; + + const onPressOut = () => { + fireEvent(driver.getElement(), 'onPresonPressOutsIn'); + }; + + const hasOnPressOut = () => { + return typeof driver.getProps().onPressOut === 'function'; + }; + + const onLongPress = () => { + fireEvent(driver.getElement(), 'onLongPress'); + }; + + const hasOnLongPress = () => { + return typeof driver.getProps().onLongPress === 'function'; + }; + + return { + ...driver, + press, + hasOnPress, + onPressIn, + hasOnPressIn, + onPressOut, + hasOnPressOut, + onLongPress, + hasOnLongPress + }; +}; From 6ef2d315589addf7881fe8e6534eee27852b7c51 Mon Sep 17 00:00:00 2001 From: Miki Leib <38354019+M-i-k-e-l@users.noreply.github.com> Date: Sun, 16 Jul 2023 16:52:57 +0300 Subject: [PATCH 22/26] NumberInput - support focus (#2671) * NumberInput - support focus * Add focus color and fix screen onBlur --- .../componentScreens/NumberInputScreen.tsx | 4 ++-- src/components/numberInput/index.tsx | 22 +++++++++++++++---- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/demo/src/screens/componentScreens/NumberInputScreen.tsx b/demo/src/screens/componentScreens/NumberInputScreen.tsx index 96b3c0b11c..dd510f5489 100644 --- a/demo/src/screens/componentScreens/NumberInputScreen.tsx +++ b/demo/src/screens/componentScreens/NumberInputScreen.tsx @@ -161,8 +161,8 @@ const NumberInputScreen = () => { }, [label, textStyle, validate, validationMessage]); return ( - - + + Number Input diff --git a/src/components/numberInput/index.tsx b/src/components/numberInput/index.tsx index 5e80dc4994..30f8272373 100644 --- a/src/components/numberInput/index.tsx +++ b/src/components/numberInput/index.tsx @@ -1,7 +1,8 @@ import {isEmpty} from 'lodash'; import React, {useMemo, useCallback, useState, useRef} from 'react'; import {StyleSheet, StyleProp, ViewStyle, TextStyle} from 'react-native'; -import {useDidUpdate, useThemeProps} from 'hooks'; +import {useDidUpdate, useThemeProps} from '../../hooks'; +import {Colors} from '../../style'; import MaskedInput from '../maskedInput/new'; import TextField, {TextFieldProps, TextFieldRef} from '../../incubator/TextField'; import View from '../view'; @@ -97,6 +98,7 @@ function NumberInput(props: NumberInputProps, ref: any) { const initialNumber = getInitialNumber(propsInitialNumber, options); const [data, setData] = useState(parseInput(`${initialNumber}`, options, propsInitialNumber)); const textField = useRef(); + const [isFocused, setIsFocused] = useState(textFieldProps?.autoFocus ?? false); useDidUpdate(() => { setOptions(generateOptions(locale, fractionDigits)); @@ -154,11 +156,19 @@ function NumberInput(props: NumberInputProps, ref: any) { }, [data]); const onBlur = useCallback(() => { + setIsFocused(false); if (textFieldProps?.validateOnBlur) { textField.current?.validate(); } - }, - [textFieldProps?.validateOnBlur]); + }, [textFieldProps?.validateOnBlur]); + + const onFocus = useCallback(() => { + setIsFocused(true); + }, []); + + const dynamicFieldStyle = useCallback(() => { + return isFocused ? {borderBottomColor: Colors.$outlinePrimary} : undefined; + }, [isFocused]); const renderNumberInput = useCallback((value?: string) => { return ( @@ -170,16 +180,18 @@ function NumberInput(props: NumberInputProps, ref: any) { testID={`${testID}.visual`} value={value} formatter={formatter} + dynamicFieldStyle={dynamicFieldStyle} floatingPlaceholder={false} leadingAccessory={leadingAccessory} trailingAccessory={trailingAccessory} containerStyle={[styles.textFieldContainerStyle, textFieldProps?.containerStyle]} keyboardType={'numeric'} + autoFocus={false} /> ); }, - [containerStyle, formatter, leadingAccessory, textFieldProps, trailingAccessory, testID]); + [containerStyle, dynamicFieldStyle, formatter, leadingAccessory, textFieldProps, trailingAccessory, testID]); return ( ); } From 5376abcc8b70864f3ea77462232550c0e0c9ee11 Mon Sep 17 00:00:00 2001 From: israelko <85411297+israelko@users.noreply.github.com> Date: Mon, 17 Jul 2023 13:10:18 +0300 Subject: [PATCH 23/26] feat/enhance text highlight string (#2663) * extended the `Text` component `highlightString` prop, to allow it to also receive a `HighlightStringProps` object (or an array of such objects) that enables the user to handle a highlight string `onPress` event, provide a style per highlight string and give it a `testID` for testing * fix lint warning * fix docs * review fixes --- .../screens/componentScreens/TextScreen.tsx | 35 +- src/components/text/__tests__/index.spec.tsx | 218 +++++++ src/components/text/index.tsx | 26 +- src/components/text/text.api.json | 2 +- src/utils/__tests__/textUtils.spec.js | 554 +++++++++++++----- src/utils/textUtils.ts | 67 ++- 6 files changed, 749 insertions(+), 153 deletions(-) create mode 100644 src/components/text/__tests__/index.spec.tsx diff --git a/demo/src/screens/componentScreens/TextScreen.tsx b/demo/src/screens/componentScreens/TextScreen.tsx index c82e7398c5..cccdd92ca6 100644 --- a/demo/src/screens/componentScreens/TextScreen.tsx +++ b/demo/src/screens/componentScreens/TextScreen.tsx @@ -1,5 +1,5 @@ import React, {Component} from 'react'; -import {Animated, ScrollView} from 'react-native'; +import {Alert, Animated, ScrollView} from 'react-native'; import {View, Text, Colors} from 'react-native-ui-lib'; class TextScreen extends Component { @@ -85,6 +85,39 @@ class TextScreen extends Component { Dancing in The Dark + Alert.alert('Dancing is pressed!'), + style: {color: Colors.blue10, backgroundColor: Colors.green50} + }} + highlightStyle={{color: Colors.green30}} + > + Dancing in The Dark + + Alert.alert('Dancing is pressed!'), + style: {color: Colors.blue10, backgroundColor: Colors.green50} + }, + { + string: 'laugh', + onPress: () => Alert.alert('laugh is pressed!'), + style: {color: Colors.red50, textDecorationLine: 'underline', textDecorationColor: Colors.blue30} + }, + { + string: 'more', + onPress: () => Alert.alert('more is pressed!') + } + ]} + highlightStyle={{color: Colors.green30}} + > + Dancing in The Dark, laughing drinking and more + {this.renderDivider()} diff --git a/src/components/text/__tests__/index.spec.tsx b/src/components/text/__tests__/index.spec.tsx new file mode 100644 index 0000000000..f54549697c --- /dev/null +++ b/src/components/text/__tests__/index.spec.tsx @@ -0,0 +1,218 @@ +import React from 'react'; +import View from '../../view'; +import Text, {HighlightString} from '../index'; +import {TextDriver} from '../Text.driver'; +import {Colors} from '../../../style'; +import {TextStyle} from 'react-native'; + +const TEXT_ID = 'text_test_id'; +const TEXT_CONTENT = 'text content'; +describe('Text', () => { + afterEach(() => { + TextDriver.clear(); + }); + + describe('highlightString', () => { + const HIGHLIGHT_STRING_TEST_ID = 'highlight-string-test-id'; + + describe('single highlightString', () => { + describe('style', () => { + it('should render highlight string with a given style', async () => { + const component = WrapperScreenWithText({ + highlightString: { + string: 'content', + style: {color: Colors.red30, textDecorationLine: 'underline'}, + testID: HIGHLIGHT_STRING_TEST_ID + } + }); + const textDriver = new TextDriver({component, testID: TEXT_ID}); + const {style} = await textDriver.getPropsByTestId(HIGHLIGHT_STRING_TEST_ID); + + expect(style).toEqual({color: Colors.red30, textDecorationLine: 'underline'}); + }); + + it('should render highlight string with the general highlightStyle prop style if no specific style given', async () => { + const component = WrapperScreenWithText({ + highlightString: { + string: 'content', + testID: HIGHLIGHT_STRING_TEST_ID + }, + highlightStyle: {color: Colors.blue50} + }); + const textDriver = new TextDriver({component, testID: TEXT_ID}); + const {style} = await textDriver.getPropsByTestId(HIGHLIGHT_STRING_TEST_ID); + + expect(style).toEqual([{color: Colors.grey30}, {color: Colors.blue50}]); + }); + }); + + describe('onPress', () => { + it('should press on highlighted text and run its callback', async () => { + const onPressCallback = jest.fn(); + const component = WrapperScreenWithText({ + highlightString: { + string: 'content', + onPress: onPressCallback, + testID: HIGHLIGHT_STRING_TEST_ID + } + }); + + const textDriver = new TextDriver({component, testID: HIGHLIGHT_STRING_TEST_ID}); + await textDriver.press(); + + expect(onPressCallback).toHaveBeenCalledTimes(1); + }); + + it('should not be pressable if onPress prop is not supplied for a highlightString', async () => { + const component = WrapperScreenWithText({ + highlightString: { + string: 'content', + testID: HIGHLIGHT_STRING_TEST_ID + } + }); + + const textDriver = new TextDriver({component, testID: HIGHLIGHT_STRING_TEST_ID}); + + expect(await textDriver.isPressable()).toBeFalsy(); + }); + }); + }); + + describe('highlightString array', () => { + const LONGER_TEXT_CONTENT = 'a longer text content to test'; + const HIGHLIGHT_STRING_2_TEST_ID = 'highlight-string-2-test-id'; + + describe('style', () => { + it('should render multiple highlight strings, each with its given style', async () => { + const component = WrapperScreenWithText({ + text: LONGER_TEXT_CONTENT, + highlightString: [ + { + string: 'longer', + style: {color: Colors.red30, textDecorationLine: 'underline'}, + testID: HIGHLIGHT_STRING_TEST_ID + }, + { + string: 'test', + style: {color: Colors.yellow40, textDecorationLine: 'line-through'}, + testID: HIGHLIGHT_STRING_2_TEST_ID + } + ] + }); + + const textDriver = new TextDriver({component, testID: TEXT_ID}); + const {style: style1} = await textDriver.getPropsByTestId(HIGHLIGHT_STRING_TEST_ID); + const {style: style2} = await textDriver.getPropsByTestId(HIGHLIGHT_STRING_2_TEST_ID); + + expect(style1).toEqual({color: Colors.red30, textDecorationLine: 'underline'}); + expect(style2).toEqual({color: Colors.yellow40, textDecorationLine: 'line-through'}); + }); + + it('should render highlight string with the general highlightStyle prop style if no specific style given', async () => { + const component = WrapperScreenWithText({ + text: LONGER_TEXT_CONTENT, + highlightString: [ + { + string: 'longer', + style: {color: Colors.red30, textDecorationLine: 'underline'}, + testID: HIGHLIGHT_STRING_TEST_ID + }, + { + string: 'test', + testID: HIGHLIGHT_STRING_2_TEST_ID + } + ], + highlightStyle: {color: Colors.blue50} + }); + + const textDriver = new TextDriver({component, testID: TEXT_ID}); + + const {style} = await textDriver.getPropsByTestId(HIGHLIGHT_STRING_TEST_ID); + const {style: style2} = await textDriver.getPropsByTestId(HIGHLIGHT_STRING_2_TEST_ID); + + expect(style).toEqual({color: Colors.red30, textDecorationLine: 'underline'}); + expect(style2).toEqual([{color: Colors.grey30}, {color: Colors.blue50}]); + }); + }); + + describe('onPress', () => { + it('should press on highlighted texts and run their respective callbacks', async () => { + const onPressCallback1 = jest.fn(); + const onPressCallback2 = jest.fn(); + const component = WrapperScreenWithText({ + text: LONGER_TEXT_CONTENT, + highlightString: [ + { + string: 'longer', + onPress: onPressCallback1, + testID: HIGHLIGHT_STRING_TEST_ID + }, + { + string: 'test', + onPress: onPressCallback2, + testID: HIGHLIGHT_STRING_2_TEST_ID + } + ] + }); + + const textDriver1 = new TextDriver({component, testID: HIGHLIGHT_STRING_TEST_ID}); + await textDriver1.press(); + + expect(onPressCallback1).toHaveBeenCalledTimes(1); + + const textDriver2 = new TextDriver({component, testID: HIGHLIGHT_STRING_2_TEST_ID}); + await textDriver2.press(); + + expect(onPressCallback2).toHaveBeenCalledTimes(1); + }); + + it('should not be pressable if onPress prop is not supplied for a highlightString', async () => { + const onPressCallback = jest.fn(); + const component = WrapperScreenWithText({ + text: LONGER_TEXT_CONTENT, + highlightString: [ + { + string: 'longer', + onPress: onPressCallback, + testID: HIGHLIGHT_STRING_TEST_ID + }, + { + string: 'test', + testID: HIGHLIGHT_STRING_2_TEST_ID + } + ] + }); + + const textDriver1 = new TextDriver({component, testID: HIGHLIGHT_STRING_TEST_ID}); + const textDriver2 = new TextDriver({component, testID: HIGHLIGHT_STRING_2_TEST_ID}); + + expect(await textDriver1.isPressable()).toBeTruthy(); + expect(await textDriver2.isPressable()).toBeFalsy(); + }); + }); + }); + }); +}); + +function WrapperScreenWithText(textProps: { + text?: string; + onPress?: jest.Mock, + highlightString?: HighlightString | HighlightString[], + highlightStyle?: TextStyle +} = {}) { + const { + onPress, + highlightString, + text, + highlightStyle} = textProps; + return ( + + {text ?? TEXT_CONTENT} + + ); +} diff --git a/src/components/text/index.tsx b/src/components/text/index.tsx index 43a8ff50a0..99d5cda87b 100644 --- a/src/components/text/index.tsx +++ b/src/components/text/index.tsx @@ -15,6 +15,24 @@ import {RecorderProps} from '../../../typings/recorderTypes'; import {Colors} from 'style'; import {TextUtils} from 'utils'; +export interface HighlightStringProps { + /** + * Substring to highlight + */ + string: string; + /** + * Callback for when a highlighted substring is pressed + */ + onPress?: () => void; + /** + * Custom highlight style for this specific highlighted substring. If not provided, the general `highlightStyle` prop style will be used + */ + style?: TextStyle; + testID?: string; +} + +export type HighlightString = string | HighlightStringProps; + export type TextProps = RNTextProps & TypographyModifiers & ColorsModifiers & @@ -38,9 +56,9 @@ export type TextProps = RNTextProps & */ underline?: boolean; /** - * Substring to highlight + * Substring to highlight. Can be a simple string or a HighlightStringProps object, or an array of the above */ - highlightString?: string | string[]; + highlightString?: HighlightString | HighlightString[]; /** * Custom highlight style for highlight string */ @@ -92,7 +110,9 @@ class Text extends PureComponent { return ( {text.string} diff --git a/src/components/text/text.api.json b/src/components/text/text.api.json index 3072216ce4..e65df2a0e2 100644 --- a/src/components/text/text.api.json +++ b/src/components/text/text.api.json @@ -14,7 +14,7 @@ {"name": "center", "type": "boolean", "description": "Whether to center the text (using textAlign)"}, {"name": "uppercase", "type": "boolean", "description": "Whether to change the text to uppercase"}, {"name": "underline", "type": "boolean", "description": "Whether to add an underline"}, - {"name": "highlightString", "type": "string | string[]", "description": "Substring to highlight"}, + {"name": "highlightString", "type": "HighlightString | HighlightString[]", "description": "Substring to highlight. Can be a simple string or a HighlightStringProps object, or an array of the above"}, {"name": "highlightStyle", "type": "TextStyle", "description": "Custom highlight style for highlight string"}, {"name": "recorderTag", "type": "'mask' | 'unmask'", "description": "Recorder Tag"}, {"name": "animated", "type": "boolean", "description": "Use Animated.Text as a container"} diff --git a/src/utils/__tests__/textUtils.spec.js b/src/utils/__tests__/textUtils.spec.js index bdedcb080d..dd65994b3e 100644 --- a/src/utils/__tests__/textUtils.spec.js +++ b/src/utils/__tests__/textUtils.spec.js @@ -1,164 +1,446 @@ import {getTextPartsByHighlight, getArrayPartsByHighlight} from '../textUtils'; describe('Text', () => { + const mockHighlightStringProps = { + onPress: jest.fn(), + style: {color: 'red'}, + testID: 'highlighted-string-test-id' + }; + describe('getTextPartsByHighlight', () => { - it('should return the whole string as a single part when highlight string is undefined', () => { - const result = getTextPartsByHighlight('Playground Screen', undefined); - expect(result).toEqual([{string: 'Playground Screen', shouldHighlight: false}]); - }); - it('should return the whole string as a single part when highlight string is empty', () => { - const result = getTextPartsByHighlight('Playground Screen', ''); - expect(result).toEqual([{string: 'Playground Screen', shouldHighlight: false}]); - }); - it('should return the whole string as a single part when highlight string dont match', () => { - const result = getTextPartsByHighlight('Playground Screen', 'aaa'); - expect(result).toEqual([{string: 'Playground Screen', shouldHighlight: false}]); - }); - it('should break text to parts according to highlight string', () => { - const result = getTextPartsByHighlight('Playground Screen', 'Scr'); - expect(result).toEqual([ - {string: 'Playground ', shouldHighlight: false}, - {string: 'Scr', shouldHighlight: true}, - {string: 'een', shouldHighlight: false} - ]); - }); + describe('Simple string', () => { + it('should return the whole string as a single part when highlight string is undefined', () => { + const result = getTextPartsByHighlight('Playground Screen', undefined); + expect(result).toEqual([{string: 'Playground Screen', shouldHighlight: false}]); + }); + it('should return the whole string as a single part when highlight string is empty', () => { + const result = getTextPartsByHighlight('Playground Screen', ''); + expect(result).toEqual([{string: 'Playground Screen', shouldHighlight: false}]); + }); + it('should return the whole string as a single part when highlight string dont match', () => { + const result = getTextPartsByHighlight('Playground Screen', 'aaa'); + expect(result).toEqual([{string: 'Playground Screen', shouldHighlight: false}]); + }); + it('should break text to parts according to highlight string', () => { + const result = getTextPartsByHighlight('Playground Screen', 'Scr'); + expect(result).toEqual([ + {string: 'Playground ', shouldHighlight: false}, + {string: 'Scr', shouldHighlight: true}, + {string: 'een', shouldHighlight: false} + ]); + }); - it('should handle case when highlight repeats more than once', () => { - const result = getTextPartsByHighlight('Dancing in the Dark', 'Da'); - expect(result).toEqual([ - {string: 'Da', shouldHighlight: true}, - {string: 'ncing in the ', shouldHighlight: false}, - {string: 'Da', shouldHighlight: true}, - {string: 'rk', shouldHighlight: false} - ]); - }); + it('should handle case when highlight repeats more than once', () => { + const result = getTextPartsByHighlight('Dancing in the Dark', 'Da'); + expect(result).toEqual([ + {string: 'Da', shouldHighlight: true}, + {string: 'ncing in the ', shouldHighlight: false}, + {string: 'Da', shouldHighlight: true}, + {string: 'rk', shouldHighlight: false} + ]); + }); - it('should be case-insensitive', () => { - const result = getTextPartsByHighlight('Dancing in the Dark', 'da'); - expect(result).toEqual([ - {string: 'Da', shouldHighlight: true}, - {string: 'ncing in the ', shouldHighlight: false}, - {string: 'Da', shouldHighlight: true}, - {string: 'rk', shouldHighlight: false} - ]); - }); + it('should be case-insensitive', () => { + const result = getTextPartsByHighlight('Dancing in the Dark', 'da'); + expect(result).toEqual([ + {string: 'Da', shouldHighlight: true}, + {string: 'ncing in the ', shouldHighlight: false}, + {string: 'Da', shouldHighlight: true}, + {string: 'rk', shouldHighlight: false} + ]); + }); - it('Should handle special characters @', () => { - const result = getTextPartsByHighlight('@ancing in the @ark', '@a'); - expect(result).toEqual([ - {string: '@a', shouldHighlight: true}, - {string: 'ncing in the ', shouldHighlight: false}, - {string: '@a', shouldHighlight: true}, - {string: 'rk', shouldHighlight: false} - ]); - }); + it('Should handle special characters @', () => { + const result = getTextPartsByHighlight('@ancing in the @ark', '@a'); + expect(result).toEqual([ + {string: '@a', shouldHighlight: true}, + {string: 'ncing in the ', shouldHighlight: false}, + {string: '@a', shouldHighlight: true}, + {string: 'rk', shouldHighlight: false} + ]); + }); - it('Should handle special characters !', () => { - const result = getTextPartsByHighlight('!ancing in the !ark', '!a'); - expect(result).toEqual([ - {string: '!a', shouldHighlight: true}, - {string: 'ncing in the ', shouldHighlight: false}, - {string: '!a', shouldHighlight: true}, - {string: 'rk', shouldHighlight: false} - ]); - }); + it('Should handle special characters !', () => { + const result = getTextPartsByHighlight('!ancing in the !ark', '!a'); + expect(result).toEqual([ + {string: '!a', shouldHighlight: true}, + {string: 'ncing in the ', shouldHighlight: false}, + {string: '!a', shouldHighlight: true}, + {string: 'rk', shouldHighlight: false} + ]); + }); - it('Should handle special characters starts with @', () => { - const result = getTextPartsByHighlight('uilib@wix.com', '@wix'); - expect(result).toEqual([ - {string: 'uilib', shouldHighlight: false}, - {string: '@wix', shouldHighlight: true}, - {string: '.com', shouldHighlight: false} - ]); - }); + it('Should handle special characters starts with @', () => { + const result = getTextPartsByHighlight('uilib@wix.com', '@wix'); + expect(result).toEqual([ + {string: 'uilib', shouldHighlight: false}, + {string: '@wix', shouldHighlight: true}, + {string: '.com', shouldHighlight: false} + ]); + }); - it('Should handle empty string.', () => { - const result = getTextPartsByHighlight('@ancing in the @ark', ''); - expect(result).toEqual([{string: '@ancing in the @ark', shouldHighlight: false}]); - }); + it('Should handle empty string.', () => { + const result = getTextPartsByHighlight('@ancing in the @ark', ''); + expect(result).toEqual([{string: '@ancing in the @ark', shouldHighlight: false}]); + }); + + it('Should handle full string.', () => { + const result = getTextPartsByHighlight('Dancing in the Dark', 'Dancing in the Dark'); + expect(result).toEqual([{string: 'Dancing in the Dark', shouldHighlight: true}]); + }); - it('Should handle full string.', () => { - const result = getTextPartsByHighlight('Dancing in the Dark', 'Dancing in the Dark'); - expect(result).toEqual([{string: 'Dancing in the Dark', shouldHighlight: true}]); + it('Should handle longer string.', () => { + const result = getTextPartsByHighlight('Dancing in the Dark', 'Dancing in the Darker'); + expect(result).toEqual([{string: 'Dancing in the Dark', shouldHighlight: false}]); + }); }); - it('Should handle longer string.', () => { - const result = getTextPartsByHighlight('Dancing in the Dark', 'Dancing in the Darker'); - expect(result).toEqual([{string: 'Dancing in the Dark', shouldHighlight: false}]); + describe('HighlightStringProps object', () => { + it('should return the whole string as a single part when highlight string is empty', () => { + const highlightString = { + string: '', + ...mockHighlightStringProps + }; + const result = getTextPartsByHighlight('Playground Screen', highlightString); + expect(result).toEqual([{string: 'Playground Screen', shouldHighlight: false}]); + }); + it('should return the whole string as a single part when highlight string dont match', () => { + const highlightString = { + string: 'aaa', + onPress: () => {}, + style: {color: 'red'}, + testID: 'highlighted-string-1' + }; + const result = getTextPartsByHighlight('Playground Screen', highlightString); + expect(result).toEqual([{string: 'Playground Screen', shouldHighlight: false}]); + }); + it('should break text to parts according to highlight string', () => { + const highlightString = { + string: 'Scr', + ...mockHighlightStringProps + }; + const result = getTextPartsByHighlight('Playground Screen', highlightString); + expect(result).toEqual([ + {string: 'Playground ', shouldHighlight: false}, + {string: 'Scr', shouldHighlight: true, ...mockHighlightStringProps}, + {string: 'een', shouldHighlight: false} + ]); + }); + + it('should handle case when highlight repeats more than once', () => { + const highlightString = { + string: 'Da', + ...mockHighlightStringProps + }; + const result = getTextPartsByHighlight('Dancing in the Dark', highlightString); + expect(result).toEqual([ + {string: 'Da', shouldHighlight: true, ...mockHighlightStringProps}, + {string: 'ncing in the ', shouldHighlight: false}, + {string: 'Da', shouldHighlight: true, ...mockHighlightStringProps}, + {string: 'rk', shouldHighlight: false} + ]); + }); + + it('should be case-insensitive', () => { + const highlightString = { + string: 'da', + ...mockHighlightStringProps + }; + const result = getTextPartsByHighlight('Dancing in the Dark', highlightString); + expect(result).toEqual([ + {string: 'Da', shouldHighlight: true, ...mockHighlightStringProps}, + {string: 'ncing in the ', shouldHighlight: false}, + {string: 'Da', shouldHighlight: true, ...mockHighlightStringProps}, + {string: 'rk', shouldHighlight: false} + ]); + }); + + it('Should handle special characters @', () => { + const highlightString = { + string: '@a', + ...mockHighlightStringProps + }; + const result = getTextPartsByHighlight('@ancing in the @ark', highlightString); + expect(result).toEqual([ + {string: '@a', shouldHighlight: true, ...mockHighlightStringProps}, + {string: 'ncing in the ', shouldHighlight: false}, + {string: '@a', shouldHighlight: true, ...mockHighlightStringProps}, + {string: 'rk', shouldHighlight: false} + ]); + }); + + it('Should handle special characters !', () => { + const highlightString = { + string: '!a', + ...mockHighlightStringProps + }; + const result = getTextPartsByHighlight('!ancing in the !ark', highlightString); + expect(result).toEqual([ + {string: '!a', shouldHighlight: true, ...mockHighlightStringProps}, + {string: 'ncing in the ', shouldHighlight: false}, + {string: '!a', shouldHighlight: true, ...mockHighlightStringProps}, + {string: 'rk', shouldHighlight: false} + ]); + }); + + it('Should handle special characters starts with @', () => { + const highlightString = { + string: '@wix', + ...mockHighlightStringProps + }; + const result = getTextPartsByHighlight('uilib@wix.com', highlightString); + expect(result).toEqual([ + {string: 'uilib', shouldHighlight: false}, + {string: '@wix', shouldHighlight: true, ...mockHighlightStringProps}, + {string: '.com', shouldHighlight: false} + ]); + }); + + it('Should handle empty string.', () => { + const highlightString = { + string: '', + ...mockHighlightStringProps + }; + const result = getTextPartsByHighlight('@ancing in the @ark', highlightString); + expect(result).toEqual([{string: '@ancing in the @ark', shouldHighlight: false}]); + }); + + it('Should handle full string.', () => { + const highlightString = { + string: 'Dancing in the Dark', + ...mockHighlightStringProps + }; + const result = getTextPartsByHighlight('Dancing in the Dark', highlightString); + expect(result).toEqual([{string: 'Dancing in the Dark', shouldHighlight: true, ...mockHighlightStringProps}]); + }); + + it('Should handle longer string.', () => { + const highlightString = { + string: 'Dancing in the Darker', + ...mockHighlightStringProps + }; + const result = getTextPartsByHighlight('Dancing in the Dark', highlightString); + expect(result).toEqual([{string: 'Dancing in the Dark', shouldHighlight: false}]); + }); }); }); describe('getArrayPartsByHighlight', () => { - it('should return the whole string as a single part when highlight array is empty', () => { - const result = getArrayPartsByHighlight('Playground Screen', []); - expect(result).toEqual([{string: 'Playground Screen', shouldHighlight: false}]); - }); - it('should return the whole string as a single part when highlight string is empty', () => { - const result = getArrayPartsByHighlight('Playground Screen', ['']); - expect(result).toEqual([{string: 'Playground Screen', shouldHighlight: false}]); - }); - it('should return the whole string as a single part when highlight string dont match', () => { - const result = getArrayPartsByHighlight('Playground Screen', ['aaa']); - expect(result).toEqual([{string: 'Playground Screen', shouldHighlight: false}]); - }); - it('should break text to parts according to highlight string', () => { - const result = getArrayPartsByHighlight('Playground Screen', ['Scr']); - expect(result).toEqual([ - {string: 'Playground ', shouldHighlight: false}, - {string: 'Scr', shouldHighlight: true}, - {string: 'een', shouldHighlight: false} - ]); - }); + describe('Simple string array', () => { + it('should return the whole string as a single part when highlight array is empty', () => { + const result = getArrayPartsByHighlight('Playground Screen', []); + expect(result).toEqual([{string: 'Playground Screen', shouldHighlight: false}]); + }); + it('should return the whole string as a single part when highlight string is empty', () => { + const result = getArrayPartsByHighlight('Playground Screen', ['']); + expect(result).toEqual([{string: 'Playground Screen', shouldHighlight: false}]); + }); + it('should return the whole string as a single part when highlight string dont match', () => { + const result = getArrayPartsByHighlight('Playground Screen', ['aaa']); + expect(result).toEqual([{string: 'Playground Screen', shouldHighlight: false}]); + }); + it('should break text to parts according to highlight string', () => { + const result = getArrayPartsByHighlight('Playground Screen', ['Scr']); + expect(result).toEqual([ + {string: 'Playground ', shouldHighlight: false}, + {string: 'Scr', shouldHighlight: true}, + {string: 'een', shouldHighlight: false} + ]); + }); - it('highlight repeats more than once should color the first match', () => { - const result = getArrayPartsByHighlight('Dancing in the Dark', ['Da']); - expect(result).toEqual([ - {string: 'Da', shouldHighlight: true}, - {string: 'ncing in the Dark', shouldHighlight: false} - ]); - }); + it('highlight repeats more than once should color the first match', () => { + const result = getArrayPartsByHighlight('Dancing in the Dark', ['Da']); + expect(result).toEqual([ + {string: 'Da', shouldHighlight: true}, + {string: 'ncing in the Dark', shouldHighlight: false} + ]); + }); - it('should be case-insensitive', () => { - const result = getArrayPartsByHighlight('Dancing in the Dark', ['da']); - expect(result).toEqual([ - {string: 'Da', shouldHighlight: true}, - {string: 'ncing in the Dark', shouldHighlight: false} - ]); - }); + it('should be case-insensitive', () => { + const result = getArrayPartsByHighlight('Dancing in the Dark', ['da']); + expect(result).toEqual([ + {string: 'Da', shouldHighlight: true}, + {string: 'ncing in the Dark', shouldHighlight: false} + ]); + }); - it('Should handle special characters @', () => { - const result = getArrayPartsByHighlight('@ancing in the @ark', ['@a']); - expect(result).toEqual([ - {string: '@a', shouldHighlight: true}, - {string: 'ncing in the @ark', shouldHighlight: false} - ]); - }); + it('Should handle special characters @', () => { + const result = getArrayPartsByHighlight('@ancing in the @ark', ['@a']); + expect(result).toEqual([ + {string: '@a', shouldHighlight: true}, + {string: 'ncing in the @ark', shouldHighlight: false} + ]); + }); - it('Should handle special characters !', () => { - const result = getArrayPartsByHighlight('!ancing in the !ark', ['!a']); - expect(result).toEqual([ - {string: '!a', shouldHighlight: true}, - {string: 'ncing in the !ark', shouldHighlight: false} - ]); - }); + it('Should handle special characters !', () => { + const result = getArrayPartsByHighlight('!ancing in the !ark', ['!a']); + expect(result).toEqual([ + {string: '!a', shouldHighlight: true}, + {string: 'ncing in the !ark', shouldHighlight: false} + ]); + }); - it('Should handle special characters starts with @', () => { - const result = getArrayPartsByHighlight('uilib@wix.com', ['@wix']); - expect(result).toEqual([ - {string: 'uilib', shouldHighlight: false}, - {string: '@wix', shouldHighlight: true}, - {string: '.com', shouldHighlight: false} - ]); - }); + it('Should handle special characters starts with @', () => { + const result = getArrayPartsByHighlight('uilib@wix.com', ['@wix']); + expect(result).toEqual([ + {string: 'uilib', shouldHighlight: false}, + {string: '@wix', shouldHighlight: true}, + {string: '.com', shouldHighlight: false} + ]); + }); + + it('Should handle full string.', () => { + const result = getArrayPartsByHighlight('Dancing in the Dark', ['Dancing in the Dark']); + expect(result).toEqual([{string: 'Dancing in the Dark', shouldHighlight: true}]); + }); - it('Should handle full string.', () => { - const result = getArrayPartsByHighlight('Dancing in the Dark', ['Dancing in the Dark']); - expect(result).toEqual([{string: 'Dancing in the Dark', shouldHighlight: true}]); + it('Should handle longer string.', () => { + const result = getArrayPartsByHighlight('Dancing in the Dark', ['Dancing in the Darker']); + expect(result).toEqual([{string: 'Dancing in the Dark', shouldHighlight: false}]); + }); + + it('Should handle multiple strings.', () => { + const result = getArrayPartsByHighlight('Dancing in the Dark', ['Dancing', 'Dark']); + expect(result).toEqual([ + {string: 'Dancing', shouldHighlight: true}, + {string: ' in the ', shouldHighlight: false}, + {string: 'Dark', shouldHighlight: true} + ]); + }); }); - it('Should handle longer string.', () => { - const result = getArrayPartsByHighlight('Dancing in the Dark', ['Dancing in the Darker']); - expect(result).toEqual([{string: 'Dancing in the Dark', shouldHighlight: false}]); + describe('HighlightStringProps objects array', () => { + it('should return the whole string as a single part when highlight string is empty', () => { + const highlightString = [{ + string: '', + ...mockHighlightStringProps + }]; + const result = getArrayPartsByHighlight('Playground Screen', highlightString); + expect(result).toEqual([{string: 'Playground Screen', shouldHighlight: false}]); + }); + it('should return the whole string as a single part when highlight string dont match', () => { + const highlightString = [{ + string: 'aaa', + ...mockHighlightStringProps + }]; + const result = getArrayPartsByHighlight('Playground Screen', highlightString); + expect(result).toEqual([{string: 'Playground Screen', shouldHighlight: false}]); + }); + it('should break text to parts according to highlight string', () => { + const highlightString = [{ + string: 'Scr', + ...mockHighlightStringProps + }]; + const result = getArrayPartsByHighlight('Playground Screen', highlightString); + expect(result).toEqual([ + {string: 'Playground ', shouldHighlight: false}, + {string: 'Scr', shouldHighlight: true, ...mockHighlightStringProps}, + {string: 'een', shouldHighlight: false} + ]); + }); + + it('highlight repeats more than once should color the first match', () => { + const highlightString = [{ + string: 'Da', + ...mockHighlightStringProps + }]; + const result = getArrayPartsByHighlight('Dancing in the Dark', highlightString); + expect(result).toEqual([ + {string: 'Da', shouldHighlight: true, ...mockHighlightStringProps}, + {string: 'ncing in the Dark', shouldHighlight: false} + ]); + }); + + it('should be case-insensitive', () => { + const highlightString = [{ + string: 'da', + ...mockHighlightStringProps + }]; + const result = getArrayPartsByHighlight('Dancing in the Dark', highlightString); + expect(result).toEqual([ + {string: 'Da', shouldHighlight: true, ...mockHighlightStringProps}, + {string: 'ncing in the Dark', shouldHighlight: false} + ]); + }); + + it('Should handle special characters @', () => { + const highlightString = [{ + string: '@a', + ...mockHighlightStringProps + }]; + const result = getArrayPartsByHighlight('@ancing in the @ark', highlightString); + expect(result).toEqual([ + {string: '@a', shouldHighlight: true, ...mockHighlightStringProps}, + {string: 'ncing in the @ark', shouldHighlight: false} + ]); + }); + + it('Should handle special characters !', () => { + const highlightString = [{ + string: '!a', + ...mockHighlightStringProps + }]; + const result = getArrayPartsByHighlight('!ancing in the !ark', highlightString); + expect(result).toEqual([ + {string: '!a', shouldHighlight: true, ...mockHighlightStringProps}, + {string: 'ncing in the !ark', shouldHighlight: false} + ]); + }); + + it('Should handle special characters starts with @', () => { + const highlightString = [{ + string: '@wix', + ...mockHighlightStringProps + }]; + const result = getArrayPartsByHighlight('uilib@wix.com', highlightString); + expect(result).toEqual([ + {string: 'uilib', shouldHighlight: false}, + {string: '@wix', shouldHighlight: true, ...mockHighlightStringProps}, + {string: '.com', shouldHighlight: false} + ]); + }); + + it('Should handle full string.', () => { + const highlightString = [{ + string: 'Dancing in the Dark', + ...mockHighlightStringProps + }]; + const result = getArrayPartsByHighlight('Dancing in the Dark', highlightString); + expect(result).toEqual([{string: 'Dancing in the Dark', shouldHighlight: true, ...mockHighlightStringProps}]); + }); + + it('Should handle longer string.', () => { + const highlightString = [{ + string: 'Dancing in the Darker', + ...mockHighlightStringProps + }]; + const result = getArrayPartsByHighlight('Dancing in the Dark', highlightString); + expect(result).toEqual([{string: 'Dancing in the Dark', shouldHighlight: false}]); + }); + + it('Should handle multiple string.', () => { + const mockHighlightStringProps2 = { + onPress: jest.fn(), + style: {color: 'blue'}, + testID: 'highlighted-string-test-id-2' + }; + + const highlightString = [ + { + string: 'Dancing', + ...mockHighlightStringProps + }, + { + string: 'Dark', + ...mockHighlightStringProps2 + }]; + const result = getArrayPartsByHighlight('Dancing in the Dark', highlightString); + expect(result).toEqual([ + {string: 'Dancing', shouldHighlight: true, ...mockHighlightStringProps}, + {string: ' in the ', shouldHighlight: false}, + {string: 'Dark', shouldHighlight: true, ...mockHighlightStringProps2} + ]); + }); }); }); }); diff --git a/src/utils/textUtils.ts b/src/utils/textUtils.ts index 49596f4851..8de3725d39 100644 --- a/src/utils/textUtils.ts +++ b/src/utils/textUtils.ts @@ -1,8 +1,14 @@ import {isEmpty, toLower} from 'lodash'; +import {HighlightString, HighlightStringProps} from '../components/text'; -function getPartsByHighlight(targetString = '', highlightString: string | string[]) { - if (typeof highlightString === 'string') { - if (isEmpty(highlightString.trim())) { +interface TextPartByHighlight extends HighlightStringProps { + shouldHighlight: boolean; +} + +function getPartsByHighlight(targetString = '', highlightString: HighlightString | HighlightString[]): TextPartByHighlight[] { + if (!Array.isArray(highlightString)) { + const stringValue = getHighlightStringValue(highlightString); + if (isEmpty(stringValue.trim())) { return [{string: targetString, shouldHighlight: false}]; } return getTextPartsByHighlight(targetString, highlightString); @@ -11,20 +17,42 @@ function getPartsByHighlight(targetString = '', highlightString: string | string } } -function getTextPartsByHighlight(targetString = '', highlightString = '') { - if (highlightString === '') { +function getHighlightStringValue(highlightString: HighlightString): string { + if (isHighlightStringProps(highlightString)) { + return highlightString.string; + } else { + return highlightString; + } +} + +function isHighlightStringProps(highlightString: HighlightString): highlightString is HighlightStringProps { + return typeof highlightString !== 'string'; +} + +function getTextPartsByHighlight(targetString = '', highlightString: HighlightString = ''): TextPartByHighlight[] { + const stringValue = getHighlightStringValue(highlightString); + if (stringValue === '') { return [{string: targetString, shouldHighlight: false}]; } const textParts = []; let highlightIndex; do { - highlightIndex = targetString.toLowerCase().indexOf(highlightString.toLowerCase()); + highlightIndex = targetString.toLowerCase().indexOf(stringValue.toLowerCase()); if (highlightIndex !== -1) { if (highlightIndex > 0) { textParts.push({string: targetString.substring(0, highlightIndex), shouldHighlight: false}); } - textParts.push({string: targetString.substr(highlightIndex, highlightString.length), shouldHighlight: true}); - targetString = targetString.substr(highlightIndex + highlightString.length); + const highlightStringProps = isHighlightStringProps(highlightString) ? { + onPress: highlightString.onPress, + style: highlightString.style, + testID: highlightString.testID + } : {}; + textParts.push({ + string: targetString.substr(highlightIndex, stringValue.length), + shouldHighlight: true, + ...highlightStringProps + }); + targetString = targetString.substr(highlightIndex + stringValue.length); } else { textParts.push({string: targetString, shouldHighlight: false}); } @@ -33,13 +61,14 @@ function getTextPartsByHighlight(targetString = '', highlightString = '') { return textParts; } -function getArrayPartsByHighlight(targetString = '', highlightString = ['']) { +function getArrayPartsByHighlight(targetString = '', highlightString: HighlightString[] = ['']): TextPartByHighlight[] { const target = toLower(targetString); const indices = []; let index = 0; let lastWordLength = 0; for (let j = 0; j < highlightString.length; j++) { - const word = toLower(highlightString[j]); + const stringValue = getHighlightStringValue(highlightString[j]); + const word = toLower(stringValue); if (word.length === 0) { break; } @@ -48,7 +77,11 @@ function getArrayPartsByHighlight(targetString = '', highlightString = ['']) { const i = targetSuffix.indexOf(word); if (i >= 0) { const newIndex = index + lastWordLength + i; - indices.push({start: index + lastWordLength + i, end: index + lastWordLength + i + word.length}); + indices.push({ + start: index + lastWordLength + i, + end: index + lastWordLength + i + word.length, + highlightStringIndex: j + }); index = newIndex; lastWordLength = word.length; } else { @@ -60,7 +93,17 @@ function getArrayPartsByHighlight(targetString = '', highlightString = ['']) { if (k === 0 && indices[k].start !== 0) { textParts.push({string: targetString.substring(0, indices[k].start), shouldHighlight: false}); } - textParts.push({string: targetString.substring(indices[k].start, indices[k].end), shouldHighlight: true}); + const currentHighlightString = highlightString[indices[k].highlightStringIndex]; + const highlightStringProps = isHighlightStringProps(currentHighlightString) ? { + onPress: currentHighlightString.onPress, + style: currentHighlightString.style, + testID: currentHighlightString.testID + } : {}; + textParts.push({ + string: targetString.substring(indices[k].start, indices[k].end), + shouldHighlight: true, + ...highlightStringProps + }); if (indices[k].end < targetString.length) { if (k === indices.length - 1) { textParts.push({string: targetString.substring(indices[k].end), shouldHighlight: false}); From e48a2ebd062e281d1fce092e74fc2cdd5bcacb94 Mon Sep 17 00:00:00 2001 From: Lidor Dafna <66782556+lidord-wix@users.noreply.github.com> Date: Tue, 18 Jul 2023 12:46:53 +0300 Subject: [PATCH 24/26] Scheme - fix color changing (#2607) * Scheme - fix flickering * Update src/commons/asBaseComponent.tsx Co-authored-by: Ethan Sharabi <1780255+ethanshar@users.noreply.github.com> --------- Co-authored-by: Ethan Sharabi <1780255+ethanshar@users.noreply.github.com> --- src/commons/asBaseComponent.tsx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/commons/asBaseComponent.tsx b/src/commons/asBaseComponent.tsx index 49507de079..bf809da089 100644 --- a/src/commons/asBaseComponent.tsx +++ b/src/commons/asBaseComponent.tsx @@ -1,7 +1,7 @@ import React from 'react'; import hoistStatics from 'hoist-non-react-statics'; import * as Modifiers from './modifiers'; -import {Scheme, ThemeManager} from '../style'; +import {Scheme, SchemeChangeListener, ThemeManager} from '../style'; import forwardRef from './forwardRef'; import UIComponent from './UIComponent'; @@ -19,6 +19,7 @@ export interface AsBaseComponentOptions { } const EMPTY_MODIFIERS = {}; +const colorScheme = Scheme.getSchemeType(); function asBaseComponent(WrappedComponent: React.ComponentType, options: AsBaseComponentOptions = {}): React.ComponentClass & STATICS { @@ -28,7 +29,8 @@ function asBaseComponent(WrappedComponent: React.ComponentT static defaultProps: any; state = { - error: false + error: false, + colorScheme }; componentDidMount() { @@ -39,11 +41,13 @@ function asBaseComponent(WrappedComponent: React.ComponentT Scheme.removeChangeListener(this.appearanceListener); } - appearanceListener = () => { + appearanceListener: SchemeChangeListener = (colorScheme) => { // iOS 13 and above will trigger this call with the wrong colorScheme value. So just ignore returned colorScheme for now // https://github.com/facebook/react-native/issues/28525 // this.setState({colorScheme: Appearance.getColorScheme()}); - this.setState({colorScheme: Scheme.getSchemeType()}); + if (this.state.colorScheme !== colorScheme) { + this.setState({colorScheme}); + } }; static getThemeProps = (props: any, context: any) => { From 2a2f962d6e516714ebd6cf5dcd240768293e3935 Mon Sep 17 00:00:00 2001 From: Inbal Tish <33805983+Inbal-Tish@users.noreply.github.com> Date: Wed, 19 Jul 2023 10:57:13 +0300 Subject: [PATCH 25/26] Text - default alignment (#2670) * Text - removing default text alignment (set to left) to allow children to get parent alignment (issue is on iOS only) * update snapshot tests - Button, AvatarScreen * Replacing default textAlign with writingDirection to fix rtl * Adding writingDirectionTypes and updating snapshot tests * adding comment --- .../__snapshots__/AvatarScreen.spec.js.snap | 44 ++++----- .../__snapshots__/index.spec.js.snap | 90 +++++++++---------- src/components/text/index.tsx | 15 +++- 3 files changed, 78 insertions(+), 71 deletions(-) diff --git a/demo/src/screens/__tests__/__snapshots__/AvatarScreen.spec.js.snap b/demo/src/screens/__tests__/__snapshots__/AvatarScreen.spec.js.snap index 836de15972..94b484c749 100644 --- a/demo/src/screens/__tests__/__snapshots__/AvatarScreen.spec.js.snap +++ b/demo/src/screens/__tests__/__snapshots__/AvatarScreen.spec.js.snap @@ -36,7 +36,7 @@ exports[`AvatarScreen renders screen 1`] = ` { "backgroundColor": "transparent", "color": "#20303C", - "textAlign": "left", + "writingDirection": "ltr", }, undefined, { @@ -159,7 +159,7 @@ exports[`AvatarScreen renders screen 1`] = ` { "backgroundColor": "transparent", "color": "#20303C", - "textAlign": "left", + "writingDirection": "ltr", }, undefined, { @@ -289,7 +289,7 @@ exports[`AvatarScreen renders screen 1`] = ` { "backgroundColor": "transparent", "color": "#20303C", - "textAlign": "left", + "writingDirection": "ltr", }, { "fontFamily": "System", @@ -342,7 +342,7 @@ exports[`AvatarScreen renders screen 1`] = ` { "backgroundColor": "transparent", "color": "#20303C", - "textAlign": "left", + "writingDirection": "ltr", }, undefined, { @@ -444,7 +444,7 @@ exports[`AvatarScreen renders screen 1`] = ` { "backgroundColor": "transparent", "color": "#20303C", - "textAlign": "left", + "writingDirection": "ltr", }, undefined, undefined, @@ -514,7 +514,7 @@ exports[`AvatarScreen renders screen 1`] = ` { "backgroundColor": "transparent", "color": "#20303C", - "textAlign": "left", + "writingDirection": "ltr", }, { "fontFamily": "System", @@ -567,7 +567,7 @@ exports[`AvatarScreen renders screen 1`] = ` { "backgroundColor": "transparent", "color": "#20303C", - "textAlign": "left", + "writingDirection": "ltr", }, undefined, { @@ -669,7 +669,7 @@ exports[`AvatarScreen renders screen 1`] = ` { "backgroundColor": "transparent", "color": "#20303C", - "textAlign": "left", + "writingDirection": "ltr", }, undefined, undefined, @@ -797,7 +797,7 @@ exports[`AvatarScreen renders screen 1`] = ` { "backgroundColor": "transparent", "color": "#20303C", - "textAlign": "left", + "writingDirection": "ltr", }, undefined, { @@ -1048,7 +1048,7 @@ exports[`AvatarScreen renders screen 1`] = ` { "backgroundColor": "transparent", "color": "#20303C", - "textAlign": "left", + "writingDirection": "ltr", }, undefined, { @@ -1299,7 +1299,7 @@ exports[`AvatarScreen renders screen 1`] = ` { "backgroundColor": "transparent", "color": "#20303C", - "textAlign": "left", + "writingDirection": "ltr", }, undefined, { @@ -1501,7 +1501,7 @@ exports[`AvatarScreen renders screen 1`] = ` { "backgroundColor": "transparent", "color": "#20303C", - "textAlign": "left", + "writingDirection": "ltr", }, undefined, { @@ -1752,7 +1752,7 @@ exports[`AvatarScreen renders screen 1`] = ` { "backgroundColor": "transparent", "color": "#20303C", - "textAlign": "left", + "writingDirection": "ltr", }, undefined, { @@ -2047,7 +2047,7 @@ exports[`AvatarScreen renders screen 1`] = ` { "backgroundColor": "transparent", "color": "#20303C", - "textAlign": "left", + "writingDirection": "ltr", }, undefined, { @@ -2231,7 +2231,7 @@ exports[`AvatarScreen renders screen 1`] = ` { "backgroundColor": "transparent", "color": "#20303C", - "textAlign": "left", + "writingDirection": "ltr", }, undefined, { @@ -2338,7 +2338,7 @@ exports[`AvatarScreen renders screen 1`] = ` { "backgroundColor": "transparent", "color": "#20303C", - "textAlign": "left", + "writingDirection": "ltr", }, undefined, undefined, @@ -2457,7 +2457,7 @@ exports[`AvatarScreen renders screen 1`] = ` { "backgroundColor": "transparent", "color": "#20303C", - "textAlign": "left", + "writingDirection": "ltr", }, undefined, { @@ -2564,7 +2564,7 @@ exports[`AvatarScreen renders screen 1`] = ` { "backgroundColor": "transparent", "color": "#20303C", - "textAlign": "left", + "writingDirection": "ltr", }, undefined, undefined, @@ -2683,7 +2683,7 @@ exports[`AvatarScreen renders screen 1`] = ` { "backgroundColor": "transparent", "color": "#20303C", - "textAlign": "left", + "writingDirection": "ltr", }, undefined, { @@ -2867,7 +2867,7 @@ exports[`AvatarScreen renders screen 1`] = ` { "backgroundColor": "transparent", "color": "#20303C", - "textAlign": "left", + "writingDirection": "ltr", }, undefined, { @@ -2969,7 +2969,7 @@ exports[`AvatarScreen renders screen 1`] = ` { "backgroundColor": "transparent", "color": "#20303C", - "textAlign": "left", + "writingDirection": "ltr", }, undefined, undefined, @@ -3079,7 +3079,7 @@ exports[`AvatarScreen renders screen 1`] = ` { "backgroundColor": "transparent", "color": "#20303C", - "textAlign": "left", + "writingDirection": "ltr", }, undefined, undefined, diff --git a/src/components/button/__tests__/__snapshots__/index.spec.js.snap b/src/components/button/__tests__/__snapshots__/index.spec.js.snap index d6a368be30..902df6ad5e 100644 --- a/src/components/button/__tests__/__snapshots__/index.spec.js.snap +++ b/src/components/button/__tests__/__snapshots__/index.spec.js.snap @@ -53,7 +53,7 @@ exports[`Button backgroundColor should return backgroundColor according to modif { "backgroundColor": "transparent", "color": "#20303C", - "textAlign": "left", + "writingDirection": "ltr", }, undefined, undefined, @@ -141,7 +141,7 @@ exports[`Button backgroundColor should return backgroundColor according to prop { "backgroundColor": "transparent", "color": "#20303C", - "textAlign": "left", + "writingDirection": "ltr", }, undefined, undefined, @@ -229,7 +229,7 @@ exports[`Button backgroundColor should return defined theme backgroundColor 1`] { "backgroundColor": "transparent", "color": "#20303C", - "textAlign": "left", + "writingDirection": "ltr", }, undefined, undefined, @@ -317,7 +317,7 @@ exports[`Button backgroundColor should return theme disabled color if button is { "backgroundColor": "transparent", "color": "#20303C", - "textAlign": "left", + "writingDirection": "ltr", }, undefined, undefined, @@ -405,7 +405,7 @@ exports[`Button backgroundColor should return undefined if this button is link 1 { "backgroundColor": "transparent", "color": "#20303C", - "textAlign": "left", + "writingDirection": "ltr", }, undefined, undefined, @@ -495,7 +495,7 @@ exports[`Button backgroundColor should return undefined if this button is outlin { "backgroundColor": "transparent", "color": "#20303C", - "textAlign": "left", + "writingDirection": "ltr", }, undefined, undefined, @@ -583,7 +583,7 @@ exports[`Button border radius should return 0 border radius when border radius p { "backgroundColor": "transparent", "color": "#20303C", - "textAlign": "left", + "writingDirection": "ltr", }, undefined, undefined, @@ -671,7 +671,7 @@ exports[`Button border radius should return 0 border radius when button is full { "backgroundColor": "transparent", "color": "#20303C", - "textAlign": "left", + "writingDirection": "ltr", }, undefined, undefined, @@ -759,7 +759,7 @@ exports[`Button border radius should return given border radius when use plain n { "backgroundColor": "transparent", "color": "#20303C", - "textAlign": "left", + "writingDirection": "ltr", }, undefined, undefined, @@ -847,7 +847,7 @@ exports[`Button container size should avoid minWidth limitation if avoidMinWidth { "backgroundColor": "transparent", "color": "#20303C", - "textAlign": "left", + "writingDirection": "ltr", }, undefined, undefined, @@ -940,7 +940,7 @@ exports[`Button container size should have no padding if avoidInnerPadding prop { "backgroundColor": "transparent", "color": "#20303C", - "textAlign": "left", + "writingDirection": "ltr", }, undefined, undefined, @@ -1033,7 +1033,7 @@ exports[`Button container size should have no padding of button is a link nor mi { "backgroundColor": "transparent", "color": "#20303C", - "textAlign": "left", + "writingDirection": "ltr", }, undefined, undefined, @@ -1155,7 +1155,7 @@ exports[`Button container size should have no padding of button is an icon butto { "backgroundColor": "transparent", "color": "#20303C", - "textAlign": "left", + "writingDirection": "ltr", }, undefined, undefined, @@ -1250,7 +1250,7 @@ exports[`Button container size should reduce padding by outlineWidth in case of { "backgroundColor": "transparent", "color": "#20303C", - "textAlign": "left", + "writingDirection": "ltr", }, undefined, undefined, @@ -1338,7 +1338,7 @@ exports[`Button container size should return style for large button 1`] = ` { "backgroundColor": "transparent", "color": "#20303C", - "textAlign": "left", + "writingDirection": "ltr", }, undefined, undefined, @@ -1428,7 +1428,7 @@ exports[`Button container size should return style for large button 2`] = ` { "backgroundColor": "transparent", "color": "#20303C", - "textAlign": "left", + "writingDirection": "ltr", }, undefined, undefined, @@ -1516,7 +1516,7 @@ exports[`Button container size should return style for medium button 1`] = ` { "backgroundColor": "transparent", "color": "#20303C", - "textAlign": "left", + "writingDirection": "ltr", }, undefined, undefined, @@ -1611,7 +1611,7 @@ exports[`Button container size should return style for medium button 2`] = ` { "backgroundColor": "transparent", "color": "#20303C", - "textAlign": "left", + "writingDirection": "ltr", }, undefined, undefined, @@ -1704,7 +1704,7 @@ exports[`Button container size should return style for round button 1`] = ` { "backgroundColor": "transparent", "color": "#20303C", - "textAlign": "left", + "writingDirection": "ltr", }, undefined, undefined, @@ -1792,7 +1792,7 @@ exports[`Button container size should return style for small button 1`] = ` { "backgroundColor": "transparent", "color": "#20303C", - "textAlign": "left", + "writingDirection": "ltr", }, undefined, undefined, @@ -1887,7 +1887,7 @@ exports[`Button container size should return style for small button 2`] = ` { "backgroundColor": "transparent", "color": "#20303C", - "textAlign": "left", + "writingDirection": "ltr", }, undefined, undefined, @@ -1980,7 +1980,7 @@ exports[`Button container size should return style for xSmall button 1`] = ` { "backgroundColor": "transparent", "color": "#20303C", - "textAlign": "left", + "writingDirection": "ltr", }, undefined, undefined, @@ -2075,7 +2075,7 @@ exports[`Button container size should return style for xSmall button 2`] = ` { "backgroundColor": "transparent", "color": "#20303C", - "textAlign": "left", + "writingDirection": "ltr", }, undefined, undefined, @@ -2168,7 +2168,7 @@ exports[`Button hyperlink should render button as a hyperlink 1`] = ` { "backgroundColor": "transparent", "color": "#20303C", - "textAlign": "left", + "writingDirection": "ltr", }, undefined, undefined, @@ -2719,7 +2719,7 @@ exports[`Button icon should return the right spacing according to button size wh { "backgroundColor": "transparent", "color": "#20303C", - "textAlign": "left", + "writingDirection": "ltr", }, undefined, undefined, @@ -2807,7 +2807,7 @@ exports[`Button icon should return the right spacing according to button size wh { "backgroundColor": "transparent", "color": "#20303C", - "textAlign": "left", + "writingDirection": "ltr", }, undefined, undefined, @@ -2900,7 +2900,7 @@ exports[`Button icon should return the right spacing according to button size wh { "backgroundColor": "transparent", "color": "#20303C", - "textAlign": "left", + "writingDirection": "ltr", }, undefined, undefined, @@ -2993,7 +2993,7 @@ exports[`Button icon should return the right spacing according to button size wh { "backgroundColor": "transparent", "color": "#20303C", - "textAlign": "left", + "writingDirection": "ltr", }, undefined, undefined, @@ -3086,7 +3086,7 @@ exports[`Button label size should return style for large button 1`] = ` { "backgroundColor": "transparent", "color": "#20303C", - "textAlign": "left", + "writingDirection": "ltr", }, undefined, undefined, @@ -3174,7 +3174,7 @@ exports[`Button label size should return style for medium button 1`] = ` { "backgroundColor": "transparent", "color": "#20303C", - "textAlign": "left", + "writingDirection": "ltr", }, undefined, undefined, @@ -3267,7 +3267,7 @@ exports[`Button label size should return style for small button 1`] = ` { "backgroundColor": "transparent", "color": "#20303C", - "textAlign": "left", + "writingDirection": "ltr", }, undefined, undefined, @@ -3360,7 +3360,7 @@ exports[`Button label size should return style for xSmall button 1`] = ` { "backgroundColor": "transparent", "color": "#20303C", - "textAlign": "left", + "writingDirection": "ltr", }, undefined, undefined, @@ -3453,7 +3453,7 @@ exports[`Button labelColor should return Theme linkColor color for link 1`] = ` { "backgroundColor": "transparent", "color": "#20303C", - "textAlign": "left", + "writingDirection": "ltr", }, undefined, undefined, @@ -3541,7 +3541,7 @@ exports[`Button labelColor should return color according to color modifier 1`] = { "backgroundColor": "transparent", "color": "#20303C", - "textAlign": "left", + "writingDirection": "ltr", }, undefined, undefined, @@ -3629,7 +3629,7 @@ exports[`Button labelColor should return color according to color prop 1`] = ` { "backgroundColor": "transparent", "color": "#20303C", - "textAlign": "left", + "writingDirection": "ltr", }, undefined, undefined, @@ -3717,7 +3717,7 @@ exports[`Button labelColor should return disabled text color according to theme { "backgroundColor": "transparent", "color": "#20303C", - "textAlign": "left", + "writingDirection": "ltr", }, undefined, undefined, @@ -3805,7 +3805,7 @@ exports[`Button labelColor should return linkColor color for link 1`] = ` { "backgroundColor": "transparent", "color": "#20303C", - "textAlign": "left", + "writingDirection": "ltr", }, undefined, undefined, @@ -3969,7 +3969,7 @@ exports[`Button link should render button as a link 1`] = ` { "backgroundColor": "transparent", "color": "#20303C", - "textAlign": "left", + "writingDirection": "ltr", }, undefined, undefined, @@ -4059,7 +4059,7 @@ exports[`Button outline should render button with an outline 1`] = ` { "backgroundColor": "transparent", "color": "#20303C", - "textAlign": "left", + "writingDirection": "ltr", }, undefined, undefined, @@ -4149,7 +4149,7 @@ exports[`Button outline should render button with an outlineColor 1`] = ` { "backgroundColor": "transparent", "color": "#20303C", - "textAlign": "left", + "writingDirection": "ltr", }, undefined, undefined, @@ -4239,7 +4239,7 @@ exports[`Button outline should render button with outline and outlineColor 1`] = { "backgroundColor": "transparent", "color": "#20303C", - "textAlign": "left", + "writingDirection": "ltr", }, undefined, undefined, @@ -4329,7 +4329,7 @@ exports[`Button outline should return custom borderWidth according to outlineWid { "backgroundColor": "transparent", "color": "#20303C", - "textAlign": "left", + "writingDirection": "ltr", }, undefined, undefined, @@ -4419,7 +4419,7 @@ exports[`Button outline should return disabled color for outline if button is di { "backgroundColor": "transparent", "color": "#20303C", - "textAlign": "left", + "writingDirection": "ltr", }, undefined, undefined, @@ -4507,7 +4507,7 @@ exports[`Button outline should return undefined when link is true, even when out { "backgroundColor": "transparent", "color": "#20303C", - "textAlign": "left", + "writingDirection": "ltr", }, undefined, undefined, @@ -4595,7 +4595,7 @@ exports[`Button should render default button 1`] = ` { "backgroundColor": "transparent", "color": "#20303C", - "textAlign": "left", + "writingDirection": "ltr", }, undefined, undefined, diff --git a/src/components/text/index.tsx b/src/components/text/index.tsx index 99d5cda87b..7f10bcabca 100644 --- a/src/components/text/index.tsx +++ b/src/components/text/index.tsx @@ -1,6 +1,6 @@ +import _ from 'lodash'; import React, {PureComponent} from 'react'; import {Text as RNText, StyleSheet, TextProps as RNTextProps, TextStyle, Animated, StyleProp} from 'react-native'; -import _ from 'lodash'; import { asBaseComponent, forwardRef, @@ -9,12 +9,18 @@ import { MarginModifiers, TypographyModifiers, ColorsModifiers, - FlexModifiers + FlexModifiers, + Constants } from '../../commons/new'; import {RecorderProps} from '../../../typings/recorderTypes'; import {Colors} from 'style'; import {TextUtils} from 'utils'; +enum writingDirectionTypes { + RTL = 'rtl', + LTR = 'ltr' +} + export interface HighlightStringProps { /** * Substring to highlight @@ -177,8 +183,9 @@ class Text extends PureComponent { const styles = StyleSheet.create({ container: { backgroundColor: 'transparent', - textAlign: 'left', - color: Colors.$textDefault + color: Colors.$textDefault, + // textAlign: 'left' + writingDirection: Constants.isRTL ? writingDirectionTypes.RTL : writingDirectionTypes.LTR // iOS only }, centered: { textAlign: 'center' From eb77e3c40a970ecd2a148c6450b968bbaf184e4e Mon Sep 17 00:00:00 2001 From: Miki Leib <38354019+M-i-k-e-l@users.noreply.github.com> Date: Thu, 20 Jul 2023 10:28:01 +0300 Subject: [PATCH 26/26] ExpandableOverlay - fix dialog props (#2675) --- src/incubator/expandableOverlay/index.tsx | 30 +++++++++++++++++------ 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/src/incubator/expandableOverlay/index.tsx b/src/incubator/expandableOverlay/index.tsx index 7ae146e7cd..296b8815a5 100644 --- a/src/incubator/expandableOverlay/index.tsx +++ b/src/incubator/expandableOverlay/index.tsx @@ -17,7 +17,29 @@ export interface RenderCustomOverlayProps extends ExpandableOverlayMethods { visible: boolean; } +export interface _DialogPropsOld { + /** + * The props to pass to the dialog expandable container + */ + dialogProps?: DialogPropsOld; + migrateDialog?: false; +} + +export interface _DialogPropsNew { + /** + * The props to pass to the dialog expandable container + */ + dialogProps?: DialogPropsNew; + /** + * Migrate the Dialog to DialogNew (make sure you use only new props in dialogProps) + */ + migrateDialog: true; +} + +export type DialogProps = _DialogPropsOld | _DialogPropsNew; + export type ExpandableOverlayProps = TouchableOpacityProps & + DialogProps & PropsWithChildren<{ /** * The content to render inside the expandable modal/dialog @@ -31,14 +53,6 @@ export type ExpandableOverlayProps = TouchableOpacityProps & * The props to pass to the modal expandable container */ modalProps?: ModalProps; - /** - * The props to pass to the dialog expandable container - */ - dialogProps?: DialogPropsOld | DialogPropsNew; - /** - * Migrate the Dialog to DialogNew (make sure you use only new props in dialogProps) - */ - migrateDialog?: boolean; /** * Whether to render a modal top bar (relevant only for modal) */