Skip to content

Fix content jump for growing multiline TextInputs on Android - #59419

Open
jasozh wants to merge 3 commits into
react:mainfrom
jasozh:export-D127048758
Open

jasozh wants to merge 3 commits into
react:mainfrom
jasozh:export-D127048758

Conversation

@jasozh

@jasozh jasozh commented Oct 23, 2026

Copy link
Copy Markdown
Contributor

Summary:
Fixes the issue described in #46813, where multiline TextInputs on Android experience a content jump when adding and removing lines that change the input's height.

Root Cause

ReactEditText hardcodes isLayoutRequested() = false which causes Android to immediately scroll the caret to the newly typed character instead of waiting until the next layout. When adding a new line to an auto-expanding input, Android scrolls to the new line before Fabric's asynchronous state update triggers a new layout. This manifests as a glitchy content jump since the current line briefly scrolls to the newly added line before being reset by the incoming layout.

In React Native, text is centered vertically in TextInputs by default. When removing a line from an auto-expanding input, Android immediately recenters the text in the remaining space before Fabric's asynchronous state update occurs, causing another glitchy content jump since the current line briefly shifts from its current position due to recentering before being reset by the incoming layout. One workaround for this is to set the textAlignVertical CSS style in the opposite direction of the growth direction, such as textAlignVertical: 'top' for a text field that grows downwards.

This PR

Update ReactTextInputTextWatcher to trigger an unstable_Immediate synchronous state update when a multiline text field's line count changes. This ensures that a layout arrives in the next frame, avoiding the existing race issues where Android scrolls or recenters before the next layout.

Gated behind fixMultilineContentJumpAndroid.

Limitations

This fix depends on synchronous state updates, which could have performance concerns. This is mitigated by limiting synchronous updates only to when the input expands or shrinks, with all other updates remaining async.

One potential alternative is to use scroll deferral to fix the content jump when adding new lines. By conditionally setting isLayoutRequested() = true in ReactEditText, the content jump can be avoided by delaying the scroll in cases when we can guarantee a layout is pending. However, this means ReactEditText needs to predict when Fabric guarantees a layout, which means hardcoding assumptions about Fabric measurement that break if the measurement code changes. Alternatively, ReactEditText would need a working requestLayout(), but this goes against existing architectural patterns (see ReactViewGroup.requestLayout()) where UIManager is meant to handle laying out children. Even then, would only fix the content jump when adding new lines and not when removing lines.

Changelog: [Android]Fixed] - Fix content jump for expanding multiline TextInputs on Android

Differential Revision: D127048758

jasozh and others added 3 commits October 22, 2026 18:00
Summary:
Add a new RNTester example in TextInputSharedExamples to reproduce iOS and Android issues with auto-expanding multiline text fields. There is already an existing example, but an empty, uncontrolled variant is needed to reproduce certain issues.

Changelog: [Internal]

Differential Revision: D127048669
Summary:
Fixes an issue where on iOS, multiline expanding TextInputs do not expand if the input starts off as empty and uncontrolled.

### Root Cause

When a multiline TextInput on iOS is provided a `value` and `onChange()` prop, as shown in the [React Native docs](https://reactnative.dev/docs/textinput), the input grows as expected. However, uncontrolled inputs do not grow past the first initial line.

On mount, `BaseTextInputShadowNode::updateStateIfNeeded()` writes state only if the text changes, so the first state write is skipped for empty, uncontrolled inputs. As a result, `TextAttributes` keeps the default `fontSizeMultiplier = NaN` which is carried over since iOS copies the previous state for subsequent keystrokes. During measurement, `attributedStringBoxToMeasure()` sees that the current multiplier doesn't match the stored multiplier of NaN and falls back to `props.text` which is empty for uncontrolled inputs. This causes the input to stay one-line tall.

### This PR

Modify `BaseTextInputShadowNode::updateStateIfNeeded()` to update the state if base text attributes differ. On mount, the default font multiplier of NaN now differs from the real multiplier, so a state update occurs and the text field grows normally.

Changelog: [iOS][Fixed] - Fix multiline TextInput not growing with its content when uncontrolled and initially empty

Differential Revision: D127048714
Summary:
Fixes the issue described in [react#46813](react#46813), where multiline TextInputs on Android experience a content jump when adding and removing lines that change the input's height.

### Root Cause

ReactEditText hardcodes `isLayoutRequested() = false` which causes Android to immediately scroll the caret to the newly typed character instead of waiting until the next layout. When adding a new line to an auto-expanding input, Android scrolls to the new line before Fabric's asynchronous state update triggers a new layout. This manifests as a glitchy content jump since the current line briefly scrolls to the newly added line before being reset by the incoming layout.

In React Native, text is centered vertically in TextInputs by default. When removing a line from an auto-expanding input, Android immediately recenters the text in the remaining space before Fabric's asynchronous state update occurs, causing another glitchy content jump since the current line briefly shifts from its current position due to recentering before being reset by the incoming layout. One workaround for this is to set the `textAlignVertical` CSS style in the opposite direction of the growth direction, such as `textAlignVertical: 'top'` for a text field that grows downwards.

### This PR

Update `ReactTextInputTextWatcher` to trigger an `unstable_Immediate` synchronous state update when a multiline text field's line count changes. This ensures that a layout arrives in the next frame, avoiding the existing race issues where Android scrolls or recenters before the next layout.

Gated behind `fixMultilineContentJumpAndroid`.

### Limitations

This fix depends on synchronous state updates, which could have performance concerns. This is mitigated by limiting synchronous updates only to when the input expands or shrinks, with all other updates remaining async.

One potential alternative is to use scroll deferral to fix the content jump when adding new lines. By conditionally setting `isLayoutRequested() = true` in ReactEditText, the content jump can be avoided by delaying the scroll in cases when we can guarantee a layout is pending. However, this means ReactEditText needs to predict when Fabric guarantees a layout, which means hardcoding assumptions about Fabric measurement that break if the measurement code changes. Alternatively, ReactEditText would need a working `requestLayout()`, but this goes against existing architectural patterns (see `ReactViewGroup.requestLayout()`) where UIManager is meant to handle laying out children. Even then, would only fix the content jump when adding new lines and not when removing lines.

Changelog: [Android]Fixed] - Fix content jump for expanding multiline TextInputs on Android

Differential Revision: D127048758
@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Oct 23, 2026
@meta-codesync

meta-codesync Bot commented Oct 23, 2026

Copy link
Copy Markdown

@jasozh has exported this pull request. If you are a Meta employee, you can view the originating Diff in D127048758.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. meta-exported p: Facebook Partner: Facebook Partner

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant