diff --git a/src/components/TextInput/BaseTextInput/implementation/index.native.tsx b/src/components/TextInput/BaseTextInput/implementation/index.native.tsx index e8ea3cb0b2ff..7e323051474f 100644 --- a/src/components/TextInput/BaseTextInput/implementation/index.native.tsx +++ b/src/components/TextInput/BaseTextInput/implementation/index.native.tsx @@ -79,6 +79,7 @@ function BaseTextInput({ iconContainerStyle, shouldUseDefaultLineHeightForPrefix = true, ref, + sentryLabel, ...props }: BaseTextInputProps) { const InputComponent = InputComponentMap.get(type) ?? RNTextInput; @@ -274,6 +275,11 @@ function BaseTextInput({ shouldAddPaddingBottom && styles.pb1, ]); + // TextInputMeasurement is absolutely positioned, so it doesn’t inherit padding/border. + // We extract the horizontal padding/border from the input container to get an accurate width. + // This is used by the TextInputMeasurement for autoGrow height calculation. + const autoGrowMeasurementStyles = StyleUtils.getTextInputMeasurementStyles(newTextInputContainerStyles); + const verticalPaddingDiff = StyleUtils.getVerticalPaddingDiffFromStyle(newTextInputContainerStyles); const inputPaddingLeft = !!prefixCharacter && StyleUtils.getPaddingLeft(prefixCharacterPadding + styles.pl1.paddingLeft); const inputPaddingRight = !!suffixCharacter && StyleUtils.getPaddingRight(StyleUtils.getCharacterPadding(suffixCharacter) + styles.pr1.paddingRight); @@ -305,6 +311,7 @@ function BaseTextInput({ !isMultiline && styles.componentHeightLarge, touchableInputWrapperStyle, ]} + sentryLabel={sentryLabel} > ); diff --git a/src/components/TextInput/BaseTextInput/implementation/index.tsx b/src/components/TextInput/BaseTextInput/implementation/index.tsx index 6fc2988bee93..1f4100a74923 100644 --- a/src/components/TextInput/BaseTextInput/implementation/index.tsx +++ b/src/components/TextInput/BaseTextInput/implementation/index.tsx @@ -27,7 +27,7 @@ import useMarkdownStyle from '@hooks/useMarkdownStyle'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import {isMobileChrome} from '@libs/Browser'; +import {isMobileChrome, isMobileSafari, isSafari} from '@libs/Browser'; import {scrollToRight} from '@libs/InputUtils'; import isInputAutoFilled from '@libs/isInputAutoFilled'; import variables from '@styles/variables'; @@ -257,6 +257,32 @@ function BaseTextInput({ } }; + /** + * Forces Safari to recalculate text layout when input height changes. + * Safari's Shadow DOM doesn't reflow text automatically when container height + * changes, causing text to remain stuck on the previous number of lines. + * @see https://github.com/Expensify/App/issues/76785 + */ + useEffect(() => { + if (!input.current || !(isSafari() || isMobileSafari()) || textInputHeight === 0) { + return; + } + + const {style} = input.current; + const original = style.whiteSpace || ''; + + style.whiteSpace = 'nowrap'; + const id = requestAnimationFrame(() => { + style.whiteSpace = original; + }); + + // Prevent whiteSpace from getting stuck on 'nowrap' if component unmounts or re-renders + return () => { + cancelAnimationFrame(id); + style.whiteSpace = original; + }; + }, [textInputHeight]); + const togglePasswordVisibility = useCallback(() => { setPasswordHidden((prevPasswordHidden: boolean | undefined) => !prevPasswordHidden); }, []); @@ -269,12 +295,15 @@ function BaseTextInput({ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const inputHelpText = errorText || hint; const newPlaceholder = !!prefixCharacter || !!suffixCharacter || isFocused || !hasLabel || (hasLabel && forceActiveLabel) ? placeholder : undefined; + // autoGrow uses autoGrowMeasurementStyles (includes padding), contentWidth doesn't - add padding manually + const containerPadding = !autoGrow && shouldApplyPaddingToContainer ? styles.textInputContainer.padding * 2 : 0; + const newTextInputContainerStyles: StyleProp = StyleSheet.flatten([ styles.textInputContainer, !shouldApplyPaddingToContainer && styles.p0, !hasLabel && styles.pt0, textInputContainerStyles, - (autoGrow || !!contentWidth) && StyleUtils.getWidthStyle(textInputWidth + (shouldApplyPaddingToContainer ? styles.textInputContainer.padding * 2 : 0)), + (autoGrow || !!contentWidth) && StyleUtils.getWidthStyle(textInputWidth + containerPadding), !hideFocusedState && isFocused && styles.borderColorFocus, (!!hasError || !!errorText) && styles.borderColorDanger, autoGrowHeight && {scrollPaddingTop: typeof maxAutoGrowHeight === 'number' ? 2 * maxAutoGrowHeight : undefined}, @@ -283,6 +312,11 @@ function BaseTextInput({ shouldAddPaddingBottom && styles.pb1, ]); + // TextInputMeasurement is absolutely positioned, so it doesn’t inherit padding/border. + // We extract the horizontal padding/border from the input container to get an accurate width. + // This is used by the TextInputMeasurement for autoGrow height calculation. + const autoGrowMeasurementStyles = StyleUtils.getTextInputMeasurementStyles(newTextInputContainerStyles); + const verticalPaddingDiff = StyleUtils.getVerticalPaddingDiffFromStyle(newTextInputContainerStyles); const inputPaddingLeft = !!prefixCharacter && StyleUtils.getPaddingLeft(prefixCharacterPadding + styles.pl1.paddingLeft); const inputPaddingRight = !!suffixCharacter && StyleUtils.getPaddingRight(StyleUtils.getCharacterPadding(suffixCharacter) + styles.pr1.paddingRight); @@ -540,6 +574,7 @@ function BaseTextInput({ onSetTextInputWidth={setTextInputWidth} onSetTextInputHeight={setTextInputHeight} isPrefixCharacterPaddingCalculated={isPrefixCharacterPaddingCalculated} + autoGrowMeasurementStyles={autoGrowMeasurementStyles} /> ); diff --git a/src/components/TextInput/TextInputMeasurement/index.tsx b/src/components/TextInput/TextInputMeasurement/index.tsx index 5de306879430..c86ff46cf049 100644 --- a/src/components/TextInput/TextInputMeasurement/index.tsx +++ b/src/components/TextInput/TextInputMeasurement/index.tsx @@ -20,6 +20,7 @@ function TextInputMeasurement({ onSetTextInputWidth, onSetTextInputHeight, isPrefixCharacterPaddingCalculated, + autoGrowMeasurementStyles, }: TextInputMeasurementProps) { const styles = useThemeStyles(); @@ -62,6 +63,7 @@ function TextInputMeasurement({ ; + /** Callback to set the text input width */ onSetTextInputWidth: (width: number) => void; diff --git a/src/styles/utils/index.ts b/src/styles/utils/index.ts index 2ad579618885..223d268934ae 100644 --- a/src/styles/utils/index.ts +++ b/src/styles/utils/index.ts @@ -724,6 +724,24 @@ function getPaddingRight(paddingRight: number): ViewStyle { }; } +/** + * Extract horizontal padding and border widths from a flattened style object, + * respecting RN precedence (specific → horizontal → all). + */ +function getTextInputMeasurementStyles(style: ViewStyle): TextStyle { + const paddingLeft = style.paddingLeft ?? style.paddingHorizontal ?? style.padding; + const paddingRight = style.paddingRight ?? style.paddingHorizontal ?? style.padding; + const borderLeftWidth = style.borderLeftWidth ?? style.borderWidth; + const borderRightWidth = style.borderRightWidth ?? style.borderWidth; + + return { + ...(paddingLeft && {paddingLeft}), + ...(paddingRight && {paddingRight}), + ...(borderLeftWidth && {borderLeftWidth}), + ...(borderRightWidth && {borderRightWidth}), + }; +} + /** * Get variable padding-bottom as style */ @@ -1342,6 +1360,7 @@ const staticStyleUtils = { getCharacterWidth, getAmountWidth, getBorderRadiusStyle, + getTextInputMeasurementStyles, getHighResolutionInfoWrapperStyle, getItemBackgroundColorStyle, getNavigationBarType,