Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,11 @@ public BaseViewManager(@Nullable ReactApplicationContext reactContext) {
view.setTag(R.id.accessibility_actions, null);
view.setTag(R.id.accessibility_value, null);
view.setTag(R.id.accessibility_state_expanded, null);
view.setTag(R.id.accessibility_collection, null);
view.setTag(R.id.accessibility_collection_item, null);
view.setTag(R.id.role, null);
view.setTag(R.id.accessibility_order_parent, null);
view.setTag(R.id.original_focusability, null);
view.setTag(R.id.accessibility_state_disabled, null);
view.setEnabled(true);
view.setTag(R.id.view_clipped, null);
Expand Down Expand Up @@ -161,6 +166,17 @@ public BaseViewManager(@Nullable ReactApplicationContext reactContext) {
view.setFocusable(false);
view.setFocusableInTouchMode(false);

// Accessibility state the props apply to the view itself, as a new view has it. The delegate
// is cleared on the view: ViewCompat would install an empty one and mark the view important.
view.setAccessibilityDelegate(null);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure about this one - the accessibility delegate dynamically updates with the state of the view, there should be no need to explicitly reset it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most of what it reports follows the view's tags, but performAccessibilityAction routes through accessibilityActionsMap, which still holds the previous props' actions: a view recycled from one with an activate action dispatches that action on TalkBack's click instead of clicking. testRecycledViewPerformsItsOwnAccessibilityClick in a7847e5 fails without this line.

view.setContentDescription(null);
view.setSelected(false);
view.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_AUTO);
ViewCompat.setAccessibilityLiveRegion(view, ViewCompat.ACCESSIBILITY_LIVE_REGION_NONE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
view.setScreenReaderFocusable(false);
}

// Predictably, alpha defaults to 1:
// https://android.googlesource.com/platform/frameworks/base/+/a175a5b/core/java/android/view/View.java#2186
// This accounts for resetting mBackfaceOpacity and mBackfaceVisibility
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,8 @@ private void initView() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
setFocusable(View.FOCUSABLE_AUTO);
}
// TextView's constructor makes the view important for accessibility
setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);

setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NONE);
updateView(); // call after changing ellipsizeLocation in particular
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ public open class ReactViewGroup public constructor(context: Context?) :
allChildrenCount = 0
clippingRect = null
hitSlopRect = null
axOrderList = null
// pointerEvents setter reads _overflow, so _overflow must be assigned first.
_overflow = Overflow.VISIBLE
pointerEvents = PointerEvents.AUTO
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,23 @@
package com.facebook.react.uimanager

import android.view.View.OnFocusChangeListener
import android.view.accessibility.AccessibilityNodeInfo
import androidx.core.view.ViewCompat
import com.facebook.react.R
import com.facebook.react.bridge.BridgeReactContext
import com.facebook.react.bridge.DynamicFromObject
import com.facebook.react.bridge.JavaOnlyArray
import com.facebook.react.bridge.JavaOnlyMap
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsDefaults
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsForTests
import com.facebook.react.touch.JSResponderHandler
import com.facebook.react.views.view.ReactViewGroup
import com.facebook.react.views.view.ReactViewManager
import com.facebook.testutils.shadows.ShadowArguments
import java.util.Locale
import org.assertj.core.api.Assertions
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
Expand All @@ -41,12 +47,24 @@ class BaseViewManagerTest {
@Before
fun setUp() {
ReactNativeFeatureFlagsForTests.setUp()
ReactNativeFeatureFlags.override(
object : ReactNativeFeatureFlagsDefaults() {
override fun enableViewRecycling(): Boolean = true
},
)
DisplayMetricsHolder.initDisplayMetricsIfNotInitialized(RuntimeEnvironment.getApplication())
viewManager = ReactViewManager()
val context = BridgeReactContext(RuntimeEnvironment.getApplication())
themedReactContext = ThemedReactContext(context, context, null, -1)
view = ReactViewGroup(themedReactContext)
}

@After
fun tearDown() {
DisplayMetricsHolder.setScreenDisplayMetrics(null)
ReactNativeFeatureFlags.dangerouslyReset()
}

@Test
fun testAccessibilityRoleNone() {
viewManager.setAccessibilityRole(view, "none")
Expand Down Expand Up @@ -162,4 +180,144 @@ class BaseViewManagerTest {
viewManager.setAccessibilityLabelledBy(view, DynamicFromObject(JavaOnlyArray()))
Assertions.assertThat(view.getTag(R.id.labelled_by)).isNull()
}

@Test
fun testRecycledViewHasTheAccessibilityStateOfANewView() {
val recycled =
recycleView(
JavaOnlyMap.of(
ViewProps.ACCESSIBILITY_LABEL,
"Dismiss sheet",
ViewProps.ACCESSIBILITY_STATE,
JavaOnlyMap.of("selected", true),
ViewProps.ACCESSIBILITY_LIVE_REGION,
"assertive",
ViewProps.IMPORTANT_FOR_ACCESSIBILITY,
"no-hide-descendants",
ViewProps.SCREEN_READER_FOCUSABLE,
true,
)
)

Assertions.assertThat(recycled.contentDescription).isEqualTo(view.contentDescription)
Assertions.assertThat(recycled.isSelected).isEqualTo(view.isSelected)
Assertions.assertThat(recycled.accessibilityLiveRegion).isEqualTo(view.accessibilityLiveRegion)
Assertions.assertThat(recycled.importantForAccessibility)
.isEqualTo(view.importantForAccessibility)
Assertions.assertThat(recycled.isScreenReaderFocusable).isEqualTo(view.isScreenReaderFocusable)
}

@Test
fun testRecycledViewDropsTheRoleAndCollectionOfItsPreviousProps() {
val recycled =
recycleView(
JavaOnlyMap.of(
ViewProps.ROLE,
"list",
ViewProps.ACCESSIBILITY_COLLECTION,
JavaOnlyMap.of("rowCount", 1, "columnCount", 1, "hierarchical", false),
ViewProps.ACCESSIBILITY_COLLECTION_ITEM,
JavaOnlyMap.of(
"rowIndex",
0,
"rowSpan",
1,
"columnIndex",
0,
"columnSpan",
1,
"heading",
false,
),
)
)

Assertions.assertThat(recycled.getTag(R.id.role)).isNull()
Assertions.assertThat(recycled.getTag(R.id.accessibility_collection)).isNull()
Assertions.assertThat(recycled.getTag(R.id.accessibility_collection_item)).isNull()
}

@Test
fun testRecycledViewWithoutAccessibilityPropsHasNoAccessibilityDelegate() {
val recycled = recycleView(JavaOnlyMap.of(ViewProps.ROLE, "button"))

Assertions.assertThat(ViewCompat.hasAccessibilityDelegate(recycled))
.isEqualTo(ViewCompat.hasAccessibilityDelegate(view))
}

@Test
fun testRecycledViewPerformsItsOwnAccessibilityClick() {
val recycled =
recycleView(
JavaOnlyMap.of(
ViewProps.ACCESSIBILITY_ACTIONS,
JavaOnlyArray.of(JavaOnlyMap.of("name", "activate")),
)
) { previous ->
previous.createAccessibilityNodeInfo()
}
var clicks = 0
recycled.setOnClickListener { clicks++ }

recycled.performAccessibilityAction(AccessibilityNodeInfo.ACTION_CLICK, null)

Assertions.assertThat(clicks).isEqualTo(1)
}

@Test
fun testRecycledViewDropsTheAccessibilityOrderOfItsPreviousProps() {
ReactNativeFeatureFlags.dangerouslyReset()
ReactNativeFeatureFlags.override(
object : ReactNativeFeatureFlagsDefaults() {
override fun enableViewRecycling(): Boolean = true

override fun enableAccessibilityOrder(): Boolean = true
},
)

val recycled =
recycleView(JavaOnlyMap.of(ViewProps.ACCESSIBILITY_ORDER, JavaOnlyArray.of("first")))

Assertions.assertThat(recycled.axOrderList).isNull()
}

@Test
fun testRecycledViewDropsTheAccessibilityOrderItsParentApplied() {
val parent = ReactViewGroup(themedReactContext)

val recycled =
recycleView(JavaOnlyMap()) { previous ->
ReactAxOrderHelper.buildAxOrderList(
previous,
parent,
mutableListOf<String>(),
arrayOfNulls(0),
)
ReactAxOrderHelper.disableFocusForSubtree(previous, mutableListOf<String>())
}

Assertions.assertThat(recycled.getTag(R.id.accessibility_order_parent)).isNull()
Assertions.assertThat(recycled.getTag(R.id.original_focusability)).isNull()
}

private fun recycleView(
previousProps: JavaOnlyMap,
beforeDrop: (ReactViewGroup) -> Unit = {},
): ReactViewGroup {
val previous = createView(previousProps)
beforeDrop(previous)
viewManager.onDropViewInstance(previous)
val recycled = createView(JavaOnlyMap())
Assertions.assertThat(recycled).isSameAs(previous)
return recycled
}

private fun createView(props: JavaOnlyMap): ReactViewGroup =
viewManager.createView(
1,
themedReactContext,
ReactStylesDiffMap(props),
null,
JSResponderHandler(),
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ import com.facebook.react.bridge.BridgeReactContext
import com.facebook.react.bridge.CatalystInstance
import com.facebook.react.bridge.JavaOnlyMap
import com.facebook.react.bridge.ReactTestHelper.createMockCatalystInstance
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsDefaults
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsForTests
import com.facebook.react.touch.JSResponderHandler
import com.facebook.react.uimanager.BackgroundStyleApplicator
import com.facebook.react.uimanager.DisplayMetricsHolder
import com.facebook.react.uimanager.LengthPercentage
Expand Down Expand Up @@ -44,6 +47,11 @@ class ReactTextViewPropertyTest {
@Before
fun setup() {
ReactNativeFeatureFlagsForTests.setUp()
ReactNativeFeatureFlags.override(
object : ReactNativeFeatureFlagsDefaults() {
override fun enableViewRecycling(): Boolean = true
},
)
context = BridgeReactContext(RuntimeEnvironment.getApplication())
catalystInstanceMock = createMockCatalystInstance()
context.initializeWithInstance(catalystInstanceMock)
Expand All @@ -55,6 +63,7 @@ class ReactTextViewPropertyTest {
@After
fun teardown() {
DisplayMetricsHolder.setScreenDisplayMetrics(null)
ReactNativeFeatureFlags.dangerouslyReset()
}

private fun buildStyles(vararg keysAndValues: Any?): ReactStylesDiffMap {
Expand All @@ -78,4 +87,22 @@ class ReactTextViewPropertyTest {
assertThat(BackgroundStyleApplicator.getBorderRadius(view, BorderRadiusProp.BORDER_RADIUS))
.isNull()
}

@Test
fun testRecycledViewIsAsImportantForAccessibilityAsANewView() {
val previous =
manager.createView(
1,
themedContext,
buildStyles("importantForAccessibility", "no-hide-descendants"),
null,
JSResponderHandler(),
)
manager.onDropViewInstance(previous)
val recycled = manager.createView(2, themedContext, buildStyles(), null, JSResponderHandler())

assertThat(recycled).isSameAs(previous)
assertThat(recycled.importantForAccessibility)
.isEqualTo(manager.createViewInstance(themedContext).importantForAccessibility)
}
}
Loading