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
2 changes: 2 additions & 0 deletions packages/react-native/ReactAndroid/api/ReactAndroid.api
Original file line number Diff line number Diff line change
Expand Up @@ -6540,6 +6540,8 @@ public class com/facebook/react/views/view/ReactViewManager : com/facebook/react
public fun setTVPreferredFocus (Lcom/facebook/react/views/view/ReactViewGroup;Z)V
public synthetic fun setTransformProperty (Landroid/view/View;Lcom/facebook/react/bridge/ReadableArray;Lcom/facebook/react/bridge/ReadableArray;)V
protected fun setTransformProperty (Lcom/facebook/react/views/view/ReactViewGroup;Lcom/facebook/react/bridge/ReadableArray;Lcom/facebook/react/bridge/ReadableArray;)V
public synthetic fun setViewState (Landroid/view/View;Lcom/facebook/react/bridge/ReadableMap;)V
public fun setViewState (Lcom/facebook/react/views/view/ReactViewGroup;Lcom/facebook/react/bridge/ReadableMap;)V
}

public final class com/facebook/react/views/view/ReactViewManager$Companion {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ public open class ReactViewGroup public constructor(context: Context?) :
*/
private fun initView() {
clipChildren = false
isEnabled = true

_removeClippedSubviews = false
inSubviewClippingLoop = false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ package com.facebook.react.views.view
import android.graphics.Rect
import android.view.View
import com.facebook.common.logging.FLog
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.Dynamic
import com.facebook.react.bridge.DynamicFromObject
import com.facebook.react.bridge.JSApplicationIllegalArgumentException
Expand Down Expand Up @@ -97,6 +98,26 @@ public open class ReactViewManager : ReactClippingViewManager<ReactViewGroup>()
view.isFocusable = accessible
}

@ReactProp(name = ViewProps.ACCESSIBILITY_STATE)
override fun setViewState(view: ReactViewGroup, accessibilityState: ReadableMap?) {
if (
javaClass != ReactViewManager::class.java ||
accessibilityState != null &&
accessibilityState.hasKey("disabled") &&
!accessibilityState.isNull("disabled")
) {
super.setViewState(view, accessibilityState)
return
}

val normalizedState = Arguments.createMap()
if (accessibilityState != null) {
normalizedState.merge(accessibilityState)
}
normalizedState.putBoolean("disabled", false)
super.setViewState(view, normalizedState)
}

@ReactProp(name = ViewProps.ACCESSIBILITY_ORDER)
public open fun setAccessibilityOrder(view: ReactViewGroup, nativeIds: ReadableArray?) {
if (!ReactNativeFeatureFlags.enableAccessibilityOrder()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@
package com.facebook.react.uimanager

import android.view.View.OnFocusChangeListener
import android.widget.FrameLayout
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.bridge.ReadableMap
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsForTests
import com.facebook.react.views.view.ReactViewGroup
import com.facebook.react.views.view.ReactViewManager
Expand Down Expand Up @@ -67,10 +69,59 @@ class BaseViewManagerTest {
val accessibilityState = JavaOnlyMap()
accessibilityState.putBoolean("selected", true)
viewManager.setViewState(view, accessibilityState)
Assertions.assertThat(view.getTag(R.id.accessibility_state)).isEqualTo(accessibilityState)
Assertions.assertThat(view.getTag(R.id.accessibility_state))
.isEqualTo(JavaOnlyMap.of("selected", true, "disabled", false))
Assertions.assertThat(view.isSelected).isEqualTo(true)
}

@Test
fun testClearingAccessibilityDisabledRestoresTapTarget() {
val root = FrameLayout(themedReactContext)
root.id = 1
root.layout(0, 0, 300, 300)
view.id = 3
root.addView(view)
view.layout(0, 0, 200, 100)

for (state in listOf(JavaOnlyMap(), JavaOnlyMap.of("disabled", null), null)) {
viewManager.setViewState(view, JavaOnlyMap.of("disabled", true))
Assertions.assertThat(TouchTargetHelper.findTargetTagForTouch(20f, 50f, root)).isEqualTo(1)

viewManager.setViewState(view, state)

Assertions.assertThat(view.isEnabled).isTrue()
Assertions.assertThat(TouchTargetHelper.findTargetTagForTouch(20f, 50f, root)).isEqualTo(3)
Assertions.assertThat(
(view.getTag(R.id.accessibility_state) as ReadableMap).getBoolean("disabled")
)
.isFalse()
}
}

@Test
fun testClearingAccessibilityDisabledPreservesOtherState() {
val state = JavaOnlyMap.of("busy", true, "checked", "mixed")
viewManager.setViewState(view, JavaOnlyMap.of("disabled", true))

viewManager.setViewState(view, state)

val nativeState = view.getTag(R.id.accessibility_state) as ReadableMap
Assertions.assertThat(view.isEnabled).isTrue()
Assertions.assertThat(nativeState.getBoolean("busy")).isTrue()
Assertions.assertThat(nativeState.getString("checked")).isEqualTo("mixed")
Assertions.assertThat(state.hasKey("disabled")).isFalse()
}

@Test
fun testViewManagerSubclassPreservesDisabledDuringPartialUpdates() {
val subclassManager = object : ReactViewManager() {}
subclassManager.setViewState(view, JavaOnlyMap.of("disabled", true))

subclassManager.setViewState(view, JavaOnlyMap.of("busy", true))

Assertions.assertThat(view.isEnabled).isFalse()
}

@Test
fun testRoleList() {
viewManager.setRole(view, "list")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,18 @@ class ReactTextInputPropertyTest {
assertThat(view.isEnabled).isTrue
}

@Test
fun testClearingAccessibilityDisabledPreservesNonEditableInput() {
manager.updateProperties(
view,
buildStyles("editable", false, "accessibilityState", JavaOnlyMap.of("disabled", true)),
)

manager.updateProperties(view, buildStyles("accessibilityState", JavaOnlyMap.of("busy", true)))

assertThat(view.isEnabled).isFalse()
}

@Test
fun testAutoCompleteExtendedHints() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,16 @@ class ReactViewGroupTest {
context = Robolectric.buildActivity(Activity::class.java).create().get()
}

@Test
fun `recycling restores enabled state`() {
val view = ReactViewGroup(context)
view.isEnabled = false

view.recycleView()

assertThat(view.isEnabled).isTrue()
}

@Test
fun `View clipping - ensure allChildren properly resizes when adding views in sequence`() {
val rvg = ReactViewGroup(context)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include "HostPlatformViewProps.h"

#include <algorithm>
#include <cstring>

#include <react/renderer/components/view/accessibilityPropsConversions.h>
#include <react/renderer/components/view/conversions.h>
Expand Down Expand Up @@ -842,6 +843,10 @@ folly::dynamic HostPlatformViewProps::getDiffProps(
if (accessibilityState != oldProps->accessibilityState) {
updateAccessibilityStateProp(
result, accessibilityState, oldProps->accessibilityState);
if (std::strcmp(getDiffPropsImplementationTarget(), "View") == 0) {
result["accessibilityState"]["disabled"] =
accessibilityState.has_value() && accessibilityState->disabled;
}
}

if (accessibilityLabel != oldProps->accessibilityLabel) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -185,4 +185,65 @@ TEST(AccessibilityPropsTest, unset_selected_omits_selected_trait) {
hasTrait(props.accessibilityTraits, AccessibilityTraits::Selected));
}

#if defined(ANDROID) && defined(RN_SERIALIZABLE_STATE)

TEST(AccessibilityPropsTest, keeps_view_disabled_during_busy_updates) {
ViewProps oldProps;
oldProps.accessibilityState = AccessibilityState{.disabled = true};
ViewProps newProps = oldProps;
newProps.accessibilityState->busy = true;

auto diff = newProps.getDiffProps(&oldProps);

EXPECT_EQ(diff["accessibilityState"]["disabled"], true);
EXPECT_EQ(diff["accessibilityState"]["busy"], true);
}

TEST(AccessibilityPropsTest, keeps_view_enabled_during_busy_updates) {
ViewProps oldProps;
oldProps.accessibilityState = AccessibilityState{};
ViewProps newProps = oldProps;
newProps.accessibilityState->busy = true;

auto diff = newProps.getDiffProps(&oldProps);

EXPECT_EQ(diff["accessibilityState"]["disabled"], false);
}

TEST(
AccessibilityPropsTest,
removing_view_accessibility_state_clears_disabled) {
ViewProps oldProps;
oldProps.accessibilityState = AccessibilityState{.disabled = true};
ViewProps newProps = oldProps;
newProps.accessibilityState.reset();

auto diff = newProps.getDiffProps(&oldProps);

EXPECT_EQ(diff["accessibilityState"]["disabled"], false);
}

TEST(
AccessibilityPropsTest,
leaves_other_components_disabled_updates_unchanged) {
class CustomViewProps : public ViewProps {
public:
ComponentName getDiffPropsImplementationTarget() const override {
return "CustomView";
}
};

CustomViewProps oldProps;
oldProps.accessibilityState = AccessibilityState{.disabled = true};
CustomViewProps newProps = oldProps;
newProps.accessibilityState->busy = true;

auto diff = newProps.getDiffProps(&oldProps);

EXPECT_EQ(diff["accessibilityState"].count("disabled"), 0);
EXPECT_EQ(diff["accessibilityState"]["busy"], true);
}

#endif

} // namespace facebook::react
Loading