diff --git a/src/components/TextInput/BaseTextInput/implementation/index.native.tsx b/src/components/TextInput/BaseTextInput/implementation/index.native.tsx index 7ac60cb0ca05..06bfaa5c43f3 100644 --- a/src/components/TextInput/BaseTextInput/implementation/index.native.tsx +++ b/src/components/TextInput/BaseTextInput/implementation/index.native.tsx @@ -18,12 +18,14 @@ import type {BaseTextInputProps, BaseTextInputRef} from '@components/TextInput/B import * as styleConst from '@components/TextInput/styleConst'; import TextInputClearButton from '@components/TextInput/TextInputClearButton'; import TextInputLabel from '@components/TextInput/TextInputLabel'; +import TextInputMeasurement from '@components/TextInput/TextInputMeasurement'; import useHtmlPaste from '@hooks/useHtmlPaste'; import useLocalize from '@hooks/useLocalize'; import useMarkdownStyle from '@hooks/useMarkdownStyle'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; +import getPlatform from '@libs/getPlatform'; import isInputAutoFilled from '@libs/isInputAutoFilled'; import variables from '@styles/variables'; import CONST from '@src/CONST'; @@ -80,6 +82,10 @@ function BaseTextInput( }: BaseTextInputProps, ref: ForwardedRef, ) { + // For iOS, we don't need to measure the text input because it already has auto grow behavior + // See TextInputMeasurement.ios.tsx for more details + const isExternalAutoGrowMeasurement = getPlatform() !== CONST.PLATFORM.IOS && autoGrow; + const InputComponent = InputComponentMap.get(type) ?? RNTextInput; const isMarkdownEnabled = type === 'markdown'; const isAutoGrowHeightMarkdown = isMarkdownEnabled && autoGrowHeight; @@ -261,7 +267,7 @@ function BaseTextInput( styles.textInputContainer, textInputContainerStyles, !!contentWidth && StyleUtils.getWidthStyle(textInputWidth), - autoGrow && StyleUtils.getAutoGrowWidthInputContainerStyles(textInputWidth, autoGrowExtraSpace), + isExternalAutoGrowMeasurement && StyleUtils.getAutoGrowWidthInputContainerStyles(textInputWidth, autoGrowExtraSpace), !hideFocusedState && isFocused && styles.borderColorFocus, (!!hasError || !!errorText) && styles.borderColorDanger, autoGrowHeight && {scrollPaddingTop: typeof maxAutoGrowHeight === 'number' ? 2 * maxAutoGrowHeight : undefined}, @@ -273,6 +279,10 @@ function BaseTextInput( // Height fix is needed only for Text single line inputs const shouldApplyHeight = !isMultiline && !isMarkdownEnabled; + + // Fix iOS cursor jumping when entering first character using HW keyboard https://github.com/Expensify/App/pull/59078#issuecomment-2802834037 + const selection = inputProps.selection?.end === 0 && inputProps.selection?.start === 0 ? undefined : inputProps.selection; + return ( <> @@ -360,8 +370,8 @@ function BaseTextInput( placeholderTextColor={placeholderTextColor ?? theme.placeholderText} underlineColorAndroid="transparent" style={[ - styles.flex1, - styles.w100, + !autoGrow && styles.flex1, + !autoGrow && styles.w100, inputStyle, (!hasLabel || isMultiline) && styles.pv0, inputPaddingLeft, @@ -392,7 +402,7 @@ function BaseTextInput( keyboardType={inputProps.keyboardType} inputMode={!disableKeyboard ? inputProps.inputMode : CONST.INPUT_MODE.NONE} value={uncontrolled ? undefined : value} - selection={inputProps.selection} + selection={selection} readOnly={isReadOnly} defaultValue={defaultValue} markdownStyle={markdownStyle} @@ -456,55 +466,21 @@ function BaseTextInput( /> )} - {!!contentWidth && isPrefixCharacterPaddingCalculated && ( - { - if (e.nativeEvent.layout.width === 0 && e.nativeEvent.layout.height === 0) { - return; - } - 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) */} - {value ? `${value}${value.endsWith('\n') ? '\u200B' : ''}` : placeholder} - - - )} - {/* - Text input component doesn't support auto grow by default. - 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) && !isAutoGrowHeightMarkdown && ( - { - if (e.nativeEvent.layout.width === 0 && e.nativeEvent.layout.height === 0) { - return; - } - // Add +2 to width so that cursor is not cut off / covered at the end of text content - setTextInputWidth(e.nativeEvent.layout.width + 2); - 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) */} - {value ? `${value}${value.endsWith('\n') ? '\u200B' : ''}` : placeholder} - - )} + ); } diff --git a/src/components/TextInput/BaseTextInput/implementation/index.tsx b/src/components/TextInput/BaseTextInput/implementation/index.tsx index 0e3049464edc..148263d669ad 100644 --- a/src/components/TextInput/BaseTextInput/implementation/index.tsx +++ b/src/components/TextInput/BaseTextInput/implementation/index.tsx @@ -19,13 +19,14 @@ import type {BaseTextInputProps, BaseTextInputRef} from '@components/TextInput/B import {ACTIVE_LABEL_SCALE, ACTIVE_LABEL_TRANSLATE_Y, INACTIVE_LABEL_SCALE, INACTIVE_LABEL_TRANSLATE_Y} from '@components/TextInput/styleConst'; import TextInputClearButton from '@components/TextInput/TextInputClearButton'; import TextInputLabel from '@components/TextInput/TextInputLabel'; +import TextInputMeasurement from '@components/TextInput/TextInputMeasurement'; import useHtmlPaste from '@hooks/useHtmlPaste'; import useLocalize from '@hooks/useLocalize'; import useMarkdownStyle from '@hooks/useMarkdownStyle'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import {isMobileChrome, isMobileSafari, isSafari} from '@libs/Browser'; +import {isMobileChrome} from '@libs/Browser'; import {scrollToRight} from '@libs/InputUtils'; import isInputAutoFilled from '@libs/isInputAutoFilled'; import variables from '@styles/variables'; @@ -472,63 +473,21 @@ function BaseTextInput( /> )} - {!!contentWidth && isPrefixCharacterPaddingCalculated && ( - { - if (e.nativeEvent.layout.width === 0 && e.nativeEvent.layout.height === 0) { - return; - } - 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) */} - {value ? `${value}${value.endsWith('\n') ? '\u200B' : ''}` : placeholder} - - - )} - {/* - 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) && !isAutoGrowHeightMarkdown && ( - // Add +2 to width on Safari browsers so that text is not cut off due to the cursor or when changing the value - // Reference: https://github.com/Expensify/App/issues/8158, https://github.com/Expensify/App/issues/26628 - // For mobile Chrome, ensure proper display of the text selection handle (blue bubble down). - // Reference: https://github.com/Expensify/App/issues/34921 - { - if (e.nativeEvent.layout.width === 0 && e.nativeEvent.layout.height === 0) { - return; - } - let additionalWidth = 0; - if (isMobileSafari() || isSafari() || isMobileChrome()) { - additionalWidth = 2; - } - setTextInputWidth(e.nativeEvent.layout.width + additionalWidth); - 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) */} - {value ? `${value}${value.endsWith('\n') ? '\u200B' : ''}` : placeholder} - - )} + ); } diff --git a/src/components/TextInput/TextInputMeasurement/index.ios.tsx b/src/components/TextInput/TextInputMeasurement/index.ios.tsx new file mode 100644 index 000000000000..2db4c300bd9b --- /dev/null +++ b/src/components/TextInput/TextInputMeasurement/index.ios.tsx @@ -0,0 +1,7 @@ +function TextInputMeasurement() { + return null; +} + +TextInputMeasurement.displayName = 'TextInputMeasurement'; + +export default TextInputMeasurement; diff --git a/src/components/TextInput/TextInputMeasurement/index.tsx b/src/components/TextInput/TextInputMeasurement/index.tsx new file mode 100644 index 000000000000..0f0b982a9af6 --- /dev/null +++ b/src/components/TextInput/TextInputMeasurement/index.tsx @@ -0,0 +1,91 @@ +import React from 'react'; +import type {ViewStyle} from 'react-native'; +import {View} from 'react-native'; +import Text from '@components/Text'; +import useThemeStyles from '@hooks/useThemeStyles'; +import {isMobileChrome, isMobileSafari, isSafari} from '@libs/Browser'; +import type TextInputMeasurementProps from './types'; + +function TextInputMeasurement({ + value, + placeholder, + contentWidth, + autoGrowHeight, + maxAutoGrowHeight, + width, + inputStyle, + inputPaddingLeft, + autoGrow, + isAutoGrowHeightMarkdown, + onSetTextInputWidth, + onSetTextInputHeight, + isPrefixCharacterPaddingCalculated, +}: TextInputMeasurementProps) { + const styles = useThemeStyles(); + + return ( + <> + {!!contentWidth && isPrefixCharacterPaddingCalculated && ( + { + if (e.nativeEvent.layout.width === 0 && e.nativeEvent.layout.height === 0) { + return; + } + onSetTextInputWidth(e.nativeEvent.layout.width); + onSetTextInputHeight(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) */} + {value ? `${value}${value.endsWith('\n') ? '\u200B' : ''}` : placeholder} + + + )} + {/* + 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) && !isAutoGrowHeightMarkdown && ( + // Add +2 to width on Safari browsers so that text is not cut off due to the cursor or when changing the value + // Reference: https://github.com/Expensify/App/issues/8158, https://github.com/Expensify/App/issues/26628 + // For mobile Chrome, ensure proper display of the text selection handle (blue bubble down). + // Reference: https://github.com/Expensify/App/issues/34921 + { + if (e.nativeEvent.layout.width === 0 && e.nativeEvent.layout.height === 0) { + return; + } + let additionalWidth = 0; + if (isMobileSafari() || isSafari() || isMobileChrome()) { + additionalWidth = 2; + } + onSetTextInputWidth(e.nativeEvent.layout.width + additionalWidth); + onSetTextInputHeight(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) */} + {value ? `${value}${value.endsWith('\n') ? '\u200B' : ''}` : placeholder} + + )} + + ); +} + +TextInputMeasurement.displayName = 'TextInputMeasurement'; + +export default TextInputMeasurement; diff --git a/src/components/TextInput/TextInputMeasurement/types.ts b/src/components/TextInput/TextInputMeasurement/types.ts new file mode 100644 index 000000000000..44625d067574 --- /dev/null +++ b/src/components/TextInput/TextInputMeasurement/types.ts @@ -0,0 +1,44 @@ +import type {StyleProp, TextStyle, ViewStyle} from 'react-native'; + +type TextInputMeasurementProps = { + /** The value to measure */ + value?: string; + + /** The placeholder to measure */ + placeholder?: string; + + /** The width to measure */ + contentWidth?: number; + + /** Whether to auto grow height */ + autoGrowHeight?: boolean; + + /** The maximum height for auto grow */ + maxAutoGrowHeight?: number; + + /** The width of the container */ + width: number | null; + + /** The input style */ + inputStyle?: StyleProp; + + /** The input padding left */ + inputPaddingLeft?: StyleProp; + + /** Whether to auto grow */ + autoGrow?: boolean; + + /** Whether the input is markdown */ + isAutoGrowHeightMarkdown?: boolean; + + /** Callback to set the text input width */ + onSetTextInputWidth: (width: number) => void; + + /** Callback to set the text input height */ + onSetTextInputHeight: (height: number) => void; + + /** Whether the prefix character padding is calculated */ + isPrefixCharacterPaddingCalculated: boolean; +}; + +export default TextInputMeasurementProps;