Skip to content

Preserve TextInput selection color when changing multiline - #57748

Closed
fallintoplace wants to merge 1 commit into
react:mainfrom
fallintoplace:fix-textinput-selection-color
Closed

fallintoplace wants to merge 1 commit into
react:mainfrom
fallintoplace:fix-textinput-selection-color

Conversation

@fallintoplace

@fallintoplace fallintoplace commented Jul 28, 2026 •

Copy link
Copy Markdown
Contributor

Summary:

  • Preserve the native text input tint color when switching between single-line and multiline backing views.
  • Add a native regression test for copying the tint from RCTUITextField to RCTUITextView.

Fabric replaces the backing text input view when multiline changes. RCTCopyBackedTextInput preserved the other native presentation properties but omitted tintColor, which implements selectionColor on iOS. Since the React prop itself was unchanged, the normal prop diff did not apply it again and the selection color fell back to the system tint.

Fixes #57105

Changelog:

[IOS] [FIXED] - Preserve TextInput selection color when changing multiline.

Test Plan:

  • Added RCTTextInputUtilsTest.testCopyBackedTextInputPreservesTintColor.
  • Ran clang-format validation and git diff --check.

@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 Jul 28, 2026
@facebook-github-tools facebook-github-tools Bot added the Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team. label Jul 28, 2026
@meta-codesync

meta-codesync Bot commented Jul 28, 2026

Copy link
Copy Markdown

@Abbondanzo has imported this pull request. If you are a Meta employee, you can view this in D113980213.

@meta-codesync meta-codesync Bot closed this in 56c284e Jul 29, 2026
@meta-codesync meta-codesync Bot added the Merged This PR has been merged. label Jul 29, 2026
@meta-codesync

meta-codesync Bot commented Jul 29, 2026

Copy link
Copy Markdown

@Abbondanzo merged this pull request in 56c284e.

meta-codesync Bot pushed a commit that referenced this pull request Sep 8, 2026
Summary:
`RCTCopyBackedTextInput` is called when a `TextInput` switches between
single-line and multiline and the Fabric component view swaps its backing
UIKit view (`RCTUITextField` <-> `RCTUITextView`). It copies the keyboard
traits across to the replacement view, but `returnKeyType` is not among them —
`keyboardType` is copied on the line directly above it.

Because the prop itself has not changed across the transition, the prop diff in
`RCTTextInputComponentView` sees `done` -> `done` and does not re-run the
setter. The replacement view therefore keeps its default Return key, so an
input declared `returnKeyType="done"` renders the newline key instead of Done
once it has flipped to multiline.

This is the same class of omission that was fixed for `tintColor` in #57748,
and the fix is the same shape: copy the property along with its neighbours.

Repro: a `TextInput` with a stable key and `returnKeyType="done"` whose
`multiline` prop goes `false` -> `true`. After the transition, focusing it shows
the default Return key. Fabric view recycling can produce the same backing-view
transition even for an input that is always multiline, which makes it
intermittent in real apps.

Verified against 0.86.3; the line is still missing on 0.87.1 and on `main`.

## Changelog:

[IOS] [FIXED] - Preserve TextInput returnKeyType when switching between single-line and multiline backing views.

Pull Request resolved: #58355

Test Plan:
Added `testCopyBackedTextInputPreservesReturnKeyType` to
`React/Tests/Text/RCTTextInputUtilsTest.mm`, alongside the existing
`testCopyBackedTextInputPreservesTintColor` added in #57748. It sets
`returnKeyType = UIReturnKeyDone` on an `RCTUITextField`, runs
`RCTCopyBackedTextInput` into an `RCTUITextView`, and asserts the value
survives. The test fails without the one-line change and passes with it.

Also confirmed in a production app: a multiline `TextInput` with
`returnKeyType="done"` intermittently displayed the newline key, and adding
this single line fixed it.

Reviewed By: christophpurrer

Differential Revision: D119030795

Pulled By: Abbondanzo

fbshipit-source-id: 0804a0b013f23b6c4533e4bb3d11cb13df81835c
shashikant-panchal pushed a commit to shashikant-panchal/react-native that referenced this pull request Sep 10, 2026
Summary:
`RCTCopyBackedTextInput` is called when a `TextInput` switches between
single-line and multiline and the Fabric component view swaps its backing
UIKit view (`RCTUITextField` <-> `RCTUITextView`). It copies the keyboard
traits across to the replacement view, but `returnKeyType` is not among them —
`keyboardType` is copied on the line directly above it.

Because the prop itself has not changed across the transition, the prop diff in
`RCTTextInputComponentView` sees `done` -> `done` and does not re-run the
setter. The replacement view therefore keeps its default Return key, so an
input declared `returnKeyType="done"` renders the newline key instead of Done
once it has flipped to multiline.

This is the same class of omission that was fixed for `tintColor` in react#57748,
and the fix is the same shape: copy the property along with its neighbours.

Repro: a `TextInput` with a stable key and `returnKeyType="done"` whose
`multiline` prop goes `false` -> `true`. After the transition, focusing it shows
the default Return key. Fabric view recycling can produce the same backing-view
transition even for an input that is always multiline, which makes it
intermittent in real apps.

Verified against 0.86.3; the line is still missing on 0.87.1 and on `main`.

## Changelog:

[IOS] [FIXED] - Preserve TextInput returnKeyType when switching between single-line and multiline backing views.

Pull Request resolved: react#58355

Test Plan:
Added `testCopyBackedTextInputPreservesReturnKeyType` to
`React/Tests/Text/RCTTextInputUtilsTest.mm`, alongside the existing
`testCopyBackedTextInputPreservesTintColor` added in react#57748. It sets
`returnKeyType = UIReturnKeyDone` on an `RCTUITextField`, runs
`RCTCopyBackedTextInput` into an `RCTUITextView`, and asserts the value
survives. The test fails without the one-line change and passes with it.

Also confirmed in a production app: a multiline `TextInput` with
`returnKeyType="done"` intermittently displayed the newline key, and adding
this single line fixed it.

Reviewed By: christophpurrer

Differential Revision: D119030795

Pulled By: Abbondanzo

fbshipit-source-id: 0804a0b013f23b6c4533e4bb3d11cb13df81835c
tvinhas pushed a commit to tvinhas/react-native-macos that referenced this pull request Oct 21, 2026
Summary:
- Preserve the native text input tint color when switching between single-line and multiline backing views.
- Add a native regression test for copying the tint from `RCTUITextField` to `RCTUITextView`.

Fabric replaces the backing text input view when `multiline` changes. `RCTCopyBackedTextInput` preserved the other native presentation properties but omitted `tintColor`, which implements `selectionColor` on iOS. Since the React prop itself was unchanged, the normal prop diff did not apply it again and the selection color fell back to the system tint.

Fixes react#57105

## Changelog:

[IOS] [FIXED] - Preserve TextInput selection color when changing multiline.

Pull Request resolved: react#57748

Test Plan:
- Added `RCTTextInputUtilsTest.testCopyBackedTextInputPreservesTintColor`.
- Ran clang-format validation and `git diff --check`.

Reviewed By: javache

Differential Revision: D113980213

Pulled By: Abbondanzo

fbshipit-source-id: 73a5f87e1b45b504f4640572b8c1a26b4d97f43f
meta-codesync Bot pushed a commit that referenced this pull request Oct 22, 2026
Summary:
Fixes #59132.

On iOS, a single-line `TextInput` with a `placeholder` and no `lineHeight` can draw its placeholder below the field's vertical centre. It happens when the field's native view was last used by a `TextInput` with a `lineHeight` (Fabric recycles `RCTTextInputComponentView`s), and when a `lineHeight` is removed from a mounted single-line field. Typed text is not affected.

UITextField's placeholder label keeps the paragraph style of the previous `attributedPlaceholder` and applies it to a new placeholder that has none. `RCTNSTextAttributesFromTextAttributes` only adds an `NSParagraphStyle` when an alignment, writing direction, line break mode or strategy, or line height is set, so once the `lineHeight` is gone the new placeholder has no paragraph style, and the label keeps measuring and drawing it with the old line height. Setting `attributedPlaceholder` to `nil` first doesn't reset it.

This change gives the placeholder a paragraph style of its own when the text attributes have none: an `NSMutableParagraphStyle` with `lineBreakMode = NSLineBreakByTruncatingTail`, which is what a fresh placeholder label uses. A paragraph style from the text attributes is used as before. In a right-to-left layout `RCTNSTextAttributesFromTextAttributes` always adds a paragraph style, so the new one only applies to left-to-right layouts without `textAlign`. In a plain UIKit test on the simulator, compared with a fresh `UITextField`, the placeholder renders to the same pixels (English and Hebrew text, with and without `semanticContentAttribute` forced to right-to-left), and `sizeWithAttributes:`, which `intrinsicContentSize` uses, returns the same size.

The multiline `RCTUITextView` is unchanged: its placeholder is a plain `UILabel`, which doesn't keep the previous paragraph style.

Related: #56487 changes `_placeholderTextAttributes` for fields whose `lineHeight` is larger than the font; this change is about fields without a `lineHeight`. #59184 (fixed in #59190) is another piece of state that outlives the recycling of `RCTTextInputComponentView`. #57748 and #58355 fixed other properties lost on the same single-line/multiline view swap.

## Changelog:

[IOS] [FIXED] - Fix `TextInput` placeholder drawn below centre after its view is reused from a `TextInput` with `lineHeight`

Pull Request resolved: #59195

Test Plan:
Added `RCTUITextFieldTests` with two cases:
- the placeholder loses a line height that was removed from the text attributes
- a paragraph style from the text attributes is kept for the placeholder

`React/Tests` isn't compiled by any open source target, so I added the file to the `RNTesterUnitTests` target locally (not part of this change) and ran it on an iPhone 17 Pro simulator, iOS 26.5:
- without the fix: `testPlaceholderDropsLineHeightRemovedFromTextAttributes` fails (`((paragraphStyle) != nil) failed`), the other test passes
- with the fix: `** TEST SUCCEEDED **`, both tests pass

I tested manually in RNTester (Debug, new architecture, iPhone 17 Pro simulator, iOS 26.5) with this Playground code:

<details>
<summary>RNTesterPlayground.js</summary>

```js
function Field({label, children}) {
  return (
    <View style={styles.field}>
      <RNTesterText style={styles.label}>{label}</RNTesterText>
      <View style={styles.box}>
        {children}
        <View pointerEvents="none" style={styles.centreLine} />
      </View>
    </View>
  );
}

function Playground() {
  const [stage, setStage] = useState(0);
  const [lineHeight, setLineHeight] = useState(true);
  return (
    <View style={styles.container}>
      <Field label="Reference (fresh view)">
        <TextInput placeholder="Search…" style={styles.singleLine} />
      </Field>
      <Button title="1. Mount multiline" onPress={() => setStage(1)} />
      <Button title="2. Unmount it" onPress={() => setStage(2)} />
      <Button title="3. Mount single-line" onPress={() => setStage(3)} />
      {stage === 1 ? (
        <TextInput
          key="multiline"
          multiline
          style={{fontSize: 17, lineHeight: 34, borderWidth: 1}}
        />
      ) : null}
      {stage === 3 ? (
        <Field label="Single-line (reused view)">
          <TextInput
            key="singleline"
            placeholder="Search…"
            style={styles.singleLine}
          />
        </Field>
      ) : null}
      <Button
        title={lineHeight ? 'Remove lineHeight' : 'Add lineHeight'}
        onPress={() => setLineHeight(value => !value)}
      />
      <Field label={`Single-line, lineHeight ${lineHeight ? '34' : 'unset'}`}>
        <TextInput
          placeholder="Search…"
          style={[styles.singleLine, lineHeight ? {lineHeight: 34} : null]}
        />
      </Field>
    </View>
  );
}

const styles = StyleSheet.create({
  box: {borderColor: '#888', borderWidth: StyleSheet.hairlineWidth},
  centreLine: {
    backgroundColor: 'red',
    height: 1,
    left: 0,
    position: 'absolute',
    right: 0,
    top: 20.5,
  },
  container: {gap: 8, padding: 10},
  field: {gap: 4},
  label: {fontSize: 12},
  singleLine: {fontSize: 17, height: 42, paddingHorizontal: 12},
});
```

</details>

Steps: tap **1**, **2** and **3**. The single-line field mounted in step 3 reuses the view of the multiline field from step 1. Then tap **Remove lineHeight**.

| Before | After |
| --- | --- |
| <img width="300" alt="59132-before" src="https://github.com/user-attachments/assets/34076c45-1736-4af2-98d4-396b57f82400" /> | <img width="300" alt="59132-after" src="https://github.com/user-attachments/assets/1c995d1c-9bfb-4efd-bbb1-c87a04e47d19" /> |

Before: the placeholder of the reused field and the placeholder of the field whose `lineHeight` was removed sit below the red centre line. After: both are centred like the reference field. Tapping **Add lineHeight** again gives the field its `lineHeight` placeholder back, the same as without this change.

Commands:

```sh
yarn install
yarn --cwd packages/react-native-codegen build
cd packages/rn-tester
pod install
pod update hermes-engine --no-repo-update  # Podfile.lock pinned an older Hermes
cd ../..
xcodebuild -workspace packages/rn-tester/RNTesterPods.xcworkspace -scheme RNTester -configuration Debug \
  -destination 'platform=iOS Simulator,id=<iPhone 17 Pro, iOS 26.5 simulator UDID>' build
# ** BUILD SUCCEEDED **
xcodebuild test -workspace packages/rn-tester/RNTesterPods.xcworkspace -scheme RNTester -configuration Debug \
  -destination 'platform=iOS Simulator,id=<iPhone 17 Pro, iOS 26.5 simulator UDID>' \
  -only-testing:RNTesterUnitTests/RCTUITextFieldTests  # with RCTUITextFieldTests.mm added to the target locally
# ** TEST SUCCEEDED **
node scripts/clang-format.js --check \
  packages/react-native/Libraries/Text/TextInput/Singleline/RCTUITextField.mm \
  packages/react-native/React/Tests/Text/RCTUITextFieldTests.mm
# no output, exit code 0
```

Reviewed By: christophpurrer

Differential Revision: D126309241

Pulled By: cipolleschi

fbshipit-source-id: 04ffa2930e3884f7a60d2dd9407d779ffc5f7775
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. Merged This PR has been merged. Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[iOS][Fabric] TextInput selectionColor (caret/selection tint) lost when backing view is recreated (multiline change), falls back to systemBlue

1 participant