Repository navigation
fix(ios): center TextInput text when lineHeight > fontSize on Fabric - #56487
janicduplessis wants to merge 2 commits into
Conversation
606f1d7 to
4e79973
Compare
4e79973 to
bf72714
Compare
|
Looking for it |
9bcb81f to
c73a582
Compare
acf8672 to
e4b8473
Compare
|
Warning JavaScript API change detected This PR commits an update to
This change was flagged as: |
99c3b8b to
156b724
Compare
…ht > fontSize On iOS, when a TextInput's lineHeight exceeds its font's line height, UIKit anchors glyphs to the bottom of the attributed-string line box instead of centering them within it. The same misalignment affects the placeholder. On single-line UITextField the caret is also sized to the full line box. This patch fixes all three surfaces. The approach varies by UIKit rendering path: UITextView (multi-line) typed text — honors NSBaselineOffsetAttributeName. Call RCTApplyBaselineOffset in RCTTextInputComponentView._setAttributedString: to inject the offset. Re-seed NSParagraphStyleAttributeName from defaultTextAttributes on ranges missing it (or carrying a zero-line-height stub), because UIKit's typingAttributes drops the paragraph style between keystrokes — without the re-seed the helper sees maximumLineHeight == 0 and bails for typed content. UITextField (single-line) typed text — does NOT honor NSBaselineOffsetAttributeName. Per-range zero out paragraphStyle.minimumLineHeight / maximumLineHeight on the displayed attributedText so UITextField uses the font's natural line height; its built-in vertical centering then positions the glyph in the bounds, and the caret rect (sized from the same line box) shrinks to match. Other paragraph-style fields (alignment, indent) are preserved so nested <Text> styling survives. The shadow node measures from state, so undo the strip in _updateState before pushing — restore stripped line heights from defaultTextAttributes so the cell height stays at the configured lineHeight across edits. Placeholder on both UITextField.attributedPlaceholder (UILabel draw) and RCTUITextView._placeholderView — both honor NSBaselineOffsetAttributeName. Add the offset computation to _placeholderTextAttributes on both backing views so the placeholder is centered to match (single string, single set of attributes — a direct computation is equivalent to RCTApplyBaselineOffset). Bug only manifests on Fabric. Paper has had a similar baseline-offset fix in RCTUITextView.attributedText since 0.66 (see RCTUITextView setAttributedText:), which never made it into the Fabric component view.
156b724 to
3fbf76f
Compare
|
Haven’t had any feedback on this, I would recommend to avoid line height as a workaround. |
|
We ended up updating our TextInput wrapper to float a placeholder on top of the input. function BaseTextInput() {
// ...
return (
<View style={wrapperStyle}>
{input}
{hasPlaceholder && (
<Animated.View
style={[StyleSheet.absoluteFill, placeholderAnimatedStyle]}
pointerEvents="none"
>
<Text style={placeholderStyle} numberOfLines={props.multiline ? undefined : 1}>
{placeholder}
</Text>
</Animated.View>
)}
</View>
);
} |
|
Tested this, and it fixes the issue in every case I tried. I didn't find any regressions. Setup: current main (1c6e0c7) with this PR rebased on top. The only conflicts were the two The test screen puts a
Before (left) and after (right): The caret with this PR: I also tried the simpler fix first: adding the baseline offset to the input's text attributes, the way We also backported this to our app on 0.88.0-rc.3 and built it from source. The chat input's text is centred without the manual offset we've been using, and typing an emoji doesn't change its height. The test screen, if anyone wants to repeat this: https://gist.github.com/emmanuelchucks/40bc34ce0538ee6410b0455e4b5d4ad4 |




Summary
On iOS, when a
TextInputhaslineHeight > fontSize, UIKit anchors typed text and the placeholder to the bottom of the paragraph line box, and sizes the single-line caret to the full line-box height instead of the font height.fontSize: 16, lineHeight: 80. iOS 26 simulator, Fabric.Why
The fix splits two ways because UIKit's draw paths don't all honor the same attributes:
NSBaselineOffsetAttributeName?UITextView)UILabel)UITextField/UIFieldEditor)RCTApplyBaselineOffsethelper that<Text>already uses, so per-fragment fonts (nested<Text>with differentfontSize) are handled correctly.paragraphStyle.minimumLineHeight/maximumLineHeightper range on the attributed string handed 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 — derived from the same line box — shrinks to match. OtherparagraphStylefields (alignment, indent) are preserved per range. The shadow node measures fromTextInputState, so_updateStatere-inflates stripped line heights fromdefaultTextAttributesbefore pushing — without that step the cell would shrink to the font's natural height after the first keystroke.The typed-text fix lives in
RCTTextInputComponentView._setAttributedString:(single Fabric entry point, preserves the_textOf:equals:early-return). The placeholder fix lives in_placeholderTextAttributesof each backing view.Performance
When
lineHeight ≤ font.lineHeight(common case): zero allocations — guarded out before any mutation.Changelog:
[IOS] [FIXED] - Center typed TextInput text, placeholder, and single-line caret when
lineHeight > fontSize.Test Plan
RN Tester → TextInput → lineHeight baseline. Verified on iOS 26 simulator (iPhone 16 Pro / iPhone 17 Pro Max) on Fabric, including nested
<Text>mixed-font cases. Confirmed cell height stays stable across edits.Related: #38359, #39145, #53092.