From 7fac6ecb73ab461068151d7e724ebe44eff1ca23 Mon Sep 17 00:00:00 2001 From: Mohammad Dahmardeh Date: Thu, 20 Aug 2026 16:14:00 +0330 Subject: [PATCH] fix: render dotted and dashed outlines on iOS views without border radii Views with no border radius took the Core Animation outline path, which can only draw a solid border, so `outlineStyle: 'dotted'` and `'dashed'` were silently rendered as solid. Restrict that path to solid outlines so non-solid styles are drawn with Core Graphics, matching how non-solid borders are already handled. --- .../View/RCTViewComponentView.mm | 5 +- .../Mounting/RCTViewComponentViewTests.mm | 44 ++++++++++++++ .../rn-tester/js/examples/View/ViewExample.js | 57 +++++++++++++++++++ 3 files changed, 105 insertions(+), 1 deletion(-) 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 b46c5e6cd334..0912c50fec13 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm @@ -1191,7 +1191,10 @@ - (void)invalidateLayer _outlineLayer.frame = CGRectInset( layer.bounds, -_props->outlineOffset - _props->outlineWidth, -_props->outlineOffset - _props->outlineWidth); - if (areBorderRadiiCircular(borderMetrics.borderRadii) && borderMetrics.borderRadii.topLeft.horizontal == 0) { + // Core Animation can only draw solid contours, so dotted and dashed outlines + // 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); _outlineLayer.borderWidth = _props->outlineWidth; _outlineLayer.borderColor = outlineColor.CGColor; diff --git a/packages/react-native/React/Tests/Mounting/RCTViewComponentViewTests.mm b/packages/react-native/React/Tests/Mounting/RCTViewComponentViewTests.mm index db374b35f10a..91e100d28df1 100644 --- a/packages/react-native/React/Tests/Mounting/RCTViewComponentViewTests.mm +++ b/packages/react-native/React/Tests/Mounting/RCTViewComponentViewTests.mm @@ -9,6 +9,7 @@ #import #import #import +#import using namespace facebook::react; @@ -237,4 +238,47 @@ - (void)testViewWithoutRoleIsNotKeyboardFocusable XCTAssertFalse(view.canBecomeFocused); } +#pragma mark - outline style on square corners (#57841) + +static RCTViewComponentView *makeViewWithOutlineStyle(OutlineStyle outlineStyle) +{ + RCTViewComponentView *view = [RCTViewComponentView new]; + view.frame = CGRectMake(0, 0, 100, 100); + + auto props = std::make_shared(); + props->outlineWidth = 8; + props->outlineColor = colorFromRGBA(255, 165, 0, 255); + props->outlineStyle = outlineStyle; + + [view updateProps:props oldProps:ViewShadowNode::defaultSharedProps()]; + [view finalizeUpdates:RNComponentViewUpdateMaskProps]; + + return view; +} + +- (void)testSquareSolidOutlineUsesCoreAnimationBorder +{ + // Solid outlines keep the cheaper Core Animation path. + RCTViewComponentView *view = makeViewWithOutlineStyle(OutlineStyle::Solid); + CALayer *outlineLayer = [view valueForKey:@"_outlineLayer"]; + + XCTAssertNotNil(outlineLayer); + XCTAssertEqualWithAccuracy(outlineLayer.borderWidth, 8.0, 0.001); + XCTAssertNil(outlineLayer.contents); +} + +- (void)testSquareDottedAndDashedOutlinesAreDrawnWithCoreGraphics +{ + // A view without border radii used to take the Core Animation path, which can + // only render solid contours, so `outlineStyle` was silently dropped. + for (OutlineStyle outlineStyle : {OutlineStyle::Dotted, OutlineStyle::Dashed}) { + RCTViewComponentView *view = makeViewWithOutlineStyle(outlineStyle); + CALayer *outlineLayer = [view valueForKey:@"_outlineLayer"]; + + XCTAssertNotNil(outlineLayer); + XCTAssertEqualWithAccuracy(outlineLayer.borderWidth, 0.0, 0.001); + XCTAssertNotNil(outlineLayer.contents); + } +} + @end diff --git a/packages/rn-tester/js/examples/View/ViewExample.js b/packages/rn-tester/js/examples/View/ViewExample.js index 6069a64a955e..d72f0ec264a6 100644 --- a/packages/rn-tester/js/examples/View/ViewExample.js +++ b/packages/rn-tester/js/examples/View/ViewExample.js @@ -541,6 +541,33 @@ function BoxShadowExample(): React.Node { ); } +function OutlineStyleExample({ + outlineStyle, + borderRadius, + label, +}: { + outlineStyle: 'solid' | 'dashed' | 'dotted', + borderRadius: number, + label: string, +}): React.Node { + return ( + + + {label} + + ); +} + function OutlineExample(): React.Node { const defaultStyleSize = {width: 50, height: 50}; @@ -665,6 +692,36 @@ function OutlineExample(): React.Node { }, ]} /> + + + + + + ); }