Skip to content

Commit 609af21

Browse files
fix(ios): center TextInput typed text when lineHeight > fontSize on Fabric
On Fabric, typed text in a TextInput renders too low when lineHeight > fontSize — glyphs anchor to the bottom of the attributed-string line box. Paper's facebook#38359 addressed this via NSBaselineOffsetAttributeName, but the equivalent helper RCTApplyBaselineOffset (already used by <Text> in RCTTextLayoutManager) was never wired into the <TextInput> path. Call RCTApplyBaselineOffset inside RCTTextInputComponentView._setAttributedString: to add the baseline offset before assigning the attributedString to the backed view. Widen RCTNSAttributedStringFromAttributedString[Box] to return NSMutableAttributedString * so the typed-text path avoids an extra mutableCopy; the Value-mode path already built a mutable and returned it typed as immutable, and OpaquePointer-mode now mutableCopy's explicitly. The helper alone is not enough: on each keystroke the attributedText that round-trips UIKit → Fabric state → back drops NSParagraphStyleAttributeName (UIKit's typingAttributes does not carry the paragraph style, and _updateState stores the stripped attributedText verbatim as an OpaquePointer). Without a paragraph style on the incoming string, RCTApplyBaselineOffsetForRange reads maximumLineHeight == 0 and returns early, so the offset is never applied to typed text. Re-seed the paragraph style from defaultTextAttributes on any range that lacks it (or carries a zero-lineHeight stub) before calling the helper, so the offset is computed and applied for typed content too. UITextView (multi-line) honors NSBaselineOffsetAttributeName for rendered text; UITextField (single-line) does not — that path is addressed in a separate commit.
1 parent 9b8589a commit 609af21

3 files changed

Lines changed: 30 additions & 8 deletions

File tree

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

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -575,8 +575,8 @@ - (void)setTextAndSelection:(NSInteger)eventCount
575575
}
576576
_comingFromJS = YES;
577577
if (value && ![value isEqualToString:_backedTextInputView.attributedText.string]) {
578-
NSAttributedString *attributedString =
579-
[[NSAttributedString alloc] initWithString:value attributes:_backedTextInputView.defaultTextAttributes];
578+
NSMutableAttributedString *attributedString =
579+
[[NSMutableAttributedString alloc] initWithString:value attributes:_backedTextInputView.defaultTextAttributes];
580580
[self _setAttributedString:attributedString];
581581
[self _updateState];
582582
}
@@ -766,8 +766,30 @@ - (void)_restoreTextSelectionAndIgnoreCaretChange:(BOOL)ignore
766766
[_backedTextInputView setSelectedTextRange:range notifyDelegate:YES];
767767
}
768768

769-
- (void)_setAttributedString:(NSAttributedString *)attributedString
769+
- (void)_setAttributedString:(NSMutableAttributedString *)attributedString
770770
{
771+
// When the user types, UIKit's typingAttributes drop NSParagraphStyleAttributeName, so the
772+
// attributedText round-tripping back through state lacks the paragraph style that
773+
// RCTApplyBaselineOffset needs. Re-seed paragraph style from defaultTextAttributes on ranges
774+
// that are missing it or carry a zero-lineHeight stub, so the helper can compute the offset.
775+
NSParagraphStyle *defaultParagraphStyle =
776+
_backedTextInputView.defaultTextAttributes[NSParagraphStyleAttributeName];
777+
if (defaultParagraphStyle && attributedString.length > 0) {
778+
[attributedString
779+
enumerateAttribute:NSParagraphStyleAttributeName
780+
inRange:NSMakeRange(0, attributedString.length)
781+
options:0
782+
usingBlock:^(NSParagraphStyle *style, NSRange range, __unused BOOL *stop) {
783+
if (!style || style.maximumLineHeight == 0) {
784+
[attributedString addAttribute:NSParagraphStyleAttributeName
785+
value:defaultParagraphStyle
786+
range:range];
787+
}
788+
}];
789+
}
790+
791+
RCTApplyBaselineOffset(attributedString);
792+
771793
if ([self _textOf:attributedString equals:_backedTextInputView.attributedText]) {
772794
return;
773795
}

packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,10 @@ NSMutableDictionary<NSAttributedStringKey, id> *RCTNSTextAttributesFromTextAttri
2828
/*
2929
* Conversions amond `NSAttributedString`, `AttributedString` and `AttributedStringBox`.
3030
*/
31-
NSAttributedString *RCTNSAttributedStringFromAttributedString(
31+
NSMutableAttributedString *RCTNSAttributedStringFromAttributedString(
3232
const facebook::react::AttributedString &attributedString);
3333

34-
NSAttributedString *RCTNSAttributedStringFromAttributedStringBox(
34+
NSMutableAttributedString *RCTNSAttributedStringFromAttributedStringBox(
3535
const facebook::react::AttributedStringBox &attributedStringBox);
3636

3737
facebook::react::AttributedStringBox RCTAttributedStringBoxFromNSAttributedString(

packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.mm

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -405,7 +405,7 @@ void RCTApplyBaselineOffset(NSMutableAttributedString *attributedText)
405405
return nsAttributedStringFragment;
406406
}
407407

408-
NSAttributedString *RCTNSAttributedStringFromAttributedString(const AttributedString &attributedString)
408+
NSMutableAttributedString *RCTNSAttributedStringFromAttributedString(const AttributedString &attributedString)
409409
{
410410
static UIImage *placeholderImage;
411411
static dispatch_once_t onceToken;
@@ -428,13 +428,13 @@ void RCTApplyBaselineOffset(NSMutableAttributedString *attributedText)
428428
return nsAttributedString;
429429
}
430430

431-
NSAttributedString *RCTNSAttributedStringFromAttributedStringBox(const AttributedStringBox &attributedStringBox)
431+
NSMutableAttributedString *RCTNSAttributedStringFromAttributedStringBox(const AttributedStringBox &attributedStringBox)
432432
{
433433
switch (attributedStringBox.getMode()) {
434434
case AttributedStringBox::Mode::Value:
435435
return RCTNSAttributedStringFromAttributedString(attributedStringBox.getValue());
436436
case AttributedStringBox::Mode::OpaquePointer:
437-
return (NSAttributedString *)unwrapManagedObject(attributedStringBox.getOpaquePointer());
437+
return [(NSAttributedString *)unwrapManagedObject(attributedStringBox.getOpaquePointer()) mutableCopy];
438438
}
439439
}
440440

0 commit comments

Comments
 (0)