Skip to content

Commit 7df5045

Browse files
Keep a recycled view's accessibility delegate and reset its cached actions
Clearing the whole delegate in prepareToRecycleView also removed the ReactScrollViewAccessibilityDelegate that ReactScrollView, ReactHorizontalScrollView and ReactNestedScrollView install in their constructors, which recycleView never installs again. The delegate now stays, and ReactAccessibilityDelegate.prepareToRecycle() clears the actions it cached from the previous props. accessibilityLabelledBy was only read in the call that set it, so it is a local there.
1 parent a7847e5 commit 7df5045

3 files changed

Lines changed: 54 additions & 16 deletions

File tree

‎packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import androidx.annotation.ColorInt;
2020
import androidx.annotation.NonNull;
2121
import androidx.annotation.Nullable;
22+
import androidx.core.view.AccessibilityDelegateCompat;
2223
import androidx.core.view.ViewCompat;
2324
import com.facebook.common.logging.FLog;
2425
import com.facebook.react.R;
@@ -167,8 +168,11 @@ public BaseViewManager(@Nullable ReactApplicationContext reactContext) {
167168
view.setFocusableInTouchMode(false);
168169

169170
// Accessibility state the props apply to the view itself, as a new view has it. The delegate
170-
// is cleared on the view: ViewCompat would install an empty one and mark the view important.
171-
view.setAccessibilityDelegate(null);
171+
// stays, since a view's constructor may have installed it, and forgets what it cached.
172+
AccessibilityDelegateCompat accessibilityDelegate = ViewCompat.getAccessibilityDelegate(view);
173+
if (accessibilityDelegate instanceof ReactAccessibilityDelegate) {
174+
((ReactAccessibilityDelegate) accessibilityDelegate).internal_prepareToRecycle();
175+
}
172176
view.setContentDescription(null);
173177
view.setSelected(false);
174178
view.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_AUTO);

‎packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactAccessibilityDelegate.kt‎

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,6 @@ public open class ReactAccessibilityDelegate( // The View this delegate is attac
5757
}
5858
}
5959
private val accessibilityActionsMap = HashMap<Int, String?>()
60-
private var accessibilityLabelledBy: View? = null
6160

6261
init {
6362
// We need to reset these two properties, as ExploreByTouchHelper sets focusable to "true" and
@@ -68,6 +67,12 @@ public open class ReactAccessibilityDelegate( // The View this delegate is attac
6867
hostView.importantForAccessibility = originalImportantForAccessibility
6968
}
7069

70+
/** Forgets the actions cached from the props of a view that is about to be recycled. */
71+
@JvmName("internal_prepareToRecycle")
72+
internal fun prepareToRecycle() {
73+
accessibilityActionsMap.clear()
74+
}
75+
7176
override fun onInitializeAccessibilityNodeInfo(host: View, info: AccessibilityNodeInfoCompat) {
7277
super.onInitializeAccessibilityNodeInfo(host, info)
7378

@@ -90,9 +95,9 @@ public open class ReactAccessibilityDelegate( // The View this delegate is attac
9095

9196
val accessibilityLabelledBy = host.getTag(R.id.labelled_by)
9297
if (accessibilityLabelledBy != null) {
93-
this.accessibilityLabelledBy = findView(host.rootView, accessibilityLabelledBy as String)
94-
if (this.accessibilityLabelledBy != null) {
95-
info.setLabeledBy(this.accessibilityLabelledBy)
98+
val labellingView = findView(host.rootView, accessibilityLabelledBy as String)
99+
if (labellingView != null) {
100+
info.setLabeledBy(labellingView)
96101
}
97102
}
98103

‎packages/react-native/ReactAndroid/src/test/java/com/facebook/react/uimanager/BaseViewManagerTest.kt‎

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
package com.facebook.react.uimanager
1111

12+
import android.view.View
1213
import android.view.View.OnFocusChangeListener
1314
import android.view.accessibility.AccessibilityNodeInfo
1415
import androidx.core.view.ViewCompat
@@ -21,6 +22,10 @@ import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags
2122
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsDefaults
2223
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsForTests
2324
import com.facebook.react.touch.JSResponderHandler
25+
import com.facebook.react.views.scroll.ReactHorizontalScrollViewManager
26+
import com.facebook.react.views.scroll.ReactNestedScrollViewManager
27+
import com.facebook.react.views.scroll.ReactScrollViewAccessibilityDelegate
28+
import com.facebook.react.views.scroll.ReactScrollViewManager
2429
import com.facebook.react.views.view.ReactViewGroup
2530
import com.facebook.react.views.view.ReactViewManager
2631
import com.facebook.testutils.shadows.ShadowArguments
@@ -238,11 +243,27 @@ class BaseViewManagerTest {
238243
}
239244

240245
@Test
241-
fun testRecycledViewWithoutAccessibilityPropsHasNoAccessibilityDelegate() {
242-
val recycled = recycleView(JavaOnlyMap.of(ViewProps.ROLE, "button"))
246+
fun testRecycledScrollViewsKeepTheAccessibilityDelegateTheirConstructorsInstall() {
247+
ReactNativeFeatureFlags.dangerouslyReset()
248+
ReactNativeFeatureFlags.override(
249+
object : ReactNativeFeatureFlagsDefaults() {
250+
override fun enableViewRecycling(): Boolean = true
251+
252+
override fun enableViewRecyclingForScrollView(): Boolean = true
253+
},
254+
)
255+
256+
val recycledScrollViews =
257+
listOf(
258+
recycleView(ReactScrollViewManager(), JavaOnlyMap()),
259+
recycleView(ReactHorizontalScrollViewManager(), JavaOnlyMap()),
260+
recycleView(ReactNestedScrollViewManager(), JavaOnlyMap()),
261+
)
243262

244-
Assertions.assertThat(ViewCompat.hasAccessibilityDelegate(recycled))
245-
.isEqualTo(ViewCompat.hasAccessibilityDelegate(view))
263+
for (recycled in recycledScrollViews) {
264+
Assertions.assertThat(ViewCompat.getAccessibilityDelegate(recycled))
265+
.isInstanceOf(ReactScrollViewAccessibilityDelegate::class.java)
266+
}
246267
}
247268

248269
@Test
@@ -303,17 +324,25 @@ class BaseViewManagerTest {
303324
private fun recycleView(
304325
previousProps: JavaOnlyMap,
305326
beforeDrop: (ReactViewGroup) -> Unit = {},
306-
): ReactViewGroup {
307-
val previous = createView(previousProps)
327+
): ReactViewGroup = recycleView(viewManager, previousProps, beforeDrop)
328+
329+
private fun <T : View> recycleView(
330+
manager: ViewManager<T, *>,
331+
previousProps: JavaOnlyMap,
332+
beforeDrop: (T) -> Unit = {},
333+
): T {
334+
val previous = createView(manager, previousProps)
308335
beforeDrop(previous)
309-
viewManager.onDropViewInstance(previous)
310-
val recycled = createView(JavaOnlyMap())
336+
manager.onDropViewInstance(previous)
337+
val recycled = createView(manager, JavaOnlyMap())
311338
Assertions.assertThat(recycled).isSameAs(previous)
312339
return recycled
313340
}
314341

315-
private fun createView(props: JavaOnlyMap): ReactViewGroup =
316-
viewManager.createView(
342+
private fun createView(props: JavaOnlyMap): ReactViewGroup = createView(viewManager, props)
343+
344+
private fun <T : View> createView(manager: ViewManager<T, *>, props: JavaOnlyMap): T =
345+
manager.createView(
317346
1,
318347
themedReactContext,
319348
ReactStylesDiffMap(props),

0 commit comments

Comments
 (0)