Skip to content
Closed
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 @@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<eb0bafceba60cb439a4be3d68b890725>>
* @generated SignedSource<<35388e99bb5b778abade6d1d435eb797>>
*/

/**
Expand Down Expand Up @@ -324,6 +324,12 @@ public object ReactNativeFeatureFlags {
@JvmStatic
public fun enablePropsUpdateReconciliationAndroid(): Boolean = accessor.enablePropsUpdateReconciliationAndroid()

/**
* When enabled, inline text with a link-like role produces a ClickableSpan and appears in the TalkBack links rotor.
*/
@JvmStatic
public fun enableRelaxedLinkRole(): Boolean = accessor.enableRelaxedLinkRole()

/**
* Enables the ResizeObserver Web API in React Native.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<0107593110db613063b6f4ceb50d8e58>>
* @generated SignedSource<<407328ccc8d2611b10d7c39458b859f9>>
*/

/**
Expand Down Expand Up @@ -69,6 +69,7 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces
private var enablePreallocatedPropsDiffOnInsertAndroidCache: Boolean? = null
private var enablePreparedTextLayoutCache: Boolean? = null
private var enablePropsUpdateReconciliationAndroidCache: Boolean? = null
private var enableRelaxedLinkRoleCache: Boolean? = null
private var enableResizeObserverByDefaultCache: Boolean? = null
private var enableSwiftUIBasedFiltersCache: Boolean? = null
private var enableViewCullingCache: Boolean? = null
Expand Down Expand Up @@ -555,6 +556,15 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces
return cached
}

override fun enableRelaxedLinkRole(): Boolean {
var cached = enableRelaxedLinkRoleCache
if (cached == null) {
cached = ReactNativeFeatureFlagsCxxInterop.enableRelaxedLinkRole()
enableRelaxedLinkRoleCache = cached
}
return cached
}

override fun enableResizeObserverByDefault(): Boolean {
var cached = enableResizeObserverByDefaultCache
if (cached == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<fa0374808e3cbe7122ca993780c0b627>>
* @generated SignedSource<<5bda11ded6763e8ed9c953203a619662>>
*/

/**
Expand Down Expand Up @@ -126,6 +126,8 @@ public object ReactNativeFeatureFlagsCxxInterop {

@DoNotStrip @JvmStatic public external fun enablePropsUpdateReconciliationAndroid(): Boolean

@DoNotStrip @JvmStatic public external fun enableRelaxedLinkRole(): Boolean

@DoNotStrip @JvmStatic public external fun enableResizeObserverByDefault(): Boolean

@DoNotStrip @JvmStatic public external fun enableSwiftUIBasedFilters(): Boolean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<768077a22c110aa98ab02bc96eec695b>>
* @generated SignedSource<<7f1fbd35f6088e754c3a5b74cb8c579a>>
*/

/**
Expand Down Expand Up @@ -121,6 +121,8 @@ public open class ReactNativeFeatureFlagsDefaults : ReactNativeFeatureFlagsProvi

override fun enablePropsUpdateReconciliationAndroid(): Boolean = false

override fun enableRelaxedLinkRole(): Boolean = false

override fun enableResizeObserverByDefault(): Boolean = false

override fun enableSwiftUIBasedFilters(): Boolean = false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<86ebc7307e7ebe2023e909a868ed3117>>
* @generated SignedSource<<33d7cb35437e6bd722507abeaac7845e>>
*/

/**
Expand Down Expand Up @@ -73,6 +73,7 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc
private var enablePreallocatedPropsDiffOnInsertAndroidCache: Boolean? = null
private var enablePreparedTextLayoutCache: Boolean? = null
private var enablePropsUpdateReconciliationAndroidCache: Boolean? = null
private var enableRelaxedLinkRoleCache: Boolean? = null
private var enableResizeObserverByDefaultCache: Boolean? = null
private var enableSwiftUIBasedFiltersCache: Boolean? = null
private var enableViewCullingCache: Boolean? = null
Expand Down Expand Up @@ -608,6 +609,16 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc
return cached
}

override fun enableRelaxedLinkRole(): Boolean {
var cached = enableRelaxedLinkRoleCache
if (cached == null) {
cached = currentProvider.enableRelaxedLinkRole()
accessedFeatureFlags.add("enableRelaxedLinkRole")
enableRelaxedLinkRoleCache = cached
}
return cached
}

override fun enableResizeObserverByDefault(): Boolean {
var cached = enableResizeObserverByDefaultCache
if (cached == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<c83fba10dc6f7d596e3bcd9725c64e6e>>
* @generated SignedSource<<a4bf2736bf202873ffbfbcc9ddcb970c>>
*/

/**
Expand Down Expand Up @@ -121,6 +121,8 @@ public interface ReactNativeFeatureFlagsProvider {

@DoNotStrip public fun enablePropsUpdateReconciliationAndroid(): Boolean

@DoNotStrip public fun enableRelaxedLinkRole(): Boolean

@DoNotStrip public fun enableResizeObserverByDefault(): Boolean

@DoNotStrip public fun enableSwiftUIBasedFilters(): Boolean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,21 @@ internal object TextLayoutManager {
}
}

private fun isRoleLinkLike(
role: ReactAccessibilityDelegate.Role?,
accessibilityRole: ReactAccessibilityDelegate.AccessibilityRole?,
): Boolean {
if (!ReactNativeFeatureFlags.enableRelaxedLinkRole()) {
return if (role != null) (role == ReactAccessibilityDelegate.Role.LINK)
else (accessibilityRole == ReactAccessibilityDelegate.AccessibilityRole.LINK)
}

return role == ReactAccessibilityDelegate.Role.LINK ||
accessibilityRole == ReactAccessibilityDelegate.AccessibilityRole.LINK ||
role == ReactAccessibilityDelegate.Role.BUTTON ||
accessibilityRole == ReactAccessibilityDelegate.AccessibilityRole.BUTTON
}

@OptIn(UnstableReactNativeAPI::class)
private fun buildSpannableFromFragments(
assets: AssetManager,
Expand Down Expand Up @@ -334,13 +349,7 @@ internal object TextLayoutManager {
),
)
} else if (end >= start) {
val roleIsLink =
if (textAttributes.role != null)
(textAttributes.role == ReactAccessibilityDelegate.Role.LINK)
else
(textAttributes.accessibilityRole ==
ReactAccessibilityDelegate.AccessibilityRole.LINK)
if (roleIsLink) {
if (isRoleLinkLike(textAttributes.role, textAttributes.accessibilityRole)) {
if (ReactNativeFeatureFlags.enablePreparedTextLayout()) {
ops.add(SetSpanOperation(start, end, ReactLinkSpan(i)))
} else {
Expand Down Expand Up @@ -556,14 +565,7 @@ internal object TextLayoutManager {
spanFlags,
)
} else {
val roleIsLink =
if (fragment.props.role != null)
(fragment.props.role == ReactAccessibilityDelegate.Role.LINK)
else
(fragment.props.accessibilityRole ==
ReactAccessibilityDelegate.AccessibilityRole.LINK)

if (roleIsLink) {
if (isRoleLinkLike(fragment.props.role, fragment.props.accessibilityRole)) {
if (ReactNativeFeatureFlags.enablePreparedTextLayout()) {
spannable.setSpan(ReactLinkSpan(i), start, end, spanFlags)
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<200266835197634d13de07182d80a2c1>>
* @generated SignedSource<<41b604c34178a3c904f96ad4bcaadd97>>
*/

/**
Expand Down Expand Up @@ -333,6 +333,12 @@ class ReactNativeFeatureFlagsJavaProvider
return method(javaProvider_);
}

bool enableRelaxedLinkRole() override {
static const auto method =
getReactNativeFeatureFlagsProviderJavaClass()->getMethod<jboolean()>("enableRelaxedLinkRole");
return method(javaProvider_);
}

bool enableResizeObserverByDefault() override {
static const auto method =
getReactNativeFeatureFlagsProviderJavaClass()->getMethod<jboolean()>("enableResizeObserverByDefault");
Expand Down Expand Up @@ -846,6 +852,11 @@ bool JReactNativeFeatureFlagsCxxInterop::enablePropsUpdateReconciliationAndroid(
return ReactNativeFeatureFlags::enablePropsUpdateReconciliationAndroid();
}

bool JReactNativeFeatureFlagsCxxInterop::enableRelaxedLinkRole(
facebook::jni::alias_ref<JReactNativeFeatureFlagsCxxInterop> /*unused*/) {
return ReactNativeFeatureFlags::enableRelaxedLinkRole();
}

bool JReactNativeFeatureFlagsCxxInterop::enableResizeObserverByDefault(
facebook::jni::alias_ref<JReactNativeFeatureFlagsCxxInterop> /*unused*/) {
return ReactNativeFeatureFlags::enableResizeObserverByDefault();
Expand Down Expand Up @@ -1244,6 +1255,9 @@ void JReactNativeFeatureFlagsCxxInterop::registerNatives() {
makeNativeMethod(
"enablePropsUpdateReconciliationAndroid",
JReactNativeFeatureFlagsCxxInterop::enablePropsUpdateReconciliationAndroid),
makeNativeMethod(
"enableRelaxedLinkRole",
JReactNativeFeatureFlagsCxxInterop::enableRelaxedLinkRole),
makeNativeMethod(
"enableResizeObserverByDefault",
JReactNativeFeatureFlagsCxxInterop::enableResizeObserverByDefault),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<13aa248dae556b06b31c25a28e448460>>
* @generated SignedSource<<70b5fc340fe72a4abc3b05e01c87b38b>>
*/

/**
Expand Down Expand Up @@ -177,6 +177,9 @@ class JReactNativeFeatureFlagsCxxInterop
static bool enablePropsUpdateReconciliationAndroid(
facebook::jni::alias_ref<JReactNativeFeatureFlagsCxxInterop>);

static bool enableRelaxedLinkRole(
facebook::jni::alias_ref<JReactNativeFeatureFlagsCxxInterop>);

static bool enableResizeObserverByDefault(
facebook::jni::alias_ref<JReactNativeFeatureFlagsCxxInterop>);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

package com.facebook.react.views.text

import android.text.Spannable
import android.text.style.ClickableSpan
import com.facebook.react.common.mapbuffer.WritableMapBuffer
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.uimanager.DisplayMetricsHolder
import com.facebook.react.uimanager.ReactAccessibilityDelegate.Role
import org.assertj.core.api.Assertions.assertThat
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.RuntimeEnvironment
import org.robolectric.annotation.Config

@RunWith(RobolectricTestRunner::class)
@Config(sdk = [34])
class TextLayoutManagerRelaxedLinkRoleTest {

@Before
fun setUp() {
ReactNativeFeatureFlagsForTests.setUp()
DisplayMetricsHolder.initDisplayMetricsIfNotInitialized(RuntimeEnvironment.getApplication())
}

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

@Test
fun `role=button becomes a link span when enableRelaxedLinkRole is on`() {
setRelaxedLinkRole(true)
assertThat(spannable(role = Role.BUTTON).clickableSpanCount()).isEqualTo(1)
}

@Test
fun `role=button stays plain when enableRelaxedLinkRole is off`() {
setRelaxedLinkRole(false)
assertThat(spannable(role = Role.BUTTON).clickableSpanCount()).isZero()
}

@Test
fun `heading role with link accessibilityRole is detected when enableRelaxedLinkRole is on`() {
setRelaxedLinkRole(true)
assertThat(spannable(role = Role.HEADING, accessibilityRole = "link").clickableSpanCount())
.isEqualTo(1)
}

@Test
fun `heading role with link accessibilityRole stays plain when enableRelaxedLinkRole is off`() {
setRelaxedLinkRole(false)
assertThat(spannable(role = Role.HEADING, accessibilityRole = "link").clickableSpanCount())
.isZero()
}

private fun setRelaxedLinkRole(enabled: Boolean) =
ReactNativeFeatureFlags.override(
object : ReactNativeFeatureFlagsDefaults() {
override fun enableRelaxedLinkRole(): Boolean = enabled
}
)

private fun Spannable.clickableSpanCount(): Int =
getSpans(0, length, ClickableSpan::class.java).size

private fun spannable(role: Role? = null, accessibilityRole: String? = null): Spannable {
val attrs =
WritableMapBuffer().apply {
put(TextAttributeProps.TA_KEY_FONT_SIZE, 16.0)
role?.let { put(TextAttributeProps.TA_KEY_ROLE, it.ordinal) }
accessibilityRole?.let { put(TextAttributeProps.TA_KEY_ACCESSIBILITY_ROLE, it) }
}
val fragment =
WritableMapBuffer().apply {
put(TextLayoutManager.FR_KEY_STRING, "Follow")
put(TextLayoutManager.FR_KEY_REACT_TAG, 1)
put(TextLayoutManager.FR_KEY_TEXT_ATTRIBUTES, attrs)
}
val attributedString =
WritableMapBuffer().apply {
put(TextLayoutManager.AS_KEY_STRING, "Follow")
put(TextLayoutManager.AS_KEY_FRAGMENTS, WritableMapBuffer().put(0, fragment))
}
return TextLayoutManager.getOrCreateSpannableForText(
RuntimeEnvironment.getApplication().assets,
attributedString,
null,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<fc4b67b43c846eac0085ed4a5528a170>>
* @generated SignedSource<<5c621a54a34dab8ac2de392b062675d0>>
*/

/**
Expand Down Expand Up @@ -222,6 +222,10 @@ bool ReactNativeFeatureFlags::enablePropsUpdateReconciliationAndroid() {
return getAccessor().enablePropsUpdateReconciliationAndroid();
}

bool ReactNativeFeatureFlags::enableRelaxedLinkRole() {
return getAccessor().enableRelaxedLinkRole();
}

bool ReactNativeFeatureFlags::enableResizeObserverByDefault() {
return getAccessor().enableResizeObserverByDefault();
}
Expand Down
Loading
Loading