Repro for #57780: removing borderWidth hides children of a clipped rounded View on Android - #57781
Conversation
…ed rounded view on Android
|
Hi @SatyamBansal! Thank you for your pull request and welcome to our community. Action RequiredIn order to merge any pull request (code, docs, etc.), we require contributors to sign our Contributor License Agreement, and we don't seem to have one on file for you. ProcessIn order for us to review and merge your suggested changes, please sign at https://code.facebook.com/cla. If you are contributing on behalf of someone else (eg your employer), the individual CLA may not be sufficient and your employer may need to sign the corporate CLA. Once the CLA is signed, our tooling will perform checks and validations. Afterwards, the pull request will be tagged with If you have received this in error or have any questions, please contact us at cla@meta.com. Thanks! |
Summary:
Reproducer for #57780, as requested by
react-native-bot(Needs: Repro). Not intended to be merged — it editsRNTesterPlayground.jsper the How to report a bug guidance.On Android, a
ViewwithborderRadiusandoverflow: 'hidden'stops drawing all of its children once itsborderWidthprop is removed — as opposed to being set to0. The view's own background keeps painting, so the row renders as a correctly sized, correctly rounded, completely empty box. The children remain mounted and laid out (the a11y tree still reports their labels and frames); they just are not drawn. Re-addingborderWidthrestores them.Suspected cause, traced in the issue:
ReactViewManagerdeclares the borderWidth prop group withdefaultFloat = Float.NaN, so removing the prop calls the setter withNaN(the parameter is a primitiveFloat, so it can never benullthere).BackgroundStyleApplicator.setBorderWidthdocumentsnullas the removal signal but receivesNaN, stores it, and allocatesBorderInsetson that same call.BorderInsets.resolve()is an elvis chain ending in?: 0f, which only fires onnull— soNaNpasses through and it returnsRectF(NaN, NaN, NaN, NaN).clipToPaddingBoxWithAntiAliasingsubtracts those insets fromcomposite.bounds, producing an all-NaNpadding box; withborderRadiusset it becomes aPathinstalled viacanvas.clipPath(), which clips every child away.ReactViewGroup.dispatchDrawonly applies that clip when overflow is notvisible, which is whyoverflow: 'hidden'is required. The background survives because background drawables paint inView.draw(), beforedispatchDraw().iOS is unaffected:
RCTViewuses_borderWidth = -1as its unset sentinel and clamps withMAX(0, _borderWidth). A negative sentinel is neutralised by arithmetic;NaNpropagates.This pattern is common in "selectable card" UI, where the selected style adds
borderWidth: 1and the unselected style omits it.Changelog:
[INTERNAL] [CHANGED] - RNTesterPlayground repro for #57780 (not for merge)
Test Plan:
Open RNTester on Android and go to the Playground example. Two groups of three rows:
borderWidth: 0— tap "Row B" → "Row A" keeps its text, as expected.Group B is the control that isolates the cause.
borderWidth: 0and "noborderWidthat all" produce an identical 1px geometry change, so this is not a layout or measurement problem — only the removed prop triggers it. The broken state is also sticky: scrolling the row out of view and back does not restore the text, only re-adding aborderWidthdoes.On iOS, both groups behave correctly.
Verification provenance
To be precise about what I actually ran: I verified this on a fresh
@react-native-community/cli initapp on 0.86.0, release build, on a Pixel 8 (Android 17, SDK 37, Hermes, New Architecture) — no third-party libraries, no patches to React Native, noReactNativeFeatureFlagsoverrides. Four variants were compared there:overflow: hiddenborderWidthNaN)0explicit1constant, only colour togglesNaN)I have not built RNTester from
mainon Android myself, so this Playground patch is a faithful transcription of that verified reproducer rather than an independently re-run one. What I did check onmainis the source:BorderInsets.resolve()still has only the null fallback and noNaNguard, on bothv0.86.2andmain, so the defect should still be present.Suggested fix
Treat
NaNas unset inBorderInsets— either?.takeUnless { it.isNaN() }per edge inresolve(), or normaliseNaNtonullinsetBorderWidthso the stored value matches that function's own documented contract. The latter fixes every consumer ofBorderInsetsrather than only the clip path.