Skip to content

Commit 9bcb81f

Browse files
fix(ios): center TextInput text, placeholder, and caret when lineHeight > fontSize
On iOS, when a `TextInput` has `lineHeight > fontSize`, UIKit misrenders three surfaces: 1. Typed text — glyphs anchor to the bottom of the attributed-string line box instead of centering within it. 2. Placeholder — inherits the paragraph style from `defaultTextAttributes` and sits low. 3. Single-line caret — sized to the full paragraph line-box height rather than the font height, visibly taller than the glyph. The fix splits two ways depending on which UIKit draw path is involved: - UITextView (multi-line) and the UILabel-based placeholder paths honor `NSBaselineOffsetAttributeName`. Computing `(maxLineHeight - font.lineHeight) / 2` and applying it as a baseline offset re-centers the glyph in the line box. - UITextField's UIFieldEditor draw path does not honor `NSBaselineOffsetAttributeName` for typed text, and the caret rect is sized from the same paragraph-style line box. For single-line inputs we instead zero the paragraph-style line height for the value passed to UIKit. UITextField then renders at the font's natural line height and its built-in vertical centering positions the glyph in the bounds; the caret rect shrinks to match. `defaultTextAttributes` keeps the unmodified paragraphStyle so the placeholder path still sees the real lineHeight, and Yoga's frame-height measurement is unaffected. The typed-text fix lives in `RCTTextInputComponentView._setAttributedString:` so it runs against text round-tripped through UIKit's `typingAttributes` (which drops `NSParagraphStyleAttributeName`); we re-seed paragraph style from `defaultTextAttributes` before applying the offset / strip. The placeholder fixes are local to `_placeholderTextAttributes` in each backing view since both backing views are shared between Paper and Fabric. ## Changelog [IOS] [FIXED] - Center typed TextInput text, placeholder, and single-line caret when `lineHeight > fontSize`. ## Test Plan RN Tester → **TextInput → lineHeight baseline** renders one single-line and one multi-line `TextInput` with `fontSize: 16, lineHeight: 32`. Before: placeholder sits low, typed glyphs anchor to the bottom of the line box, single-line caret overshoots the glyph. After: placeholder and typed text render vertically centered; single-line caret matches the glyph height.
1 parent 46f6d16 commit 9bcb81f

3 files changed

Lines changed: 39 additions & 0 deletions

File tree

packages/react-native/Libraries/Text/TextInput/Multiline/RCTUITextView.mm

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,13 @@ - (void)_updatePlaceholder
364364
[textAttributes setValue:defaultPlaceholderFont() forKey:NSFontAttributeName];
365365
}
366366

367+
// The placeholder UILabel honors NSBaselineOffsetAttributeName.
368+
NSParagraphStyle *paragraphStyle = textAttributes[NSParagraphStyleAttributeName];
369+
UIFont *font = textAttributes[NSFontAttributeName];
370+
if (paragraphStyle && font && paragraphStyle.maximumLineHeight > font.lineHeight) {
371+
textAttributes[NSBaselineOffsetAttributeName] = @((paragraphStyle.maximumLineHeight - font.lineHeight) / 2.0);
372+
}
373+
367374
return textAttributes;
368375
}
369376

packages/react-native/Libraries/Text/TextInput/Singleline/RCTUITextField.mm

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,13 @@ - (void)setDisableKeyboardShortcuts:(BOOL)disableKeyboardShortcuts
169169
[textAttributes removeObjectForKey:NSForegroundColorAttributeName];
170170
}
171171

172+
// UILabel-based draw path used by `attributedPlaceholder` honors NSBaselineOffsetAttributeName.
173+
NSParagraphStyle *paragraphStyle = textAttributes[NSParagraphStyleAttributeName];
174+
UIFont *font = textAttributes[NSFontAttributeName];
175+
if (paragraphStyle && font && paragraphStyle.maximumLineHeight > font.lineHeight) {
176+
textAttributes[NSBaselineOffsetAttributeName] = @((paragraphStyle.maximumLineHeight - font.lineHeight) / 2.0);
177+
}
178+
172179
return textAttributes;
173180
}
174181

packages/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -768,6 +768,31 @@ - (void)_restoreTextSelectionAndIgnoreCaretChange:(BOOL)ignore
768768

769769
- (void)_setAttributedString:(NSAttributedString *)attributedString
770770
{
771+
// When `lineHeight > font.lineHeight`, UIKit's draw paths anchor glyphs to the bottom of the
772+
// paragraph line box. UITextView honors NSBaselineOffsetAttributeName to re-center; UITextField
773+
// does not, so we zero the paragraph line height instead and let UITextField's built-in
774+
// vertical centering position the glyph (this also shrinks the otherwise-too-tall caret).
775+
NSDictionary<NSAttributedStringKey, id> *defaults = _backedTextInputView.defaultTextAttributes;
776+
NSParagraphStyle *paragraphStyle = defaults[NSParagraphStyleAttributeName];
777+
UIFont *font = defaults[NSFontAttributeName];
778+
if (attributedString.length > 0 && paragraphStyle && font &&
779+
paragraphStyle.maximumLineHeight > font.lineHeight) {
780+
NSMutableAttributedString *mutableString = [attributedString mutableCopy];
781+
NSRange fullRange = NSMakeRange(0, mutableString.length);
782+
if ([_backedTextInputView isKindOfClass:[RCTUITextView class]]) {
783+
[mutableString addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:fullRange];
784+
[mutableString addAttribute:NSBaselineOffsetAttributeName
785+
value:@((paragraphStyle.maximumLineHeight - font.lineHeight) / 2.0)
786+
range:fullRange];
787+
} else {
788+
NSMutableParagraphStyle *stripped = [paragraphStyle mutableCopy];
789+
stripped.minimumLineHeight = 0;
790+
stripped.maximumLineHeight = 0;
791+
[mutableString addAttribute:NSParagraphStyleAttributeName value:stripped range:fullRange];
792+
}
793+
attributedString = mutableString;
794+
}
795+
771796
if ([self _textOf:attributedString equals:_backedTextInputView.attributedText]) {
772797
return;
773798
}

0 commit comments

Comments
 (0)