@@ -32,6 +32,11 @@ @interface RCTParagraphTextView : UIView
3232@property (nonatomic ) ParagraphAttributes paragraphAttributes;
3333@property (nonatomic ) LayoutMetrics layoutMetrics;
3434
35+ // How much glyphs extend beyond line height on each side (when lineHeight < font line height).
36+ - (CGFloat)glyphOverflow ;
37+ // Recomputes glyphOverflow from the current attributed string. Call after state changes.
38+ - (void )updateGlyphOverflow ;
39+
3540@end
3641
3742#if !TARGET_OS_TV
@@ -123,6 +128,7 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &
123128- (void )updateState : (const State::Shared &)state oldState : (const State::Shared &)oldState
124129{
125130 _textView.state = std::static_pointer_cast<const ParagraphShadowNode::ConcreteState>(state);
131+ [_textView updateGlyphOverflow ];
126132 [_textView setNeedsDisplay ];
127133 [self setNeedsLayout ];
128134}
@@ -142,14 +148,25 @@ - (void)prepareForRecycle
142148{
143149 [super prepareForRecycle ];
144150 _textView.state = nullptr ;
151+ [_textView updateGlyphOverflow ];
145152 _accessibilityProvider = nil ;
146153}
147154
148155- (void )layoutSubviews
149156{
150157 [super layoutSubviews ];
151158
152- _textView.frame = self.bounds ;
159+ CGFloat overflow = _textView.glyphOverflow ;
160+ if (overflow > 0 ) {
161+ // When lineHeight < font line height, expand the frame so glyphs can be rendered to match web behaiour.
162+ _textView.frame = CGRectMake (
163+ self.bounds .origin .x ,
164+ self.bounds .origin .y - overflow,
165+ self.bounds .size .width ,
166+ self.bounds .size .height + overflow * 2 );
167+ } else {
168+ _textView.frame = self.bounds ;
169+ }
153170}
154171
155172#pragma mark - Accessibility
@@ -382,6 +399,58 @@ - (void)disableContextMenu
382399
383400@implementation RCTParagraphTextView {
384401 CAShapeLayer *_highlightLayer;
402+ CGFloat _glyphOverflow;
403+ }
404+
405+ - (CGFloat)glyphOverflow
406+ {
407+ return _glyphOverflow;
408+ }
409+
410+ - (void )updateGlyphOverflow
411+ {
412+ _glyphOverflow = 0 ;
413+
414+ if (!_state) {
415+ return ;
416+ }
417+
418+ NSAttributedString *attributedText =
419+ RCTNSAttributedStringFromAttributedString (_state->getData ().attributedString );
420+ if (!attributedText || attributedText.length == 0 ) {
421+ return ;
422+ }
423+
424+ __block CGFloat maxLineHeight = 0 ;
425+ __block CGFloat maxFontLineHeight = 0 ;
426+
427+ [attributedText enumerateAttribute: NSParagraphStyleAttributeName
428+ inRange: NSMakeRange (0 , attributedText.length)
429+ options: NSAttributedStringEnumerationLongestEffectiveRangeNotRequired
430+ usingBlock: ^(NSParagraphStyle *style, __unused NSRange range, __unused BOOL *stop) {
431+ if (style && style.maximumLineHeight > 0 ) {
432+ maxLineHeight = MAX (maxLineHeight, style.maximumLineHeight );
433+ }
434+ }];
435+
436+ if (maxLineHeight == 0 ) {
437+ return ;
438+ }
439+
440+ [attributedText enumerateAttribute: NSFontAttributeName
441+ inRange: NSMakeRange (0 , attributedText.length)
442+ options: NSAttributedStringEnumerationLongestEffectiveRangeNotRequired
443+ usingBlock: ^(UIFont *font, __unused NSRange range, __unused BOOL *stop) {
444+ if (font) {
445+ maxFontLineHeight = MAX (maxFontLineHeight, font.lineHeight );
446+ }
447+ }];
448+
449+ if (maxLineHeight >= maxFontLineHeight) {
450+ return ;
451+ }
452+
453+ _glyphOverflow = ceil ((maxFontLineHeight - maxLineHeight) / 2.0 );
385454}
386455
387456- (UIView *)hitTest : (CGPoint)point withEvent : (UIEvent *)event
@@ -406,6 +475,13 @@ - (void)drawRect:(CGRect)rect
406475
407476 CGRect frame = RCTCGRectFromRect (_layoutMetrics.getContentFrame ());
408477
478+ if (_glyphOverflow > 0 ) {
479+ // Offset origin to account for the expanded text view, and expand height
480+ // so the NSTextContainer has room for full glyph rendering.
481+ frame.origin .y += _glyphOverflow;
482+ frame.size .height += _glyphOverflow * 2 ;
483+ }
484+
409485 [nativeTextLayoutManager drawAttributedString: stateData.attributedString
410486 paragraphAttributes: _paragraphAttributes
411487 frame: frame
0 commit comments