diff --git a/src/components/Checkbox.tsx b/src/components/Checkbox.tsx index 715603ea362e..ac18b550501d 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, {type ForwardedRef, forwardRef, type MouseEventHandler, type KeyboardEvent as ReactKeyboardEvent} from 'react'; import {GestureResponderEvent, StyleProp, View, ViewStyle} from 'react-native'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; @@ -29,7 +29,7 @@ type CheckboxProps = Partial & { 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/RNTextInput.tsx b/src/components/RNTextInput.tsx index ff812e7c799b..40dfc109ba6b 100644 --- a/src/components/RNTextInput.tsx +++ b/src/components/RNTextInput.tsx @@ -1,13 +1,13 @@ -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'; import useTheme from '@hooks/useTheme'; +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>>) { const theme = useTheme(); @@ -31,3 +31,5 @@ function RNTextInputWithRef(props: TextInputProps, ref: ForwardedRef {}, + shouldDelayFocus = false, + submitOnEnter = false, + multiline = false, + shouldInterceptSwipe = false, + autoCorrect = true, + prefixCharacter = '', + inputID, + ...props + }: BaseTextInputProps, + ref: BaseTextInputRef, +) { + const inputProps = {shouldSaveDraft: false, shouldUseDefaultValue: false, ...props}; const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); - const initialValue = props.value || props.defaultValue || ''; - const initialActiveLabel = props.forceActiveLabel || initialValue.length > 0 || Boolean(props.prefixCharacter); - const isMultiline = props.multiline || props.autoGrowHeight; + const {translate} = useLocalize(); + + const {hasError = false} = inputProps; + // 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; + const isMultiline = multiline || autoGrowHeight; 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(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: 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) { - const focusTimeout = setTimeout(() => input.current.focus(), CONST.ANIMATED_TRANSITION); + if (shouldDelayFocus) { + const focusTimeout = setTimeout(() => input.current?.focus(), CONST.ANIMATED_TRANSITION); return () => clearTimeout(focusTimeout); } input.current.focus(); @@ -60,16 +99,14 @@ function BaseTextInput(props) { }, []); const animateLabel = useCallback( - (translateY, scale) => { + (translateY: number, scale: number) => { 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(); @@ -78,72 +115,66 @@ function BaseTextInput(props) { ); 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) => { + inputProps.onFocus?.(event); setIsFocused(true); }; - const onBlur = (event) => { - if (props.onBlur) { - props.onBlur(event); - } + const onBlur = (event: NativeSyntheticEvent) => { + 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); - } + inputProps.onPress?.(event); - if (!event.isDefaultPrevented()) { - input.current.focus(); + 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 | null) => (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 @@ -165,31 +196,28 @@ 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 (value === undefined || value) { return; } hasValueRef.current = false; - }, [props.value]); + }, [value]); /** * Set Value & activateLabel - * - * @param {String} val - * @memberof BaseTextInput */ - const setValue = (val) => { - const value = isMultiline ? val : val.replace(/\n/g, ' '); + const setValue = (newValue: string) => { + const formattedValue = isMultiline ? newValue : newValue.replace(/\n/g, ' '); - if (props.onInputChange) { - props.onInputChange(value); - } + onInputChange?.(formattedValue); - Str.result(props.onChangeText, value); + if (inputProps.onChangeText) { + Str.result(inputProps.onChangeText, formattedValue); + } - if (value && value.length > 0) { + if (formattedValue && formattedValue.length > 0) { hasValueRef.current = true; - // When the component is uncontrolled, we need to manually activate the label - if (props.value === undefined) { + // When the component is uncontrolled, we need to manually activate the label: + if (value === undefined) { activateLabel(); } } else { @@ -207,7 +235,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): number => { switch (prefix) { case CONST.POLICY.ROOM_PREFIX: return 10; @@ -216,20 +244,18 @@ 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 inputHelpText = props.errorText || props.hint; - const placeholder = props.prefixCharacter || isFocused || !hasLabel || (hasLabel && props.forceActiveLabel) ? props.placeholder : null; - const maxHeight = StyleSheet.flatten(props.containerStyles).maxHeight; - const textInputContainerStyles = StyleSheet.flatten([ + 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: 2 * maxHeight}, + textInputContainerStyles, + autoGrow && StyleUtils.getWidthStyle(textInputWidth), + !hideFocusedState && isFocused && styles.borderColorFocus, + (!!hasError || !!errorText) && styles.borderColorDanger, + autoGrowHeight && {scrollPaddingTop: typeof maxHeight === 'number' ? 2 * maxHeight : undefined}, ]); return ( @@ -237,16 +263,16 @@ function BaseTextInput(props) { {hasLabel ? ( @@ -267,88 +293,93 @@ function BaseTextInput(props) { {isMultiline && } ) : null} - {Boolean(props.prefixCharacter) && ( + {!!prefixCharacter && ( - {props.prefixCharacter} + {prefixCharacter} )} { - if (typeof props.innerRef === 'function') { - props.innerRef(ref); - } else if (props.innerRef && _.has(props.innerRef, 'current')) { + ref={(element) => { + if (typeof ref === 'function') { + ref(element); + } else if (ref && 'current' in ref) { // eslint-disable-next-line no-param-reassign - props.innerRef.current = ref; + ref.current = element; } - input.current = ref; + + (input.current as AnimatedTextInputRef | null) = element; }} // eslint-disable-next-line {...inputProps} - autoCorrect={props.secureTextEntry ? false : props.autoCorrect} - placeholder={placeholder} + autoCorrect={inputProps.secureTextEntry ? false : autoCorrect} + placeholder={placeholderValue} placeholderTextColor={theme.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), styles.verticalAlignTop] : []), + ...(autoGrowHeight + ? [StyleUtils.getAutoGrowHeightInputStyle(textInputHeight, typeof maxHeight === 'number' ? maxHeight : 0), styles.verticalAlignTop] + : []), // 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) && ( + {!!inputProps.secureTextEntry && ( e.preventDefault()} - accessibilityLabel={props.translate('common.visible')} + onMouseDown={(event) => { + event.preventDefault(); + }} + accessibilityLabel={translate('common.visible')} > )} - {!props.secureTextEntry && Boolean(props.icon) && ( + {!inputProps.secureTextEntry && icon && ( @@ -367,32 +398,37 @@ function BaseTextInput(props) { - {!_.isEmpty(inputHelpText) && ( + {!!inputHelpText && ( )} {/* - Text input component doesn't support auto grow by default. - We're using a hidden text input to achieve that. - 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) && ( + Text input component doesn't support auto grow by default. + We're using a hidden text input to achieve that. + 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. + */} + {(!!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 { setTextInputWidth(e.nativeEvent.layout.width); setTextInputHeight(e.nativeEvent.layout.height); }} > {/* \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} )} @@ -400,7 +436,5 @@ function BaseTextInput(props) { } BaseTextInput.displayName = 'BaseTextInput'; -BaseTextInput.propTypes = baseTextInputPropTypes.propTypes; -BaseTextInput.defaultProps = baseTextInputPropTypes.defaultProps; -export default withLocalize(BaseTextInput); +export default forwardRef(BaseTextInput); diff --git a/src/components/TextInput/BaseTextInput/index.js b/src/components/TextInput/BaseTextInput/index.tsx similarity index 59% rename from src/components/TextInput/BaseTextInput/index.js rename to src/components/TextInput/BaseTextInput/index.tsx index 67776d6c7e91..a66df0496a1a 100644 --- a/src/components/TextInput/BaseTextInput/index.js +++ b/src/components/TextInput/BaseTextInput/index.tsx @@ -1,18 +1,18 @@ 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 React, {forwardRef, useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import {ActivityIndicator, Animated, StyleSheet, TextInput, View} 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'; 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'; import TextInputLabel from '@components/TextInput/TextInputLabel'; -import withLocalize from '@components/withLocalize'; +import useLocalize from '@hooks/useLocalize'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -21,36 +21,74 @@ import isInputAutoFilled from '@libs/isInputAutoFilled'; import useNativeDriver from '@libs/useNativeDriver'; import variables from '@styles/variables'; import CONST from '@src/CONST'; -import * as baseTextInputPropTypes from './baseTextInputPropTypes'; - -function BaseTextInput(props) { +import type {BaseTextInputProps, BaseTextInputRef} from './types'; + +function BaseTextInput( + { + label = '', + /** + * 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, + placeholder = '', + errorText = '', + icon = null, + textInputContainerStyles, + containerStyles, + inputStyle, + forceActiveLabel = false, + autoFocus = false, + disableKeyboard = false, + autoGrow = false, + autoGrowHeight = false, + hideFocusedState = false, + maxLength = undefined, + hint = '', + onInputChange = () => {}, + shouldDelayFocus = false, + submitOnEnter = false, + multiline = false, + shouldInterceptSwipe = false, + autoCorrect = true, + prefixCharacter = '', + inputID, + ...inputProps + }: BaseTextInputProps, + ref: BaseTextInputRef, +) { const theme = useTheme(); const styles = useThemeStyles(); + const {hasError = false} = inputProps; const StyleUtils = useStyleUtils(); - const initialValue = props.value || props.defaultValue || ''; - const initialActiveLabel = props.forceActiveLabel || initialValue.length > 0 || Boolean(props.prefixCharacter); + 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; 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(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: 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) { - const focusTimeout = setTimeout(() => input.current.focus(), CONST.ANIMATED_TRANSITION); + if (shouldDelayFocus) { + const focusTimeout = setTimeout(() => input?.current?.focus(), CONST.ANIMATED_TRANSITION); return () => clearTimeout(focusTimeout); } input.current.focus(); @@ -59,16 +97,14 @@ function BaseTextInput(props) { }, []); const animateLabel = useCallback( - (translateY, scale) => { + (translateY: number, scale: number) => { 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(); @@ -77,72 +113,66 @@ function BaseTextInput(props) { ); 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) => { - if (props.onFocus) { - props.onFocus(event); - } + const onFocus = (event: NativeSyntheticEvent) => { + inputProps.onFocus?.(event); setIsFocused(true); }; - const onBlur = (event) => { - if (props.onBlur) { - props.onBlur(event); - } + const onBlur = (event: NativeSyntheticEvent) => { + 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); - } + inputProps.onPress?.(event); - if (!event.isDefaultPrevented()) { - input.current.focus(); + 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 | null) => (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 @@ -164,29 +194,25 @@ 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 (value === undefined || value) { return; } hasValueRef.current = false; - }, [props.value]); + }, [value]); /** * Set Value & activateLabel - * - * @param {String} value - * @memberof BaseTextInput */ - const setValue = (value) => { - if (props.onInputChange) { - props.onInputChange(value); - } - - Str.result(props.onChangeText, value); + const setValue = (newValue: string) => { + onInputChange?.(newValue); - if (value && value.length > 0) { + if (inputProps.onChangeText) { + Str.result(inputProps.onChangeText, newValue); + } + 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 { @@ -195,7 +221,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. @@ -204,7 +230,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): number => { switch (prefix) { case CONST.POLICY.ROOM_PREFIX: return 10; @@ -213,22 +239,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 inputHelpText = props.errorText || props.hint; - const placeholder = props.prefixCharacter || isFocused || !hasLabel || (hasLabel && props.forceActiveLabel) ? props.placeholder : null; - const maxHeight = StyleSheet.flatten(props.containerStyles).maxHeight; - const textInputContainerStyles = 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 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: 2 * maxHeight}, + 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`. @@ -238,31 +262,31 @@ 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 (lineHeightValue) { - return lineHeightValue.lineHeight; + if (Browser.isSafari() || Browser.isMobileChrome()) { + const lineHeightValue = StyleSheet.flatten(inputStyle).lineHeight; + if (lineHeightValue !== undefined) { + return lineHeightValue; } } return undefined; - }, [props.inputStyle]); + }, [inputStyle]); return ( <> {hasLabel ? ( @@ -283,48 +307,49 @@ function BaseTextInput(props) { {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')) { + ref={(element) => { + if (typeof ref === 'function') { + ref(element); + } else if (ref && 'current' in ref) { // eslint-disable-next-line no-param-reassign - props.innerRef.current = ref; + ref.current = element; } - input.current = ref; + + (input.current as AnimatedTextInputRef | null) = element; }} // eslint-disable-next-line {...inputProps} - autoCorrect={props.secureTextEntry ? false : props.autoCorrect} - placeholder={placeholder} + autoCorrect={inputProps.secureTextEntry ? false : autoCorrect} + placeholder={newPlaceholder} placeholderTextColor={theme.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, // 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) @@ -335,42 +360,46 @@ function BaseTextInput(props) { !isMultiline && Browser.isMobileChrome() && {boxSizing: 'content-box', height: undefined}, // Stop scrollbar flashing when breaking lines with autoGrowHeight enabled. - ...(props.autoGrowHeight ? [StyleUtils.getAutoGrowHeightInputStyle(textInputHeight, maxHeight), styles.verticalAlignTop] : []), + ...(autoGrowHeight + ? [StyleUtils.getAutoGrowHeightInputStyle(textInputHeight, typeof maxHeight === 'number' ? maxHeight : 0), styles.verticalAlignTop] + : []), // 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')} + onMouseDown={(e) => { + e.preventDefault(); + }} + accessibilityLabel={translate('common.visible')} > )} - {!props.secureTextEntry && Boolean(props.icon) && ( + {!inputProps.secureTextEntry && icon && ( @@ -389,25 +418,30 @@ function BaseTextInput(props) { - {!_.isEmpty(inputHelpText) && ( + {!!inputHelpText && ( )} {/* - Text input component doesn't support auto grow by default. - We're using a hidden text input to achieve that. - 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) && ( + Text input component doesn't support auto grow by default. + We're using a hidden text input to achieve that. + 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. + */} + {(!!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 { let additionalWidth = 0; if (Browser.isMobileSafari() || Browser.isSafari()) { @@ -418,7 +452,7 @@ function BaseTextInput(props) { }} > {/* \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} )} @@ -426,7 +460,5 @@ function BaseTextInput(props) { } BaseTextInput.displayName = 'BaseTextInput'; -BaseTextInput.propTypes = baseTextInputPropTypes.propTypes; -BaseTextInput.defaultProps = baseTextInputPropTypes.defaultProps; -export default withLocalize(BaseTextInput); +export default forwardRef(BaseTextInput); diff --git a/src/components/TextInput/BaseTextInput/types.ts b/src/components/TextInput/BaseTextInput/types.ts new file mode 100644 index 000000000000..37dcb0de8f31 --- /dev/null +++ b/src/components/TextInput/BaseTextInput/types.ts @@ -0,0 +1,116 @@ +import React from 'react'; +import type {Component, ForwardedRef} from 'react'; +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'; + +type CustomBaseTextInputProps = { + /** 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?: MaybePhraseKey; + + /** Icon to display in right side of text input */ + icon: ((props: SrcProps) => React.ReactNode) | null; + + /** 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; + + /** 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; + + /** 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; +}; + +type BaseTextInputRef = ForwardedRef>>; + +type BaseTextInputProps = CustomBaseTextInputProps & TextInputProps; + +export type {CustomBaseTextInputProps, BaseTextInputRef, 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 eb0f8b17e8b7..569d590dbb8d 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 useThemeStyles from '@hooks/useThemeStyles'; -import * as TextInputLabelPropTypes from './TextInputLabelPropTypes'; +import type TextInputLabelProps from './types'; -function TextInputLabel(props) { +function TextInputLabel({isLabelActive, label, labelScale, labelTranslateY}: TextInputLabelProps) { const styles = useThemeStyles(); const [width, setWidth] = useState(0); @@ -17,29 +17,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 73% rename from src/components/TextInput/TextInputLabel/index.js rename to src/components/TextInput/TextInputLabel/index.tsx index 61af88fe317b..628de2970331 100644 --- a/src/components/TextInput/TextInputLabel/index.js +++ b/src/components/TextInput/TextInputLabel/index.tsx @@ -1,12 +1,12 @@ import React, {useEffect, useRef} from 'react'; -import {Animated} from 'react-native'; +import {Animated, Text} from 'react-native'; import useThemeStyles from '@hooks/useThemeStyles'; import CONST from '@src/CONST'; -import {defaultProps, propTypes} from './TextInputLabelPropTypes'; +import type TextInputLabelProps from './types'; -function TextInputLabel({for: inputId, label, labelTranslateY, labelScale}) { +function TextInputLabel({for: inputId = '', label, labelTranslateY, labelScale}: TextInputLabelProps) { const styles = useThemeStyles(); - const labelRef = useRef(null); + const labelRef = useRef(null); useEffect(() => { if (!inputId || !labelRef.current) { @@ -28,7 +28,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; diff --git a/src/components/TextInput/index.native.js b/src/components/TextInput/index.native.tsx similarity index 75% rename from src/components/TextInput/index.native.js rename to src/components/TextInput/index.native.tsx index e5aba76957ad..656f0657dd26 100644 --- a/src/components/TextInput/index.native.js +++ b/src/components/TextInput/index.native.tsx @@ -2,10 +2,11 @@ import React, {forwardRef, useEffect} from 'react'; import {AppState, Keyboard} from 'react-native'; import useThemeStyles from '@hooks/useThemeStyles'; import BaseTextInput from './BaseTextInput'; -import * as baseTextInputPropTypes from './BaseTextInput/baseTextInputPropTypes'; +import type {BaseTextInputProps, BaseTextInputRef} from './BaseTextInput/types'; -const TextInput = forwardRef((props, ref) => { +function TextInput(props: BaseTextInputProps, ref: BaseTextInputRef) { const styles = useThemeStyles(); + useEffect(() => { if (!props.disableKeyboard) { return; @@ -30,15 +31,13 @@ const TextInput = forwardRef((props, ref) => { {...props} // 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 + ref={ref} autoCompleteType={props.autoCompleteType === 'new-password' ? 'password' : props.autoCompleteType} - innerRef={ref} - inputStyle={[styles.baseTextInput, ...props.inputStyle]} + inputStyle={[styles.baseTextInput, props.inputStyle]} /> ); -}); +} -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.js b/src/components/TextInput/index.tsx similarity index 50% rename from src/components/TextInput/index.js rename to src/components/TextInput/index.tsx index 87db18754ed8..3043edbd26a5 100644 --- a/src/components/TextInput/index.js +++ b/src/components/TextInput/index.tsx @@ -1,28 +1,31 @@ import React, {useEffect, useRef} from 'react'; -import _ from 'underscore'; +import type {StyleProp, ViewStyle} from 'react-native'; import useThemeStyles from '@hooks/useThemeStyles'; import * as Browser from '@libs/Browser'; import DomUtils from '@libs/DomUtils'; import Visibility from '@libs/Visibility'; import BaseTextInput from './BaseTextInput'; -import * as baseTextInputPropTypes from './BaseTextInput/baseTextInputPropTypes'; +import type {BaseTextInputProps, BaseTextInputRef} from './BaseTextInput/types'; import * as styleConst from './styleConst'; -function TextInput(props) { +type RemoveVisibilityListener = () => void; + +function TextInput(props: BaseTextInputProps, ref: BaseTextInputRef) { const styles = useThemeStyles(); - const textInputRef = useRef(null); - const removeVisibilityListenerRef = useRef(null); + const textInputRef = useRef(null); + const removeVisibilityListenerRef = useRef(null); useEffect(() => { + let removeVisibilityListener = removeVisibilityListenerRef.current; 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); } - removeVisibilityListenerRef.current = Visibility.onVisibilityChange(() => { + removeVisibilityListener = Visibility.onVisibilityChange(() => { if (!Browser.isMobileChrome() || !Visibility.isVisible() || !textInputRef.current || DomUtils.getActiveElement() !== textInputRef.current) { return; } @@ -31,18 +34,21 @@ function TextInput(props) { }); return () => { - if (!removeVisibilityListenerRef.current) { + if (!removeVisibilityListener) { return; } - removeVisibilityListenerRef.current(); + removeVisibilityListener(); }; // 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`, }; @@ -50,38 +56,27 @@ function TextInput(props) { { - textInputRef.current = el; - if (!props.innerRef) { + ref={(element) => { + textInputRef.current = element as HTMLElement; + + if (!ref) { return; } - if (_.isFunction(props.innerRef)) { - props.innerRef(el); + if (typeof ref === 'function') { + ref(element); return; } // eslint-disable-next-line no-param-reassign - props.innerRef.current = el; + ref.current = element; }} - 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 as StyleProp, props.textInputContainerStyles]} /> ); } 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); 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/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; diff --git a/src/libs/isInputAutoFilled.ts b/src/libs/isInputAutoFilled.ts index e1b9942b0e78..fbe6240def47 100644 --- a/src/libs/isInputAutoFilled.ts +++ b/src/libs/isInputAutoFilled.ts @@ -1,10 +1,11 @@ +import {TextInput} from 'react-native'; import isSelectorSupported from './isSelectorSupported'; /** * Check the input is auto filled or not */ -export default function isInputAutoFilled(input: Element): 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')) {