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,