Skip to content

Fix TextInput placeholder keeping a removed lineHeight on iOS - #59195

Open
jkasprzyk17 wants to merge 1 commit into
react:mainfrom
jkasprzyk17:fix/textinput-placeholder-reused-view
Open

jkasprzyk17 wants to merge 1 commit into
react:mainfrom
jkasprzyk17:fix/textinput-placeholder-reused-view

Conversation

@jkasprzyk17

@jkasprzyk17 jkasprzyk17 commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

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 RCTTextInputComponentViews), 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

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:

RNTesterPlayground.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},
});

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
59132-before 59132-after

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:

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

UITextField's placeholder label keeps the paragraph style of the previous
attributedPlaceholder and applies it to a new placeholder that has none.
When a single-line TextInput reuses the view of a TextInput with a
lineHeight, or a lineHeight is removed from a mounted one, the new text
attributes have no paragraph style, so the placeholder is still measured
and drawn with the old line height, below the field's vertical centre.

Give the placeholder a paragraph style of its own when the text attributes
have none, matching the one a fresh placeholder label uses.

Fixes react#59132.
@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 19, 2026
@jkasprzyk17
jkasprzyk17 marked this pull request as ready for review October 19, 2026 14:35
@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 Oct 19, 2026

@cipolleschi cipolleschi left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

THanks for the fix

@meta-codesync

meta-codesync Bot commented Oct 20, 2026

Copy link
Copy Markdown

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

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. 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] TextInput placeholder drawn off-centre when its view is reused from a multiline TextInput with lineHeight

2 participants