From 326e443c3f2868cca74f1cec9816f2c8b53c8e13 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Wed, 15 Nov 2023 12:42:02 +0100 Subject: [PATCH 01/26] ref: migrating TextInput to TS --- src/components/Icon/index.tsx | 2 + .../{index.native.js => index.native.tsx} | 0 .../BaseTextInput/{index.js => index.tsx} | 95 ++++++++------- .../TextInput/BaseTextInput/types.ts | 109 ++++++++++++++++++ .../TextInputLabel/TextInputLabelPropTypes.js | 25 ---- .../{index.native.js => index.native.tsx} | 16 ++- .../TextInputLabel/{index.js => index.tsx} | 12 +- .../TextInput/TextInputLabel/types.ts | 20 ++++ 8 files changed, 188 insertions(+), 91 deletions(-) rename src/components/TextInput/BaseTextInput/{index.native.js => index.native.tsx} (100%) rename src/components/TextInput/BaseTextInput/{index.js => index.tsx} (85%) create mode 100644 src/components/TextInput/BaseTextInput/types.ts delete mode 100644 src/components/TextInput/TextInputLabel/TextInputLabelPropTypes.js rename src/components/TextInput/TextInputLabel/{index.native.js => index.native.tsx} (76%) rename src/components/TextInput/TextInputLabel/{index.js => index.tsx} (67%) create mode 100644 src/components/TextInput/TextInputLabel/types.ts diff --git a/src/components/Icon/index.tsx b/src/components/Icon/index.tsx index 022c740907ea..86e4a592045c 100644 --- a/src/components/Icon/index.tsx +++ b/src/components/Icon/index.tsx @@ -100,3 +100,5 @@ class Icon extends PureComponent { } export default Icon; + +export type {SrcProps}; diff --git a/src/components/TextInput/BaseTextInput/index.native.js b/src/components/TextInput/BaseTextInput/index.native.tsx similarity index 100% rename from src/components/TextInput/BaseTextInput/index.native.js rename to src/components/TextInput/BaseTextInput/index.native.tsx diff --git a/src/components/TextInput/BaseTextInput/index.js b/src/components/TextInput/BaseTextInput/index.tsx similarity index 85% rename from src/components/TextInput/BaseTextInput/index.js rename to src/components/TextInput/BaseTextInput/index.tsx index e643c6ff6b4f..8f5b8e4f5320 100644 --- a/src/components/TextInput/BaseTextInput/index.js +++ b/src/components/TextInput/BaseTextInput/index.tsx @@ -1,7 +1,6 @@ import Str from 'expensify-common/lib/str'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; -import {ActivityIndicator, Animated, StyleSheet, View} from 'react-native'; -import _ from 'underscore'; +import {ActivityIndicator, Animated, GestureResponderEvent, LayoutChangeEvent, NativeSyntheticEvent, StyleSheet, TextInput, TextInputFocusEventData, View} from 'react-native'; import Checkbox from '@components/Checkbox'; import FormHelpMessage from '@components/FormHelpMessage'; import Icon from '@components/Icon'; @@ -22,21 +21,22 @@ import themeColors from '@styles/themes/default'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import * as baseTextInputPropTypes from './baseTextInputPropTypes'; +import BaseTextInputProps from './types'; -function BaseTextInput(props) { - const initialValue = props.value || props.defaultValue || ''; - const initialActiveLabel = props.forceActiveLabel || initialValue.length > 0 || Boolean(props.prefixCharacter); +function BaseTextInput(props: BaseTextInputProps) { + const initialValue = props.value ?? props.defaultValue ?? ''; + const initialActiveLabel = !!props.forceActiveLabel || initialValue.length > 0 || Boolean(props.prefixCharacter); const [isFocused, setIsFocused] = useState(false); const [passwordHidden, setPasswordHidden] = useState(props.secureTextEntry); const [textInputWidth, setTextInputWidth] = useState(0); const [textInputHeight, setTextInputHeight] = useState(0); - const [height, setHeight] = useState(variables.componentSizeLarge); - const [width, setWidth] = useState(); + const [height, setHeight] = useState(variables.componentSizeLarge); + const [width, setWidth] = useState(); const labelScale = useRef(new Animated.Value(initialActiveLabel ? styleConst.ACTIVE_LABEL_SCALE : styleConst.INACTIVE_LABEL_SCALE)).current; const labelTranslateY = useRef(new Animated.Value(initialActiveLabel ? styleConst.ACTIVE_LABEL_TRANSLATE_Y : styleConst.INACTIVE_LABEL_TRANSLATE_Y)).current; - const input = useRef(null); + const input = useRef(null); const isLabelActive = useRef(initialActiveLabel); // AutoFocus which only works on mount: @@ -47,7 +47,7 @@ function BaseTextInput(props) { } if (props.shouldDelayFocus) { - const focusTimeout = setTimeout(() => input.current.focus(), CONST.ANIMATED_TRANSITION); + const focusTimeout = setTimeout(() => input?.current?.focus(), CONST.ANIMATED_TRANSITION); return () => clearTimeout(focusTimeout); } input.current.focus(); @@ -56,16 +56,16 @@ function BaseTextInput(props) { }, []); const animateLabel = useCallback( - (translateY, scale) => { + (translateY: number, scale: number) => { Animated.parallel([ Animated.spring(labelTranslateY, { toValue: translateY, - duration: styleConst.LABEL_ANIMATION_DURATION, + // duration: styleConst.LABEL_ANIMATION_DURATION, useNativeDriver, }), Animated.spring(labelScale, { toValue: scale, - duration: styleConst.LABEL_ANIMATION_DURATION, + // duration: styleConst.LABEL_ANIMATION_DURATION, useNativeDriver, }), ]).start(); @@ -74,7 +74,7 @@ function BaseTextInput(props) { ); const activateLabel = useCallback(() => { - const value = props.value || ''; + const value = props.value ?? ''; if (value.length < 0 || isLabelActive.current) { return; @@ -85,9 +85,9 @@ function BaseTextInput(props) { }, [animateLabel, props.value]); const deactivateLabel = useCallback(() => { - const value = props.value || ''; + const value = props.value ?? ''; - if (props.forceActiveLabel || value.length !== 0 || props.prefixCharacter) { + if (!!props.forceActiveLabel || value.length !== 0 || props.prefixCharacter) { return; } @@ -95,22 +95,22 @@ function BaseTextInput(props) { isLabelActive.current = false; }, [animateLabel, props.forceActiveLabel, props.prefixCharacter, props.value]); - const onFocus = (event) => { + const onFocus = (event: NativeSyntheticEvent) => { if (props.onFocus) { props.onFocus(event); } setIsFocused(true); }; - const onBlur = (event) => { + const onBlur = (event: NativeSyntheticEvent) => { if (props.onBlur) { props.onBlur(event); } setIsFocused(false); }; - const onPress = (event) => { - if (props.disabled) { + const onPress = (event?: GestureResponderEvent | KeyboardEvent) => { + if (!!props.disabled || !event) { return; } @@ -118,28 +118,28 @@ function BaseTextInput(props) { props.onPress(event); } - if (!event.isDefaultPrevented()) { - input.current.focus(); + if ('isDefaultPrevented' in event && !event?.isDefaultPrevented()) { + input.current?.focus(); } }; const onLayout = useCallback( - (event) => { + (event: LayoutChangeEvent) => { if (!props.autoGrowHeight && props.multiline) { return; } const layout = event.nativeEvent.layout; - setWidth((prevWidth) => (props.autoGrowHeight ? layout.width : prevWidth)); - setHeight((prevHeight) => (!props.multiline ? layout.height : prevHeight)); + setWidth((prevWidth: number | undefined) => (props.autoGrowHeight ? layout.width : prevWidth)); + setHeight((prevHeight: number) => (!props.multiline ? layout.height : prevHeight)); }, [props.autoGrowHeight, props.multiline], ); // The ref is needed when the component is uncontrolled and we don't have a value prop const hasValueRef = useRef(initialValue.length > 0); - const inputValue = props.value || ''; + const inputValue = props.value ?? ''; const hasValue = inputValue.length > 0 || hasValueRef.current; // Activate or deactivate the label when either focus changes, or for controlled @@ -161,7 +161,7 @@ function BaseTextInput(props) { // When the value prop gets cleared externally, we need to keep the ref in sync: useEffect(() => { // Return early when component uncontrolled, or we still have a value - if (props.value === undefined || !_.isEmpty(props.value)) { + if (props.value === undefined || !props.value) { return; } hasValueRef.current = false; @@ -169,17 +169,14 @@ function BaseTextInput(props) { /** * Set Value & activateLabel - * - * @param {String} value - * @memberof BaseTextInput */ - const setValue = (value) => { + const setValue = (value: string) => { if (props.onInputChange) { props.onInputChange(value); } - - Str.result(props.onChangeText, value); - + if (props.onChangeText) { + Str.result(props.onChangeText, value); + } if (value && value.length > 0) { hasValueRef.current = true; // When the componment is uncontrolled, we need to manually activate the label: @@ -192,7 +189,7 @@ function BaseTextInput(props) { }; const togglePasswordVisibility = useCallback(() => { - setPasswordHidden((prevPasswordHidden) => !prevPasswordHidden); + setPasswordHidden((prevPasswordHidden: boolean | undefined) => !prevPasswordHidden); }, []); // When adding a new prefix character, adjust this method to add expected character width. @@ -201,7 +198,7 @@ function BaseTextInput(props) { // Some characters are wider than the others when rendered, e.g. '@' vs '#'. Chosen font-family and font-size // also have an impact on the width of the character, but as long as there's only one font-family and one font-size, // this method will produce reliable results. - const getCharacterPadding = (prefix) => { + const getCharacterPadding = (prefix: string) => { switch (prefix) { case CONST.POLICY.ROOM_PREFIX: return 10; @@ -212,20 +209,20 @@ function BaseTextInput(props) { // eslint-disable-next-line react/forbid-foreign-prop-types const inputProps = _.omit(props, _.keys(baseTextInputPropTypes.propTypes)); - const hasLabel = Boolean(props.label.length); - const isReadOnly = _.isUndefined(props.readOnly) ? props.disabled : props.readOnly; + const hasLabel = Boolean(props.label?.length); + const isReadOnly = props.readOnly ?? props.disabled; const inputHelpText = props.errorText || props.hint; - const placeholder = props.prefixCharacter || isFocused || !hasLabel || (hasLabel && props.forceActiveLabel) ? props.placeholder : null; + const placeholder = !!props.prefixCharacter || isFocused || !hasLabel || (hasLabel && props.forceActiveLabel) ? props.placeholder : undefined; const maxHeight = StyleSheet.flatten(props.containerStyles).maxHeight; const textInputContainerStyles = StyleSheet.flatten([ styles.textInputContainer, - ...props.textInputContainerStyles, + props.textInputContainerStyles, props.autoGrow && StyleUtils.getWidthStyle(textInputWidth), !props.hideFocusedState && isFocused && styles.borderColorFocus, - (props.hasError || props.errorText) && styles.borderColorDanger, + (!!props.hasError || !!props.errorText) && styles.borderColorDanger, props.autoGrowHeight && {scrollPaddingTop: 2 * maxHeight}, ]); - const isMultiline = props.multiline || props.autoGrowHeight; + const isMultiline = props.multiline ?? props.autoGrowHeight; /* To prevent text jumping caused by virtual DOM calculations on Safari and mobile Chrome, make sure to include the `lineHeight`. @@ -236,8 +233,8 @@ function BaseTextInput(props) { See https://github.com/Expensify/App/issues/13802 */ const lineHeight = useMemo(() => { - if ((Browser.isSafari() || Browser.isMobileChrome()) && _.isArray(props.inputStyle)) { - const lineHeightValue = _.find(props.inputStyle, (f) => f.lineHeight !== undefined); + if ((Browser.isSafari() || Browser.isMobileChrome()) && Array.isArray(props.inputStyle)) { + const lineHeightValue = props.inputStyle.find((f) => f?.lineHeight !== undefined); if (lineHeightValue) { return lineHeightValue.lineHeight; } @@ -260,7 +257,7 @@ function BaseTextInput(props) { style={[ props.autoGrowHeight && styles.autoGrowHeightInputContainer(textInputHeight, variables.componentSizeLarge, maxHeight), !isMultiline && styles.componentHeightLarge, - ...props.containerStyles, + props.containerStyles, ]} > } e.preventDefault()} - accessibilityLabel={props.translate('common.visible')} + accessibilityLabel={props.translate?.('common.visible') ?? ''} > { let additionalWidth = 0; if (Browser.isMobileSafari() || Browser.isSafari()) { @@ -423,7 +420,5 @@ function BaseTextInput(props) { } BaseTextInput.displayName = 'BaseTextInput'; -BaseTextInput.propTypes = baseTextInputPropTypes.propTypes; -BaseTextInput.defaultProps = baseTextInputPropTypes.defaultProps; export default withLocalize(BaseTextInput); diff --git a/src/components/TextInput/BaseTextInput/types.ts b/src/components/TextInput/BaseTextInput/types.ts new file mode 100644 index 000000000000..091a5d18379f --- /dev/null +++ b/src/components/TextInput/BaseTextInput/types.ts @@ -0,0 +1,109 @@ +import React from 'react'; +import {GestureResponderEvent, NativeSyntheticEvent, StyleProp, TextInput, TextInputFocusEventData, TextInputProps, ViewStyle} from 'react-native'; +import {SrcProps} from '@components/Icon'; + +type CustomTextInputProps = { + /** Input label */ + label?: string; + + /** Name attribute for the input */ + name?: string; + + /** Input value */ + value?: string; + + /** Default value - used for non controlled inputs */ + defaultValue?: string; + + /** Input value placeholder */ + placeholder?: string; + + /** Error text to display */ + errorText: string | string[] | Record; + + /** Icon to display in right side of text input */ + icon: (props: SrcProps) => React.ReactNode; + + /** Customize the TextInput container */ + textInputContainerStyles: StyleProp; + + /** Customize the main container */ + containerStyles: StyleProp; + + /** input style */ + inputStyle: StyleProp; + + /** If present, this prop forces the label to remain in a position where it will not collide with input text */ + forceActiveLabel?: boolean; + + /** Should the input auto focus? */ + autoFocus?: boolean; + + /** Disable the virtual keyboard */ + disableKeyboard?: boolean; + + /** + * Autogrow input container length based on the entered text. + * Note: If you use this prop, the text input has to be controlled + * by a value prop. + */ + autoGrow?: boolean; + + /** + * Autogrow input container height based on the entered text + * Note: If you use this prop, the text input has to be controlled + * by a value prop. + */ + autoGrowHeight?: boolean; + + /** Hide the focus styles on TextInput */ + hideFocusedState?: boolean; + + /** Forward the inner ref */ + innerRef?: React.RefObject; + + /** Maximum characters allowed */ + maxLength?: number; + + /** Hint text to display below the TextInput */ + hint?: string; + + /** Prefix character */ + prefixCharacter?: string; + + /** Whether autoCorrect functionality should enable */ + autoCorrect?: boolean; + + /** Form props */ + /** The ID used to uniquely identify the input in a Form */ + inputID?: string; + + /** Saves a draft of the input value when used in a form */ + shouldSaveDraft?: boolean; + + /** Callback to update the value on Form when input is used in the Form component. */ + onInputChange?: (value: string) => void; + + /** Whether we should wait before focusing the TextInput, useful when using transitions */ + shouldDelayFocus?: boolean; + + /** Indicate whether pressing Enter on multiline input is allowed to submit the form. */ + submitOnEnter?: boolean; + + /** Indicate whether input is multiline */ + multiline?: boolean; + + /** Set the default value to the input if there is a valid saved value */ + shouldUseDefaultValue?: boolean; + + /** Indicate whether or not the input should prevent swipe actions in tabs */ + shouldInterceptSwipe?: boolean; + + hasError?: boolean; + onPress?: (event: GestureResponderEvent | KeyboardEvent) => void; + isLoading?: boolean; + translate?: (key: string) => string; +}; + +type BaseTextInputProps = CustomTextInputProps & TextInputProps; +export default BaseTextInputProps; diff --git a/src/components/TextInput/TextInputLabel/TextInputLabelPropTypes.js b/src/components/TextInput/TextInputLabel/TextInputLabelPropTypes.js deleted file mode 100644 index 82b98f17d808..000000000000 --- a/src/components/TextInput/TextInputLabel/TextInputLabelPropTypes.js +++ /dev/null @@ -1,25 +0,0 @@ -import PropTypes from 'prop-types'; -import {Animated} from 'react-native'; - -const propTypes = { - /** Label */ - label: PropTypes.string.isRequired, - - /** Label vertical translate */ - labelTranslateY: PropTypes.instanceOf(Animated.Value).isRequired, - - /** Label scale */ - labelScale: PropTypes.instanceOf(Animated.Value).isRequired, - - /** Whether the label is currently active or not */ - isLabelActive: PropTypes.bool.isRequired, - - /** For attribute for label */ - for: PropTypes.string, -}; - -const defaultProps = { - for: '', -}; - -export {propTypes, defaultProps}; diff --git a/src/components/TextInput/TextInputLabel/index.native.js b/src/components/TextInput/TextInputLabel/index.native.tsx similarity index 76% rename from src/components/TextInput/TextInputLabel/index.native.js rename to src/components/TextInput/TextInputLabel/index.native.tsx index 51b231287b1f..7260e7bc9976 100644 --- a/src/components/TextInput/TextInputLabel/index.native.js +++ b/src/components/TextInput/TextInputLabel/index.native.tsx @@ -2,9 +2,9 @@ import React, {useState} from 'react'; import {Animated} from 'react-native'; import * as styleConst from '@components/TextInput/styleConst'; import styles from '@styles/styles'; -import * as TextInputLabelPropTypes from './TextInputLabelPropTypes'; +import TextInputLabelProps from './types'; -function TextInputLabel(props) { +function TextInputLabel({isLabelActive, label, labelScale, labelTranslateY}: TextInputLabelProps) { const [width, setWidth] = useState(0); return ( @@ -16,29 +16,27 @@ function TextInputLabel(props) { style={[ styles.textInputLabel, styles.textInputLabelTransformation( - props.labelTranslateY, - props.labelScale.interpolate({ + labelTranslateY, + labelScale.interpolate({ inputRange: [styleConst.ACTIVE_LABEL_SCALE, styleConst.INACTIVE_LABEL_SCALE], outputRange: [-(width - width * styleConst.ACTIVE_LABEL_SCALE) / 2, 0], }), - props.labelScale, + labelScale, ), // If the label is active but the width is not ready yet, the above translateX value will be 0, // making the label sits at the top center instead of the top left of the input. To solve it // move the label by a percentage value with left style as translateX doesn't support percentage value. width === 0 && - props.isLabelActive && { + isLabelActive && { left: `${-((1 - styleConst.ACTIVE_LABEL_SCALE) * 100) / 2}%`, }, ]} > - {props.label} + {label} ); } -TextInputLabel.propTypes = TextInputLabelPropTypes.propTypes; -TextInputLabel.defaultProps = TextInputLabelPropTypes.defaultProps; TextInputLabel.displayName = 'TextInputLabel'; export default TextInputLabel; diff --git a/src/components/TextInput/TextInputLabel/index.js b/src/components/TextInput/TextInputLabel/index.tsx similarity index 67% rename from src/components/TextInput/TextInputLabel/index.js rename to src/components/TextInput/TextInputLabel/index.tsx index b49635b91d96..1db173c24d0c 100644 --- a/src/components/TextInput/TextInputLabel/index.js +++ b/src/components/TextInput/TextInputLabel/index.tsx @@ -1,17 +1,17 @@ import React, {useEffect, useRef} from 'react'; -import {Animated} from 'react-native'; +import {Animated, Text} from 'react-native'; import styles from '@styles/styles'; import CONST from '@src/CONST'; -import {defaultProps, propTypes} from './TextInputLabelPropTypes'; +import TextInputLabelProps from './types'; -function TextInputLabel({for: inputId, label, labelTranslateY, labelScale}) { - const labelRef = useRef(null); +function TextInputLabel({for: inputId = '', label, labelTranslateY, labelScale}: TextInputLabelProps) { + const labelRef = useRef(null); useEffect(() => { if (!inputId || !labelRef.current) { return; } - labelRef.current.setAttribute('for', inputId); + (labelRef.current as unknown as HTMLFormElement).setAttribute('for', inputId); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -27,7 +27,5 @@ function TextInputLabel({for: inputId, label, labelTranslateY, labelScale}) { } TextInputLabel.displayName = 'TextInputLabel'; -TextInputLabel.propTypes = propTypes; -TextInputLabel.defaultProps = defaultProps; export default React.memo(TextInputLabel); diff --git a/src/components/TextInput/TextInputLabel/types.ts b/src/components/TextInput/TextInputLabel/types.ts new file mode 100644 index 000000000000..6f85eef18f42 --- /dev/null +++ b/src/components/TextInput/TextInputLabel/types.ts @@ -0,0 +1,20 @@ +import {Animated} from 'react-native'; + +type TextInputLabelProps = { + /** Label */ + label: string; + + /** Label vertical translate */ + labelTranslateY: Animated.Value; + + /** Label scale */ + labelScale: Animated.Value; + + /** Whether the label is currently active or not */ + isLabelActive: boolean; + + /** For attribute for label */ + for?: string; +}; + +export default TextInputLabelProps; From 255058f4adf1540438b10807ce5a8eaa2f857318 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Wed, 15 Nov 2023 16:39:04 +0100 Subject: [PATCH 02/26] fix: fixing types problems in BaseTextInput for Web --- .../TextInput/BaseTextInput/index.native.tsx | 62 ++++++++++--------- .../TextInput/BaseTextInput/index.tsx | 45 ++++++++++---- .../TextInput/BaseTextInput/types.ts | 18 +++--- 3 files changed, 75 insertions(+), 50 deletions(-) diff --git a/src/components/TextInput/BaseTextInput/index.native.tsx b/src/components/TextInput/BaseTextInput/index.native.tsx index 5c3e19a2d94c..89c8f67a1026 100644 --- a/src/components/TextInput/BaseTextInput/index.native.tsx +++ b/src/components/TextInput/BaseTextInput/index.native.tsx @@ -1,6 +1,6 @@ import Str from 'expensify-common/lib/str'; import React, {useCallback, useEffect, useRef, useState} from 'react'; -import {ActivityIndicator, Animated, StyleSheet, View} from 'react-native'; +import {ActivityIndicator, Animated, StyleSheet, TextInput, View} from 'react-native'; import _ from 'underscore'; import Checkbox from '@components/Checkbox'; import FormHelpMessage from '@components/FormHelpMessage'; @@ -22,10 +22,11 @@ import themeColors from '@styles/themes/default'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import * as baseTextInputPropTypes from './baseTextInputPropTypes'; +import BaseTextInputProps from './types'; -function BaseTextInput(props) { - const initialValue = props.value || props.defaultValue || ''; - const initialActiveLabel = props.forceActiveLabel || initialValue.length > 0 || Boolean(props.prefixCharacter); +function BaseTextInput(props: BaseTextInputProps) { + const initialValue = props.value ?? props.defaultValue ?? ''; + const initialActiveLabel = !!props.forceActiveLabel || initialValue.length > 0 || Boolean(props.prefixCharacter); const [isFocused, setIsFocused] = useState(false); const [passwordHidden, setPasswordHidden] = useState(props.secureTextEntry); @@ -36,7 +37,7 @@ function BaseTextInput(props) { const labelScale = useRef(new Animated.Value(initialActiveLabel ? styleConst.ACTIVE_LABEL_SCALE : styleConst.INACTIVE_LABEL_SCALE)).current; const labelTranslateY = useRef(new Animated.Value(initialActiveLabel ? styleConst.ACTIVE_LABEL_TRANSLATE_Y : styleConst.INACTIVE_LABEL_TRANSLATE_Y)).current; - const input = useRef(null); + const input = useRef(null); const isLabelActive = useRef(initialActiveLabel); // AutoFocus which only works on mount: @@ -47,7 +48,7 @@ function BaseTextInput(props) { } if (props.shouldDelayFocus) { - const focusTimeout = setTimeout(() => input.current.focus(), CONST.ANIMATED_TRANSITION); + const focusTimeout = setTimeout(() => input.current?.focus(), CONST.ANIMATED_TRANSITION); return () => clearTimeout(focusTimeout); } input.current.focus(); @@ -56,16 +57,16 @@ function BaseTextInput(props) { }, []); const animateLabel = useCallback( - (translateY, scale) => { + (translateY: number, scale: number) => { Animated.parallel([ Animated.spring(labelTranslateY, { toValue: translateY, - duration: styleConst.LABEL_ANIMATION_DURATION, + // duration: styleConst.LABEL_ANIMATION_DURATION, useNativeDriver, }), Animated.spring(labelScale, { toValue: scale, - duration: styleConst.LABEL_ANIMATION_DURATION, + // duration: styleConst.LABEL_ANIMATION_DURATION, useNativeDriver, }), ]).start(); @@ -85,9 +86,9 @@ function BaseTextInput(props) { }, [animateLabel, props.value]); const deactivateLabel = useCallback(() => { - const value = props.value || ''; + const value = props.value ?? ''; - if (props.forceActiveLabel || value.length !== 0 || props.prefixCharacter) { + if (!!props.forceActiveLabel || value.length !== 0 || props.prefixCharacter) { return; } @@ -119,7 +120,7 @@ function BaseTextInput(props) { } if (!event.isDefaultPrevented()) { - input.current.focus(); + input.current?.focus(); } }; @@ -139,7 +140,7 @@ function BaseTextInput(props) { // The ref is needed when the component is uncontrolled and we don't have a value prop const hasValueRef = useRef(initialValue.length > 0); - const inputValue = props.value || ''; + const inputValue = props.value ?? ''; const hasValue = inputValue.length > 0 || hasValueRef.current; // Activate or deactivate the label when either focus changes, or for controlled @@ -169,9 +170,6 @@ function BaseTextInput(props) { /** * Set Value & activateLabel - * - * @param {String} value - * @memberof BaseTextInput */ const setValue = (value) => { if (props.onInputChange) { @@ -201,7 +199,7 @@ function BaseTextInput(props) { // Some characters are wider than the others when rendered, e.g. '@' vs '#'. Chosen font-family and font-size // also have an impact on the width of the character, but as long as there's only one font-family and one font-size, // this method will produce reliable results. - const getCharacterPadding = (prefix) => { + const getCharacterPadding = (prefix: string) => { switch (prefix) { case CONST.POLICY.ROOM_PREFIX: return 10; @@ -212,20 +210,21 @@ function BaseTextInput(props) { // eslint-disable-next-line react/forbid-foreign-prop-types const inputProps = _.omit(props, _.keys(baseTextInputPropTypes.propTypes)); - const hasLabel = Boolean(props.label.length); + const hasLabel = Boolean(props.label?.length); const isReadOnly = _.isUndefined(props.readOnly) ? props.disabled : props.readOnly; const inputHelpText = props.errorText || props.hint; - const placeholder = props.prefixCharacter || isFocused || !hasLabel || (hasLabel && props.forceActiveLabel) ? props.placeholder : null; + const placeholder = !!props.prefixCharacter || isFocused || !hasLabel || (hasLabel && props.forceActiveLabel) ? props.placeholder : undefined; const maxHeight = StyleSheet.flatten(props.containerStyles).maxHeight; + console.log('maxHeight', maxHeight); const textInputContainerStyles = StyleSheet.flatten([ styles.textInputContainer, - ...props.textInputContainerStyles, + props.textInputContainerStyles, props.autoGrow && StyleUtils.getWidthStyle(textInputWidth), !props.hideFocusedState && isFocused && styles.borderColorFocus, - (props.hasError || props.errorText) && styles.borderColorDanger, - props.autoGrowHeight && {scrollPaddingTop: 2 * maxHeight}, + (!!props.hasError || props.errorText) && styles.borderColorDanger, + props.autoGrowHeight && {scrollPaddingTop: typeof maxHeight === 'number' ? 2 * maxHeight : undefined}, ]); - const isMultiline = props.multiline || props.autoGrowHeight; + const isMultiline = !!props.multiline || props.autoGrowHeight; return ( <> @@ -241,7 +240,7 @@ function BaseTextInput(props) { style={[ props.autoGrowHeight && styles.autoGrowHeightInputContainer(textInputHeight, variables.componentSizeLarge, maxHeight), !isMultiline && styles.componentHeightLarge, - ...props.containerStyles, + props.containerStyles, ]} > } - {!_.isEmpty(inputHelpText) && ( + {!inputHelpText && ( )} @@ -375,12 +374,17 @@ function BaseTextInput(props) { This text view is used to calculate width or height of the input value given textStyle in this component. This Text component is intentionally positioned out of the screen. */} - {(props.autoGrow || props.autoGrowHeight) && ( + {(!!props.autoGrow || props.autoGrowHeight) && ( // Add +2 to width on Safari browsers so that text is not cut off due to the cursor or when changing the value // https://github.com/Expensify/App/issues/8158 // https://github.com/Expensify/App/issues/26628 { setTextInputWidth(e.nativeEvent.layout.width); setTextInputHeight(e.nativeEvent.layout.height); diff --git a/src/components/TextInput/BaseTextInput/index.tsx b/src/components/TextInput/BaseTextInput/index.tsx index 8f5b8e4f5320..77b2458c6b41 100644 --- a/src/components/TextInput/BaseTextInput/index.tsx +++ b/src/components/TextInput/BaseTextInput/index.tsx @@ -1,6 +1,19 @@ import Str from 'expensify-common/lib/str'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; -import {ActivityIndicator, Animated, GestureResponderEvent, LayoutChangeEvent, NativeSyntheticEvent, StyleSheet, TextInput, TextInputFocusEventData, View} from 'react-native'; +import { + ActivityIndicator, + Animated, + FlexStyle, + GestureResponderEvent, + LayoutChangeEvent, + NativeSyntheticEvent, + StyleProp, + StyleSheet, + TextInput, + TextInputFocusEventData, + View, + ViewStyle, +} from 'react-native'; import Checkbox from '@components/Checkbox'; import FormHelpMessage from '@components/FormHelpMessage'; import Icon from '@components/Icon'; @@ -207,20 +220,19 @@ function BaseTextInput(props: BaseTextInputProps) { } }; - // eslint-disable-next-line react/forbid-foreign-prop-types const inputProps = _.omit(props, _.keys(baseTextInputPropTypes.propTypes)); const hasLabel = Boolean(props.label?.length); const isReadOnly = props.readOnly ?? props.disabled; const inputHelpText = props.errorText || props.hint; const placeholder = !!props.prefixCharacter || isFocused || !hasLabel || (hasLabel && props.forceActiveLabel) ? props.placeholder : undefined; const maxHeight = StyleSheet.flatten(props.containerStyles).maxHeight; - const textInputContainerStyles = StyleSheet.flatten([ + const textInputContainerStyles: StyleProp = StyleSheet.flatten([ styles.textInputContainer, props.textInputContainerStyles, props.autoGrow && StyleUtils.getWidthStyle(textInputWidth), !props.hideFocusedState && isFocused && styles.borderColorFocus, (!!props.hasError || !!props.errorText) && styles.borderColorDanger, - props.autoGrowHeight && {scrollPaddingTop: 2 * maxHeight}, + props.autoGrowHeight && {scrollPaddingTop: typeof maxHeight === 'number' ? 2 * maxHeight : undefined}, ]); const isMultiline = props.multiline ?? props.autoGrowHeight; @@ -234,8 +246,8 @@ function BaseTextInput(props: BaseTextInputProps) { const lineHeight = useMemo(() => { if ((Browser.isSafari() || Browser.isMobileChrome()) && Array.isArray(props.inputStyle)) { - const lineHeightValue = props.inputStyle.find((f) => f?.lineHeight !== undefined); - if (lineHeightValue) { + const lineHeightValue = props.inputStyle.find((f) => f && 'lineHeight' in f && f.lineHeight !== undefined); + if (lineHeightValue && 'lineHeight' in lineHeightValue) { return lineHeightValue.lineHeight; } } @@ -253,7 +265,8 @@ function BaseTextInput(props: BaseTextInputProps) { { if (typeof props.innerRef === 'function') { props.innerRef(ref); - } else if (props.innerRef && _.has(props.innerRef, 'current')) { + } else if (props.innerRef && 'current' in props.innerRef) { // eslint-disable-next-line no-param-reassign props.innerRef.current = ref; } + // @ts-expect-error We need to reassign this ref to the input ref input.current = ref; }} // eslint-disable-next-line @@ -318,7 +332,7 @@ function BaseTextInput(props: BaseTextInputProps) { styles.w100, props.inputStyle, (!hasLabel || isMultiline) && styles.pv0, - props.prefixCharacter && StyleUtils.getPaddingLeft(getCharacterPadding(props.prefixCharacter) + styles.pl1.paddingLeft), + !!props.prefixCharacter && StyleUtils.getPaddingLeft(getCharacterPadding(props.prefixCharacter) + styles.pl1.paddingLeft), props.secureTextEntry && styles.secureInput, // Explicitly remove `lineHeight` from single line inputs so that long text doesn't disappear @@ -330,7 +344,7 @@ function BaseTextInput(props: BaseTextInputProps) { !isMultiline && Browser.isMobileChrome() && {boxSizing: 'content-box', height: undefined}, // Stop scrollbar flashing when breaking lines with autoGrowHeight enabled. - props.autoGrowHeight && StyleUtils.getAutoGrowHeightInputStyle(textInputHeight, maxHeight), + props.autoGrowHeight && StyleUtils.getAutoGrowHeightInputStyle(textInputHeight, typeof maxHeight === 'number' ? maxHeight : 0), // Add disabled color theme when field is not editable. props.disabled && styles.textInputDisabled, styles.pointerEventsAuto, @@ -383,9 +397,9 @@ function BaseTextInput(props: BaseTextInputProps) { - {!_.isEmpty(inputHelpText) && ( + {!inputHelpText && ( )} @@ -401,7 +415,12 @@ function BaseTextInput(props: BaseTextInputProps) { // https://github.com/Expensify/App/issues/8158 // https://github.com/Expensify/App/issues/26628 { let additionalWidth = 0; if (Browser.isMobileSafari() || Browser.isSafari()) { diff --git a/src/components/TextInput/BaseTextInput/types.ts b/src/components/TextInput/BaseTextInput/types.ts index 091a5d18379f..de453802b2b6 100644 --- a/src/components/TextInput/BaseTextInput/types.ts +++ b/src/components/TextInput/BaseTextInput/types.ts @@ -1,8 +1,9 @@ -import React from 'react'; -import {GestureResponderEvent, NativeSyntheticEvent, StyleProp, TextInput, TextInputFocusEventData, TextInputProps, ViewStyle} from 'react-native'; +import React, {Component} from 'react'; +import {FlexStyle, GestureResponderEvent, NativeSyntheticEvent, StyleProp, TextInput, TextInputFocusEventData, TextInputProps, TextStyle, ViewStyle} from 'react-native'; +import {AnimatedProps} from 'react-native-reanimated'; import {SrcProps} from '@components/Icon'; -type CustomTextInputProps = { +type CustomBaseTextInputProps = { /** Input label */ label?: string; @@ -25,13 +26,13 @@ type CustomTextInputProps = { icon: (props: SrcProps) => React.ReactNode; /** Customize the TextInput container */ - textInputContainerStyles: StyleProp; + textInputContainerStyles: StyleProp; /** Customize the main container */ - containerStyles: StyleProp; + containerStyles: StyleProp; /** input style */ - inputStyle: StyleProp; + inputStyle: StyleProp; /** If present, this prop forces the label to remain in a position where it will not collide with input text */ forceActiveLabel?: boolean; @@ -60,7 +61,7 @@ type CustomTextInputProps = { hideFocusedState?: boolean; /** Forward the inner ref */ - innerRef?: React.RefObject; + innerRef?: React.ForwardedRef, unknown, unknown> | null>; /** Maximum characters allowed */ maxLength?: number; @@ -105,5 +106,6 @@ type CustomTextInputProps = { translate?: (key: string) => string; }; -type BaseTextInputProps = CustomTextInputProps & TextInputProps; +type BaseTextInputProps = CustomBaseTextInputProps & TextInputProps; export default BaseTextInputProps; +export type {CustomBaseTextInputProps}; From b573de9b56fe0cb4516c7d3fd744699324bc4685 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Thu, 16 Nov 2023 11:40:08 +0100 Subject: [PATCH 03/26] fix: move inputProps const into native ones --- .../TextInput/BaseTextInput/index.native.tsx | 239 +++++++++++------- .../TextInput/BaseTextInput/index.tsx | 5 +- .../{index.native.js => index.native.tsx} | 0 .../TextInput/{index.js => index.tsx} | 0 .../{styleConst.js => styleConst.ts} | 0 src/components/TextInput/types.ts | 0 src/libs/getSecureEntryKeyboardType/types.ts | 4 +- 7 files changed, 149 insertions(+), 99 deletions(-) rename src/components/TextInput/{index.native.js => index.native.tsx} (100%) rename src/components/TextInput/{index.js => index.tsx} (100%) rename src/components/TextInput/{styleConst.js => styleConst.ts} (100%) create mode 100644 src/components/TextInput/types.ts diff --git a/src/components/TextInput/BaseTextInput/index.native.tsx b/src/components/TextInput/BaseTextInput/index.native.tsx index 89c8f67a1026..d696a97232c6 100644 --- a/src/components/TextInput/BaseTextInput/index.native.tsx +++ b/src/components/TextInput/BaseTextInput/index.native.tsx @@ -1,7 +1,19 @@ import Str from 'expensify-common/lib/str'; import React, {useCallback, useEffect, useRef, useState} from 'react'; -import {ActivityIndicator, Animated, StyleSheet, TextInput, View} from 'react-native'; -import _ from 'underscore'; +import { + ActivityIndicator, + Animated, + FlexStyle, + GestureResponderEvent, + LayoutChangeEvent, + NativeSyntheticEvent, + StyleProp, + StyleSheet, + TextInput, + TextInputFocusEventData, + View, + ViewStyle, +} from 'react-native'; import Checkbox from '@components/Checkbox'; import FormHelpMessage from '@components/FormHelpMessage'; import Icon from '@components/Icon'; @@ -24,16 +36,50 @@ import CONST from '@src/CONST'; import * as baseTextInputPropTypes from './baseTextInputPropTypes'; import BaseTextInputProps from './types'; -function BaseTextInput(props: BaseTextInputProps) { - const initialValue = props.value ?? props.defaultValue ?? ''; - const initialActiveLabel = !!props.forceActiveLabel || initialValue.length > 0 || Boolean(props.prefixCharacter); +function BaseTextInput({ + label = '', + name = '', + value = undefined, + defaultValue = undefined, + placeholder = '', + errorText = '', + icon = null, + textInputContainerStyles = [], + containerStyles = [], + inputStyle = [], + forceActiveLabel = false, + autoFocus = false, + disableKeyboard = false, + autoGrow = false, + autoGrowHeight = false, + hideFocusedState = false, + innerRef = () => {}, + maxLength = null, + hint = '', + shouldSaveDraft = false, + onInputChange = () => {}, + shouldDelayFocus = false, + submitOnEnter = false, + multiline = false, + shouldUseDefaultValue = false, + shouldInterceptSwipe = false, + autoCorrect = true, + prefixCharacter, + inputID, + translate, + ...inputProps +}: BaseTextInputProps) { + const {hasError = false} = inputProps; + console.log({inputProps}); + const initialValue = value ?? defaultValue ?? ''; + const initialActiveLabel = !!forceActiveLabel || initialValue.length > 0 || Boolean(prefixCharacter); const [isFocused, setIsFocused] = useState(false); - const [passwordHidden, setPasswordHidden] = useState(props.secureTextEntry); + const [passwordHidden, setPasswordHidden] = useState(inputProps.secureTextEntry); const [textInputWidth, setTextInputWidth] = useState(0); const [textInputHeight, setTextInputHeight] = useState(0); - const [height, setHeight] = useState(variables.componentSizeLarge); - const [width, setWidth] = useState(); + const [height, setHeight] = useState(variables.componentSizeLarge); + const [width, setWidth] = useState(); const labelScale = useRef(new Animated.Value(initialActiveLabel ? styleConst.ACTIVE_LABEL_SCALE : styleConst.INACTIVE_LABEL_SCALE)).current; const labelTranslateY = useRef(new Animated.Value(initialActiveLabel ? styleConst.ACTIVE_LABEL_TRANSLATE_Y : styleConst.INACTIVE_LABEL_TRANSLATE_Y)).current; @@ -43,11 +89,11 @@ function BaseTextInput(props: BaseTextInputProps) { // AutoFocus which only works on mount: useEffect(() => { // We are manually managing focus to prevent this issue: https://github.com/Expensify/App/issues/4514 - if (!props.autoFocus || !input.current) { + if (!autoFocus || !input.current) { return; } - if (props.shouldDelayFocus) { + if (shouldDelayFocus) { const focusTimeout = setTimeout(() => input.current?.focus(), CONST.ANIMATED_TRANSITION); return () => clearTimeout(focusTimeout); } @@ -75,72 +121,72 @@ function BaseTextInput(props: BaseTextInputProps) { ); const activateLabel = useCallback(() => { - const value = props.value || ''; + const inputValue = value ?? ''; - if (value.length < 0 || isLabelActive.current) { + if (inputValue.length < 0 || isLabelActive.current) { return; } animateLabel(styleConst.ACTIVE_LABEL_TRANSLATE_Y, styleConst.ACTIVE_LABEL_SCALE); isLabelActive.current = true; - }, [animateLabel, props.value]); + }, [animateLabel, value]); const deactivateLabel = useCallback(() => { - const value = props.value ?? ''; + const inputValue = value ?? ''; - if (!!props.forceActiveLabel || value.length !== 0 || props.prefixCharacter) { + if (!!forceActiveLabel || inputValue.length !== 0 || prefixCharacter) { return; } animateLabel(styleConst.INACTIVE_LABEL_TRANSLATE_Y, styleConst.INACTIVE_LABEL_SCALE); isLabelActive.current = false; - }, [animateLabel, props.forceActiveLabel, props.prefixCharacter, props.value]); + }, [animateLabel, forceActiveLabel, prefixCharacter, value]); - const onFocus = (event) => { - if (props.onFocus) { - props.onFocus(event); + const onFocus = (event: NativeSyntheticEvent) => { + if (inputProps.onFocus) { + inputProps.onFocus(event); } setIsFocused(true); }; - const onBlur = (event) => { - if (props.onBlur) { - props.onBlur(event); + const onBlur = (event: NativeSyntheticEvent) => { + if (inputProps.onBlur) { + inputProps.onBlur(event); } setIsFocused(false); }; - const onPress = (event) => { - if (props.disabled) { + const onPress = (event?: GestureResponderEvent | KeyboardEvent) => { + if (!!inputProps.disabled || !event) { return; } - if (props.onPress) { - props.onPress(event); + if (inputProps.onPress) { + inputProps.onPress(event); } - if (!event.isDefaultPrevented()) { + if ('isDefaultPrevented' in event && !event?.isDefaultPrevented()) { input.current?.focus(); } }; const onLayout = useCallback( - (event) => { - if (!props.autoGrowHeight && props.multiline) { + (event: LayoutChangeEvent) => { + if (!autoGrowHeight && multiline) { return; } const layout = event.nativeEvent.layout; - setWidth((prevWidth) => (props.autoGrowHeight ? layout.width : prevWidth)); - setHeight((prevHeight) => (!props.multiline ? layout.height : prevHeight)); + setWidth((prevWidth: number | undefined) => (autoGrowHeight ? layout.width : prevWidth)); + setHeight((prevHeight: number) => (!multiline ? layout.height : prevHeight)); }, - [props.autoGrowHeight, props.multiline], + [autoGrowHeight, multiline], ); // The ref is needed when the component is uncontrolled and we don't have a value prop const hasValueRef = useRef(initialValue.length > 0); - const inputValue = props.value ?? ''; + const inputValue = value ?? ''; const hasValue = inputValue.length > 0 || hasValueRef.current; // Activate or deactivate the label when either focus changes, or for controlled @@ -162,26 +208,28 @@ function BaseTextInput(props: BaseTextInputProps) { // When the value prop gets cleared externally, we need to keep the ref in sync: useEffect(() => { // Return early when component uncontrolled, or we still have a value - if (props.value === undefined || !_.isEmpty(props.value)) { + if (value === undefined || value) { return; } hasValueRef.current = false; - }, [props.value]); + }, [value]); /** * Set Value & activateLabel */ - const setValue = (value) => { - if (props.onInputChange) { - props.onInputChange(value); + const setValue = (newValue: string) => { + if (onInputChange) { + onInputChange(newValue); } - Str.result(props.onChangeText, value); + if (inputProps.onChangeText) { + Str.result(inputProps.onChangeText, newValue); + } - if (value && value.length > 0) { + if (newValue && newValue.length > 0) { hasValueRef.current = true; // When the componment is uncontrolled, we need to manually activate the label: - if (props.value === undefined) { + if (value === undefined) { activateLabel(); } } else { @@ -209,38 +257,38 @@ function BaseTextInput(props: BaseTextInputProps) { }; // eslint-disable-next-line react/forbid-foreign-prop-types - const inputProps = _.omit(props, _.keys(baseTextInputPropTypes.propTypes)); - const hasLabel = Boolean(props.label?.length); - const isReadOnly = _.isUndefined(props.readOnly) ? props.disabled : props.readOnly; - const inputHelpText = props.errorText || props.hint; - const placeholder = !!props.prefixCharacter || isFocused || !hasLabel || (hasLabel && props.forceActiveLabel) ? props.placeholder : undefined; - const maxHeight = StyleSheet.flatten(props.containerStyles).maxHeight; - console.log('maxHeight', maxHeight); - const textInputContainerStyles = StyleSheet.flatten([ + // const inputProps = _.omit(props, _.keys(baseTextInputPropTypes.propTypes)); + const hasLabel = Boolean(label?.length); + const isReadOnly = inputProps.readOnly ?? inputProps.disabled; + const inputHelpText = errorText || hint; + const placeholderValue = !!prefixCharacter || isFocused || !hasLabel || (hasLabel && forceActiveLabel) ? placeholder : undefined; + const maxHeight = StyleSheet.flatten(containerStyles).maxHeight; + const newTextInputContainerStyles: StyleProp = StyleSheet.flatten([ styles.textInputContainer, - props.textInputContainerStyles, - props.autoGrow && StyleUtils.getWidthStyle(textInputWidth), - !props.hideFocusedState && isFocused && styles.borderColorFocus, - (!!props.hasError || props.errorText) && styles.borderColorDanger, - props.autoGrowHeight && {scrollPaddingTop: typeof maxHeight === 'number' ? 2 * maxHeight : undefined}, + textInputContainerStyles, + autoGrow && StyleUtils.getWidthStyle(textInputWidth), + !hideFocusedState && isFocused && styles.borderColorFocus, + (!!hasError || errorText) && styles.borderColorDanger, + autoGrowHeight && {scrollPaddingTop: typeof maxHeight === 'number' ? 2 * maxHeight : undefined}, ]); - const isMultiline = !!props.multiline || props.autoGrowHeight; + const isMultiline = !!multiline || autoGrowHeight; return ( <> {hasLabel ? ( @@ -261,88 +309,89 @@ function BaseTextInput(props: BaseTextInputProps) { {isMultiline && } ) : null} - {Boolean(props.prefixCharacter) && ( + {Boolean(prefixCharacter) && ( - {props.prefixCharacter} + {prefixCharacter} )} { - if (typeof props.innerRef === 'function') { - props.innerRef(ref); - } else if (props.innerRef && _.has(props.innerRef, 'current')) { + if (typeof innerRef === 'function') { + innerRef(ref); + } else if (innerRef && 'current' in innerRef) { // eslint-disable-next-line no-param-reassign - props.innerRef.current = ref; + innerRef.current = ref; } + // @ts-expect-error We need to reassign this ref to the input ref input.current = ref; }} // eslint-disable-next-line {...inputProps} - autoCorrect={props.secureTextEntry ? false : props.autoCorrect} - placeholder={placeholder} + autoCorrect={inputProps.secureTextEntry ? false : autoCorrect} + placeholder={placeholderValue} placeholderTextColor={themeColors.placeholderText} underlineColorAndroid="transparent" style={[ styles.flex1, styles.w100, - props.inputStyle, + inputStyle, (!hasLabel || isMultiline) && styles.pv0, - props.prefixCharacter && StyleUtils.getPaddingLeft(getCharacterPadding(props.prefixCharacter) + styles.pl1.paddingLeft), - props.secureTextEntry && styles.secureInput, + !!prefixCharacter && StyleUtils.getPaddingLeft(getCharacterPadding(prefixCharacter) + styles.pl1.paddingLeft), + inputProps.secureTextEntry && styles.secureInput, !isMultiline && {height, lineHeight: undefined}, // Stop scrollbar flashing when breaking lines with autoGrowHeight enabled. - props.autoGrowHeight && StyleUtils.getAutoGrowHeightInputStyle(textInputHeight, maxHeight), + autoGrowHeight && StyleUtils.getAutoGrowHeightInputStyle(textInputHeight, maxHeight), // Add disabled color theme when field is not editable. - props.disabled && styles.textInputDisabled, + inputProps.disabled && styles.textInputDisabled, styles.pointerEventsAuto, ]} multiline={isMultiline} - maxLength={props.maxLength} + maxLength={maxLength} onFocus={onFocus} onBlur={onBlur} onChangeText={setValue} secureTextEntry={passwordHidden} - onPressOut={props.onPress} - showSoftInputOnFocus={!props.disableKeyboard} - keyboardType={getSecureEntryKeyboardType(props.keyboardType, props.secureTextEntry, passwordHidden)} - inputMode={!props.disableKeyboard ? props.inputMode : CONST.INPUT_MODE.NONE} - value={props.value} - selection={props.selection} + onPressOut={inputProps.onPress} + showSoftInputOnFocus={!disableKeyboard} + keyboardType={getSecureEntryKeyboardType(inputProps.keyboardType, inputProps.secureTextEntry ?? false, passwordHidden ?? false)} + inputMode={!disableKeyboard ? inputProps.inputMode : CONST.INPUT_MODE.NONE} + value={value} + selection={inputProps.selection} readOnly={isReadOnly} - defaultValue={props.defaultValue} + defaultValue={defaultValue} // FormSubmit Enter key handler does not have access to direct props. // `dataset.submitOnEnter` is used to indicate that pressing Enter on this input should call the submit callback. - dataSet={{submitOnEnter: isMultiline && props.submitOnEnter}} + dataSet={{submitOnEnter: isMultiline && submitOnEnter}} /> - {props.isLoading && ( + {inputProps.isLoading && ( )} - {Boolean(props.secureTextEntry) && ( + {Boolean(inputProps.secureTextEntry) && ( e.preventDefault()} - accessibilityLabel={props.translate('common.visible')} + accessibilityLabel={translate?.('common.visible') ?? ''} > )} - {!props.secureTextEntry && Boolean(props.icon) && ( + {!inputProps.secureTextEntry && Boolean(icon) && ( @@ -363,7 +412,7 @@ function BaseTextInput(props: BaseTextInputProps) { {!inputHelpText && ( )} @@ -374,14 +423,14 @@ function BaseTextInput(props: BaseTextInputProps) { This text view is used to calculate width or height of the input value given textStyle in this component. This Text component is intentionally positioned out of the screen. */} - {(!!props.autoGrow || props.autoGrowHeight) && ( + {(!!autoGrow || autoGrowHeight) && ( // Add +2 to width on Safari browsers so that text is not cut off due to the cursor or when changing the value // https://github.com/Expensify/App/issues/8158 // https://github.com/Expensify/App/issues/26628 {/* \u200B added to solve the issue of not expanding the text input enough when the value ends with '\n' (https://github.com/Expensify/App/issues/21271) */} - {props.value ? `${props.value}${props.value.endsWith('\n') ? '\u200B' : ''}` : props.placeholder} + {value ? `${value}${value.endsWith('\n') ? '\u200B' : ''}` : placeholder} )} diff --git a/src/components/TextInput/BaseTextInput/index.tsx b/src/components/TextInput/BaseTextInput/index.tsx index 77b2458c6b41..547e11b6cbb6 100644 --- a/src/components/TextInput/BaseTextInput/index.tsx +++ b/src/components/TextInput/BaseTextInput/index.tsx @@ -3,7 +3,6 @@ import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import { ActivityIndicator, Animated, - FlexStyle, GestureResponderEvent, LayoutChangeEvent, NativeSyntheticEvent, @@ -174,7 +173,7 @@ function BaseTextInput(props: BaseTextInputProps) { // When the value prop gets cleared externally, we need to keep the ref in sync: useEffect(() => { // Return early when component uncontrolled, or we still have a value - if (props.value === undefined || !props.value) { + if (props.value === undefined || props.value) { return; } hasValueRef.current = false; @@ -226,7 +225,7 @@ function BaseTextInput(props: BaseTextInputProps) { const inputHelpText = props.errorText || props.hint; const placeholder = !!props.prefixCharacter || isFocused || !hasLabel || (hasLabel && props.forceActiveLabel) ? props.placeholder : undefined; const maxHeight = StyleSheet.flatten(props.containerStyles).maxHeight; - const textInputContainerStyles: StyleProp = StyleSheet.flatten([ + const textInputContainerStyles: StyleProp = StyleSheet.flatten([ styles.textInputContainer, props.textInputContainerStyles, props.autoGrow && StyleUtils.getWidthStyle(textInputWidth), diff --git a/src/components/TextInput/index.native.js b/src/components/TextInput/index.native.tsx similarity index 100% rename from src/components/TextInput/index.native.js rename to src/components/TextInput/index.native.tsx diff --git a/src/components/TextInput/index.js b/src/components/TextInput/index.tsx similarity index 100% rename from src/components/TextInput/index.js rename to src/components/TextInput/index.tsx diff --git a/src/components/TextInput/styleConst.js b/src/components/TextInput/styleConst.ts similarity index 100% rename from src/components/TextInput/styleConst.js rename to src/components/TextInput/styleConst.ts diff --git a/src/components/TextInput/types.ts b/src/components/TextInput/types.ts new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/libs/getSecureEntryKeyboardType/types.ts b/src/libs/getSecureEntryKeyboardType/types.ts index fe79440e3109..750c84133ea2 100644 --- a/src/libs/getSecureEntryKeyboardType/types.ts +++ b/src/libs/getSecureEntryKeyboardType/types.ts @@ -1,3 +1,5 @@ -type GetSecureEntryKeyboardType = (keyboardType: string, secureTextEntry: boolean, passwordHidden: boolean) => string; +import {KeyboardTypeOptions} from 'react-native'; + +type GetSecureEntryKeyboardType = (keyboardType: KeyboardTypeOptions | undefined, secureTextEntry: boolean, passwordHidden: boolean) => KeyboardTypeOptions | undefined; export default GetSecureEntryKeyboardType; From b568d32025186f0cd4ce39fcb1d211249662dbc1 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Thu, 16 Nov 2023 14:48:31 +0100 Subject: [PATCH 04/26] fix: destructure props --- .../TextInput/BaseTextInput/index.native.tsx | 2 - .../TextInput/BaseTextInput/index.tsx | 44 ++++++++++++++++--- 2 files changed, 38 insertions(+), 8 deletions(-) diff --git a/src/components/TextInput/BaseTextInput/index.native.tsx b/src/components/TextInput/BaseTextInput/index.native.tsx index d696a97232c6..bde3f880cfbf 100644 --- a/src/components/TextInput/BaseTextInput/index.native.tsx +++ b/src/components/TextInput/BaseTextInput/index.native.tsx @@ -66,11 +66,9 @@ function BaseTextInput({ autoCorrect = true, prefixCharacter, inputID, - translate, ...inputProps }: BaseTextInputProps) { const {hasError = false} = inputProps; - console.log({inputProps}); const initialValue = value ?? defaultValue ?? ''; const initialActiveLabel = !!forceActiveLabel || initialValue.length > 0 || Boolean(prefixCharacter); diff --git a/src/components/TextInput/BaseTextInput/index.tsx b/src/components/TextInput/BaseTextInput/index.tsx index 547e11b6cbb6..4f83e87084ed 100644 --- a/src/components/TextInput/BaseTextInput/index.tsx +++ b/src/components/TextInput/BaseTextInput/index.tsx @@ -35,12 +35,44 @@ import CONST from '@src/CONST'; import * as baseTextInputPropTypes from './baseTextInputPropTypes'; import BaseTextInputProps from './types'; -function BaseTextInput(props: BaseTextInputProps) { - const initialValue = props.value ?? props.defaultValue ?? ''; - const initialActiveLabel = !!props.forceActiveLabel || initialValue.length > 0 || Boolean(props.prefixCharacter); +function BaseTextInput({ + label = '', + name = '', + value = undefined, + defaultValue = undefined, + placeholder = '', + errorText = '', + icon = null, + textInputContainerStyles = [], + containerStyles = [], + inputStyle = [], + forceActiveLabel = false, + autoFocus = false, + disableKeyboard = false, + autoGrow = false, + autoGrowHeight = false, + hideFocusedState = false, + innerRef = () => {}, + maxLength = null, + hint = '', + shouldSaveDraft = false, + onInputChange = () => {}, + shouldDelayFocus = false, + submitOnEnter = false, + multiline = false, + shouldUseDefaultValue = false, + shouldInterceptSwipe = false, + autoCorrect = true, + prefixCharacter, + inputID, + ...inputProps +}: BaseTextInputProps) { + const {hasError = false} = inputProps; + const initialValue = value ?? defaultValue ?? ''; + const initialActiveLabel = !!forceActiveLabel || initialValue.length > 0 || Boolean(prefixCharacter); const [isFocused, setIsFocused] = useState(false); - const [passwordHidden, setPasswordHidden] = useState(props.secureTextEntry); + const [passwordHidden, setPasswordHidden] = useState(inputProps.secureTextEntry); const [textInputWidth, setTextInputWidth] = useState(0); const [textInputHeight, setTextInputHeight] = useState(0); const [height, setHeight] = useState(variables.componentSizeLarge); @@ -54,11 +86,11 @@ function BaseTextInput(props: BaseTextInputProps) { // AutoFocus which only works on mount: useEffect(() => { // We are manually managing focus to prevent this issue: https://github.com/Expensify/App/issues/4514 - if (!props.autoFocus || !input.current) { + if (!autoFocus || !input.current) { return; } - if (props.shouldDelayFocus) { + if (shouldDelayFocus) { const focusTimeout = setTimeout(() => input?.current?.focus(), CONST.ANIMATED_TRANSITION); return () => clearTimeout(focusTimeout); } From e90c503a36c75447619c9d8ef26a76108ecbecd0 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Fri, 17 Nov 2023 10:54:41 +0100 Subject: [PATCH 05/26] fix: fixing types --- .../TextInput/BaseTextInput/index.tsx | 163 +++++++++--------- src/components/TextInput/index.tsx | 52 +++--- 2 files changed, 104 insertions(+), 111 deletions(-) diff --git a/src/components/TextInput/BaseTextInput/index.tsx b/src/components/TextInput/BaseTextInput/index.tsx index 4f83e87084ed..f86e33d1eab9 100644 --- a/src/components/TextInput/BaseTextInput/index.tsx +++ b/src/components/TextInput/BaseTextInput/index.tsx @@ -3,6 +3,7 @@ import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import { ActivityIndicator, Animated, + FlexStyle, GestureResponderEvent, LayoutChangeEvent, NativeSyntheticEvent, @@ -32,7 +33,6 @@ import * as StyleUtils from '@styles/StyleUtils'; import themeColors from '@styles/themes/default'; import variables from '@styles/variables'; import CONST from '@src/CONST'; -import * as baseTextInputPropTypes from './baseTextInputPropTypes'; import BaseTextInputProps from './types'; function BaseTextInput({ @@ -118,48 +118,48 @@ function BaseTextInput({ ); const activateLabel = useCallback(() => { - const value = props.value ?? ''; + const newValue = value ?? ''; - if (value.length < 0 || isLabelActive.current) { + if (newValue.length < 0 || isLabelActive.current) { return; } animateLabel(styleConst.ACTIVE_LABEL_TRANSLATE_Y, styleConst.ACTIVE_LABEL_SCALE); isLabelActive.current = true; - }, [animateLabel, props.value]); + }, [animateLabel, value]); const deactivateLabel = useCallback(() => { - const value = props.value ?? ''; + const newValue = value ?? ''; - if (!!props.forceActiveLabel || value.length !== 0 || props.prefixCharacter) { + if (!!forceActiveLabel || newValue.length !== 0 || prefixCharacter) { return; } animateLabel(styleConst.INACTIVE_LABEL_TRANSLATE_Y, styleConst.INACTIVE_LABEL_SCALE); isLabelActive.current = false; - }, [animateLabel, props.forceActiveLabel, props.prefixCharacter, props.value]); + }, [animateLabel, forceActiveLabel,prefixCharacter, value]); const onFocus = (event: NativeSyntheticEvent) => { - if (props.onFocus) { - props.onFocus(event); + if (inputProps.onFocus) { + onFocus(event); } setIsFocused(true); }; const onBlur = (event: NativeSyntheticEvent) => { - if (props.onBlur) { - props.onBlur(event); + if (inputProps.onBlur) { + inputProps.onBlur(event); } setIsFocused(false); }; const onPress = (event?: GestureResponderEvent | KeyboardEvent) => { - if (!!props.disabled || !event) { + if (!!inputProps.disabled || !event) { return; } - if (props.onPress) { - props.onPress(event); + if (inputProps.onPress) { + inputProps.onPress(event); } if ('isDefaultPrevented' in event && !event?.isDefaultPrevented()) { @@ -169,21 +169,21 @@ function BaseTextInput({ const onLayout = useCallback( (event: LayoutChangeEvent) => { - if (!props.autoGrowHeight && props.multiline) { + if (!autoGrowHeight && multiline) { return; } const layout = event.nativeEvent.layout; - setWidth((prevWidth: number | undefined) => (props.autoGrowHeight ? layout.width : prevWidth)); - setHeight((prevHeight: number) => (!props.multiline ? layout.height : prevHeight)); + setWidth((prevWidth: number | undefined) => (autoGrowHeight ? layout.width : prevWidth)); + setHeight((prevHeight: number) => (!multiline ? layout.height : prevHeight)); }, - [props.autoGrowHeight, props.multiline], + [autoGrowHeight, multiline], ); // The ref is needed when the component is uncontrolled and we don't have a value prop const hasValueRef = useRef(initialValue.length > 0); - const inputValue = props.value ?? ''; + const inputValue = value ?? ''; const hasValue = inputValue.length > 0 || hasValueRef.current; // Activate or deactivate the label when either focus changes, or for controlled @@ -205,26 +205,26 @@ function BaseTextInput({ // When the value prop gets cleared externally, we need to keep the ref in sync: useEffect(() => { // Return early when component uncontrolled, or we still have a value - if (props.value === undefined || props.value) { + if (value === undefined || value) { return; } hasValueRef.current = false; - }, [props.value]); + }, [value]); /** * Set Value & activateLabel */ - const setValue = (value: string) => { - if (props.onInputChange) { - props.onInputChange(value); + const setValue = (newValue: string) => { + if (onInputChange) { + onInputChange(newValue); } - if (props.onChangeText) { - Str.result(props.onChangeText, value); + if (inputProps.onChangeText) { + Str.result(inputProps.onChangeText, newValue); } - if (value && value.length > 0) { + if (newValue && newValue.length > 0) { hasValueRef.current = true; // When the componment is uncontrolled, we need to manually activate the label: - if (props.value === undefined) { + if (value === undefined) { activateLabel(); } } else { @@ -251,21 +251,20 @@ function BaseTextInput({ } }; - const inputProps = _.omit(props, _.keys(baseTextInputPropTypes.propTypes)); - const hasLabel = Boolean(props.label?.length); - const isReadOnly = props.readOnly ?? props.disabled; - const inputHelpText = props.errorText || props.hint; - const placeholder = !!props.prefixCharacter || isFocused || !hasLabel || (hasLabel && props.forceActiveLabel) ? props.placeholder : undefined; - const maxHeight = StyleSheet.flatten(props.containerStyles).maxHeight; - const textInputContainerStyles: StyleProp = StyleSheet.flatten([ + const hasLabel = Boolean(label?.length); + const isReadOnly = inputProps.readOnly ?? inputProps.disabled; + const inputHelpText = errorText || hint; + const newPlaceholder = !!prefixCharacter || isFocused || !hasLabel || (hasLabel && forceActiveLabel) ? placeholder : undefined; + const maxHeight = StyleSheet.flatten(containerStyles).maxHeight; + const textInputContainerStyles: StyleProp = StyleSheet.flatten([ styles.textInputContainer, - props.textInputContainerStyles, - props.autoGrow && StyleUtils.getWidthStyle(textInputWidth), - !props.hideFocusedState && isFocused && styles.borderColorFocus, - (!!props.hasError || !!props.errorText) && styles.borderColorDanger, - props.autoGrowHeight && {scrollPaddingTop: typeof maxHeight === 'number' ? 2 * maxHeight : undefined}, + textInputContainerStyles, + autoGrow && StyleUtils.getWidthStyle(textInputWidth), + !hideFocusedState && isFocused && styles.borderColorFocus, + (!!hasError || !!errorText) && styles.borderColorDanger, + autoGrowHeight && {scrollPaddingTop: typeof maxHeight === 'number' ? 2 * maxHeight : undefined}, ]); - const isMultiline = props.multiline ?? props.autoGrowHeight; + const isMultiline = multiline ?? autoGrowHeight; /* To prevent text jumping caused by virtual DOM calculations on Safari and mobile Chrome, make sure to include the `lineHeight`. @@ -276,32 +275,32 @@ function BaseTextInput({ See https://github.com/Expensify/App/issues/13802 */ const lineHeight = useMemo(() => { - if ((Browser.isSafari() || Browser.isMobileChrome()) && Array.isArray(props.inputStyle)) { - const lineHeightValue = props.inputStyle.find((f) => f && 'lineHeight' in f && f.lineHeight !== undefined); + if ((Browser.isSafari() || Browser.isMobileChrome()) && Array.isArray(inputStyle)) { + const lineHeightValue = inputStyle?.find((f) => f && 'lineHeight' in f && f.lineHeight !== undefined); if (lineHeightValue && 'lineHeight' in lineHeightValue) { return lineHeightValue.lineHeight; } } return undefined; - }, [props.inputStyle]); + }, [inputStyle]); return ( <> {hasLabel ? ( @@ -322,49 +321,49 @@ function BaseTextInput({ {isMultiline && } ) : null} - {Boolean(props.prefixCharacter) && ( + {Boolean(prefixCharacter) && ( - {props.prefixCharacter} + {prefixCharacter} )} { - if (typeof props.innerRef === 'function') { - props.innerRef(ref); - } else if (props.innerRef && 'current' in props.innerRef) { + if (typeof innerRef === 'function') { + innerRef(ref); + } else if (innerRef && 'current' in innerRef) { // eslint-disable-next-line no-param-reassign - props.innerRef.current = ref; + innerRef.current = ref; } // @ts-expect-error We need to reassign this ref to the input ref input.current = ref; }} // eslint-disable-next-line {...inputProps} - autoCorrect={props.secureTextEntry ? false : props.autoCorrect} - placeholder={placeholder} + autoCorrect={inputProps.secureTextEntry ? false : autoCorrect} + placeholder={newPlaceholder} placeholderTextColor={themeColors.placeholderText} underlineColorAndroid="transparent" style={[ styles.flex1, styles.w100, - props.inputStyle, + inputStyle, (!hasLabel || isMultiline) && styles.pv0, - !!props.prefixCharacter && StyleUtils.getPaddingLeft(getCharacterPadding(props.prefixCharacter) + styles.pl1.paddingLeft), - props.secureTextEntry && styles.secureInput, + !!prefixCharacter && StyleUtils.getPaddingLeft(getCharacterPadding(props.prefixCharacter) + styles.pl1.paddingLeft), + inputProps.secureTextEntry && styles.secureInput, // Explicitly remove `lineHeight` from single line inputs so that long text doesn't disappear // once it exceeds the input space (See https://github.com/Expensify/App/issues/13802) @@ -375,41 +374,41 @@ function BaseTextInput({ !isMultiline && Browser.isMobileChrome() && {boxSizing: 'content-box', height: undefined}, // Stop scrollbar flashing when breaking lines with autoGrowHeight enabled. - props.autoGrowHeight && StyleUtils.getAutoGrowHeightInputStyle(textInputHeight, typeof maxHeight === 'number' ? maxHeight : 0), + autoGrowHeight && StyleUtils.getAutoGrowHeightInputStyle(textInputHeight, typeof maxHeight === 'number' ? maxHeight : 0), // Add disabled color theme when field is not editable. - props.disabled && styles.textInputDisabled, + inputProps.disabled && styles.textInputDisabled, styles.pointerEventsAuto, ]} multiline={isMultiline} - maxLength={props.maxLength} + maxLength={maxLength} onFocus={onFocus} onBlur={onBlur} onChangeText={setValue} secureTextEntry={passwordHidden} - onPressOut={props.onPress} - showSoftInputOnFocus={!props.disableKeyboard} - inputMode={props.inputMode} - value={props.value} - selection={props.selection} + onPressOut={inputProps.onPress} + showSoftInputOnFocus={!disableKeyboard} + inputMode={inputProps.inputMode} + value={value} + selection={inputProps.selection} readOnly={isReadOnly} - defaultValue={props.defaultValue} + defaultValue={defaultValue} // FormSubmit Enter key handler does not have access to direct props. // `dataset.submitOnEnter` is used to indicate that pressing Enter on this input should call the submit callback. - dataSet={{submitOnEnter: isMultiline && props.submitOnEnter}} + dataSet={{submitOnEnter: isMultiline && submitOnEnter}} /> - {props.isLoading && ( + {inputProps.isLoading && ( )} - {Boolean(props.secureTextEntry) && ( + {Boolean(inputProps.secureTextEntry) && ( e.preventDefault()} - accessibilityLabel={props.translate?.('common.visible') ?? ''} + accessibilityLabel={inputProps.translate?.('common.visible') ?? ''} > )} - {!props.secureTextEntry && Boolean(props.icon) && ( + {!inputProps.secureTextEntry && Boolean(icon) && ( @@ -430,7 +429,7 @@ function BaseTextInput({ {!inputHelpText && ( )} @@ -441,14 +440,14 @@ function BaseTextInput({ This text view is used to calculate width or height of the input value given textStyle in this component. This Text component is intentionally positioned out of the screen. */} - {(!!props.autoGrow || props.autoGrowHeight) && ( + {(!!autoGrow || autoGrowHeight) && ( // Add +2 to width on Safari browsers so that text is not cut off due to the cursor or when changing the value // https://github.com/Expensify/App/issues/8158 // https://github.com/Expensify/App/issues/26628 {/* \u200B added to solve the issue of not expanding the text input enough when the value ends with '\n' (https://github.com/Expensify/App/issues/21271) */} - {props.value ? `${props.value}${props.value.endsWith('\n') ? '\u200B' : ''}` : props.placeholder} + {value ? `${value}${value.endsWith('\n') ? '\u200B' : ''}` : placeholder} )} diff --git a/src/components/TextInput/index.tsx b/src/components/TextInput/index.tsx index 044399ec6e11..c8dc21b97310 100644 --- a/src/components/TextInput/index.tsx +++ b/src/components/TextInput/index.tsx @@ -1,26 +1,26 @@ -import React, {useEffect, useRef} from 'react'; -import _ from 'underscore'; +import React, {ForwardedRef, useEffect, useRef} from 'react'; import * as Browser from '@libs/Browser'; import DomUtils from '@libs/DomUtils'; import Visibility from '@libs/Visibility'; import styles from '@styles/styles'; import BaseTextInput from './BaseTextInput'; import * as baseTextInputPropTypes from './BaseTextInput/baseTextInputPropTypes'; +import BaseTextInputProps from './BaseTextInput/types'; import * as styleConst from './styleConst'; -function TextInput(props) { - const textInputRef = useRef(null); - const removeVisibilityListenerRef = useRef(null); +function TextInput(props: BaseTextInputProps, ref: ForwardedRef) { + const textInputRef = useRef(null); + const removeVisibilityListenerRef = useRef<() => void>(null); useEffect(() => { if (props.disableKeyboard) { - textInputRef.current.setAttribute('inputmode', 'none'); + textInputRef.current?.setAttribute('inputmode', 'none'); } if (props.name) { - textInputRef.current.setAttribute('name', props.name); + textInputRef.current?.setAttribute('name', props.name); } - + // @ts-expect-error We need to reassign this ref to the input ref removeVisibilityListenerRef.current = Visibility.onVisibilityChange(() => { if (!Browser.isMobileChrome() || !Visibility.isVisible() || !textInputRef.current || DomUtils.getActiveElement() !== textInputRef.current) { return; @@ -38,33 +38,37 @@ function TextInput(props) { // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - const isLabeledMultiline = Boolean(props.label.length) && props.multiline; + const isLabeledMultiline = Boolean(props.label?.length) && props.multiline; const labelAnimationStyle = { + // eslint-disable-next-line @typescript-eslint/naming-convention '--active-label-translate-y': `${styleConst.ACTIVE_LABEL_TRANSLATE_Y}px`, + // eslint-disable-next-line @typescript-eslint/naming-convention '--active-label-scale': `${styleConst.ACTIVE_LABEL_SCALE}`, + // eslint-disable-next-line @typescript-eslint/naming-convention '--label-transition-duration': `${styleConst.LABEL_ANIMATION_DURATION}ms`, }; return ( { + innerRef={(el: HTMLFormElement | null) => { + // @ts-expect-error We need to reassign this ref to the input ref textInputRef.current = el; - if (!props.innerRef) { + if (!ref) { return; } - if (_.isFunction(props.innerRef)) { - props.innerRef(el); + if (typeof ref === 'function') { + ref(el); return; } // eslint-disable-next-line no-param-reassign - props.innerRef.current = el; + ref.current = el; }} - inputStyle={[styles.baseTextInput, styles.textInputDesktop, isLabeledMultiline ? styles.textInputMultiline : {}, ...props.inputStyle]} - textInputContainerStyles={[labelAnimationStyle, ...props.textInputContainerStyles]} + inputStyle={[styles.baseTextInput, styles.textInputDesktop, isLabeledMultiline ? styles.textInputMultiline : {}, props.inputStyle]} + textInputContainerStyles={[labelAnimationStyle, props.textInputContainerStyles]} + // eslint-disable-next-line react/jsx-props-no-spreading + {...props} /> ); } @@ -73,14 +77,4 @@ TextInput.displayName = 'TextInput'; TextInput.propTypes = baseTextInputPropTypes.propTypes; TextInput.defaultProps = baseTextInputPropTypes.defaultProps; -const TextInputWithRef = React.forwardRef((props, ref) => ( - -)); - -TextInputWithRef.displayName = 'TextInputWithRef'; - -export default TextInputWithRef; +export default React.forwardRef(TextInput); From 6a3a27004030f0d47c2ebff20974beb23190a4e4 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Fri, 17 Nov 2023 15:38:55 +0100 Subject: [PATCH 06/26] fix: resolving types issues --- .../TextInput/BaseTextInput/index.native.tsx | 101 ++++++++--------- .../TextInput/BaseTextInput/index.tsx | 102 +++++++++--------- .../TextInput/BaseTextInput/types.ts | 15 ++- src/components/TextInput/index.native.tsx | 30 +++--- src/components/TextInput/index.tsx | 27 +++-- src/components/TextInput/types.ts | 0 6 files changed, 143 insertions(+), 132 deletions(-) delete mode 100644 src/components/TextInput/types.ts diff --git a/src/components/TextInput/BaseTextInput/index.native.tsx b/src/components/TextInput/BaseTextInput/index.native.tsx index bde3f880cfbf..d8ecb67517f2 100644 --- a/src/components/TextInput/BaseTextInput/index.native.tsx +++ b/src/components/TextInput/BaseTextInput/index.native.tsx @@ -1,5 +1,5 @@ import Str from 'expensify-common/lib/str'; -import React, {useCallback, useEffect, useRef, useState} from 'react'; +import React, {Component, ForwardedRef, forwardRef, useCallback, useEffect, useRef, useState} from 'react'; import { ActivityIndicator, Animated, @@ -11,9 +11,11 @@ import { StyleSheet, TextInput, TextInputFocusEventData, + TextInputProps, View, ViewStyle, } from 'react-native'; +import {AnimatedProps} from 'react-native-reanimated'; import Checkbox from '@components/Checkbox'; import FormHelpMessage from '@components/FormHelpMessage'; import Icon from '@components/Icon'; @@ -33,41 +35,42 @@ import * as StyleUtils from '@styles/StyleUtils'; import themeColors from '@styles/themes/default'; import variables from '@styles/variables'; import CONST from '@src/CONST'; -import * as baseTextInputPropTypes from './baseTextInputPropTypes'; import BaseTextInputProps from './types'; -function BaseTextInput({ - label = '', - name = '', - value = undefined, - defaultValue = undefined, - placeholder = '', - errorText = '', - icon = null, - textInputContainerStyles = [], - containerStyles = [], - inputStyle = [], - forceActiveLabel = false, - autoFocus = false, - disableKeyboard = false, - autoGrow = false, - autoGrowHeight = false, - hideFocusedState = false, - innerRef = () => {}, - maxLength = null, - hint = '', - shouldSaveDraft = false, - onInputChange = () => {}, - shouldDelayFocus = false, - submitOnEnter = false, - multiline = false, - shouldUseDefaultValue = false, - shouldInterceptSwipe = false, - autoCorrect = true, - prefixCharacter, - inputID, - ...inputProps -}: BaseTextInputProps) { +function BaseTextInput( + { + label = '', + // name = '', + value = undefined, + defaultValue = undefined, + placeholder = '', + errorText = '', + icon = null, + textInputContainerStyles = [], + containerStyles = [], + inputStyle = [], + forceActiveLabel = false, + autoFocus = false, + disableKeyboard = false, + autoGrow = false, + autoGrowHeight = false, + hideFocusedState = false, + maxLength = undefined, + hint = '', + // shouldSaveDraft = false, + onInputChange = () => {}, + shouldDelayFocus = false, + submitOnEnter = false, + multiline = false, + // shouldUseDefaultValue = false, + shouldInterceptSwipe = false, + autoCorrect = true, + prefixCharacter, + inputID, + ...inputProps + }: BaseTextInputProps, + ref: ForwardedRef, unknown, unknown>>, +) { const {hasError = false} = inputProps; const initialValue = value ?? defaultValue ?? ''; const initialActiveLabel = !!forceActiveLabel || initialValue.length > 0 || Boolean(prefixCharacter); @@ -195,7 +198,7 @@ function BaseTextInput({ isFocused || // If the text has been supplied by Chrome autofill, the value state is not synced with the value // as Chrome doesn't trigger a change event. When there is autofill text, keep the label activated. - isInputAutoFilled(input.current) + isInputAutoFilled(input.current as unknown as Element) ) { activateLabel(); } else { @@ -266,7 +269,7 @@ function BaseTextInput({ textInputContainerStyles, autoGrow && StyleUtils.getWidthStyle(textInputWidth), !hideFocusedState && isFocused && styles.borderColorFocus, - (!!hasError || errorText) && styles.borderColorDanger, + (!!hasError || !!errorText) && styles.borderColorDanger, autoGrowHeight && {scrollPaddingTop: typeof maxHeight === 'number' ? 2 * maxHeight : undefined}, ]); const isMultiline = !!multiline || autoGrowHeight; @@ -284,7 +287,7 @@ function BaseTextInput({ accessibilityLabel={label ?? ''} accessible style={[ - autoGrowHeight && styles.autoGrowHeightInputContainer(textInputHeight, variables.componentSizeLarge, maxHeight), + autoGrowHeight && styles.autoGrowHeightInputContainer(textInputHeight, variables.componentSizeLarge, typeof maxHeight === 'number' ? maxHeight : 0), !isMultiline && styles.componentHeightLarge, containerStyles, ]} @@ -327,15 +330,15 @@ function BaseTextInput({ )} { - if (typeof innerRef === 'function') { - innerRef(ref); - } else if (innerRef && 'current' in innerRef) { + ref={(el) => { + if (typeof ref === 'function') { + ref(el); + } else if (ref && 'current' in ref) { // eslint-disable-next-line no-param-reassign - innerRef.current = ref; + ref.current = el; } // @ts-expect-error We need to reassign this ref to the input ref - input.current = ref; + input.current = el; }} // eslint-disable-next-line {...inputProps} @@ -354,7 +357,7 @@ function BaseTextInput({ !isMultiline && {height, lineHeight: undefined}, // Stop scrollbar flashing when breaking lines with autoGrowHeight enabled. - autoGrowHeight && StyleUtils.getAutoGrowHeightInputStyle(textInputHeight, maxHeight), + autoGrowHeight && StyleUtils.getAutoGrowHeightInputStyle(textInputHeight, typeof maxHeight === 'number' ? maxHeight : 0), // Add disabled color theme when field is not editable. inputProps.disabled && styles.textInputDisabled, styles.pointerEventsAuto, @@ -389,7 +392,7 @@ function BaseTextInput({ style={[styles.flex1, styles.textInputIconContainer]} onPress={togglePasswordVisibility} onMouseDown={(e) => e.preventDefault()} - accessibilityLabel={translate?.('common.visible') ?? ''} + accessibilityLabel={inputProps.translate?.('common.visible') ?? ''} > )} - {!inputProps.secureTextEntry && Boolean(icon) && ( + {!inputProps.secureTextEntry && icon && ( - {!inputHelpText && ( + {!!inputHelpText && ( {}, - maxLength = null, - hint = '', - shouldSaveDraft = false, - onInputChange = () => {}, - shouldDelayFocus = false, - submitOnEnter = false, - multiline = false, - shouldUseDefaultValue = false, - shouldInterceptSwipe = false, - autoCorrect = true, - prefixCharacter, - inputID, - ...inputProps -}: BaseTextInputProps) { +function BaseTextInput( + { + label = '', + // name = '', + value = undefined, + defaultValue = undefined, + placeholder = '', + errorText = '', + icon = null, + textInputContainerStyles = [], + containerStyles = [], + inputStyle = [], + forceActiveLabel = false, + autoFocus = false, + disableKeyboard = false, + autoGrow = false, + autoGrowHeight = false, + hideFocusedState = false, + maxLength = undefined, + hint = '', + // shouldSaveDraft = false, + onInputChange = () => {}, + shouldDelayFocus = false, + submitOnEnter = false, + multiline = false, + // shouldUseDefaultValue = false, + shouldInterceptSwipe = false, + autoCorrect = true, + prefixCharacter, + inputID, + ...inputProps + }: BaseTextInputProps, + ref: ForwardedRef, unknown, unknown>>, +) { const {hasError = false} = inputProps; const initialValue = value ?? defaultValue ?? ''; const initialActiveLabel = !!forceActiveLabel || initialValue.length > 0 || Boolean(prefixCharacter); @@ -137,7 +141,7 @@ function BaseTextInput({ animateLabel(styleConst.INACTIVE_LABEL_TRANSLATE_Y, styleConst.INACTIVE_LABEL_SCALE); isLabelActive.current = false; - }, [animateLabel, forceActiveLabel,prefixCharacter, value]); + }, [animateLabel, forceActiveLabel, prefixCharacter, value]); const onFocus = (event: NativeSyntheticEvent) => { if (inputProps.onFocus) { @@ -194,7 +198,7 @@ function BaseTextInput({ isFocused || // If the text has been supplied by Chrome autofill, the value state is not synced with the value // as Chrome doesn't trigger a change event. When there is autofill text, keep the label activated. - isInputAutoFilled(input.current) + isInputAutoFilled(input.current as unknown as Element) ) { activateLabel(); } else { @@ -256,7 +260,7 @@ function BaseTextInput({ const inputHelpText = errorText || hint; const newPlaceholder = !!prefixCharacter || isFocused || !hasLabel || (hasLabel && forceActiveLabel) ? placeholder : undefined; const maxHeight = StyleSheet.flatten(containerStyles).maxHeight; - const textInputContainerStyles: StyleProp = StyleSheet.flatten([ + const newTextInputContainerStyles: StyleProp = StyleSheet.flatten([ styles.textInputContainer, textInputContainerStyles, autoGrow && StyleUtils.getWidthStyle(textInputWidth), @@ -298,7 +302,7 @@ function BaseTextInput({ accessibilityLabel={label ?? ''} accessible style={[ - autoGrowHeight && styles.autoGrowHeightInputContainer(textInputHeight, variables.componentSizeLarge, maxHeight), + autoGrowHeight && styles.autoGrowHeightInputContainer(textInputHeight, variables.componentSizeLarge, typeof maxHeight === 'number' ? maxHeight : 0), !isMultiline && styles.componentHeightLarge, containerStyles, ]} @@ -308,10 +312,10 @@ function BaseTextInput({ // or if multiline is not supplied we calculate the textinput height, using onLayout. onLayout={onLayout} style={[ - textInputContainerStyles, + newTextInputContainerStyles, // When autoGrow is on and minWidth is not supplied, add a minWidth to allow the input to be focusable. - autoGrow && !textInputContainerStyles?.minWidth && styles.mnw2, + autoGrow && !newTextInputContainerStyles?.minWidth && styles.mnw2, ]} > {hasLabel ? ( @@ -341,15 +345,15 @@ function BaseTextInput({ )} { - if (typeof innerRef === 'function') { - innerRef(ref); - } else if (innerRef && 'current' in innerRef) { + ref={(el) => { + if (typeof ref === 'function') { + ref(el); + } else if (ref && 'current' in ref) { // eslint-disable-next-line no-param-reassign - innerRef.current = ref; + ref.current = el; } // @ts-expect-error We need to reassign this ref to the input ref - input.current = ref; + input.current = el; }} // eslint-disable-next-line {...inputProps} @@ -362,7 +366,7 @@ function BaseTextInput({ styles.w100, inputStyle, (!hasLabel || isMultiline) && styles.pv0, - !!prefixCharacter && StyleUtils.getPaddingLeft(getCharacterPadding(props.prefixCharacter) + styles.pl1.paddingLeft), + !!prefixCharacter && StyleUtils.getPaddingLeft(getCharacterPadding(prefixCharacter) + styles.pl1.paddingLeft), inputProps.secureTextEntry && styles.secureInput, // Explicitly remove `lineHeight` from single line inputs so that long text doesn't disappear @@ -416,7 +420,7 @@ function BaseTextInput({ /> )} - {!inputProps.secureTextEntry && Boolean(icon) && ( + {!inputProps.secureTextEntry && icon && ( - {!inputHelpText && ( + {!!inputHelpText && ( ; /** Icon to display in right side of text input */ - icon: (props: SrcProps) => React.ReactNode; + icon: ((props: SrcProps) => React.ReactNode) | null; /** Customize the TextInput container */ textInputContainerStyles: StyleProp; @@ -60,9 +60,6 @@ type CustomBaseTextInputProps = { /** Hide the focus styles on TextInput */ hideFocusedState?: boolean; - /** Forward the inner ref */ - innerRef?: React.ForwardedRef, unknown, unknown> | null>; - /** Maximum characters allowed */ maxLength?: number; @@ -103,9 +100,9 @@ type CustomBaseTextInputProps = { hasError?: boolean; onPress?: (event: GestureResponderEvent | KeyboardEvent) => void; isLoading?: boolean; - translate?: (key: string) => string; + autoCompleteType?: string; }; -type BaseTextInputProps = CustomBaseTextInputProps & TextInputProps; +type BaseTextInputProps = CustomBaseTextInputProps & TextInputProps & LocaleContextProps; export default BaseTextInputProps; export type {CustomBaseTextInputProps}; diff --git a/src/components/TextInput/index.native.tsx b/src/components/TextInput/index.native.tsx index a4d0c76337ab..4ddc64c50de4 100644 --- a/src/components/TextInput/index.native.tsx +++ b/src/components/TextInput/index.native.tsx @@ -1,12 +1,17 @@ -import React, {forwardRef, useEffect} from 'react'; -import {AppState, Keyboard} from 'react-native'; +import React, {Component, ForwardedRef, forwardRef, useEffect} from 'react'; +import {AppState, Keyboard, TextInputProps} from 'react-native'; +import {AnimatedProps} from 'react-native-reanimated'; import styles from '@styles/styles'; import BaseTextInput from './BaseTextInput'; -import * as baseTextInputPropTypes from './BaseTextInput/baseTextInputPropTypes'; +import BaseTextInputProps from './BaseTextInput/types'; -const TextInput = forwardRef((props, ref) => { +// eslint-disable-next-line react/function-component-definition +const TextInput = ( + {inputStyle, disableKeyboard = false, prefixCharacter, inputID, ...props}: BaseTextInputProps, + ref: ForwardedRef, unknown, unknown>>, +) => { useEffect(() => { - if (!props.disableKeyboard) { + if (!disableKeyboard) { return; } @@ -21,8 +26,7 @@ const TextInput = forwardRef((props, ref) => { return () => { appStateSubscription.remove(); }; - }, [props.disableKeyboard]); - + }, [disableKeyboard]); return ( { // Setting autoCompleteType to new-password throws an error on Android/iOS, so fall back to password in that case // eslint-disable-next-line react/jsx-props-no-multi-spaces autoCompleteType={props.autoCompleteType === 'new-password' ? 'password' : props.autoCompleteType} - innerRef={ref} - inputStyle={[styles.baseTextInput, ...props.inputStyle]} + ref={ref} + inputStyle={[styles.baseTextInput, inputStyle]} /> ); -}); +}; -TextInput.propTypes = baseTextInputPropTypes.propTypes; -TextInput.defaultProps = baseTextInputPropTypes.defaultProps; +// TextInput.propTypes = baseTextInputPropTypes.propTypes; +// TextInput.defaultProps = baseTextInputPropTypes.defaultProps; TextInput.displayName = 'TextInput'; -export default TextInput; +export default forwardRef(TextInput); diff --git a/src/components/TextInput/index.tsx b/src/components/TextInput/index.tsx index c8dc21b97310..582c35e94958 100644 --- a/src/components/TextInput/index.tsx +++ b/src/components/TextInput/index.tsx @@ -1,4 +1,6 @@ -import React, {ForwardedRef, useEffect, useRef} from 'react'; +import React, {Component, ForwardedRef, useEffect, useRef} from 'react'; +import {StyleProp, TextInputProps, ViewStyle} from 'react-native'; +import {AnimatedProps} from 'react-native-reanimated'; import * as Browser from '@libs/Browser'; import DomUtils from '@libs/DomUtils'; import Visibility from '@libs/Visibility'; @@ -8,17 +10,20 @@ import * as baseTextInputPropTypes from './BaseTextInput/baseTextInputPropTypes' import BaseTextInputProps from './BaseTextInput/types'; import * as styleConst from './styleConst'; -function TextInput(props: BaseTextInputProps, ref: ForwardedRef) { +function TextInput( + {label = '', name = '', textInputContainerStyles, inputStyle, disableKeyboard = false, multiline = false, prefixCharacter, inputID, ...props}: BaseTextInputProps, + ref: ForwardedRef, unknown, unknown>>, +) { const textInputRef = useRef(null); const removeVisibilityListenerRef = useRef<() => void>(null); useEffect(() => { - if (props.disableKeyboard) { + if (disableKeyboard) { textInputRef.current?.setAttribute('inputmode', 'none'); } - if (props.name) { - textInputRef.current?.setAttribute('name', props.name); + if (name) { + textInputRef.current?.setAttribute('name', name); } // @ts-expect-error We need to reassign this ref to the input ref removeVisibilityListenerRef.current = Visibility.onVisibilityChange(() => { @@ -38,7 +43,7 @@ function TextInput(props: BaseTextInputProps, ref: ForwardedRef // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - const isLabeledMultiline = Boolean(props.label?.length) && props.multiline; + const isLabeledMultiline = Boolean(label?.length) && multiline; const labelAnimationStyle = { // eslint-disable-next-line @typescript-eslint/naming-convention '--active-label-translate-y': `${styleConst.ACTIVE_LABEL_TRANSLATE_Y}px`, @@ -50,7 +55,9 @@ function TextInput(props: BaseTextInputProps, ref: ForwardedRef return ( { + // eslint-disable-next-line react/jsx-props-no-spreading + {...props} + ref={(el) => { // @ts-expect-error We need to reassign this ref to the input ref textInputRef.current = el; if (!ref) { @@ -65,10 +72,8 @@ function TextInput(props: BaseTextInputProps, ref: ForwardedRef // eslint-disable-next-line no-param-reassign ref.current = el; }} - inputStyle={[styles.baseTextInput, styles.textInputDesktop, isLabeledMultiline ? styles.textInputMultiline : {}, props.inputStyle]} - textInputContainerStyles={[labelAnimationStyle, props.textInputContainerStyles]} - // eslint-disable-next-line react/jsx-props-no-spreading - {...props} + inputStyle={[styles.baseTextInput, styles.textInputDesktop, isLabeledMultiline ? styles.textInputMultiline : {}, inputStyle]} + textInputContainerStyles={[labelAnimationStyle as StyleProp, textInputContainerStyles]} /> ); } diff --git a/src/components/TextInput/types.ts b/src/components/TextInput/types.ts deleted file mode 100644 index e69de29bb2d1..000000000000 From bddaeb2f30d441548816d2fb435f6525dced5d3d Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Mon, 4 Dec 2023 09:35:29 +0100 Subject: [PATCH 07/26] fix: types --- src/components/Checkbox.tsx | 4 +- .../BaseTextInput/baseTextInputPropTypes.js | 138 ------------------ .../TextInput/BaseTextInput/index.native.tsx | 16 +- .../TextInput/BaseTextInput/index.tsx | 4 +- src/components/TextInput/index.tsx | 3 - 5 files changed, 13 insertions(+), 152 deletions(-) delete mode 100644 src/components/TextInput/BaseTextInput/baseTextInputPropTypes.js diff --git a/src/components/Checkbox.tsx b/src/components/Checkbox.tsx index 22577ec2b7f9..39c8a484613b 100644 --- a/src/components/Checkbox.tsx +++ b/src/components/Checkbox.tsx @@ -1,4 +1,4 @@ -import React, {ForwardedRef, forwardRef, KeyboardEvent as ReactKeyboardEvent} from 'react'; +import React, {ForwardedRef, forwardRef, MouseEventHandler, KeyboardEvent as ReactKeyboardEvent} from 'react'; import {GestureResponderEvent, StyleProp, View, ViewStyle} from 'react-native'; import * as StyleUtils from '@styles/StyleUtils'; import useTheme from '@styles/themes/useTheme'; @@ -29,7 +29,7 @@ type CheckboxProps = ChildrenProps & { containerStyle?: StyleProp; /** Callback that is called when mousedown is triggered. */ - onMouseDown?: () => void; + onMouseDown?: MouseEventHandler; /** The size of the checkbox container */ containerSize?: number; diff --git a/src/components/TextInput/BaseTextInput/baseTextInputPropTypes.js b/src/components/TextInput/BaseTextInput/baseTextInputPropTypes.js deleted file mode 100644 index 5387d1ff81d1..000000000000 --- a/src/components/TextInput/BaseTextInput/baseTextInputPropTypes.js +++ /dev/null @@ -1,138 +0,0 @@ -import PropTypes from 'prop-types'; - -const propTypes = { - /** Input label */ - label: PropTypes.string, - - /** Name attribute for the input */ - name: PropTypes.string, - - /** Input value */ - value: PropTypes.string, - - /** Default value - used for non controlled inputs */ - defaultValue: PropTypes.string, - - /** Input value placeholder */ - placeholder: PropTypes.string, - - /** Error text to display */ - errorText: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.object]))]), - - /** Icon to display in right side of text input */ - icon: PropTypes.func, - - /** Customize the TextInput container */ - textInputContainerStyles: PropTypes.arrayOf(PropTypes.object), - - /** Customize the main container */ - containerStyles: PropTypes.arrayOf(PropTypes.object), - - /** input style */ - inputStyle: PropTypes.arrayOf(PropTypes.object), - - /** If present, this prop forces the label to remain in a position where it will not collide with input text */ - forceActiveLabel: PropTypes.bool, - - /** Should the input auto focus? */ - autoFocus: PropTypes.bool, - - /** Disable the virtual keyboard */ - disableKeyboard: PropTypes.bool, - - /** - * Autogrow input container length based on the entered text. - * Note: If you use this prop, the text input has to be controlled - * by a value prop. - */ - autoGrow: PropTypes.bool, - - /** - * Autogrow input container height based on the entered text - * Note: If you use this prop, the text input has to be controlled - * by a value prop. - */ - autoGrowHeight: PropTypes.bool, - - /** Hide the focus styles on TextInput */ - hideFocusedState: PropTypes.bool, - - /** Forward the inner ref */ - innerRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), - - /** Maximum characters allowed */ - maxLength: PropTypes.number, - - /** Hint text to display below the TextInput */ - hint: PropTypes.string, - - /** Prefix character */ - prefixCharacter: PropTypes.string, - - /** Whether autoCorrect functionality should enable */ - autoCorrect: PropTypes.bool, - - /** Form props */ - /** The ID used to uniquely identify the input in a Form */ - inputID: PropTypes.string, - - /** Saves a draft of the input value when used in a form */ - shouldSaveDraft: PropTypes.bool, - - /** Callback to update the value on Form when input is used in the Form component. */ - onInputChange: PropTypes.func, - - /** Whether we should wait before focusing the TextInput, useful when using transitions */ - shouldDelayFocus: PropTypes.bool, - - /** Indicate whether pressing Enter on multiline input is allowed to submit the form. */ - submitOnEnter: PropTypes.bool, - - /** Indicate whether input is multiline */ - multiline: PropTypes.bool, - - /** Set the default value to the input if there is a valid saved value */ - shouldUseDefaultValue: PropTypes.bool, - - /** Indicate whether or not the input should prevent swipe actions in tabs */ - shouldInterceptSwipe: PropTypes.bool, -}; - -const defaultProps = { - label: '', - name: '', - errorText: '', - placeholder: '', - hasError: false, - containerStyles: [], - textInputContainerStyles: [], - inputStyle: [], - autoFocus: false, - autoCorrect: true, - - /** - * To be able to function as either controlled or uncontrolled component we should not - * assign a default prop value for `value` or `defaultValue` props - */ - value: undefined, - defaultValue: undefined, - forceActiveLabel: false, - disableKeyboard: false, - autoGrow: false, - autoGrowHeight: false, - hideFocusedState: false, - innerRef: () => {}, - shouldSaveDraft: false, - maxLength: null, - hint: '', - prefixCharacter: '', - onInputChange: () => {}, - shouldDelayFocus: false, - submitOnEnter: false, - icon: null, - shouldUseDefaultValue: false, - multiline: false, - shouldInterceptSwipe: false, -}; - -export {propTypes, defaultProps}; diff --git a/src/components/TextInput/BaseTextInput/index.native.tsx b/src/components/TextInput/BaseTextInput/index.native.tsx index 1a4108bc1803..c5b57a8bc1f6 100644 --- a/src/components/TextInput/BaseTextInput/index.native.tsx +++ b/src/components/TextInput/BaseTextInput/index.native.tsx @@ -222,17 +222,17 @@ function BaseTextInput( * Set Value & activateLabel */ const setValue = (newValue: string) => { - const value = isMultiline ? newValue : newValue.replace(/\n/g, ' '); + const formattedValue = isMultiline ? newValue : newValue.replace(/\n/g, ' '); if (onInputChange) { - onInputChange(newValue); + onInputChange(formattedValue); } if (inputProps.onChangeText) { - Str.result(inputProps.onChangeText, newValue); + Str.result(inputProps.onChangeText, formattedValue); } - if (newValue && newValue.length > 0) { + if (formattedValue && formattedValue.length > 0) { hasValueRef.current = true; // When the componment is uncontrolled, we need to manually activate the label: if (value === undefined) { @@ -361,7 +361,7 @@ function BaseTextInput( !isMultiline && {height, lineHeight: undefined}, // Stop scrollbar flashing when breaking lines with autoGrowHeight enabled. - autoGrowHeight && StyleUtils.getAutoGrowHeightInputStyle(styles, textInputHeight, maxHeight), + autoGrowHeight && StyleUtils.getAutoGrowHeightInputStyle(styles, textInputHeight, typeof maxHeight === 'number' ? maxHeight : 0), // Add disabled color theme when field is not editable. inputProps.disabled && styles.textInputDisabled, styles.pointerEventsAuto, @@ -395,7 +395,9 @@ function BaseTextInput( e.preventDefault()} + onMouseDown={(e) => { + e.preventDefault(); + }} accessibilityLabel={inputProps.translate?.('common.visible') ?? ''} > diff --git a/src/components/TextInput/BaseTextInput/index.tsx b/src/components/TextInput/BaseTextInput/index.tsx index 365715f588cd..01b727f9875f 100644 --- a/src/components/TextInput/BaseTextInput/index.tsx +++ b/src/components/TextInput/BaseTextInput/index.tsx @@ -293,7 +293,7 @@ function BaseTextInput( return ( <> @@ -378,7 +378,7 @@ function BaseTextInput( !isMultiline && Browser.isMobileChrome() && {boxSizing: 'content-box', height: undefined}, // Stop scrollbar flashing when breaking lines with autoGrowHeight enabled. - autoGrowHeight && StyleUtils.getAutoGrowHeightInputStyle(styles, textInputHeight, maxHeight), + autoGrowHeight && StyleUtils.getAutoGrowHeightInputStyle(styles, textInputHeight, typeof maxHeight === 'number' ? maxHeight : 0), // Add disabled color theme when field is not editable. inputProps.disabled && styles.textInputDisabled, styles.pointerEventsAuto, diff --git a/src/components/TextInput/index.tsx b/src/components/TextInput/index.tsx index 761ad84ae2ca..550bcd0a6e18 100644 --- a/src/components/TextInput/index.tsx +++ b/src/components/TextInput/index.tsx @@ -6,7 +6,6 @@ import DomUtils from '@libs/DomUtils'; import Visibility from '@libs/Visibility'; import useThemeStyles from '@styles/useThemeStyles'; import BaseTextInput from './BaseTextInput'; -import * as baseTextInputPropTypes from './BaseTextInput/baseTextInputPropTypes'; import BaseTextInputProps from './BaseTextInput/types'; import * as styleConst from './styleConst'; @@ -80,7 +79,5 @@ function TextInput( } TextInput.displayName = 'TextInput'; -TextInput.propTypes = baseTextInputPropTypes.propTypes; -TextInput.defaultProps = baseTextInputPropTypes.defaultProps; export default React.forwardRef(TextInput); From 4bc71cd09ed669d7565b81a58aa33a64059f4d31 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Tue, 5 Dec 2023 16:16:55 +0100 Subject: [PATCH 08/26] fix: removed commented code --- src/components/TextInput/BaseTextInput/index.native.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/components/TextInput/BaseTextInput/index.native.tsx b/src/components/TextInput/BaseTextInput/index.native.tsx index 5432d485531c..3aae53046b97 100644 --- a/src/components/TextInput/BaseTextInput/index.native.tsx +++ b/src/components/TextInput/BaseTextInput/index.native.tsx @@ -111,12 +111,10 @@ function BaseTextInput( Animated.parallel([ Animated.spring(labelTranslateY, { toValue: translateY, - // duration: styleConst.LABEL_ANIMATION_DURATION, useNativeDriver, }), Animated.spring(labelScale, { toValue: scale, - // duration: styleConst.LABEL_ANIMATION_DURATION, useNativeDriver, }), ]).start(); From bbe0c5df315db8ca83fc7b4d30a6ba902f4b8a58 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Tue, 5 Dec 2023 17:16:24 +0100 Subject: [PATCH 09/26] fix: resolve comments --- src/components/Checkbox.tsx | 2 +- .../TextInput/BaseTextInput/index.native.tsx | 12 ++++++------ src/components/TextInput/BaseTextInput/index.tsx | 7 ++----- src/components/TextInput/BaseTextInput/types.ts | 7 +++++++ 4 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/components/Checkbox.tsx b/src/components/Checkbox.tsx index 39c8a484613b..90546296083e 100644 --- a/src/components/Checkbox.tsx +++ b/src/components/Checkbox.tsx @@ -1,4 +1,4 @@ -import React, {ForwardedRef, forwardRef, MouseEventHandler, KeyboardEvent as ReactKeyboardEvent} from 'react'; +import React, {type ForwardedRef, forwardRef, type MouseEventHandler, type KeyboardEvent as ReactKeyboardEvent} from 'react'; import {GestureResponderEvent, StyleProp, View, ViewStyle} from 'react-native'; import * as StyleUtils from '@styles/StyleUtils'; import useTheme from '@styles/themes/useTheme'; diff --git a/src/components/TextInput/BaseTextInput/index.native.tsx b/src/components/TextInput/BaseTextInput/index.native.tsx index 3aae53046b97..507882bca9ff 100644 --- a/src/components/TextInput/BaseTextInput/index.native.tsx +++ b/src/components/TextInput/BaseTextInput/index.native.tsx @@ -4,9 +4,9 @@ import { ActivityIndicator, Animated, FlexStyle, - GestureResponderEvent, - LayoutChangeEvent, - NativeSyntheticEvent, + type GestureResponderEvent, + type LayoutChangeEvent, + type NativeSyntheticEvent, StyleProp, StyleSheet, TextInput, @@ -35,7 +35,7 @@ import useTheme from '@styles/themes/useTheme'; import useThemeStyles from '@styles/useThemeStyles'; import variables from '@styles/variables'; import CONST from '@src/CONST'; -import BaseTextInputProps from './types'; +import type BaseTextInputProps from './types'; function BaseTextInput( { @@ -83,7 +83,7 @@ function BaseTextInput( const [textInputWidth, setTextInputWidth] = useState(0); const [textInputHeight, setTextInputHeight] = useState(0); const [height, setHeight] = useState(variables.componentSizeLarge); - const [width, setWidth] = useState(); + const [width, setWidth] = useState(null); const labelScale = useRef(new Animated.Value(initialActiveLabel ? styleConst.ACTIVE_LABEL_SCALE : styleConst.INACTIVE_LABEL_SCALE)).current; const labelTranslateY = useRef(new Animated.Value(initialActiveLabel ? styleConst.ACTIVE_LABEL_TRANSLATE_Y : styleConst.INACTIVE_LABEL_TRANSLATE_Y)).current; @@ -180,7 +180,7 @@ function BaseTextInput( const layout = event.nativeEvent.layout; - setWidth((prevWidth: number | undefined) => (autoGrowHeight ? layout.width : prevWidth)); + setWidth((prevWidth: number | null) => (autoGrowHeight ? layout.width : prevWidth)); setHeight((prevHeight: number) => (!multiline ? layout.height : prevHeight)); }, [autoGrowHeight, multiline], diff --git a/src/components/TextInput/BaseTextInput/index.tsx b/src/components/TextInput/BaseTextInput/index.tsx index bf74d6b1cfc8..2fec81618fd7 100644 --- a/src/components/TextInput/BaseTextInput/index.tsx +++ b/src/components/TextInput/BaseTextInput/index.tsx @@ -40,7 +40,6 @@ import BaseTextInputProps from './types'; function BaseTextInput( { label = '', - // name = '', value = undefined, defaultValue = undefined, placeholder = '', @@ -57,12 +56,10 @@ function BaseTextInput( hideFocusedState = false, maxLength = undefined, hint = '', - // shouldSaveDraft = false, onInputChange = () => {}, shouldDelayFocus = false, submitOnEnter = false, multiline = false, - // shouldUseDefaultValue = false, shouldInterceptSwipe = false, autoCorrect = true, prefixCharacter, @@ -82,7 +79,7 @@ function BaseTextInput( const [textInputWidth, setTextInputWidth] = useState(0); const [textInputHeight, setTextInputHeight] = useState(0); const [height, setHeight] = useState(variables.componentSizeLarge); - const [width, setWidth] = useState(); + const [width, setWidth] = useState(null); const labelScale = useRef(new Animated.Value(initialActiveLabel ? styleConst.ACTIVE_LABEL_SCALE : styleConst.INACTIVE_LABEL_SCALE)).current; const labelTranslateY = useRef(new Animated.Value(initialActiveLabel ? styleConst.ACTIVE_LABEL_TRANSLATE_Y : styleConst.INACTIVE_LABEL_TRANSLATE_Y)).current; @@ -179,7 +176,7 @@ function BaseTextInput( const layout = event.nativeEvent.layout; - setWidth((prevWidth: number | undefined) => (autoGrowHeight ? layout.width : prevWidth)); + setWidth((prevWidth: number | null) => (autoGrowHeight ? layout.width : prevWidth)); setHeight((prevHeight: number) => (!multiline ? layout.height : prevHeight)); }, [autoGrowHeight, multiline], diff --git a/src/components/TextInput/BaseTextInput/types.ts b/src/components/TextInput/BaseTextInput/types.ts index e1a123fb0691..99f5d7d728ea 100644 --- a/src/components/TextInput/BaseTextInput/types.ts +++ b/src/components/TextInput/BaseTextInput/types.ts @@ -97,9 +97,16 @@ type CustomBaseTextInputProps = { /** Indicate whether or not the input should prevent swipe actions in tabs */ shouldInterceptSwipe?: boolean; + /** Should there be an error displayed */ hasError?: boolean; + + /** On Press handler */ onPress?: (event: GestureResponderEvent | KeyboardEvent) => void; + + /** Should loading state should be displayed */ isLoading?: boolean; + + /** Type of autocomplete */ autoCompleteType?: string; }; From 1f5cac75d088c89f2e5442d42b5c28ad2a308542 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Wed, 6 Dec 2023 11:21:30 +0100 Subject: [PATCH 10/26] fix: address comments --- .../TextInput/BaseTextInput/index.native.tsx | 29 ++++++++----------- .../TextInput/BaseTextInput/index.tsx | 22 +++++++------- .../TextInput/BaseTextInput/types.ts | 1 + .../TextInput/TextInputLabel/index.tsx | 4 +-- src/components/TextInput/index.tsx | 11 +++---- 5 files changed, 32 insertions(+), 35 deletions(-) diff --git a/src/components/TextInput/BaseTextInput/index.native.tsx b/src/components/TextInput/BaseTextInput/index.native.tsx index 507882bca9ff..f2066a8b8271 100644 --- a/src/components/TextInput/BaseTextInput/index.native.tsx +++ b/src/components/TextInput/BaseTextInput/index.native.tsx @@ -40,29 +40,26 @@ import type BaseTextInputProps from './types'; function BaseTextInput( { label = '', - // name = '', - value = undefined, - defaultValue = undefined, + value, + defaultValue, placeholder = '', errorText = '', icon = null, - textInputContainerStyles = [], - containerStyles = [], - inputStyle = [], + textInputContainerStyles, + containerStyles, + inputStyle, forceActiveLabel = false, autoFocus = false, disableKeyboard = false, autoGrow = false, autoGrowHeight = false, hideFocusedState = false, - maxLength = undefined, + maxLength, hint = '', - // shouldSaveDraft = false, onInputChange = () => {}, shouldDelayFocus = false, submitOnEnter = false, multiline = false, - // shouldUseDefaultValue = false, shouldInterceptSwipe = false, autoCorrect = true, prefixCharacter, @@ -75,7 +72,7 @@ function BaseTextInput( const styles = useThemeStyles(); const {hasError = false} = inputProps; const initialValue = value ?? defaultValue ?? ''; - const initialActiveLabel = !!forceActiveLabel || initialValue.length > 0 || Boolean(prefixCharacter); + const initialActiveLabel = !!forceActiveLabel || initialValue.length > 0 || !!prefixCharacter; const isMultiline = multiline || autoGrowHeight; const [isFocused, setIsFocused] = useState(false); @@ -251,7 +248,7 @@ function BaseTextInput( // Some characters are wider than the others when rendered, e.g. '@' vs '#'. Chosen font-family and font-size // also have an impact on the width of the character, but as long as there's only one font-family and one font-size, // this method will produce reliable results. - const getCharacterPadding = (prefix: string) => { + const getCharacterPadding = (prefix: string): number => { switch (prefix) { case CONST.POLICY.ROOM_PREFIX: return 10; @@ -260,8 +257,6 @@ function BaseTextInput( } }; - // eslint-disable-next-line react/forbid-foreign-prop-types - // const inputProps = _.omit(props, _.keys(baseTextInputPropTypes.propTypes)); const hasLabel = Boolean(label?.length); const isReadOnly = inputProps.readOnly ?? inputProps.disabled; const inputHelpText = errorText || hint; @@ -332,15 +327,15 @@ function BaseTextInput( )} { + ref={(element) => { if (typeof ref === 'function') { - ref(el); + ref(element); } else if (ref && 'current' in ref) { // eslint-disable-next-line no-param-reassign - ref.current = el; + ref.current = element; } // @ts-expect-error We need to reassign this ref to the input ref - input.current = el; + input.current = element; }} // eslint-disable-next-line {...inputProps} diff --git a/src/components/TextInput/BaseTextInput/index.tsx b/src/components/TextInput/BaseTextInput/index.tsx index 2fec81618fd7..602d7c7cf6de 100644 --- a/src/components/TextInput/BaseTextInput/index.tsx +++ b/src/components/TextInput/BaseTextInput/index.tsx @@ -40,8 +40,8 @@ import BaseTextInputProps from './types'; function BaseTextInput( { label = '', - value = undefined, - defaultValue = undefined, + value, + defaultValue, placeholder = '', errorText = '', icon = null, @@ -54,7 +54,7 @@ function BaseTextInput( autoGrow = false, autoGrowHeight = false, hideFocusedState = false, - maxLength = undefined, + maxLength, hint = '', onInputChange = () => {}, shouldDelayFocus = false, @@ -66,13 +66,13 @@ function BaseTextInput( inputID, ...inputProps }: BaseTextInputProps, - ref: ForwardedRef, unknown, unknown>>, + ref: ForwardedRef>>, ) { const theme = useTheme(); const styles = useThemeStyles(); const {hasError = false} = inputProps; const initialValue = value ?? defaultValue ?? ''; - const initialActiveLabel = !!forceActiveLabel || initialValue.length > 0 || Boolean(prefixCharacter); + const initialActiveLabel = !!forceActiveLabel || initialValue.length > 0 || !!prefixCharacter; const [isFocused, setIsFocused] = useState(false); const [passwordHidden, setPasswordHidden] = useState(inputProps.secureTextEntry); @@ -243,7 +243,7 @@ function BaseTextInput( // Some characters are wider than the others when rendered, e.g. '@' vs '#'. Chosen font-family and font-size // also have an impact on the width of the character, but as long as there's only one font-family and one font-size, // this method will produce reliable results. - const getCharacterPadding = (prefix: string) => { + const getCharacterPadding = (prefix: string): number => { switch (prefix) { case CONST.POLICY.ROOM_PREFIX: return 10; @@ -276,7 +276,7 @@ function BaseTextInput( const lineHeight = useMemo(() => { if ((Browser.isSafari() || Browser.isMobileChrome()) && Array.isArray(inputStyle)) { - const lineHeightValue = inputStyle?.find((f) => f && 'lineHeight' in f && f.lineHeight !== undefined); + const lineHeightValue = inputStyle?.find((style) => style && 'lineHeight' in style && style.lineHeight !== undefined); if (lineHeightValue && 'lineHeight' in lineHeightValue) { return lineHeightValue.lineHeight; } @@ -340,15 +340,15 @@ function BaseTextInput( )} { + ref={(element) => { if (typeof ref === 'function') { - ref(el); + ref(element); } else if (ref && 'current' in ref) { // eslint-disable-next-line no-param-reassign - ref.current = el; + ref.current = element; } // @ts-expect-error We need to reassign this ref to the input ref - input.current = el; + input.current = element; }} // eslint-disable-next-line {...inputProps} diff --git a/src/components/TextInput/BaseTextInput/types.ts b/src/components/TextInput/BaseTextInput/types.ts index 99f5d7d728ea..e2d761d4c067 100644 --- a/src/components/TextInput/BaseTextInput/types.ts +++ b/src/components/TextInput/BaseTextInput/types.ts @@ -111,5 +111,6 @@ type CustomBaseTextInputProps = { }; type BaseTextInputProps = CustomBaseTextInputProps & TextInputProps & LocaleContextProps; + export default BaseTextInputProps; export type {CustomBaseTextInputProps}; diff --git a/src/components/TextInput/TextInputLabel/index.tsx b/src/components/TextInput/TextInputLabel/index.tsx index 07ae16b74fd7..086218eb38d0 100644 --- a/src/components/TextInput/TextInputLabel/index.tsx +++ b/src/components/TextInput/TextInputLabel/index.tsx @@ -6,13 +6,13 @@ import TextInputLabelProps from './types'; function TextInputLabel({for: inputId = '', label, labelTranslateY, labelScale}: TextInputLabelProps) { const styles = useThemeStyles(); - const labelRef = useRef(null); + const labelRef = useRef(null); useEffect(() => { if (!inputId || !labelRef.current) { return; } - (labelRef.current as unknown as HTMLFormElement).setAttribute('for', inputId); + labelRef.current.setAttribute('for', inputId); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); diff --git a/src/components/TextInput/index.tsx b/src/components/TextInput/index.tsx index 550bcd0a6e18..e83d3bd1efca 100644 --- a/src/components/TextInput/index.tsx +++ b/src/components/TextInput/index.tsx @@ -57,20 +57,21 @@ function TextInput( { - // @ts-expect-error We need to reassign this ref to the input ref - textInputRef.current = el; + ref={(element) => { + if (element) { + (textInputRef.current as HTMLElement | Component>) = element; + } if (!ref) { return; } if (typeof ref === 'function') { - ref(el); + ref(element); return; } // eslint-disable-next-line no-param-reassign - ref.current = el; + ref.current = element; }} inputStyle={[styles.baseTextInput, styles.textInputDesktop, isLabeledMultiline ? styles.textInputMultiline : {}, inputStyle]} textInputContainerStyles={[labelAnimationStyle as StyleProp, textInputContainerStyles]} From 052610c05133cdf2990323e85dd2cebf6f2b39ac Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Thu, 7 Dec 2023 10:03:40 +0100 Subject: [PATCH 11/26] fix: resolve comments --- src/components/RNTextInput.tsx | 5 ++++- .../TextInput/BaseTextInput/index.native.tsx | 9 ++++---- .../TextInput/BaseTextInput/index.tsx | 21 +++++++++---------- .../TextInput/BaseTextInput/types.ts | 7 +++++-- src/components/TextInput/index.native.tsx | 12 ++++------- src/components/TextInput/index.tsx | 20 +++++++++++------- 6 files changed, 40 insertions(+), 34 deletions(-) diff --git a/src/components/RNTextInput.tsx b/src/components/RNTextInput.tsx index 28555abe3266..5c3a65998e25 100644 --- a/src/components/RNTextInput.tsx +++ b/src/components/RNTextInput.tsx @@ -1,8 +1,9 @@ -import React, {ForwardedRef} from 'react'; +import React, {Component, ForwardedRef} from 'react'; // eslint-disable-next-line no-restricted-imports import {TextInput, TextInputProps} from 'react-native'; import Animated, {AnimatedProps} from 'react-native-reanimated'; +type AnimatedTextInputRef = Component>; // Convert the underlying TextInput into an Animated component so that we can take an animated ref and pass it to a worklet const AnimatedTextInput = Animated.createAnimatedComponent(TextInput); @@ -27,3 +28,5 @@ function RNTextInputWithRef(props: TextInputProps, ref: ForwardedRef, unknown, unknown>>, + ref: BaseTextInputRef, ) { const theme = useTheme(); const styles = useThemeStyles(); @@ -334,8 +335,8 @@ function BaseTextInput( // eslint-disable-next-line no-param-reassign ref.current = element; } - // @ts-expect-error We need to reassign this ref to the input ref - input.current = element; + + (input.current as AnimatedTextInputRef | null) = element; }} // eslint-disable-next-line {...inputProps} diff --git a/src/components/TextInput/BaseTextInput/index.tsx b/src/components/TextInput/BaseTextInput/index.tsx index 602d7c7cf6de..148d668dbdd4 100644 --- a/src/components/TextInput/BaseTextInput/index.tsx +++ b/src/components/TextInput/BaseTextInput/index.tsx @@ -1,5 +1,5 @@ import Str from 'expensify-common/lib/str'; -import React, {Component, ForwardedRef, forwardRef, useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import React, {forwardRef, useCallback, useEffect, useMemo, useRef, useState} from 'react'; import { ActivityIndicator, Animated, @@ -11,17 +11,15 @@ import { StyleSheet, TextInput, TextInputFocusEventData, - TextInputProps, View, ViewStyle, } from 'react-native'; -import {AnimatedProps} from 'react-native-reanimated'; import Checkbox from '@components/Checkbox'; import FormHelpMessage from '@components/FormHelpMessage'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; -import RNTextInput from '@components/RNTextInput'; +import RNTextInput, {AnimatedTextInputRef} from '@components/RNTextInput'; import SwipeInterceptPanResponder from '@components/SwipeInterceptPanResponder'; import Text from '@components/Text'; import * as styleConst from '@components/TextInput/styleConst'; @@ -35,7 +33,8 @@ import useTheme from '@styles/themes/useTheme'; import useThemeStyles from '@styles/useThemeStyles'; import variables from '@styles/variables'; import CONST from '@src/CONST'; -import BaseTextInputProps from './types'; +import type BaseTextInputProps from './types'; +import type {BaseTextInputRef} from './types'; function BaseTextInput( { @@ -45,9 +44,9 @@ function BaseTextInput( placeholder = '', errorText = '', icon = null, - textInputContainerStyles = [], - containerStyles = [], - inputStyle = [], + textInputContainerStyles, + containerStyles, + inputStyle, forceActiveLabel = false, autoFocus = false, disableKeyboard = false, @@ -66,7 +65,7 @@ function BaseTextInput( inputID, ...inputProps }: BaseTextInputProps, - ref: ForwardedRef>>, + ref: BaseTextInputRef, ) { const theme = useTheme(); const styles = useThemeStyles(); @@ -347,8 +346,8 @@ function BaseTextInput( // eslint-disable-next-line no-param-reassign ref.current = element; } - // @ts-expect-error We need to reassign this ref to the input ref - input.current = element; + + (input.current as AnimatedTextInputRef | null) = element; }} // eslint-disable-next-line {...inputProps} diff --git a/src/components/TextInput/BaseTextInput/types.ts b/src/components/TextInput/BaseTextInput/types.ts index e2d761d4c067..b8171fc91b01 100644 --- a/src/components/TextInput/BaseTextInput/types.ts +++ b/src/components/TextInput/BaseTextInput/types.ts @@ -1,5 +1,6 @@ -import React from 'react'; +import React, {Component, ForwardedRef} from 'react'; import {FlexStyle, GestureResponderEvent, StyleProp, TextInputProps, TextStyle, ViewStyle} from 'react-native'; +import {AnimatedProps} from 'react-native-reanimated'; import {SrcProps} from '@components/Icon'; import {LocaleContextProps} from '@components/LocaleContextProvider'; @@ -110,7 +111,9 @@ type CustomBaseTextInputProps = { autoCompleteType?: string; }; +type BaseTextInputRef = ForwardedRef>>; + type BaseTextInputProps = CustomBaseTextInputProps & TextInputProps & LocaleContextProps; export default BaseTextInputProps; -export type {CustomBaseTextInputProps}; +export type {CustomBaseTextInputProps, BaseTextInputRef}; diff --git a/src/components/TextInput/index.native.tsx b/src/components/TextInput/index.native.tsx index 7b974e1233bc..bf8145b4b2cc 100644 --- a/src/components/TextInput/index.native.tsx +++ b/src/components/TextInput/index.native.tsx @@ -1,15 +1,11 @@ -import React, {Component, ForwardedRef, forwardRef, useEffect} from 'react'; -import {AppState, Keyboard, TextInputProps} from 'react-native'; -import {AnimatedProps} from 'react-native-reanimated'; +import React, {forwardRef, useEffect} from 'react'; +import {AppState, Keyboard} from 'react-native'; import useThemeStyles from '@styles/useThemeStyles'; import BaseTextInput from './BaseTextInput'; -import BaseTextInputProps from './BaseTextInput/types'; +import BaseTextInputProps, {BaseTextInputRef} from './BaseTextInput/types'; // eslint-disable-next-line react/function-component-definition -const TextInput = ( - {inputStyle, disableKeyboard = false, prefixCharacter, inputID, ...props}: BaseTextInputProps, - ref: ForwardedRef, unknown, unknown>>, -) => { +const TextInput = ({inputStyle, disableKeyboard = false, prefixCharacter, inputID, ...props}: BaseTextInputProps, ref: BaseTextInputRef) => { const styles = useThemeStyles(); useEffect(() => { diff --git a/src/components/TextInput/index.tsx b/src/components/TextInput/index.tsx index e83d3bd1efca..aff41d67cf88 100644 --- a/src/components/TextInput/index.tsx +++ b/src/components/TextInput/index.tsx @@ -1,4 +1,4 @@ -import React, {Component, ForwardedRef, useEffect, useRef} from 'react'; +import React, {Component, useEffect, useRef} from 'react'; import {StyleProp, TextInputProps, ViewStyle} from 'react-native'; import {AnimatedProps} from 'react-native-reanimated'; import * as Browser from '@libs/Browser'; @@ -6,18 +6,22 @@ import DomUtils from '@libs/DomUtils'; import Visibility from '@libs/Visibility'; import useThemeStyles from '@styles/useThemeStyles'; import BaseTextInput from './BaseTextInput'; -import BaseTextInputProps from './BaseTextInput/types'; +import type BaseTextInputProps from './BaseTextInput/types'; +import type {BaseTextInputRef} from './BaseTextInput/types'; import * as styleConst from './styleConst'; +type RemoveVisibilityListener = () => void; + function TextInput( {label = '', name = '', textInputContainerStyles, inputStyle, disableKeyboard = false, multiline = false, prefixCharacter, inputID, ...props}: BaseTextInputProps, - ref: ForwardedRef, unknown, unknown>>, + ref: BaseTextInputRef, ) { const styles = useThemeStyles(); const textInputRef = useRef(null); - const removeVisibilityListenerRef = useRef<() => void>(null); + const removeVisibilityListenerRef = useRef(null); useEffect(() => { + let removeVisibilityListener = removeVisibilityListenerRef.current; if (disableKeyboard) { textInputRef.current?.setAttribute('inputmode', 'none'); } @@ -25,8 +29,8 @@ function TextInput( if (name) { textInputRef.current?.setAttribute('name', name); } - // @ts-expect-error We need to reassign this ref to the input ref - removeVisibilityListenerRef.current = Visibility.onVisibilityChange(() => { + + removeVisibilityListener = Visibility.onVisibilityChange(() => { if (!Browser.isMobileChrome() || !Visibility.isVisible() || !textInputRef.current || DomUtils.getActiveElement() !== textInputRef.current) { return; } @@ -35,10 +39,10 @@ function TextInput( }); return () => { - if (!removeVisibilityListenerRef.current) { + if (!removeVisibilityListener) { return; } - removeVisibilityListenerRef.current(); + removeVisibilityListener(); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); From 0b22b3b02f2ae18fd630b2f5109393994c468eb2 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Thu, 7 Dec 2023 10:25:16 +0100 Subject: [PATCH 12/26] fix: lint error --- src/components/TextInput/BaseTextInput/index.native.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/TextInput/BaseTextInput/index.native.tsx b/src/components/TextInput/BaseTextInput/index.native.tsx index 0bc9de782b92..fe6d483c4acf 100644 --- a/src/components/TextInput/BaseTextInput/index.native.tsx +++ b/src/components/TextInput/BaseTextInput/index.native.tsx @@ -1,5 +1,5 @@ import Str from 'expensify-common/lib/str'; -import React, {Component, ForwardedRef, forwardRef, useCallback, useEffect, useRef, useState} from 'react'; +import React, {forwardRef, useCallback, useEffect, useRef, useState} from 'react'; import { ActivityIndicator, Animated, @@ -11,11 +11,9 @@ import { StyleSheet, TextInput, TextInputFocusEventData, - TextInputProps, View, ViewStyle, } from 'react-native'; -import {AnimatedProps} from 'react-native-reanimated'; import Checkbox from '@components/Checkbox'; import FormHelpMessage from '@components/FormHelpMessage'; import Icon from '@components/Icon'; From 12f6197d5e983f38f2775dabdff487ba9009fdf4 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Mon, 11 Dec 2023 10:10:51 +0100 Subject: [PATCH 13/26] fix: types --- src/components/TextInput/BaseTextInput/index.native.tsx | 1 + src/components/TextInput/BaseTextInput/index.tsx | 1 + src/components/TextInput/BaseTextInput/types.ts | 3 ++- 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/TextInput/BaseTextInput/index.native.tsx b/src/components/TextInput/BaseTextInput/index.native.tsx index 8479e30f19ae..1e4671b32e1d 100644 --- a/src/components/TextInput/BaseTextInput/index.native.tsx +++ b/src/components/TextInput/BaseTextInput/index.native.tsx @@ -71,6 +71,7 @@ function BaseTextInput( const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const {hasError = false} = inputProps; + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const initialValue = value || defaultValue || ''; const initialActiveLabel = !!forceActiveLabel || initialValue.length > 0 || !!prefixCharacter; const isMultiline = multiline || autoGrowHeight; diff --git a/src/components/TextInput/BaseTextInput/index.tsx b/src/components/TextInput/BaseTextInput/index.tsx index f22a90867dc4..939a385bccf6 100644 --- a/src/components/TextInput/BaseTextInput/index.tsx +++ b/src/components/TextInput/BaseTextInput/index.tsx @@ -71,6 +71,7 @@ function BaseTextInput( const styles = useThemeStyles(); const {hasError = false} = inputProps; const StyleUtils = useStyleUtils(); + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const initialValue = value || defaultValue || ''; const initialActiveLabel = !!forceActiveLabel || initialValue.length > 0 || !!prefixCharacter; diff --git a/src/components/TextInput/BaseTextInput/types.ts b/src/components/TextInput/BaseTextInput/types.ts index b8171fc91b01..dcdfbc4a2c34 100644 --- a/src/components/TextInput/BaseTextInput/types.ts +++ b/src/components/TextInput/BaseTextInput/types.ts @@ -3,6 +3,7 @@ import {FlexStyle, GestureResponderEvent, StyleProp, TextInputProps, TextStyle, import {AnimatedProps} from 'react-native-reanimated'; import {SrcProps} from '@components/Icon'; import {LocaleContextProps} from '@components/LocaleContextProvider'; +import {MaybePhraseKey} from '@libs/Localize'; type CustomBaseTextInputProps = { /** Input label */ @@ -21,7 +22,7 @@ type CustomBaseTextInputProps = { placeholder?: string; /** Error text to display */ - errorText: string | string[] | Record; + errorText: MaybePhraseKey; /** Icon to display in right side of text input */ icon: ((props: SrcProps) => React.ReactNode) | null; From b48cee20ef14702a7f293f8507c52562060db404 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Mon, 11 Dec 2023 12:29:41 +0100 Subject: [PATCH 14/26] fix: bring back proptypes as they are used in different file --- .../BaseTextInput/baseTextInputPropTypes.js | 138 ++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 src/components/TextInput/BaseTextInput/baseTextInputPropTypes.js diff --git a/src/components/TextInput/BaseTextInput/baseTextInputPropTypes.js b/src/components/TextInput/BaseTextInput/baseTextInputPropTypes.js new file mode 100644 index 000000000000..5387d1ff81d1 --- /dev/null +++ b/src/components/TextInput/BaseTextInput/baseTextInputPropTypes.js @@ -0,0 +1,138 @@ +import PropTypes from 'prop-types'; + +const propTypes = { + /** Input label */ + label: PropTypes.string, + + /** Name attribute for the input */ + name: PropTypes.string, + + /** Input value */ + value: PropTypes.string, + + /** Default value - used for non controlled inputs */ + defaultValue: PropTypes.string, + + /** Input value placeholder */ + placeholder: PropTypes.string, + + /** Error text to display */ + errorText: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.object]))]), + + /** Icon to display in right side of text input */ + icon: PropTypes.func, + + /** Customize the TextInput container */ + textInputContainerStyles: PropTypes.arrayOf(PropTypes.object), + + /** Customize the main container */ + containerStyles: PropTypes.arrayOf(PropTypes.object), + + /** input style */ + inputStyle: PropTypes.arrayOf(PropTypes.object), + + /** If present, this prop forces the label to remain in a position where it will not collide with input text */ + forceActiveLabel: PropTypes.bool, + + /** Should the input auto focus? */ + autoFocus: PropTypes.bool, + + /** Disable the virtual keyboard */ + disableKeyboard: PropTypes.bool, + + /** + * Autogrow input container length based on the entered text. + * Note: If you use this prop, the text input has to be controlled + * by a value prop. + */ + autoGrow: PropTypes.bool, + + /** + * Autogrow input container height based on the entered text + * Note: If you use this prop, the text input has to be controlled + * by a value prop. + */ + autoGrowHeight: PropTypes.bool, + + /** Hide the focus styles on TextInput */ + hideFocusedState: PropTypes.bool, + + /** Forward the inner ref */ + innerRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), + + /** Maximum characters allowed */ + maxLength: PropTypes.number, + + /** Hint text to display below the TextInput */ + hint: PropTypes.string, + + /** Prefix character */ + prefixCharacter: PropTypes.string, + + /** Whether autoCorrect functionality should enable */ + autoCorrect: PropTypes.bool, + + /** Form props */ + /** The ID used to uniquely identify the input in a Form */ + inputID: PropTypes.string, + + /** Saves a draft of the input value when used in a form */ + shouldSaveDraft: PropTypes.bool, + + /** Callback to update the value on Form when input is used in the Form component. */ + onInputChange: PropTypes.func, + + /** Whether we should wait before focusing the TextInput, useful when using transitions */ + shouldDelayFocus: PropTypes.bool, + + /** Indicate whether pressing Enter on multiline input is allowed to submit the form. */ + submitOnEnter: PropTypes.bool, + + /** Indicate whether input is multiline */ + multiline: PropTypes.bool, + + /** Set the default value to the input if there is a valid saved value */ + shouldUseDefaultValue: PropTypes.bool, + + /** Indicate whether or not the input should prevent swipe actions in tabs */ + shouldInterceptSwipe: PropTypes.bool, +}; + +const defaultProps = { + label: '', + name: '', + errorText: '', + placeholder: '', + hasError: false, + containerStyles: [], + textInputContainerStyles: [], + inputStyle: [], + autoFocus: false, + autoCorrect: true, + + /** + * To be able to function as either controlled or uncontrolled component we should not + * assign a default prop value for `value` or `defaultValue` props + */ + value: undefined, + defaultValue: undefined, + forceActiveLabel: false, + disableKeyboard: false, + autoGrow: false, + autoGrowHeight: false, + hideFocusedState: false, + innerRef: () => {}, + shouldSaveDraft: false, + maxLength: null, + hint: '', + prefixCharacter: '', + onInputChange: () => {}, + shouldDelayFocus: false, + submitOnEnter: false, + icon: null, + shouldUseDefaultValue: false, + multiline: false, + shouldInterceptSwipe: false, +}; + +export {propTypes, defaultProps}; From f327d28c594b9b5464604f71b869acc9b63d896b Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Mon, 11 Dec 2023 15:28:22 +0100 Subject: [PATCH 15/26] fix: tests --- .../TextInput/BaseTextInput/index.native.tsx | 11 +++---- .../TextInput/BaseTextInput/index.tsx | 7 +++-- .../TextInput/BaseTextInput/types.ts | 3 +- src/components/TextInput/index.native.tsx | 13 ++++----- src/components/TextInput/index.tsx | 29 ++++++++----------- 5 files changed, 29 insertions(+), 34 deletions(-) diff --git a/src/components/TextInput/BaseTextInput/index.native.tsx b/src/components/TextInput/BaseTextInput/index.native.tsx index 1e4671b32e1d..b6db50dfea29 100644 --- a/src/components/TextInput/BaseTextInput/index.native.tsx +++ b/src/components/TextInput/BaseTextInput/index.native.tsx @@ -24,7 +24,7 @@ import SwipeInterceptPanResponder from '@components/SwipeInterceptPanResponder'; import Text from '@components/Text'; import * as styleConst from '@components/TextInput/styleConst'; import TextInputLabel from '@components/TextInput/TextInputLabel'; -import withLocalize from '@components/withLocalize'; +import useLocalize from '@hooks/useLocalize'; import getSecureEntryKeyboardType from '@libs/getSecureEntryKeyboardType'; import isInputAutoFilled from '@libs/isInputAutoFilled'; import useNativeDriver from '@libs/useNativeDriver'; @@ -34,7 +34,7 @@ import useThemeStyles from '@styles/useThemeStyles'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import type BaseTextInputProps from './types'; -import {BaseTextInputRef} from './types'; +import type {BaseTextInputRef} from './types'; function BaseTextInput( { @@ -70,6 +70,7 @@ function BaseTextInput( const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); + const {translate} = useLocalize(); const {hasError = false} = inputProps; // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const initialValue = value || defaultValue || ''; @@ -262,7 +263,7 @@ function BaseTextInput( const isReadOnly = inputProps.readOnly ?? inputProps.disabled; const inputHelpText = errorText || hint; const placeholderValue = !!prefixCharacter || isFocused || !hasLabel || (hasLabel && forceActiveLabel) ? placeholder : undefined; - const maxHeight = StyleSheet.flatten(containerStyles).maxHeight; + const maxHeight = StyleSheet.flatten(containerStyles)?.maxHeight; const newTextInputContainerStyles: StyleProp = StyleSheet.flatten([ styles.textInputContainer, textInputContainerStyles, @@ -394,7 +395,7 @@ function BaseTextInput( onMouseDown={(e) => { e.preventDefault(); }} - accessibilityLabel={inputProps.translate?.('common.visible') ?? ''} + accessibilityLabel={translate?.('common.visible') ?? ''} > 0 || !!prefixCharacter; @@ -413,7 +414,7 @@ function BaseTextInput( onMouseDown={(e) => { e.preventDefault(); }} - accessibilityLabel={inputProps.translate?.('common.visible') ?? ''} + accessibilityLabel={translate?.('common.visible') ?? ''} > >>; -type BaseTextInputProps = CustomBaseTextInputProps & TextInputProps & LocaleContextProps; +type BaseTextInputProps = CustomBaseTextInputProps & TextInputProps; export default BaseTextInputProps; export type {CustomBaseTextInputProps, BaseTextInputRef}; diff --git a/src/components/TextInput/index.native.tsx b/src/components/TextInput/index.native.tsx index bf8145b4b2cc..58d562caa5cd 100644 --- a/src/components/TextInput/index.native.tsx +++ b/src/components/TextInput/index.native.tsx @@ -4,12 +4,11 @@ import useThemeStyles from '@styles/useThemeStyles'; import BaseTextInput from './BaseTextInput'; import BaseTextInputProps, {BaseTextInputRef} from './BaseTextInput/types'; -// eslint-disable-next-line react/function-component-definition -const TextInput = ({inputStyle, disableKeyboard = false, prefixCharacter, inputID, ...props}: BaseTextInputProps, ref: BaseTextInputRef) => { +function TextInput(props: BaseTextInputProps, ref: BaseTextInputRef) { const styles = useThemeStyles(); useEffect(() => { - if (!disableKeyboard) { + if (!props.disableKeyboard) { return; } @@ -24,20 +23,20 @@ const TextInput = ({inputStyle, disableKeyboard = false, prefixCharacter, inputI return () => { appStateSubscription.remove(); }; - }, [disableKeyboard]); + }, [props.disableKeyboard]); return ( ); -}; +} TextInput.displayName = 'TextInput'; diff --git a/src/components/TextInput/index.tsx b/src/components/TextInput/index.tsx index aff41d67cf88..6e09fb327bf3 100644 --- a/src/components/TextInput/index.tsx +++ b/src/components/TextInput/index.tsx @@ -1,6 +1,5 @@ -import React, {Component, useEffect, useRef} from 'react'; -import {StyleProp, TextInputProps, ViewStyle} from 'react-native'; -import {AnimatedProps} from 'react-native-reanimated'; +import React, {useEffect, useRef} from 'react'; +import {StyleProp, ViewStyle} from 'react-native'; import * as Browser from '@libs/Browser'; import DomUtils from '@libs/DomUtils'; import Visibility from '@libs/Visibility'; @@ -12,22 +11,19 @@ import * as styleConst from './styleConst'; type RemoveVisibilityListener = () => void; -function TextInput( - {label = '', name = '', textInputContainerStyles, inputStyle, disableKeyboard = false, multiline = false, prefixCharacter, inputID, ...props}: BaseTextInputProps, - ref: BaseTextInputRef, -) { +function TextInput(props: BaseTextInputProps, ref: BaseTextInputRef) { const styles = useThemeStyles(); - const textInputRef = useRef(null); + const textInputRef = useRef(null); const removeVisibilityListenerRef = useRef(null); useEffect(() => { let removeVisibilityListener = removeVisibilityListenerRef.current; - if (disableKeyboard) { + if (props.disableKeyboard) { textInputRef.current?.setAttribute('inputmode', 'none'); } - if (name) { - textInputRef.current?.setAttribute('name', name); + if (props.name) { + textInputRef.current?.setAttribute('name', props.name); } removeVisibilityListener = Visibility.onVisibilityChange(() => { @@ -47,7 +43,7 @@ function TextInput( // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - const isLabeledMultiline = Boolean(label?.length) && multiline; + const isLabeledMultiline = Boolean(props.label?.length) && props.multiline; const labelAnimationStyle = { // eslint-disable-next-line @typescript-eslint/naming-convention '--active-label-translate-y': `${styleConst.ACTIVE_LABEL_TRANSLATE_Y}px`, @@ -62,9 +58,8 @@ function TextInput( // eslint-disable-next-line react/jsx-props-no-spreading {...props} ref={(element) => { - if (element) { - (textInputRef.current as HTMLElement | Component>) = element; - } + textInputRef.current = element as HTMLElement; + if (!ref) { return; } @@ -77,8 +72,8 @@ function TextInput( // eslint-disable-next-line no-param-reassign ref.current = element; }} - inputStyle={[styles.baseTextInput, styles.textInputDesktop, isLabeledMultiline ? styles.textInputMultiline : {}, inputStyle]} - textInputContainerStyles={[labelAnimationStyle as StyleProp, textInputContainerStyles]} + inputStyle={[styles.baseTextInput, styles.textInputDesktop, isLabeledMultiline ? styles.textInputMultiline : {}, props.inputStyle]} + textInputContainerStyles={[labelAnimationStyle as StyleProp, props.textInputContainerStyles]} /> ); } From b4597cbe4a921ce6349429a94685292751f1f177 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Mon, 11 Dec 2023 16:13:35 +0100 Subject: [PATCH 16/26] fix: import types --- .../TextInput/BaseTextInput/index.native.tsx | 18 +++--------------- .../TextInput/BaseTextInput/index.tsx | 16 ++-------------- .../TextInput/TextInputLabel/index.native.tsx | 2 +- .../TextInput/TextInputLabel/index.tsx | 2 +- src/components/TextInput/index.native.tsx | 5 +++-- src/components/TextInput/index.tsx | 2 +- 6 files changed, 11 insertions(+), 34 deletions(-) diff --git a/src/components/TextInput/BaseTextInput/index.native.tsx b/src/components/TextInput/BaseTextInput/index.native.tsx index b6db50dfea29..c0ed78922727 100644 --- a/src/components/TextInput/BaseTextInput/index.native.tsx +++ b/src/components/TextInput/BaseTextInput/index.native.tsx @@ -1,25 +1,13 @@ import Str from 'expensify-common/lib/str'; import React, {forwardRef, useCallback, useEffect, useRef, useState} from 'react'; -import { - ActivityIndicator, - Animated, - FlexStyle, - type GestureResponderEvent, - type LayoutChangeEvent, - type NativeSyntheticEvent, - StyleProp, - StyleSheet, - TextInput, - TextInputFocusEventData, - View, - ViewStyle, -} from 'react-native'; +import {ActivityIndicator, Animated, StyleSheet, TextInput, View} from 'react-native'; +import type {FlexStyle, GestureResponderEvent, LayoutChangeEvent, NativeSyntheticEvent, StyleProp, TextInputFocusEventData, ViewStyle} from 'react-native'; import Checkbox from '@components/Checkbox'; import FormHelpMessage from '@components/FormHelpMessage'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; -import RNTextInput, {AnimatedTextInputRef} from '@components/RNTextInput'; +import RNTextInput, {type AnimatedTextInputRef} from '@components/RNTextInput'; import SwipeInterceptPanResponder from '@components/SwipeInterceptPanResponder'; import Text from '@components/Text'; import * as styleConst from '@components/TextInput/styleConst'; diff --git a/src/components/TextInput/BaseTextInput/index.tsx b/src/components/TextInput/BaseTextInput/index.tsx index 93c764c330bc..9d42d3b1be78 100644 --- a/src/components/TextInput/BaseTextInput/index.tsx +++ b/src/components/TextInput/BaseTextInput/index.tsx @@ -1,19 +1,7 @@ import Str from 'expensify-common/lib/str'; import React, {forwardRef, useCallback, useEffect, useMemo, useRef, useState} from 'react'; -import { - ActivityIndicator, - Animated, - FlexStyle, - GestureResponderEvent, - LayoutChangeEvent, - NativeSyntheticEvent, - StyleProp, - StyleSheet, - TextInput, - TextInputFocusEventData, - View, - ViewStyle, -} from 'react-native'; +import {ActivityIndicator, Animated, StyleSheet, TextInput, View} from 'react-native'; +import type {FlexStyle, GestureResponderEvent, LayoutChangeEvent, NativeSyntheticEvent, StyleProp, TextInputFocusEventData, ViewStyle} from 'react-native'; import Checkbox from '@components/Checkbox'; import FormHelpMessage from '@components/FormHelpMessage'; import Icon from '@components/Icon'; diff --git a/src/components/TextInput/TextInputLabel/index.native.tsx b/src/components/TextInput/TextInputLabel/index.native.tsx index 701ac6992d7d..c9ca3dbda7a8 100644 --- a/src/components/TextInput/TextInputLabel/index.native.tsx +++ b/src/components/TextInput/TextInputLabel/index.native.tsx @@ -2,7 +2,7 @@ import React, {useState} from 'react'; import {Animated} from 'react-native'; import * as styleConst from '@components/TextInput/styleConst'; import useThemeStyles from '@styles/useThemeStyles'; -import TextInputLabelProps from './types'; +import type TextInputLabelProps from './types'; function TextInputLabel({isLabelActive, label, labelScale, labelTranslateY}: TextInputLabelProps) { const styles = useThemeStyles(); diff --git a/src/components/TextInput/TextInputLabel/index.tsx b/src/components/TextInput/TextInputLabel/index.tsx index 086218eb38d0..3a441e6ecaa0 100644 --- a/src/components/TextInput/TextInputLabel/index.tsx +++ b/src/components/TextInput/TextInputLabel/index.tsx @@ -2,7 +2,7 @@ import React, {useEffect, useRef} from 'react'; import {Animated, Text} from 'react-native'; import useThemeStyles from '@styles/useThemeStyles'; import CONST from '@src/CONST'; -import TextInputLabelProps from './types'; +import type TextInputLabelProps from './types'; function TextInputLabel({for: inputId = '', label, labelTranslateY, labelScale}: TextInputLabelProps) { const styles = useThemeStyles(); diff --git a/src/components/TextInput/index.native.tsx b/src/components/TextInput/index.native.tsx index 58d562caa5cd..2041f33a641a 100644 --- a/src/components/TextInput/index.native.tsx +++ b/src/components/TextInput/index.native.tsx @@ -2,7 +2,8 @@ import React, {forwardRef, useEffect} from 'react'; import {AppState, Keyboard} from 'react-native'; import useThemeStyles from '@styles/useThemeStyles'; import BaseTextInput from './BaseTextInput'; -import BaseTextInputProps, {BaseTextInputRef} from './BaseTextInput/types'; +import type BaseTextInputProps from './BaseTextInput/types'; +import type {BaseTextInputRef} from './BaseTextInput/types'; function TextInput(props: BaseTextInputProps, ref: BaseTextInputRef) { const styles = useThemeStyles(); @@ -27,12 +28,12 @@ function TextInput(props: BaseTextInputProps, ref: BaseTextInputRef) { return ( ); diff --git a/src/components/TextInput/index.tsx b/src/components/TextInput/index.tsx index 6e09fb327bf3..94640debf3d4 100644 --- a/src/components/TextInput/index.tsx +++ b/src/components/TextInput/index.tsx @@ -1,5 +1,5 @@ import React, {useEffect, useRef} from 'react'; -import {StyleProp, ViewStyle} from 'react-native'; +import type {StyleProp, ViewStyle} from 'react-native'; import * as Browser from '@libs/Browser'; import DomUtils from '@libs/DomUtils'; import Visibility from '@libs/Visibility'; From 3abace7c03af33716326c87eb2953d6f59cb5b04 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Mon, 11 Dec 2023 16:15:40 +0100 Subject: [PATCH 17/26] fix: import types --- src/components/TextInput/BaseTextInput/types.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/components/TextInput/BaseTextInput/types.ts b/src/components/TextInput/BaseTextInput/types.ts index 0430644b0357..c71be6dce94e 100644 --- a/src/components/TextInput/BaseTextInput/types.ts +++ b/src/components/TextInput/BaseTextInput/types.ts @@ -1,8 +1,9 @@ -import React, {Component, ForwardedRef} from 'react'; -import {FlexStyle, GestureResponderEvent, StyleProp, TextInputProps, TextStyle, ViewStyle} from 'react-native'; -import {AnimatedProps} from 'react-native-reanimated'; -import {SrcProps} from '@components/Icon'; -import {MaybePhraseKey} from '@libs/Localize'; +import React from 'react'; +import type {Component, ForwardedRef} from 'react'; +import type {FlexStyle, GestureResponderEvent, StyleProp, TextInputProps, TextStyle, ViewStyle} from 'react-native'; +import type {AnimatedProps} from 'react-native-reanimated'; +import type {SrcProps} from '@components/Icon'; +import type {MaybePhraseKey} from '@libs/Localize'; type CustomBaseTextInputProps = { /** Input label */ From cba69d03b7cd37e95affc751d3b3e44408212824 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Tue, 12 Dec 2023 10:56:08 +0100 Subject: [PATCH 18/26] fix: resolve comments --- src/components/TextInput/BaseTextInput/types.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/TextInput/BaseTextInput/types.ts b/src/components/TextInput/BaseTextInput/types.ts index c71be6dce94e..38341c721928 100644 --- a/src/components/TextInput/BaseTextInput/types.ts +++ b/src/components/TextInput/BaseTextInput/types.ts @@ -1,6 +1,6 @@ import React from 'react'; import type {Component, ForwardedRef} from 'react'; -import type {FlexStyle, GestureResponderEvent, StyleProp, TextInputProps, TextStyle, ViewStyle} from 'react-native'; +import type {GestureResponderEvent, StyleProp, TextInputProps, TextStyle, ViewStyle} from 'react-native'; import type {AnimatedProps} from 'react-native-reanimated'; import type {SrcProps} from '@components/Icon'; import type {MaybePhraseKey} from '@libs/Localize'; @@ -22,19 +22,19 @@ type CustomBaseTextInputProps = { placeholder?: string; /** Error text to display */ - errorText: MaybePhraseKey; + errorText?: MaybePhraseKey; /** Icon to display in right side of text input */ icon: ((props: SrcProps) => React.ReactNode) | null; /** Customize the TextInput container */ - textInputContainerStyles: StyleProp; + textInputContainerStyles?: StyleProp; /** Customize the main container */ - containerStyles: StyleProp; + containerStyles?: StyleProp; /** input style */ - inputStyle: StyleProp; + inputStyle?: StyleProp; /** If present, this prop forces the label to remain in a position where it will not collide with input text */ forceActiveLabel?: boolean; From 886aa60d76d7a07710294585c3bd20b9ab9c8c24 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Tue, 12 Dec 2023 14:10:56 +0100 Subject: [PATCH 19/26] fix: resolved comments --- src/components/RNTextInput.tsx | 1 - .../TextInput/BaseTextInput/index.native.tsx | 49 +++++++------- .../TextInput/BaseTextInput/index.tsx | 65 ++++++++++--------- .../TextInput/BaseTextInput/types.ts | 2 +- src/libs/isInputAutoFilled.ts | 3 +- 5 files changed, 61 insertions(+), 59 deletions(-) diff --git a/src/components/RNTextInput.tsx b/src/components/RNTextInput.tsx index 5c3a65998e25..db75dc936589 100644 --- a/src/components/RNTextInput.tsx +++ b/src/components/RNTextInput.tsx @@ -7,7 +7,6 @@ type AnimatedTextInputRef = Component>; // Convert the underlying TextInput into an Animated component so that we can take an animated ref and pass it to a worklet const AnimatedTextInput = Animated.createAnimatedComponent(TextInput); -// eslint-disable-next-line @typescript-eslint/no-explicit-any function RNTextInputWithRef(props: TextInputProps, ref: ForwardedRef>>) { return ( {}, shouldDelayFocus = false, @@ -49,17 +53,20 @@ function BaseTextInput( multiline = false, shouldInterceptSwipe = false, autoCorrect = true, - prefixCharacter, + prefixCharacter = '', inputID, - ...inputProps + ...props }: BaseTextInputProps, ref: BaseTextInputRef, ) { + const inputProps = {shouldSaveDraft: false, shouldUseDefaultValue: false, ...props}; const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const {translate} = useLocalize(); + const {hasError = false} = inputProps; + // Disabling this line for saftiness as nullish coalescing works only if value is undefined or null // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const initialValue = value || defaultValue || ''; const initialActiveLabel = !!forceActiveLabel || initialValue.length > 0 || !!prefixCharacter; @@ -73,8 +80,7 @@ function BaseTextInput( const [width, setWidth] = useState(null); const labelScale = useRef(new Animated.Value(initialActiveLabel ? styleConst.ACTIVE_LABEL_SCALE : styleConst.INACTIVE_LABEL_SCALE)).current; const labelTranslateY = useRef(new Animated.Value(initialActiveLabel ? styleConst.ACTIVE_LABEL_TRANSLATE_Y : styleConst.INACTIVE_LABEL_TRANSLATE_Y)).current; - - const input = useRef(null); + const input = useRef(null); const isLabelActive = useRef(initialActiveLabel); // AutoFocus which only works on mount: @@ -132,16 +138,12 @@ function BaseTextInput( }, [animateLabel, forceActiveLabel, prefixCharacter, value]); const onFocus = (event: NativeSyntheticEvent) => { - if (inputProps.onFocus) { - inputProps.onFocus(event); - } + inputProps.onFocus?.(event); setIsFocused(true); }; const onBlur = (event: NativeSyntheticEvent) => { - if (inputProps.onBlur) { - inputProps.onBlur(event); - } + inputProps.onBlur?.(event); setIsFocused(false); }; @@ -150,9 +152,7 @@ function BaseTextInput( return; } - if (inputProps.onPress) { - inputProps.onPress(event); - } + inputProps.onPress?.(event); if ('isDefaultPrevented' in event && !event?.isDefaultPrevented()) { input.current?.focus(); @@ -186,7 +186,7 @@ function BaseTextInput( isFocused || // If the text has been supplied by Chrome autofill, the value state is not synced with the value // as Chrome doesn't trigger a change event. When there is autofill text, keep the label activated. - isInputAutoFilled(input.current as unknown as Element) + isInputAutoFilled(input.current) ) { activateLabel(); } else { @@ -209,9 +209,7 @@ function BaseTextInput( const setValue = (newValue: string) => { const formattedValue = isMultiline ? newValue : newValue.replace(/\n/g, ' '); - if (onInputChange) { - onInputChange(formattedValue); - } + onInputChange?.(formattedValue); if (inputProps.onChangeText) { Str.result(inputProps.onChangeText, formattedValue); @@ -219,7 +217,7 @@ function BaseTextInput( if (formattedValue && formattedValue.length > 0) { hasValueRef.current = true; - // When the componment is uncontrolled, we need to manually activate the label: + // When the component is uncontrolled, we need to manually activate the label: if (value === undefined) { activateLabel(); } @@ -252,7 +250,7 @@ function BaseTextInput( const inputHelpText = errorText || hint; const placeholderValue = !!prefixCharacter || isFocused || !hasLabel || (hasLabel && forceActiveLabel) ? placeholder : undefined; const maxHeight = StyleSheet.flatten(containerStyles)?.maxHeight; - const newTextInputContainerStyles: StyleProp = StyleSheet.flatten([ + const newTextInputContainerStyles: StyleProp = StyleSheet.flatten([ styles.textInputContainer, textInputContainerStyles, autoGrow && StyleUtils.getWidthStyle(textInputWidth), @@ -271,8 +269,7 @@ function BaseTextInput( } {}, shouldDelayFocus = false, @@ -49,8 +53,10 @@ function BaseTextInput( multiline = false, shouldInterceptSwipe = false, autoCorrect = true, - prefixCharacter, + prefixCharacter = '', inputID, + shouldSaveDraft = false, + shouldUseDefaultValue = false, ...inputProps }: BaseTextInputProps, ref: BaseTextInputRef, @@ -60,6 +66,8 @@ function BaseTextInput( const {hasError = false} = inputProps; const StyleUtils = useStyleUtils(); const {translate} = useLocalize(); + + // Disabling this line for saftiness as nullish coalescing works only if value is undefined or null // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const initialValue = value || defaultValue || ''; const initialActiveLabel = !!forceActiveLabel || initialValue.length > 0 || !!prefixCharacter; @@ -72,8 +80,7 @@ function BaseTextInput( const [width, setWidth] = useState(null); const labelScale = useRef(new Animated.Value(initialActiveLabel ? styleConst.ACTIVE_LABEL_SCALE : styleConst.INACTIVE_LABEL_SCALE)).current; const labelTranslateY = useRef(new Animated.Value(initialActiveLabel ? styleConst.ACTIVE_LABEL_TRANSLATE_Y : styleConst.INACTIVE_LABEL_TRANSLATE_Y)).current; - - const input = useRef(null); + const input = useRef(null); const isLabelActive = useRef(initialActiveLabel); // AutoFocus which only works on mount: @@ -109,7 +116,9 @@ function BaseTextInput( ); const activateLabel = useCallback(() => { - const newValue = value ?? ''; + // Disabling this line for saftiness as nullish coalescing works only if value is undefined or null + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + const newValue = value || ''; if (newValue.length < 0 || isLabelActive.current) { return; @@ -120,7 +129,9 @@ function BaseTextInput( }, [animateLabel, value]); const deactivateLabel = useCallback(() => { - const newValue = value ?? ''; + // Disabling this line for saftiness as nullish coalescing works only if value is undefined or null + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + const newValue = value || ''; if (!!forceActiveLabel || newValue.length !== 0 || prefixCharacter) { return; @@ -131,16 +142,12 @@ function BaseTextInput( }, [animateLabel, forceActiveLabel, prefixCharacter, value]); const onFocus = (event: NativeSyntheticEvent) => { - if (inputProps.onFocus) { - onFocus(event); - } + inputProps.onFocus?.(event); setIsFocused(true); }; const onBlur = (event: NativeSyntheticEvent) => { - if (inputProps.onBlur) { - inputProps.onBlur(event); - } + inputProps.onBlur?.(event); setIsFocused(false); }; @@ -149,9 +156,7 @@ function BaseTextInput( return; } - if (inputProps.onPress) { - inputProps.onPress(event); - } + inputProps.onPress?.(event); if ('isDefaultPrevented' in event && !event?.isDefaultPrevented()) { input.current?.focus(); @@ -174,7 +179,9 @@ function BaseTextInput( // The ref is needed when the component is uncontrolled and we don't have a value prop const hasValueRef = useRef(initialValue.length > 0); - const inputValue = value ?? ''; + // Disabling this line for saftiness as nullish coalescing works only if value is undefined or null + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + const inputValue = value || ''; const hasValue = inputValue.length > 0 || hasValueRef.current; // Activate or deactivate the label when either focus changes, or for controlled @@ -185,7 +192,7 @@ function BaseTextInput( isFocused || // If the text has been supplied by Chrome autofill, the value state is not synced with the value // as Chrome doesn't trigger a change event. When there is autofill text, keep the label activated. - isInputAutoFilled(input.current as unknown as Element) + isInputAutoFilled(input.current) ) { activateLabel(); } else { @@ -206,9 +213,8 @@ function BaseTextInput( * Set Value & activateLabel */ const setValue = (newValue: string) => { - if (onInputChange) { - onInputChange(newValue); - } + onInputChange?.(newValue); + if (inputProps.onChangeText) { Str.result(inputProps.onChangeText, newValue); } @@ -247,7 +253,7 @@ function BaseTextInput( const inputHelpText = errorText || hint; const newPlaceholder = !!prefixCharacter || isFocused || !hasLabel || (hasLabel && forceActiveLabel) ? placeholder : undefined; const maxHeight = StyleSheet.flatten(containerStyles).maxHeight; - const newTextInputContainerStyles: StyleProp = StyleSheet.flatten([ + const newTextInputContainerStyles: StyleProp = StyleSheet.flatten([ styles.textInputContainer, textInputContainerStyles, autoGrow && StyleUtils.getWidthStyle(textInputWidth), @@ -265,10 +271,10 @@ function BaseTextInput( See https://github.com/Expensify/App/issues/13802 */ const lineHeight = useMemo(() => { - if ((Browser.isSafari() || Browser.isMobileChrome()) && Array.isArray(inputStyle)) { - const lineHeightValue = inputStyle?.find((style) => style && 'lineHeight' in style && style.lineHeight !== undefined); - if (lineHeightValue && 'lineHeight' in lineHeightValue) { - return lineHeightValue.lineHeight; + if (Browser.isSafari() || Browser.isMobileChrome()) { + const lineHeightValue = StyleSheet.flatten(inputStyle).lineHeight; + if (lineHeightValue !== undefined) { + return lineHeightValue; } } @@ -285,8 +291,7 @@ function BaseTextInput( } Date: Wed, 13 Dec 2023 10:49:31 +0100 Subject: [PATCH 20/26] fix: fixed default values for props --- src/components/TextInput/BaseTextInput/index.native.tsx | 2 +- src/components/TextInput/BaseTextInput/index.tsx | 6 ++---- src/components/TextInput/BaseTextInput/types.ts | 3 --- 3 files changed, 3 insertions(+), 8 deletions(-) diff --git a/src/components/TextInput/BaseTextInput/index.native.tsx b/src/components/TextInput/BaseTextInput/index.native.tsx index e7160b8d631f..49788a4646af 100644 --- a/src/components/TextInput/BaseTextInput/index.native.tsx +++ b/src/components/TextInput/BaseTextInput/index.native.tsx @@ -45,7 +45,7 @@ function BaseTextInput( autoGrow = false, autoGrowHeight = false, hideFocusedState = false, - maxLength = null, + maxLength = undefined, hint = '', onInputChange = () => {}, shouldDelayFocus = false, diff --git a/src/components/TextInput/BaseTextInput/index.tsx b/src/components/TextInput/BaseTextInput/index.tsx index 1e997018b537..cf8c58160c96 100644 --- a/src/components/TextInput/BaseTextInput/index.tsx +++ b/src/components/TextInput/BaseTextInput/index.tsx @@ -1,7 +1,7 @@ import Str from 'expensify-common/lib/str'; import React, {forwardRef, useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {ActivityIndicator, Animated, StyleSheet, TextInput, View} from 'react-native'; -import type {FlexStyle, GestureResponderEvent, LayoutChangeEvent, NativeSyntheticEvent, StyleProp, TextInputFocusEventData, ViewStyle} from 'react-native'; +import type {GestureResponderEvent, LayoutChangeEvent, NativeSyntheticEvent, StyleProp, TextInputFocusEventData, ViewStyle} from 'react-native'; import Checkbox from '@components/Checkbox'; import FormHelpMessage from '@components/FormHelpMessage'; import Icon from '@components/Icon'; @@ -45,7 +45,7 @@ function BaseTextInput( autoGrow = false, autoGrowHeight = false, hideFocusedState = false, - maxLength = null, + maxLength = undefined, hint = '', onInputChange = () => {}, shouldDelayFocus = false, @@ -55,8 +55,6 @@ function BaseTextInput( autoCorrect = true, prefixCharacter = '', inputID, - shouldSaveDraft = false, - shouldUseDefaultValue = false, ...inputProps }: BaseTextInputProps, ref: BaseTextInputRef, diff --git a/src/components/TextInput/BaseTextInput/types.ts b/src/components/TextInput/BaseTextInput/types.ts index ee3574ee6d04..33addec32784 100644 --- a/src/components/TextInput/BaseTextInput/types.ts +++ b/src/components/TextInput/BaseTextInput/types.ts @@ -62,9 +62,6 @@ type CustomBaseTextInputProps = { /** Hide the focus styles on TextInput */ hideFocusedState?: boolean; - /** Maximum characters allowed */ - maxLength?: number | null; - /** Hint text to display below the TextInput */ hint?: string; From d69e08774e3fe2bf9eb4802c7277ed555f401012 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Wed, 13 Dec 2023 13:20:42 +0100 Subject: [PATCH 21/26] fix: adress comments --- .../TextInput/BaseTextInput/index.native.tsx | 11 +++++------ src/components/TextInput/BaseTextInput/index.tsx | 15 ++++----------- src/components/TextInput/BaseTextInput/types.ts | 3 +-- src/components/TextInput/index.tsx | 3 +-- src/libs/isInputAutoFilled.ts | 4 ++-- 5 files changed, 13 insertions(+), 23 deletions(-) diff --git a/src/components/TextInput/BaseTextInput/index.native.tsx b/src/components/TextInput/BaseTextInput/index.native.tsx index 49788a4646af..671b3fdbad5f 100644 --- a/src/components/TextInput/BaseTextInput/index.native.tsx +++ b/src/components/TextInput/BaseTextInput/index.native.tsx @@ -21,8 +21,7 @@ import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import variables from '@styles/variables'; import CONST from '@src/CONST'; -import type BaseTextInputProps from './types'; -import type {BaseTextInputRef} from './types'; +import type {BaseTextInputProps, BaseTextInputRef} from './types'; function BaseTextInput( { @@ -66,7 +65,7 @@ function BaseTextInput( const {translate} = useLocalize(); const {hasError = false} = inputProps; - // Disabling this line for saftiness as nullish coalescing works only if value is undefined or null + // Disabling this line for safeness as nullish coalescing works only if the value is undefined or null // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const initialValue = value || defaultValue || ''; const initialActiveLabel = !!forceActiveLabel || initialValue.length > 0 || !!prefixCharacter; @@ -80,7 +79,7 @@ function BaseTextInput( const [width, setWidth] = useState(null); const labelScale = useRef(new Animated.Value(initialActiveLabel ? styleConst.ACTIVE_LABEL_SCALE : styleConst.INACTIVE_LABEL_SCALE)).current; const labelTranslateY = useRef(new Animated.Value(initialActiveLabel ? styleConst.ACTIVE_LABEL_TRANSLATE_Y : styleConst.INACTIVE_LABEL_TRANSLATE_Y)).current; - const input = useRef(null); + const input = useRef(null); const isLabelActive = useRef(initialActiveLabel); // AutoFocus which only works on mount: @@ -377,8 +376,8 @@ function BaseTextInput( { - e.preventDefault(); + onMouseDown={(event) => { + event.preventDefault(); }} accessibilityLabel={translate?.('common.visible') ?? ''} > diff --git a/src/components/TextInput/BaseTextInput/index.tsx b/src/components/TextInput/BaseTextInput/index.tsx index cf8c58160c96..b2eae4887baf 100644 --- a/src/components/TextInput/BaseTextInput/index.tsx +++ b/src/components/TextInput/BaseTextInput/index.tsx @@ -21,8 +21,7 @@ import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import variables from '@styles/variables'; import CONST from '@src/CONST'; -import type BaseTextInputProps from './types'; -import type {BaseTextInputRef} from './types'; +import type {BaseTextInputProps, BaseTextInputRef} from './types'; function BaseTextInput( { @@ -114,9 +113,7 @@ function BaseTextInput( ); const activateLabel = useCallback(() => { - // Disabling this line for saftiness as nullish coalescing works only if value is undefined or null - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - const newValue = value || ''; + const newValue = value ?? ''; if (newValue.length < 0 || isLabelActive.current) { return; @@ -127,9 +124,7 @@ function BaseTextInput( }, [animateLabel, value]); const deactivateLabel = useCallback(() => { - // Disabling this line for saftiness as nullish coalescing works only if value is undefined or null - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - const newValue = value || ''; + const newValue = value ?? ''; if (!!forceActiveLabel || newValue.length !== 0 || prefixCharacter) { return; @@ -177,9 +172,7 @@ function BaseTextInput( // The ref is needed when the component is uncontrolled and we don't have a value prop const hasValueRef = useRef(initialValue.length > 0); - // Disabling this line for saftiness as nullish coalescing works only if value is undefined or null - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - const inputValue = value || ''; + const inputValue = value ?? ''; const hasValue = inputValue.length > 0 || hasValueRef.current; // Activate or deactivate the label when either focus changes, or for controlled diff --git a/src/components/TextInput/BaseTextInput/types.ts b/src/components/TextInput/BaseTextInput/types.ts index 33addec32784..37dcb0de8f31 100644 --- a/src/components/TextInput/BaseTextInput/types.ts +++ b/src/components/TextInput/BaseTextInput/types.ts @@ -113,5 +113,4 @@ type BaseTextInputRef = ForwardedRef void; diff --git a/src/libs/isInputAutoFilled.ts b/src/libs/isInputAutoFilled.ts index f74b04108cd9..fbe6240def47 100644 --- a/src/libs/isInputAutoFilled.ts +++ b/src/libs/isInputAutoFilled.ts @@ -4,8 +4,8 @@ import isSelectorSupported from './isSelectorSupported'; /** * Check the input is auto filled or not */ -export default function isInputAutoFilled(input: (TextInput & HTMLElement) | null): boolean { - if (!input?.matches) { +export default function isInputAutoFilled(input: (TextInput | HTMLElement) | null): boolean { + if ((!!input && !('matches' in input)) || !input?.matches) { return false; } if (isSelectorSupported(':autofill')) { From fa251dfb5e427b3045790da5e78bb091f0f9641f Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Thu, 14 Dec 2023 14:04:55 +0100 Subject: [PATCH 22/26] fix: typecheck --- src/components/TextInput/BaseTextInput/index.tsx | 2 +- src/components/TextInput/index.native.tsx | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/TextInput/BaseTextInput/index.tsx b/src/components/TextInput/BaseTextInput/index.tsx index f1bf7c0e6b83..dae61d82b609 100644 --- a/src/components/TextInput/BaseTextInput/index.tsx +++ b/src/components/TextInput/BaseTextInput/index.tsx @@ -286,7 +286,7 @@ function BaseTextInput( style={[ autoGrowHeight && styles.autoGrowHeightInputContainer(textInputHeight, variables.componentSizeLarge, typeof maxHeight === 'number' ? maxHeight : 0), !isMultiline && styles.componentHeightLarge, - ...props.containerStyles, + containerStyles, ]} > Date: Fri, 15 Dec 2023 10:13:59 +0100 Subject: [PATCH 23/26] fix: address comments --- src/components/TextInput/BaseTextInput/index.native.tsx | 6 +++--- src/components/TextInput/BaseTextInput/index.tsx | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/TextInput/BaseTextInput/index.native.tsx b/src/components/TextInput/BaseTextInput/index.native.tsx index e01ca1d64697..4f909dcf6e64 100644 --- a/src/components/TextInput/BaseTextInput/index.native.tsx +++ b/src/components/TextInput/BaseTextInput/index.native.tsx @@ -301,7 +301,7 @@ function BaseTextInput( ) : null} - {Boolean(prefixCharacter) && ( + {!!prefixCharacter && ( )} - {Boolean(inputProps.secureTextEntry) && ( + {!!inputProps.secureTextEntry && ( { event.preventDefault(); }} - accessibilityLabel={translate?.('common.visible') ?? ''} + accessibilityLabel={translate('common.visible')} > { e.preventDefault(); }} - accessibilityLabel={translate?.('common.visible') ?? ''} + accessibilityLabel={translate('common.visible')} > Date: Mon, 18 Dec 2023 11:15:36 +0100 Subject: [PATCH 24/26] fix: issue with not expanding multiline textinput --- src/components/TextInput/BaseTextInput/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/TextInput/BaseTextInput/index.tsx b/src/components/TextInput/BaseTextInput/index.tsx index f2e7909ab820..a6509452b910 100644 --- a/src/components/TextInput/BaseTextInput/index.tsx +++ b/src/components/TextInput/BaseTextInput/index.tsx @@ -252,7 +252,7 @@ function BaseTextInput( (!!hasError || !!errorText) && styles.borderColorDanger, autoGrowHeight && {scrollPaddingTop: typeof maxHeight === 'number' ? 2 * maxHeight : undefined}, ]); - const isMultiline = multiline ?? autoGrowHeight; + const isMultiline = multiline || autoGrowHeight; /* To prevent text jumping caused by virtual DOM calculations on Safari and mobile Chrome, make sure to include the `lineHeight`. From ee636699a86c024370e8a1d5734f0b55ee5ea1ef Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Mon, 18 Dec 2023 11:23:42 +0100 Subject: [PATCH 25/26] fix: type issue --- src/components/Icon/index.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/Icon/index.tsx b/src/components/Icon/index.tsx index 5f82421c0e8e..f64de33aaf1a 100644 --- a/src/components/Icon/index.tsx +++ b/src/components/Icon/index.tsx @@ -101,3 +101,5 @@ function Icon({ Icon.displayName = 'Icon'; export default Icon; + +export type {SrcProps}; From b6ba4e1339dbe5d7da02f83c830171c8b95ddce3 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Tue, 19 Dec 2023 09:17:26 +0100 Subject: [PATCH 26/26] fix: error message color --- src/components/TextInput/BaseTextInput/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/TextInput/BaseTextInput/index.tsx b/src/components/TextInput/BaseTextInput/index.tsx index a6509452b910..a66df0496a1a 100644 --- a/src/components/TextInput/BaseTextInput/index.tsx +++ b/src/components/TextInput/BaseTextInput/index.tsx @@ -420,7 +420,7 @@ function BaseTextInput( {!!inputHelpText && ( )}