Skip to content

Commit 4e79973

Browse files
fix(ios): center typed text and caret in single-line TextInput when lineHeight > fontSize
UITextField's placeholder path (UILabel-based) honors NSBaselineOffsetAttributeName, but the UIFieldEditor path that draws typed text does not. So the Fabric baseline-offset fix centers multi-line typed text (UITextView respects the offset) but leaves single-line typed text pinned at the bottom of the paragraphStyle-sized line box. Override setDefaultTextAttributes: and setAttributedText: on RCTUITextField to forward a stripped copy of the paragraph style to super when paragraphStyle.maximumLineHeight > font.lineHeight — minimumLineHeight and maximumLineHeight are zeroed out. UITextField then renders the text at its natural font line height and its built-in vertical centering positions it correctly in the bounds. Also drop NSBaselineOffsetAttributeName on those ranges: the offset is wasted on UITextField's typed-text draw path and a non-zero value inflates the caret rect without moving the glyph. The local _defaultTextAttributes ivar keeps the original paragraph style so _placeholderTextAttributes and the caretRectForPosition: override continue to see the real lineHeight. For the caret itself: when lineHeight > font.lineHeight, UIKit sizes the caret to the full line-box height, making it visually taller than the surrounding glyph. Override caretRectForPosition: to match the caret height to the font's intrinsic line height and center it within the expanded line-box so it sits alongside the glyph.
1 parent cba30b5 commit 4e79973

1 file changed

Lines changed: 75 additions & 3 deletions

File tree

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

Lines changed: 75 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -91,12 +91,71 @@ - (void)setDefaultTextAttributes:(NSDictionary<NSAttributedStringKey, id> *)defa
9191
if ([_defaultTextAttributes isEqualToDictionary:defaultTextAttributes]) {
9292
return;
9393
}
94-
9594
_defaultTextAttributes = defaultTextAttributes;
96-
[super setDefaultTextAttributes:defaultTextAttributes];
95+
[super setDefaultTextAttributes:[self _attributesStrippedForRendering:defaultTextAttributes]];
9796
[self _updatePlaceholder];
9897
}
9998

99+
- (void)setAttributedText:(NSAttributedString *)attributedText
100+
{
101+
[super setAttributedText:[self _attributedTextStrippedForRendering:attributedText]];
102+
}
103+
104+
// UITextField / UIFieldEditor does not honor NSBaselineOffsetAttributeName when drawing typed
105+
// text, and it draws at the baseline of a paragraphStyle-sized line box — so when
106+
// `lineHeight > font.lineHeight` the glyphs sit at the bottom of that line box. Strip the
107+
// paragraphStyle line-height for the super-class rendering path so UITextField uses its
108+
// default intrinsic line height and its built-in vertical centering (contentVerticalAlignment)
109+
// centers the text in the bounds. `_defaultTextAttributes` (local) keeps the unmodified
110+
// paragraphStyle so the placeholder path (`_placeholderTextAttributes`) and caret override
111+
// (`caretRectForPosition:`) still see the real lineHeight.
112+
- (NSDictionary<NSAttributedStringKey, id> *)_attributesStrippedForRendering:
113+
(NSDictionary<NSAttributedStringKey, id> *)attributes
114+
{
115+
NSParagraphStyle *paragraphStyle = attributes[NSParagraphStyleAttributeName];
116+
UIFont *font = attributes[NSFontAttributeName];
117+
if (!paragraphStyle || !font || paragraphStyle.maximumLineHeight <= font.lineHeight) {
118+
return attributes;
119+
}
120+
NSMutableDictionary<NSAttributedStringKey, id> *copy = [attributes mutableCopy];
121+
NSMutableParagraphStyle *stripped = [paragraphStyle mutableCopy];
122+
stripped.minimumLineHeight = 0;
123+
stripped.maximumLineHeight = 0;
124+
copy[NSParagraphStyleAttributeName] = stripped;
125+
return copy;
126+
}
127+
128+
- (NSAttributedString *)_attributedTextStrippedForRendering:(NSAttributedString *)attributedText
129+
{
130+
if (attributedText.length == 0) {
131+
return attributedText;
132+
}
133+
NSMutableAttributedString *mutableStr = [attributedText mutableCopy];
134+
[mutableStr enumerateAttribute:NSParagraphStyleAttributeName
135+
inRange:NSMakeRange(0, mutableStr.length)
136+
options:0
137+
usingBlock:^(NSParagraphStyle *style, NSRange range, __unused BOOL *stop) {
138+
if (!style || style.maximumLineHeight == 0) {
139+
return;
140+
}
141+
UIFont *font = [mutableStr attribute:NSFontAttributeName
142+
atIndex:range.location
143+
effectiveRange:NULL];
144+
if (!font || style.maximumLineHeight <= font.lineHeight) {
145+
return;
146+
}
147+
NSMutableParagraphStyle *stripped = [style mutableCopy];
148+
stripped.minimumLineHeight = 0;
149+
stripped.maximumLineHeight = 0;
150+
[mutableStr addAttribute:NSParagraphStyleAttributeName value:stripped range:range];
151+
// Drop any NSBaselineOffsetAttributeName applied by the Fabric baseline-offset
152+
// helper in the same range: UITextField does not honor it for typed text
153+
// rendering, but a non-zero value still inflates the caret rect.
154+
[mutableStr removeAttribute:NSBaselineOffsetAttributeName range:range];
155+
}];
156+
return mutableStr;
157+
}
158+
100159
- (NSDictionary<NSAttributedStringKey, id> *)defaultTextAttributes
101160
{
102161
return _defaultTextAttributes;
@@ -223,7 +282,20 @@ - (CGRect)caretRectForPosition:(UITextPosition *)position
223282
return CGRectZero;
224283
}
225284

226-
return [super caretRectForPosition:position];
285+
CGRect rect = [super caretRectForPosition:position];
286+
287+
// When `lineHeight > font.lineHeight`, UIKit sizes the caret to the full
288+
// line-box height, making it visually taller than the surrounding glyph.
289+
// Match the caret height to the font's intrinsic line height and center it
290+
// so it sits alongside the glyph rather than the whole expanded line-box.
291+
NSParagraphStyle *paragraphStyle = _defaultTextAttributes[NSParagraphStyleAttributeName];
292+
UIFont *font = _defaultTextAttributes[NSFontAttributeName];
293+
if (paragraphStyle && font && paragraphStyle.maximumLineHeight > font.lineHeight) {
294+
rect.origin.y += (rect.size.height - font.lineHeight) / 2.0;
295+
rect.size.height = font.lineHeight;
296+
}
297+
298+
return rect;
227299
}
228300

229301
#pragma mark - Positioning Overrides

0 commit comments

Comments
 (0)