Skip to content

Commit a443286

Browse files
committed
Fix crash when setting a percentage borderRadius on Text, TextInput, and ScrollView (Android)
Percentage border radii arrive from JS as strings ('50%'), but the borderRadius @ReactPropGroup setters in ReactTextViewManager, PreparedLayoutTextViewManager, ReactTextInputManager, ReactScrollViewManager, ReactHorizontalScrollViewManager, and ReactNestedScrollViewManager still typed the prop as Float, so the reflection-based property updater crashed with "java.lang.String cannot be cast to java.lang.Double": JSApplicationIllegalArgumentException: Error while updating property 'borderRadius' of a view managed by: RCTText Accept a Dynamic and parse it with LengthPercentage.setFromDynamic, completing the migration ReactViewManager received in 0.75 and ReactImageManager in #57795. The Float overloads on the public managers are kept as deprecated pass-throughs for backward compatibility; the two internal managers (PreparedLayoutTextViewManager, ReactNestedScrollViewManager) are migrated outright. Changelog: [ANDROID] [FIXED] - Fix crash when setting a percentage borderRadius on Text, TextInput, and ScrollView
1 parent 4bf9d4e commit a443286

9 files changed

Lines changed: 165 additions & 28 deletions

File tree

packages/react-native/ReactAndroid/api/ReactAndroid.api

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5594,6 +5594,7 @@ public class com/facebook/react/views/scroll/ReactHorizontalScrollViewManager :
55945594
public synthetic fun scrollToEnd (Ljava/lang/Object;Lcom/facebook/react/views/scroll/ReactScrollViewCommandHelper$ScrollToEndCommandData;)V
55955595
public final fun setBorderColor (Lcom/facebook/react/views/scroll/ReactHorizontalScrollView;ILjava/lang/Integer;)V
55965596
public final fun setBorderRadius (Lcom/facebook/react/views/scroll/ReactHorizontalScrollView;IF)V
5597+
public final fun setBorderRadius (Lcom/facebook/react/views/scroll/ReactHorizontalScrollView;ILcom/facebook/react/bridge/Dynamic;)V
55975598
public final fun setBorderStyle (Lcom/facebook/react/views/scroll/ReactHorizontalScrollView;Ljava/lang/String;)V
55985599
public final fun setBorderWidth (Lcom/facebook/react/views/scroll/ReactHorizontalScrollView;IF)V
55995600
public final fun setBottomFillColor (Lcom/facebook/react/views/scroll/ReactHorizontalScrollView;I)V
@@ -5889,6 +5890,7 @@ public class com/facebook/react/views/scroll/ReactScrollViewManager : com/facebo
58895890
public synthetic fun scrollToEnd (Ljava/lang/Object;Lcom/facebook/react/views/scroll/ReactScrollViewCommandHelper$ScrollToEndCommandData;)V
58905891
public final fun setBorderColor (Lcom/facebook/react/views/scroll/ReactScrollView;ILjava/lang/Integer;)V
58915892
public final fun setBorderRadius (Lcom/facebook/react/views/scroll/ReactScrollView;IF)V
5893+
public final fun setBorderRadius (Lcom/facebook/react/views/scroll/ReactScrollView;ILcom/facebook/react/bridge/Dynamic;)V
58925894
public final fun setBorderStyle (Lcom/facebook/react/views/scroll/ReactScrollView;Ljava/lang/String;)V
58935895
public final fun setBorderWidth (Lcom/facebook/react/views/scroll/ReactScrollView;IF)V
58945896
public final fun setBottomFillColor (Lcom/facebook/react/views/scroll/ReactScrollView;I)V
@@ -6084,6 +6086,7 @@ public class com/facebook/react/views/text/ReactTextViewManager : com/facebook/r
60846086
public final fun setAndroidHyphenationFrequency (Lcom/facebook/react/views/text/ReactTextView;Ljava/lang/String;)V
60856087
public final fun setBorderColor (Lcom/facebook/react/views/text/ReactTextView;ILjava/lang/Integer;)V
60866088
public final fun setBorderRadius (Lcom/facebook/react/views/text/ReactTextView;IF)V
6089+
public final fun setBorderRadius (Lcom/facebook/react/views/text/ReactTextView;ILcom/facebook/react/bridge/Dynamic;)V
60876090
public final fun setBorderStyle (Lcom/facebook/react/views/text/ReactTextView;Ljava/lang/String;)V
60886091
public final fun setBorderWidth (Lcom/facebook/react/views/text/ReactTextView;IF)V
60896092
public final fun setDataDetectorType (Lcom/facebook/react/views/text/ReactTextView;Ljava/lang/String;)V
@@ -6346,6 +6349,7 @@ public class com/facebook/react/views/textinput/ReactTextInputManager : com/face
63466349
public final fun setAutoFocus (Lcom/facebook/react/views/textinput/ReactEditText;Z)V
63476350
public final fun setBorderColor (Lcom/facebook/react/views/textinput/ReactEditText;ILjava/lang/Integer;)V
63486351
public final fun setBorderRadius (Lcom/facebook/react/views/textinput/ReactEditText;IF)V
6352+
public final fun setBorderRadius (Lcom/facebook/react/views/textinput/ReactEditText;ILcom/facebook/react/bridge/Dynamic;)V
63496353
public final fun setBorderStyle (Lcom/facebook/react/views/textinput/ReactEditText;Ljava/lang/String;)V
63506354
public final fun setBorderWidth (Lcom/facebook/react/views/textinput/ReactEditText;IF)V
63516355
public final fun setCaretHidden (Lcom/facebook/react/views/textinput/ReactEditText;Z)V

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollViewManager.kt

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -282,9 +282,24 @@ constructor(private val fpsListener: FpsListener? = null) :
282282
ViewProps.BORDER_BOTTOM_RIGHT_RADIUS,
283283
ViewProps.BORDER_BOTTOM_LEFT_RADIUS,
284284
],
285-
defaultFloat = Float.NaN,
285+
)
286+
public fun setBorderRadius(
287+
view: ReactHorizontalScrollView?,
288+
index: Int,
289+
rawBorderRadius: Dynamic,
290+
) {
291+
if (view != null) {
292+
val borderRadius = LengthPercentage.setFromDynamic(rawBorderRadius)
293+
setBorderRadius(view, BorderRadiusProp.entries[index], borderRadius)
294+
}
295+
}
296+
297+
@Deprecated(
298+
"Don't use setBorderRadius(view, index, Float) as it was deprecated in React Native 0.88.0.",
286299
)
287300
public fun setBorderRadius(view: ReactHorizontalScrollView?, index: Int, borderRadius: Float) {
301+
// Direct body: DynamicFromObject(Float).asDouble() throws, and setFromDynamic
302+
// would not map NaN back to null like the original Float path did.
288303
if (view != null) {
289304
val radius =
290305
if (borderRadius.isNaN()) null

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactNestedScrollViewManager.kt

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ import com.facebook.react.uimanager.BackgroundStyleApplicator.setBorderRadius
3030
import com.facebook.react.uimanager.BackgroundStyleApplicator.setBorderStyle
3131
import com.facebook.react.uimanager.BackgroundStyleApplicator.setBorderWidth
3232
import com.facebook.react.uimanager.LengthPercentage
33-
import com.facebook.react.uimanager.LengthPercentageType
3433
import com.facebook.react.uimanager.PixelUtil.dpToPx
3534
import com.facebook.react.uimanager.PixelUtil.getDisplayMetricDensity
3635
import com.facebook.react.uimanager.PointerEvents.Companion.parsePointerEvents
@@ -255,14 +254,11 @@ constructor(private val fpsListener: FpsListener? = null) :
255254
ViewProps.BORDER_BOTTOM_RIGHT_RADIUS,
256255
ViewProps.BORDER_BOTTOM_LEFT_RADIUS,
257256
],
258-
defaultFloat = Float.NaN,
259257
)
260-
public fun setBorderRadius(view: ReactNestedScrollView?, index: Int, borderRadius: Float) {
258+
public fun setBorderRadius(view: ReactNestedScrollView?, index: Int, rawBorderRadius: Dynamic) {
261259
if (view != null) {
262-
val radius =
263-
if (borderRadius.isNaN()) null
264-
else LengthPercentage(borderRadius, LengthPercentageType.POINT)
265-
setBorderRadius(view, BorderRadiusProp.entries[index], radius)
260+
val borderRadius = LengthPercentage.setFromDynamic(rawBorderRadius)
261+
setBorderRadius(view, BorderRadiusProp.entries[index], borderRadius)
266262
}
267263
}
268264

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewManager.kt

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,9 +247,20 @@ constructor(private val fpsListener: FpsListener? = null) :
247247
ViewProps.BORDER_BOTTOM_RIGHT_RADIUS,
248248
ViewProps.BORDER_BOTTOM_LEFT_RADIUS,
249249
],
250-
defaultFloat = Float.NaN,
250+
)
251+
public fun setBorderRadius(view: ReactScrollView?, index: Int, rawBorderRadius: Dynamic) {
252+
if (view != null) {
253+
val borderRadius = LengthPercentage.setFromDynamic(rawBorderRadius)
254+
setBorderRadius(view, BorderRadiusProp.entries[index], borderRadius)
255+
}
256+
}
257+
258+
@Deprecated(
259+
"Don't use setBorderRadius(view, index, Float) as it was deprecated in React Native 0.88.0.",
251260
)
252261
public fun setBorderRadius(view: ReactScrollView?, index: Int, borderRadius: Float) {
262+
// Direct body: DynamicFromObject(Float).asDouble() throws, and setFromDynamic
263+
// would not map NaN back to null like the original Float path did.
253264
if (view != null) {
254265
val radius =
255266
if (borderRadius.isNaN()) null

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/PreparedLayoutTextViewManager.kt

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,14 @@ import android.text.Spannable
1313
import android.text.Spanned
1414
import android.view.View
1515
import com.facebook.react.R
16+
import com.facebook.react.bridge.Dynamic
1617
import com.facebook.react.internal.SystraceSection
1718
import com.facebook.react.module.annotations.ReactModule
1819
import com.facebook.react.uimanager.BackgroundStyleApplicator
1920
import com.facebook.react.uimanager.BaseViewManager
2021
import com.facebook.react.uimanager.IViewGroupManager
2122
import com.facebook.react.uimanager.LayoutShadowNode
2223
import com.facebook.react.uimanager.LengthPercentage
23-
import com.facebook.react.uimanager.LengthPercentageType
2424
import com.facebook.react.uimanager.PointerEvents
2525
import com.facebook.react.uimanager.ReactStylesDiffMap
2626
import com.facebook.react.uimanager.ReferenceStateWrapper
@@ -149,13 +149,10 @@ internal class PreparedLayoutTextViewManager :
149149
ViewProps.BORDER_BOTTOM_RIGHT_RADIUS,
150150
ViewProps.BORDER_BOTTOM_LEFT_RADIUS,
151151
],
152-
defaultFloat = Float.NaN,
153152
)
154-
fun setBorderRadius(view: PreparedLayoutTextView, index: Int, borderRadius: Float): Unit {
155-
val radius =
156-
if (borderRadius.isNaN()) null
157-
else LengthPercentage(borderRadius, LengthPercentageType.POINT)
158-
BackgroundStyleApplicator.setBorderRadius(view, BorderRadiusProp.values()[index], radius)
153+
fun setBorderRadius(view: PreparedLayoutTextView, index: Int, rawBorderRadius: Dynamic): Unit {
154+
val borderRadius = LengthPercentage.setFromDynamic(rawBorderRadius)
155+
BackgroundStyleApplicator.setBorderRadius(view, BorderRadiusProp.values()[index], borderRadius)
159156
}
160157

161158
@ReactProp(name = "borderStyle")

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextViewManager.kt

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import android.text.util.Linkify
1919
import android.view.Gravity
2020
import com.facebook.common.logging.FLog
2121
import com.facebook.react.R
22+
import com.facebook.react.bridge.Dynamic
2223
import com.facebook.react.common.ReactConstants
2324
import com.facebook.react.common.annotations.UnstableReactNativeAPI
2425
import com.facebook.react.common.mapbuffer.MapBuffer
@@ -335,15 +336,21 @@ public constructor(
335336
ViewProps.BORDER_BOTTOM_RIGHT_RADIUS,
336337
ViewProps.BORDER_BOTTOM_LEFT_RADIUS,
337338
],
338-
defaultFloat = Float.NaN,
339+
)
340+
public fun setBorderRadius(view: ReactTextView, index: Int, rawBorderRadius: Dynamic) {
341+
val borderRadius = LengthPercentage.setFromDynamic(rawBorderRadius)
342+
BackgroundStyleApplicator.setBorderRadius(view, BorderRadiusProp.values()[index], borderRadius)
343+
}
344+
345+
@Deprecated(
346+
"Don't use setBorderRadius(view, index, Float) as it was deprecated in React Native 0.88.0.",
339347
)
340348
public fun setBorderRadius(view: ReactTextView, index: Int, borderRadius: Float) {
349+
// Direct body: DynamicFromObject(Float).asDouble() throws, and setFromDynamic
350+
// would not map NaN back to null like the original Float path did.
341351
val radius =
342-
if (borderRadius.isNaN()) {
343-
null
344-
} else {
345-
LengthPercentage(borderRadius, LengthPercentageType.POINT)
346-
}
352+
if (borderRadius.isNaN()) null
353+
else LengthPercentage(borderRadius, LengthPercentageType.POINT)
347354
BackgroundStyleApplicator.setBorderRadius(view, BorderRadiusProp.values()[index], radius)
348355
}
349356

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.kt

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -802,15 +802,21 @@ public open class ReactTextInputManager public constructor() :
802802
ViewProps.BORDER_BOTTOM_RIGHT_RADIUS,
803803
ViewProps.BORDER_BOTTOM_LEFT_RADIUS,
804804
],
805-
defaultFloat = Float.NaN,
805+
)
806+
public fun setBorderRadius(view: ReactEditText, index: Int, rawBorderRadius: Dynamic) {
807+
val borderRadius = LengthPercentage.setFromDynamic(rawBorderRadius)
808+
setBorderRadius(view, BorderRadiusProp.entries[index], borderRadius)
809+
}
810+
811+
@Deprecated(
812+
"Don't use setBorderRadius(view, index, Float) as it was deprecated in React Native 0.88.0.",
806813
)
807814
public fun setBorderRadius(view: ReactEditText, index: Int, borderRadius: Float) {
815+
// Direct body: DynamicFromObject(Float).asDouble() throws, and setFromDynamic
816+
// would not map NaN back to null like the original Float path did.
808817
val radius =
809-
if (borderRadius.isNaN()) {
810-
null
811-
} else {
812-
LengthPercentage(borderRadius, LengthPercentageType.POINT)
813-
}
818+
if (borderRadius.isNaN()) null
819+
else LengthPercentage(borderRadius, LengthPercentageType.POINT)
814820
setBorderRadius(view, BorderRadiusProp.entries[index], radius)
815821
}
816822

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
// TODO T207169925: Migrate CatalystInstance to Reacthost and remove the Suppress("DEPRECATION")
9+
// annotation
10+
@file:Suppress("DEPRECATION")
11+
12+
package com.facebook.react.views.text
13+
14+
import android.util.DisplayMetrics
15+
import com.facebook.react.bridge.BridgeReactContext
16+
import com.facebook.react.bridge.CatalystInstance
17+
import com.facebook.react.bridge.JavaOnlyMap
18+
import com.facebook.react.bridge.ReactTestHelper.createMockCatalystInstance
19+
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsForTests
20+
import com.facebook.react.uimanager.BackgroundStyleApplicator
21+
import com.facebook.react.uimanager.DisplayMetricsHolder
22+
import com.facebook.react.uimanager.LengthPercentage
23+
import com.facebook.react.uimanager.LengthPercentageType
24+
import com.facebook.react.uimanager.ReactStylesDiffMap
25+
import com.facebook.react.uimanager.ThemedReactContext
26+
import com.facebook.react.uimanager.style.BorderRadiusProp
27+
import org.assertj.core.api.Assertions.assertThat
28+
import org.junit.After
29+
import org.junit.Before
30+
import org.junit.Test
31+
import org.junit.runner.RunWith
32+
import org.robolectric.RobolectricTestRunner
33+
import org.robolectric.RuntimeEnvironment
34+
35+
/** Verify view properties are being applied correctly by [ReactTextViewManager] */
36+
@RunWith(RobolectricTestRunner::class)
37+
class ReactTextViewPropertyTest {
38+
39+
private lateinit var context: BridgeReactContext
40+
private lateinit var catalystInstanceMock: CatalystInstance
41+
private lateinit var themedContext: ThemedReactContext
42+
private lateinit var manager: ReactTextViewManager
43+
44+
@Before
45+
fun setup() {
46+
ReactNativeFeatureFlagsForTests.setUp()
47+
context = BridgeReactContext(RuntimeEnvironment.getApplication())
48+
catalystInstanceMock = createMockCatalystInstance()
49+
context.initializeWithInstance(catalystInstanceMock)
50+
themedContext = ThemedReactContext(context, context, null, -1)
51+
manager = ReactTextViewManager()
52+
DisplayMetricsHolder.setScreenDisplayMetrics(DisplayMetrics())
53+
}
54+
55+
@After
56+
fun teardown() {
57+
DisplayMetricsHolder.setScreenDisplayMetrics(null)
58+
}
59+
60+
private fun buildStyles(vararg keysAndValues: Any?): ReactStylesDiffMap {
61+
return ReactStylesDiffMap(JavaOnlyMap.of(*keysAndValues))
62+
}
63+
64+
@Test
65+
fun testBorderRadius() {
66+
val view = manager.createViewInstance(themedContext)
67+
68+
// Percentage border radii arrive as strings and must not crash the property updater
69+
manager.updateProperties(view, buildStyles("borderRadius", "50%"))
70+
assertThat(BackgroundStyleApplicator.getBorderRadius(view, BorderRadiusProp.BORDER_RADIUS))
71+
.isEqualTo(LengthPercentage(50f, LengthPercentageType.PERCENT))
72+
73+
manager.updateProperties(view, buildStyles("borderRadius", 10.0))
74+
assertThat(BackgroundStyleApplicator.getBorderRadius(view, BorderRadiusProp.BORDER_RADIUS))
75+
.isEqualTo(LengthPercentage(10f, LengthPercentageType.POINT))
76+
77+
manager.updateProperties(view, buildStyles("borderRadius", null))
78+
assertThat(BackgroundStyleApplicator.getBorderRadius(view, BorderRadiusProp.BORDER_RADIUS))
79+
.isNull()
80+
}
81+
}

packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/textinput/ReactTextInputPropertyTest.kt

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,13 @@ import com.facebook.react.bridge.CatalystInstance
3030
import com.facebook.react.bridge.JavaOnlyMap
3131
import com.facebook.react.bridge.ReactTestHelper.createMockCatalystInstance
3232
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsForTests
33+
import com.facebook.react.uimanager.BackgroundStyleApplicator
3334
import com.facebook.react.uimanager.DisplayMetricsHolder
35+
import com.facebook.react.uimanager.LengthPercentage
36+
import com.facebook.react.uimanager.LengthPercentageType
3437
import com.facebook.react.uimanager.ReactStylesDiffMap
3538
import com.facebook.react.uimanager.ThemedReactContext
39+
import com.facebook.react.uimanager.style.BorderRadiusProp
3640
import com.facebook.react.views.text.DefaultStyleValuesUtil.getDefaultTextColorHint
3741
import com.facebook.react.views.text.ReactTextUpdate
3842
import org.assertj.core.api.Assertions.assertThat
@@ -521,6 +525,22 @@ class ReactTextInputPropertyTest {
521525
assertThat(checkNotNull(view.text).getSpans(0, view.length(), MarkerSpan::class.java)).isEmpty()
522526
}
523527

528+
@Test
529+
fun testBorderRadius() {
530+
// Percentage border radii arrive as strings and must not crash the property updater
531+
manager.updateProperties(view, buildStyles("borderRadius", "50%"))
532+
assertThat(BackgroundStyleApplicator.getBorderRadius(view, BorderRadiusProp.BORDER_RADIUS))
533+
.isEqualTo(LengthPercentage(50f, LengthPercentageType.PERCENT))
534+
535+
manager.updateProperties(view, buildStyles("borderRadius", 10.0))
536+
assertThat(BackgroundStyleApplicator.getBorderRadius(view, BorderRadiusProp.BORDER_RADIUS))
537+
.isEqualTo(LengthPercentage(10f, LengthPercentageType.POINT))
538+
539+
manager.updateProperties(view, buildStyles("borderRadius", null))
540+
assertThat(BackgroundStyleApplicator.getBorderRadius(view, BorderRadiusProp.BORDER_RADIUS))
541+
.isNull()
542+
}
543+
524544
private fun buildStyles(vararg keysAndValues: Any?): ReactStylesDiffMap {
525545
return ReactStylesDiffMap(JavaOnlyMap.of(*keysAndValues))
526546
}

0 commit comments

Comments
 (0)