From 8ff2463fe9d81607660010fdf16aca9bc80f1ecf Mon Sep 17 00:00:00 2001 From: Jacques Leupin Date: Wed, 5 Aug 2026 12:50:27 -0700 Subject: [PATCH] Resolve dynamic border and outline colors against the view trait collection in Fabric --- .../View/RCTViewComponentView.mm | 30 +++++++++----- .../Mounting/RCTViewComponentViewTests.mm | 40 +++++++++++++++++++ 2 files changed, 61 insertions(+), 9 deletions(-) diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm index 0912c50fec1..51b5a97ba1b 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm @@ -904,13 +904,21 @@ static void RCTAddContourEffectToLayer( [layer removeAllAnimations]; } -static RCTBorderColors RCTCreateRCTBorderColorsFromBorderColors(BorderColors borderColors) +// CALayer colors are plain CGColors: converting a dynamic (PlatformColor / +// DynamicColorIOS) UIColor without an explicit trait collection resolves against +// UITraitCollection.currentTraitCollection, which tracks the system appearance +// and ignores any overrideUserInterfaceStyle inherited by the view. Resolve +// against the view's own trait collection instead, matching the backgroundColor +// handling in invalidateLayer. +static RCTBorderColors RCTCreateRCTBorderColorsFromBorderColors( + BorderColors borderColors, + UITraitCollection *traitCollection) { return RCTBorderColors{ - .top = RCTUIColorFromSharedColor(borderColors.top), - .left = RCTUIColorFromSharedColor(borderColors.left), - .bottom = RCTUIColorFromSharedColor(borderColors.bottom), - .right = RCTUIColorFromSharedColor(borderColors.right)}; + .top = [RCTUIColorFromSharedColor(borderColors.top) resolvedColorWithTraitCollection:traitCollection], + .left = [RCTUIColorFromSharedColor(borderColors.left) resolvedColorWithTraitCollection:traitCollection], + .bottom = [RCTUIColorFromSharedColor(borderColors.bottom) resolvedColorWithTraitCollection:traitCollection], + .right = [RCTUIColorFromSharedColor(borderColors.right) resolvedColorWithTraitCollection:traitCollection]}; } static CALayerCornerCurve CornerCurveFromBorderCurve(BorderCurve borderCurve) @@ -1148,7 +1156,8 @@ - (void)invalidateLayer _borderLayer = nil; layer.borderWidth = (CGFloat)borderMetrics.borderWidths.left; - UIColor *borderColor = RCTUIColorFromSharedColor(borderMetrics.borderColors.left); + UIColor *borderColor = [RCTUIColorFromSharedColor(borderMetrics.borderColors.left) + resolvedColorWithTraitCollection:self.traitCollection]; layer.borderColor = borderColor.CGColor; layer.cornerRadius = (CGFloat)borderMetrics.borderRadii.topLeft.horizontal; layer.cornerCurve = CornerCurveFromBorderCurve(borderMetrics.borderCurves.topLeft); @@ -1166,7 +1175,8 @@ - (void)invalidateLayer layer.borderColor = nil; layer.cornerRadius = 0; - RCTBorderColors borderColors = RCTCreateRCTBorderColorsFromBorderColors(borderMetrics.borderColors); + RCTBorderColors borderColors = + RCTCreateRCTBorderColorsFromBorderColors(borderMetrics.borderColors, self.traitCollection); RCTAddContourEffectToLayer( _borderLayer, @@ -1195,11 +1205,13 @@ - (void)invalidateLayer // have to be drawn with Core Graphics, the same way non-solid borders are. if (_props->outlineStyle == OutlineStyle::Solid && areBorderRadiiCircular(borderMetrics.borderRadii) && borderMetrics.borderRadii.topLeft.horizontal == 0) { - UIColor *outlineColor = RCTUIColorFromSharedColor(_props->outlineColor); + UIColor *outlineColor = + [RCTUIColorFromSharedColor(_props->outlineColor) resolvedColorWithTraitCollection:self.traitCollection]; _outlineLayer.borderWidth = _props->outlineWidth; _outlineLayer.borderColor = outlineColor.CGColor; } else { - UIColor *outlineColor = RCTUIColorFromSharedColor(_props->outlineColor); + UIColor *outlineColor = + [RCTUIColorFromSharedColor(_props->outlineColor) resolvedColorWithTraitCollection:self.traitCollection]; RCTAddContourEffectToLayer( _outlineLayer, diff --git a/packages/react-native/React/Tests/Mounting/RCTViewComponentViewTests.mm b/packages/react-native/React/Tests/Mounting/RCTViewComponentViewTests.mm index 91e100d28df..4cfd7054df6 100644 --- a/packages/react-native/React/Tests/Mounting/RCTViewComponentViewTests.mm +++ b/packages/react-native/React/Tests/Mounting/RCTViewComponentViewTests.mm @@ -281,4 +281,44 @@ - (void)testSquareDottedAndDashedOutlinesAreDrawnWithCoreGraphics } } +#pragma mark - dynamic border and outline colors + +- (void)testDynamicBorderAndOutlineColorsResolveAgainstViewTraitCollection +{ + auto dynamicColor = SharedColor(Color( + DynamicColor{ + .lightColor = static_cast(0xFFFF0000u), + .darkColor = static_cast(0xFF00FF00u), + })); + UITraitCollection *lightTraits = [UITraitCollection traitCollectionWithUserInterfaceStyle:UIUserInterfaceStyleLight]; + + [lightTraits performAsCurrentTraitCollection:^{ + RCTViewComponentView *view = [RCTViewComponentView new]; + view.frame = CGRectMake(0, 0, 100, 100); + view.overrideUserInterfaceStyle = UIUserInterfaceStyleDark; + view.clipsToBounds = YES; + + XCTAssertEqual([UITraitCollection currentTraitCollection].userInterfaceStyle, UIUserInterfaceStyleLight); + XCTAssertEqual(view.traitCollection.userInterfaceStyle, UIUserInterfaceStyleDark); + + auto props = std::make_shared(); + props->yogaStyle.setBorder(facebook::yoga::Edge::All, facebook::yoga::StyleLength::points(4)); + props->borderColors.all = dynamicColor; + props->borderStyles.all = BorderStyle::Solid; + props->outlineWidth = 4; + props->outlineColor = dynamicColor; + props->outlineStyle = OutlineStyle::Solid; + + [view updateProps:props oldProps:ViewShadowNode::defaultSharedProps()]; + [view finalizeUpdates:RNComponentViewUpdateMaskProps]; + + CALayer *outlineLayer = [view valueForKey:@"_outlineLayer"]; + XCTAssertNotNil(outlineLayer); + XCTAssertNotNil((__bridge id)view.layer.borderColor); + XCTAssertNotNil((__bridge id)outlineLayer.borderColor); + XCTAssertTrue(CGColorEqualToColor(view.layer.borderColor, UIColor.greenColor.CGColor)); + XCTAssertTrue(CGColorEqualToColor(outlineLayer.borderColor, UIColor.greenColor.CGColor)); + }]; +} + @end