From 65b360fc28fb84f098e4475a66ea6b6b2e4641a2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 13 Oct 2026 23:08:49 +0000 Subject: [PATCH 1/3] Initial plan From 5adda5411a6f8936c3a6b22e066d099b66036948 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 13 Oct 2026 23:14:02 +0000 Subject: [PATCH 2/3] Fix Paper text baseline when Yoga height is undefined Co-authored-by: Saadnajmi <6722175+Saadnajmi@users.noreply.github.com> --- .../Libraries/Text/Text/RCTTextShadowView.mm | 10 +- .../RNTesterUnitTests/RCTShadowViewTests.m | 97 +++++++++++++++++++ 2 files changed, 106 insertions(+), 1 deletion(-) diff --git a/packages/react-native/Libraries/Text/Text/RCTTextShadowView.mm b/packages/react-native/Libraries/Text/Text/RCTTextShadowView.mm index e9da3d363e37..f15f25e04ab8 100644 --- a/packages/react-native/Libraries/Text/Text/RCTTextShadowView.mm +++ b/packages/react-native/Libraries/Text/Text/RCTTextShadowView.mm @@ -400,7 +400,15 @@ - (void)layoutSubviewsWithContext:(RCTLayoutContext)layoutContext - (CGFloat)lastBaselineForSize:(CGSize)size { - NSAttributedString *attributedText = [self textStorageAndLayoutManagerThatFitsSize:size exclusiveOwnership:NO]; + NSTextStorage *attributedText = [self textStorageAndLayoutManagerThatFitsSize:size exclusiveOwnership:NO]; + + // Yoga can request a descendant's baseline before measuring it during a measure-only pass. + if (size.height == CGFLOAT_MAX) { + NSLayoutManager *layoutManager = attributedText.layoutManagers.firstObject; + NSTextContainer *textContainer = layoutManager.textContainers.firstObject; + [layoutManager ensureLayoutForTextContainer:textContainer]; + size.height = [layoutManager usedRectForTextContainer:textContainer].size.height; + } __block CGFloat maximumDescender = 0.0; diff --git a/packages/rn-tester/RNTesterUnitTests/RCTShadowViewTests.m b/packages/rn-tester/RNTesterUnitTests/RCTShadowViewTests.m index f63a54b327a8..ebe0e7d24f26 100644 --- a/packages/rn-tester/RNTesterUnitTests/RCTShadowViewTests.m +++ b/packages/rn-tester/RNTesterUnitTests/RCTShadowViewTests.m @@ -7,9 +7,11 @@ #import +#import #import #import #import +#import @interface RCTShadowViewTests : XCTestCase @property (nonatomic, strong) RCTRootShadowView *parentView; @@ -180,6 +182,101 @@ - (void)testDoesNotAssignSuggestedDimensionsWhenStyledWithFlexAttribute withIntrinsicContentSize:CGSizeMake(3, 4)]; } +- (void)testTextBaselineWithUndefinedHeight +{ + RCTTextShadowView *textView = [self _textShadowViewWithText:@"Baseline"]; + YGBaselineFunc baseline = YGNodeGetBaselineFunc(textView.yogaNode); + YGMeasureFunc measure = YGNodeGetMeasureFunc(textView.yogaNode); + YGSize measuredSize = measure(textView.yogaNode, 100, YGMeasureModeAtMost, YGUndefined, YGMeasureModeUndefined); + CGFloat expectedBaseline = measuredSize.height + textView.textAttributes.effectiveFont.descender; + + for (NSNumber *height in @[ @(YGUndefined), @(INFINITY), @(-INFINITY) ]) { + float actualBaseline = baseline(textView.yogaNode, 100, height.floatValue); + XCTAssertTrue(isfinite(actualBaseline)); + XCTAssertEqualWithAccuracy(actualBaseline, expectedBaseline, 1); + } +} + +- (void)testTextBaselineWithUndefinedDimensions +{ + RCTTextShadowView *textView = [self _textShadowViewWithText:@"Baseline"]; + YGBaselineFunc baseline = YGNodeGetBaselineFunc(textView.yogaNode); + + float actualBaseline = baseline(textView.yogaNode, YGUndefined, YGUndefined); + XCTAssertTrue(isfinite(actualBaseline)); + XCTAssertGreaterThan(actualBaseline, 0); +} + +- (void)testTextBaselinePreservesExplicitHeight +{ + RCTTextShadowView *textView = [self _textShadowViewWithText:@"Baseline"]; + YGBaselineFunc baseline = YGNodeGetBaselineFunc(textView.yogaNode); + CGFloat descender = textView.textAttributes.effectiveFont.descender; + + for (NSNumber *width in @[ @100, @(YGUndefined) ]) { + XCTAssertEqualWithAccuracy(baseline(textView.yogaNode, width.floatValue, 40), 40 + descender, 0.001); + XCTAssertEqualWithAccuracy(baseline(textView.yogaNode, width.floatValue, 0), descender, 0.001); + } +} + +- (void)testEmptyTextBaselineWithUndefinedDimensions +{ + RCTTextShadowView *textView = [self _textShadowViewWithText:@""]; + YGBaselineFunc baseline = YGNodeGetBaselineFunc(textView.yogaNode); + + XCTAssertEqual(baseline(textView.yogaNode, YGUndefined, YGUndefined), 0); +} + +- (void)testBaselineAlignedTextInNestedFixedSizeView +{ + RCTShadowView *group = [RCTShadowView new]; + RCTShadowView *row = [self _shadowViewWithConfig:^(YGNodeRef node) { + YGNodeStyleSetWidth(node, 360); + YGNodeStyleSetMinHeight(node, 44); + YGNodeStyleSetPadding(node, YGEdgeAll, 8); + YGNodeStyleSetFlexDirection(node, YGFlexDirectionRow); + YGNodeStyleSetAlignItems(node, YGAlignBaseline); + }]; + RCTShadowView *edge = [self _shadowViewWithConfig:^(YGNodeRef node) { + YGNodeStyleSetAlignItems(node, YGAlignCenter); + }]; + RCTShadowView *box = [self _shadowViewWithConfig:^(YGNodeRef node) { + YGNodeStyleSetWidth(node, 28); + YGNodeStyleSetHeight(node, 28); + YGNodeStyleSetAlignItems(node, YGAlignCenter); + YGNodeStyleSetJustifyContent(node, YGJustifyCenter); + }]; + RCTTextShadowView *textView = [self _textShadowViewWithText:@"A"]; + + [box insertReactSubview:textView atIndex:0]; + [edge insertReactSubview:box atIndex:0]; + [row insertReactSubview:edge atIndex:0]; + [group insertReactSubview:row atIndex:0]; + [self.parentView insertReactSubview:group atIndex:0]; + + for (NSUInteger pass = 0; pass < 2; pass++) { + [textView dirtyLayout]; + [self.parentView layoutWithAffectedShadowViews:[NSPointerArray weakObjectsPointerArray]]; + + CGRect textFrame = [textView measureLayoutRelativeToAncestor:self.parentView]; + XCTAssertTrue(isfinite(textFrame.origin.x)); + XCTAssertTrue(isfinite(textFrame.origin.y)); + XCTAssertGreaterThan(textFrame.size.width, 0); + XCTAssertGreaterThan(textFrame.size.height, 0); + XCTAssertEqual(YGNodeLayoutGetWidth(box.yogaNode), 28); + XCTAssertEqual(YGNodeLayoutGetHeight(box.yogaNode), 28); + } +} + +- (RCTTextShadowView *)_textShadowViewWithText:(NSString *)text +{ + RCTTextShadowView *textView = [[RCTTextShadowView alloc] initWithBridge:nil]; + RCTRawTextShadowView *rawTextView = [RCTRawTextShadowView new]; + rawTextView.text = text; + [textView insertReactSubview:rawTextView atIndex:0]; + return textView; +} + - (void)_withShadowViewWithStyle:(void (^)(YGNodeRef node))configBlock assertRelativeLayout:(CGRect)expectedRect withIntrinsicContentSize:(CGSize)contentSize From bc579bcead2652a8a1da7e68346dd6f404369d8b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 13 Oct 2026 23:15:52 +0000 Subject: [PATCH 3/3] Use available native APIs in baseline regression tests Co-authored-by: Saadnajmi <6722175+Saadnajmi@users.noreply.github.com> --- .../RNTesterUnitTests/RCTShadowViewTests.m | 28 +++++++++++-------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/packages/rn-tester/RNTesterUnitTests/RCTShadowViewTests.m b/packages/rn-tester/RNTesterUnitTests/RCTShadowViewTests.m index ebe0e7d24f26..c3c0db3df71b 100644 --- a/packages/rn-tester/RNTesterUnitTests/RCTShadowViewTests.m +++ b/packages/rn-tester/RNTesterUnitTests/RCTShadowViewTests.m @@ -13,6 +13,10 @@ #import #import +@interface RCTTextShadowView (BaselineTesting) +- (CGFloat)lastBaselineForSize:(CGSize)size; +@end + @interface RCTShadowViewTests : XCTestCase @property (nonatomic, strong) RCTRootShadowView *parentView; @end @@ -185,13 +189,11 @@ - (void)testDoesNotAssignSuggestedDimensionsWhenStyledWithFlexAttribute - (void)testTextBaselineWithUndefinedHeight { RCTTextShadowView *textView = [self _textShadowViewWithText:@"Baseline"]; - YGBaselineFunc baseline = YGNodeGetBaselineFunc(textView.yogaNode); - YGMeasureFunc measure = YGNodeGetMeasureFunc(textView.yogaNode); - YGSize measuredSize = measure(textView.yogaNode, 100, YGMeasureModeAtMost, YGUndefined, YGMeasureModeUndefined); + CGSize measuredSize = [textView sizeThatFitsMinimumSize:CGSizeZero maximumSize:CGSizeMake(100, CGFLOAT_MAX)]; CGFloat expectedBaseline = measuredSize.height + textView.textAttributes.effectiveFont.descender; for (NSNumber *height in @[ @(YGUndefined), @(INFINITY), @(-INFINITY) ]) { - float actualBaseline = baseline(textView.yogaNode, 100, height.floatValue); + float actualBaseline = [self _textBaselineForView:textView width:100 height:height.floatValue]; XCTAssertTrue(isfinite(actualBaseline)); XCTAssertEqualWithAccuracy(actualBaseline, expectedBaseline, 1); } @@ -200,9 +202,8 @@ - (void)testTextBaselineWithUndefinedHeight - (void)testTextBaselineWithUndefinedDimensions { RCTTextShadowView *textView = [self _textShadowViewWithText:@"Baseline"]; - YGBaselineFunc baseline = YGNodeGetBaselineFunc(textView.yogaNode); - float actualBaseline = baseline(textView.yogaNode, YGUndefined, YGUndefined); + float actualBaseline = [self _textBaselineForView:textView width:YGUndefined height:YGUndefined]; XCTAssertTrue(isfinite(actualBaseline)); XCTAssertGreaterThan(actualBaseline, 0); } @@ -210,21 +211,20 @@ - (void)testTextBaselineWithUndefinedDimensions - (void)testTextBaselinePreservesExplicitHeight { RCTTextShadowView *textView = [self _textShadowViewWithText:@"Baseline"]; - YGBaselineFunc baseline = YGNodeGetBaselineFunc(textView.yogaNode); CGFloat descender = textView.textAttributes.effectiveFont.descender; for (NSNumber *width in @[ @100, @(YGUndefined) ]) { - XCTAssertEqualWithAccuracy(baseline(textView.yogaNode, width.floatValue, 40), 40 + descender, 0.001); - XCTAssertEqualWithAccuracy(baseline(textView.yogaNode, width.floatValue, 0), descender, 0.001); + XCTAssertEqualWithAccuracy( + [self _textBaselineForView:textView width:width.floatValue height:40], 40 + descender, 0.001); + XCTAssertEqualWithAccuracy([self _textBaselineForView:textView width:width.floatValue height:0], descender, 0.001); } } - (void)testEmptyTextBaselineWithUndefinedDimensions { RCTTextShadowView *textView = [self _textShadowViewWithText:@""]; - YGBaselineFunc baseline = YGNodeGetBaselineFunc(textView.yogaNode); - XCTAssertEqual(baseline(textView.yogaNode, YGUndefined, YGUndefined), 0); + XCTAssertEqual([self _textBaselineForView:textView width:YGUndefined height:YGUndefined], 0); } - (void)testBaselineAlignedTextInNestedFixedSizeView @@ -277,6 +277,12 @@ - (RCTTextShadowView *)_textShadowViewWithText:(NSString *)text return textView; } +- (float)_textBaselineForView:(RCTTextShadowView *)textView width:(float)width height:(float)height +{ + CGSize size = CGSizeMake(RCTCoreGraphicsFloatFromYogaFloat(width), RCTCoreGraphicsFloatFromYogaFloat(height)); + return RCTYogaFloatFromCoreGraphicsFloat([textView lastBaselineForSize:size]); +} + - (void)_withShadowViewWithStyle:(void (^)(YGNodeRef node))configBlock assertRelativeLayout:(CGRect)expectedRect withIntrinsicContentSize:(CGSize)contentSize