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 @@ -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<<2fc347cdb33327437d29e5fd91e24011>>
* @generated SignedSource<<09455266e2f609cf7874d35018493dbd>>
*/

/**
Expand Down Expand Up @@ -288,6 +288,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()

/**
* When enabled, RuntimeScheduler_Modern clears pending tasks and rendering updates before handling an error.
*/
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<<a506d2515fce404e19bd61099cc60117>>
* @generated SignedSource<<c14857970cb3acfe1f4f6f6b8b3fabf3>>
*/

/**
Expand Down Expand Up @@ -63,6 +63,7 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces
private var enableNativeCSSParsingCache: Boolean? = null
private var enablePreparedTextLayoutCache: Boolean? = null
private var enablePropsUpdateReconciliationAndroidCache: Boolean? = null
private var enableRelaxedLinkRoleCache: Boolean? = null
private var enableRuntimeSchedulerQueueClearingOnErrorCache: Boolean? = null
private var enableSchedulerDelegateInvalidationCache: Boolean? = null
private var enableSwiftUIBasedFiltersCache: Boolean? = null
Expand Down Expand Up @@ -495,6 +496,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 enableRuntimeSchedulerQueueClearingOnError(): Boolean {
var cached = enableRuntimeSchedulerQueueClearingOnErrorCache
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<<1ef72233f02973021b83bd2e2aa1f69b>>
* @generated SignedSource<<1d327a377b1bd9a139930a2e6e050b22>>
*/

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

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

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

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

@DoNotStrip @JvmStatic public external fun enableSchedulerDelegateInvalidation(): 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<<eb9958ddc04dd1cd8d1add366f5f7741>>
* @generated SignedSource<<fd949cc7d21b11cad7cb1be7223fe6f2>>
*/

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

override fun enablePropsUpdateReconciliationAndroid(): Boolean = false

override fun enableRelaxedLinkRole(): Boolean = false

override fun enableRuntimeSchedulerQueueClearingOnError(): Boolean = false

override fun enableSchedulerDelegateInvalidation(): 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<<9ae32c46a5a6310ef96eb91c9ea5b12e>>
* @generated SignedSource<<675f3186fc6ae8d242dd6785cbb123cf>>
*/

/**
Expand Down Expand Up @@ -67,6 +67,7 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc
private var enableNativeCSSParsingCache: Boolean? = null
private var enablePreparedTextLayoutCache: Boolean? = null
private var enablePropsUpdateReconciliationAndroidCache: Boolean? = null
private var enableRelaxedLinkRoleCache: Boolean? = null
private var enableRuntimeSchedulerQueueClearingOnErrorCache: Boolean? = null
private var enableSchedulerDelegateInvalidationCache: Boolean? = null
private var enableSwiftUIBasedFiltersCache: Boolean? = null
Expand Down Expand Up @@ -542,6 +543,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 enableRuntimeSchedulerQueueClearingOnError(): Boolean {
var cached = enableRuntimeSchedulerQueueClearingOnErrorCache
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<<1a1d47f2d85404c776e55db40f7dbc6e>>
* @generated SignedSource<<277f00b235bc2b974e7cfd7c89b615ff>>
*/

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

@DoNotStrip public fun enablePropsUpdateReconciliationAndroid(): Boolean

@DoNotStrip public fun enableRelaxedLinkRole(): Boolean

@DoNotStrip public fun enableRuntimeSchedulerQueueClearingOnError(): Boolean

@DoNotStrip public fun enableSchedulerDelegateInvalidation(): Boolean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,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 @@ -276,13 +291,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 @@ -496,14 +505,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<<28de1e205f30135e96d5cb94a902faec>>
* @generated SignedSource<<8cea4c9c393c9deee077f778c4c4a152>>
*/

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

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

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

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

bool JReactNativeFeatureFlagsCxxInterop::enableRuntimeSchedulerQueueClearingOnError(
facebook::jni::alias_ref<JReactNativeFeatureFlagsCxxInterop> /*unused*/) {
return ReactNativeFeatureFlags::enableRuntimeSchedulerQueueClearingOnError();
Expand Down Expand Up @@ -1160,6 +1171,9 @@ void JReactNativeFeatureFlagsCxxInterop::registerNatives() {
makeNativeMethod(
"enablePropsUpdateReconciliationAndroid",
JReactNativeFeatureFlagsCxxInterop::enablePropsUpdateReconciliationAndroid),
makeNativeMethod(
"enableRelaxedLinkRole",
JReactNativeFeatureFlagsCxxInterop::enableRelaxedLinkRole),
makeNativeMethod(
"enableRuntimeSchedulerQueueClearingOnError",
JReactNativeFeatureFlagsCxxInterop::enableRuntimeSchedulerQueueClearingOnError),
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<<535898dd9498c65f30d56122c06b408b>>
* @generated SignedSource<<5a8a97b8894d6b5aeb1168fe9930c3cc>>
*/

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

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

static bool enableRuntimeSchedulerQueueClearingOnError(
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<<8fd664cb106945e5c6e436a0cf36120d>>
* @generated SignedSource<<42d9db11b0471b5810b7c5f77c36b5ca>>
*/

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

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

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