diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index 634155d3bd47..a4f1e4ebb409 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -2343,9 +2343,11 @@ public class com/facebook/react/fabric/FabricUIManager : com/facebook/react/brid public fun dispatchCommand (IILcom/facebook/react/bridge/ReadableArray;)V public fun dispatchCommand (IILjava/lang/String;Lcom/facebook/react/bridge/ReadableArray;)V public fun dispatchCommand (ILjava/lang/String;Lcom/facebook/react/bridge/ReadableArray;)V + public fun findNextFocusableElement (III)Ljava/lang/Integer; public fun getColor (I[Ljava/lang/String;)I public fun getEventDispatcher ()Lcom/facebook/react/uimanager/events/EventDispatcher; public fun getPerformanceCounters ()Ljava/util/Map; + public fun getRelativeAncestorList (II)[I public fun getThemeData (I[F)Z public fun initialize ()V public fun invalidate ()V @@ -3975,6 +3977,7 @@ public abstract interface class com/facebook/react/uimanager/ReactClippingViewGr public abstract fun getRemoveClippedSubviews ()Z public abstract fun setRemoveClippedSubviews (Z)V public abstract fun updateClippingRect ()V + public abstract fun updateClippingRect (Ljava/util/Set;)V } public final class com/facebook/react/uimanager/ReactClippingViewGroupHelper { @@ -5871,6 +5874,7 @@ public class com/facebook/react/views/scroll/ReactHorizontalScrollView : android public fun executeKeyEvent (Landroid/view/KeyEvent;)Z public fun flashScrollIndicators ()V public fun fling (I)V + public fun focusSearch (Landroid/view/View;I)Landroid/view/View; public fun getChildVisibleRect (Landroid/view/View;Landroid/graphics/Rect;Landroid/graphics/Point;)Z public fun getClippingRect (Landroid/graphics/Rect;)V public fun getFlingAnimator ()Landroid/animation/ValueAnimator; @@ -5933,6 +5937,7 @@ public class com/facebook/react/views/scroll/ReactHorizontalScrollView : android public fun setStateWrapper (Lcom/facebook/react/uimanager/StateWrapper;)V public fun startFlingAnimator (II)V public fun updateClippingRect ()V + public fun updateClippingRect (Ljava/util/Set;)V } public class com/facebook/react/views/scroll/ReactHorizontalScrollViewManager : com/facebook/react/uimanager/ViewGroupManager, com/facebook/react/views/scroll/ReactScrollViewCommandHelper$ScrollCommandHandler { @@ -5998,6 +6003,7 @@ public class com/facebook/react/views/scroll/ReactScrollView : android/widget/Sc public fun executeKeyEvent (Landroid/view/KeyEvent;)Z public fun flashScrollIndicators ()V public fun fling (I)V + public fun focusSearch (Landroid/view/View;I)Landroid/view/View; public fun getChildVisibleRect (Landroid/view/View;Landroid/graphics/Rect;Landroid/graphics/Point;)Z public fun getClippingRect (Landroid/graphics/Rect;)V public fun getFlingAnimator ()Landroid/animation/ValueAnimator; @@ -6061,6 +6067,7 @@ public class com/facebook/react/views/scroll/ReactScrollView : android/widget/Sc public fun setStateWrapper (Lcom/facebook/react/uimanager/StateWrapper;)V public fun startFlingAnimator (II)V public fun updateClippingRect ()V + public fun updateClippingRect (Ljava/util/Set;)V } public final class com/facebook/react/views/scroll/ReactScrollViewCommandHelper { @@ -6118,6 +6125,7 @@ public final class com/facebook/react/views/scroll/ReactScrollViewHelper { public static final fun emitScrollEvent (Landroid/view/ViewGroup;FF)V public static final fun emitScrollMomentumBeginEvent (Landroid/view/ViewGroup;II)V public static final fun emitScrollMomentumEndEvent (Landroid/view/ViewGroup;)V + public static final fun findNextFocusableView (Landroid/view/ViewGroup;Landroid/view/View;IZ)Landroid/view/View; public static final fun forceUpdateState (Landroid/view/ViewGroup;)V public static final fun getDefaultScrollAnimationDuration (Landroid/content/Context;)I public static final fun getNextFlingStartValue (Landroid/view/ViewGroup;III)I @@ -6127,6 +6135,7 @@ public final class com/facebook/react/views/scroll/ReactScrollViewHelper { public final fun registerFlingAnimator (Landroid/view/ViewGroup;)V public static final fun removeLayoutChangeListener (Lcom/facebook/react/views/scroll/ReactScrollViewHelper$LayoutChangeListener;)V public static final fun removeScrollListener (Lcom/facebook/react/views/scroll/ReactScrollViewHelper$ScrollListener;)V + public static final fun resolveAbsoluteDirection (IZI)I public static final fun smoothScrollTo (Landroid/view/ViewGroup;II)V public static final fun updateFabricScrollState (Landroid/view/ViewGroup;)V public final fun updateFabricScrollState (Landroid/view/ViewGroup;II)V @@ -6926,6 +6935,7 @@ public class com/facebook/react/views/view/ReactViewGroup : android/view/ViewGro public fun setRemoveClippedSubviews (Z)V public fun setTranslucentBackgroundDrawable (Landroid/graphics/drawable/Drawable;)V public fun updateClippingRect ()V + public fun updateClippingRect (Ljava/util/Set;)V public fun updateDrawingOrder ()V } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java index 8e0c683e158f..14b4439b6ca8 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java @@ -27,6 +27,7 @@ import androidx.annotation.AnyThread; import androidx.annotation.Nullable; import androidx.annotation.UiThread; +import androidx.core.view.ViewCompat.FocusRealDirection; import com.facebook.common.logging.FLog; import com.facebook.infer.annotation.Assertions; import com.facebook.infer.annotation.Nullsafe; @@ -262,6 +263,52 @@ public int addRootView( return rootTag; } + /** + * Find the next focusable element's id and position relative to the parent from the shadow tree + * based on the current focusable element and the direction. + * + * @return A NextFocusableNode object where the 'id' is the reactId/Tag of the next focusable + * view, returns null if no view could be found + */ + public @Nullable Integer findNextFocusableElement( + int parentTag, int focusedTag, @FocusRealDirection int direction) { + if (mBinding == null) { + return null; + } + + int generalizedDirection; + + switch (direction) { + case View.FOCUS_DOWN: + generalizedDirection = 0; + break; + case View.FOCUS_UP: + generalizedDirection = 1; + break; + case View.FOCUS_RIGHT: + generalizedDirection = 2; + break; + case View.FOCUS_LEFT: + generalizedDirection = 3; + break; + default: + return null; + } + + int serializedNextFocusableNodeMetrics = + mBinding.findNextFocusableElement(parentTag, focusedTag, generalizedDirection); + + if (serializedNextFocusableNodeMetrics == -1) { + return null; + } + + return serializedNextFocusableNodeMetrics; + } + + public @Nullable int[] getRelativeAncestorList(int rootTag, int childTag) { + return mBinding != null ? mBinding.getRelativeAncestorList(rootTag, childTag) : null; + } + @Override @AnyThread @ThreadConfined(ANY) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManagerBinding.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManagerBinding.kt index 7b9f9ee45004..f22661f1d7e1 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManagerBinding.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManagerBinding.kt @@ -55,6 +55,10 @@ internal class FabricUIManagerBinding : HybridClassBase() { isMountable: Boolean ) + external fun findNextFocusableElement(parentTag: Int, focusedTag: Int, direction: Int): Int + + external fun getRelativeAncestorList(rootTag: Int, childTag: Int): IntArray + external fun stopSurface(surfaceId: Int) external fun stopSurfaceWithSurfaceHandler(surfaceHandler: SurfaceHandlerBinding) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt index 6592daef310e..7efb8c7fe55f 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt @@ -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<<8a0be46378bf8810ac52a44024bfd587>> + * @generated SignedSource<<2b53d8dbd34ed8f3f2b4310a2a597618>> */ /** @@ -84,6 +84,12 @@ public object ReactNativeFeatureFlags { @JvmStatic public fun enableCppPropsIteratorSetter(): Boolean = accessor.enableCppPropsIteratorSetter() + /** + * This enables the fabric implementation of focus search so that we can focus clipped elements + */ + @JvmStatic + public fun enableCustomFocusSearchOnClippedElementsAndroid(): Boolean = accessor.enableCustomFocusSearchOnClippedElementsAndroid() + /** * Feature flag to configure eager attachment of the root view/initialisation of the JS code. */ diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt index 9aebdb1b00a7..028bfcfbccba 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt @@ -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<<5e462e70ca55296d1651678a7243508f>> + * @generated SignedSource<<2ebb741f6b7d8e293ee112ca68dac748>> */ /** @@ -29,6 +29,7 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces private var enableAccumulatedUpdatesInRawPropsAndroidCache: Boolean? = null private var enableBridgelessArchitectureCache: Boolean? = null private var enableCppPropsIteratorSetterCache: Boolean? = null + private var enableCustomFocusSearchOnClippedElementsAndroidCache: Boolean? = null private var enableEagerRootViewAttachmentCache: Boolean? = null private var enableFabricLogsCache: Boolean? = null private var enableFabricRendererCache: Boolean? = null @@ -143,6 +144,15 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces return cached } + override fun enableCustomFocusSearchOnClippedElementsAndroid(): Boolean { + var cached = enableCustomFocusSearchOnClippedElementsAndroidCache + if (cached == null) { + cached = ReactNativeFeatureFlagsCxxInterop.enableCustomFocusSearchOnClippedElementsAndroid() + enableCustomFocusSearchOnClippedElementsAndroidCache = cached + } + return cached + } + override fun enableEagerRootViewAttachment(): Boolean { var cached = enableEagerRootViewAttachmentCache if (cached == null) { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt index cf3630aa7a6d..f8b66f125d90 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt @@ -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<> + * @generated SignedSource<> */ /** @@ -46,6 +46,8 @@ public object ReactNativeFeatureFlagsCxxInterop { @DoNotStrip @JvmStatic public external fun enableCppPropsIteratorSetter(): Boolean + @DoNotStrip @JvmStatic public external fun enableCustomFocusSearchOnClippedElementsAndroid(): Boolean + @DoNotStrip @JvmStatic public external fun enableEagerRootViewAttachment(): Boolean @DoNotStrip @JvmStatic public external fun enableFabricLogs(): Boolean diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt index 5a79e3f5eccd..b30f944823e2 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt @@ -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<> + * @generated SignedSource<<2f333814db6601fe63060af1e7fb6d5e>> */ /** @@ -41,6 +41,8 @@ public open class ReactNativeFeatureFlagsDefaults : ReactNativeFeatureFlagsProvi override fun enableCppPropsIteratorSetter(): Boolean = false + override fun enableCustomFocusSearchOnClippedElementsAndroid(): Boolean = true + override fun enableEagerRootViewAttachment(): Boolean = false override fun enableFabricLogs(): Boolean = false diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt index 0dece1c9816b..dcd4ccd2140f 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt @@ -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<<9ef4812c333ca4fa1b05c66d0c59e303>> + * @generated SignedSource<> */ /** @@ -33,6 +33,7 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc private var enableAccumulatedUpdatesInRawPropsAndroidCache: Boolean? = null private var enableBridgelessArchitectureCache: Boolean? = null private var enableCppPropsIteratorSetterCache: Boolean? = null + private var enableCustomFocusSearchOnClippedElementsAndroidCache: Boolean? = null private var enableEagerRootViewAttachmentCache: Boolean? = null private var enableFabricLogsCache: Boolean? = null private var enableFabricRendererCache: Boolean? = null @@ -156,6 +157,16 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc return cached } + override fun enableCustomFocusSearchOnClippedElementsAndroid(): Boolean { + var cached = enableCustomFocusSearchOnClippedElementsAndroidCache + if (cached == null) { + cached = currentProvider.enableCustomFocusSearchOnClippedElementsAndroid() + accessedFeatureFlags.add("enableCustomFocusSearchOnClippedElementsAndroid") + enableCustomFocusSearchOnClippedElementsAndroidCache = cached + } + return cached + } + override fun enableEagerRootViewAttachment(): Boolean { var cached = enableEagerRootViewAttachmentCache if (cached == null) { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt index cb8861af165a..b964bcae50d9 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt @@ -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<> + * @generated SignedSource<> */ /** @@ -41,6 +41,8 @@ public interface ReactNativeFeatureFlagsProvider { @DoNotStrip public fun enableCppPropsIteratorSetter(): Boolean + @DoNotStrip public fun enableCustomFocusSearchOnClippedElementsAndroid(): Boolean + @DoNotStrip public fun enableEagerRootViewAttachment(): Boolean @DoNotStrip public fun enableFabricLogs(): Boolean diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactClippingViewGroup.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactClippingViewGroup.kt index dbd63a29541f..b62626efe7c0 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactClippingViewGroup.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactClippingViewGroup.kt @@ -32,6 +32,8 @@ public interface ReactClippingViewGroup { */ public fun updateClippingRect() + public fun updateClippingRect(excludedView: Set?) + /** * Get rectangular bounds to which view is currently clipped to. Called only on views that has set * `removeCLippedSubviews` property value to `true`. diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollView.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollView.java index a3aa393ecf1f..82c60e7b8fc6 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollView.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollView.java @@ -11,6 +11,7 @@ import static com.facebook.react.views.scroll.ReactScrollViewHelper.SNAP_ALIGNMENT_DISABLED; import static com.facebook.react.views.scroll.ReactScrollViewHelper.SNAP_ALIGNMENT_END; import static com.facebook.react.views.scroll.ReactScrollViewHelper.SNAP_ALIGNMENT_START; +import static com.facebook.react.views.scroll.ReactScrollViewHelper.findNextFocusableView; import android.animation.ObjectAnimator; import android.animation.ValueAnimator; @@ -31,6 +32,7 @@ import android.widget.OverScroller; import androidx.annotation.Nullable; import androidx.core.view.ViewCompat; +import androidx.core.view.ViewCompat.FocusRealDirection; import com.facebook.common.logging.FLog; import com.facebook.infer.annotation.Assertions; import com.facebook.infer.annotation.Nullsafe; @@ -39,6 +41,7 @@ import com.facebook.react.bridge.ReactContext; import com.facebook.react.common.ReactConstants; import com.facebook.react.common.build.ReactBuildConfig; +import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags; import com.facebook.react.uimanager.BackgroundStyleApplicator; import com.facebook.react.uimanager.LengthPercentage; import com.facebook.react.uimanager.LengthPercentageType; @@ -64,6 +67,7 @@ import java.lang.reflect.Field; import java.util.ArrayList; import java.util.List; +import java.util.Set; /** Similar to {@link ReactScrollView} but only supports horizontal scrolling. */ @Nullsafe(Nullsafe.Mode.LOCAL) @@ -771,8 +775,26 @@ protected void onDetachedFromWindow() { } } + @Override + public @Nullable View focusSearch(View focused, @FocusRealDirection int direction) { + if (ReactNativeFeatureFlags.enableCustomFocusSearchOnClippedElementsAndroid()) { + @Nullable View nextfocusableView = findNextFocusableView(this, focused, direction, true); + + if (nextfocusableView != null) { + return nextfocusableView; + } + } + + return super.focusSearch(focused, direction); + } + @Override public void updateClippingRect() { + updateClippingRect(null); + } + + @Override + public void updateClippingRect(@Nullable Set excludedViewId) { if (!mRemoveClippedSubviews) { return; } @@ -784,7 +806,7 @@ public void updateClippingRect() { ReactClippingViewGroupHelper.calculateClippingRect(this, mClippingRect); View contentView = getContentView(); if (contentView instanceof ReactClippingViewGroup) { - ((ReactClippingViewGroup) contentView).updateClippingRect(); + ((ReactClippingViewGroup) contentView).updateClippingRect(excludedViewId); } } finally { Systrace.endSection(Systrace.TRACE_TAG_REACT); diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.java index 5967ea0e8187..2e0c20dfa0dc 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.java @@ -11,6 +11,7 @@ import static com.facebook.react.views.scroll.ReactScrollViewHelper.SNAP_ALIGNMENT_DISABLED; import static com.facebook.react.views.scroll.ReactScrollViewHelper.SNAP_ALIGNMENT_END; import static com.facebook.react.views.scroll.ReactScrollViewHelper.SNAP_ALIGNMENT_START; +import static com.facebook.react.views.scroll.ReactScrollViewHelper.findNextFocusableView; import android.animation.ObjectAnimator; import android.animation.ValueAnimator; @@ -31,6 +32,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.view.ViewCompat; +import androidx.core.view.ViewCompat.FocusRealDirection; import com.facebook.common.logging.FLog; import com.facebook.infer.annotation.Assertions; import com.facebook.infer.annotation.Nullsafe; @@ -39,6 +41,7 @@ import com.facebook.react.bridge.ReactContext; import com.facebook.react.bridge.ReadableMap; import com.facebook.react.common.ReactConstants; +import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags; import com.facebook.react.uimanager.BackgroundStyleApplicator; import com.facebook.react.uimanager.LengthPercentage; import com.facebook.react.uimanager.LengthPercentageType; @@ -63,6 +66,7 @@ import com.facebook.systrace.Systrace; import java.lang.reflect.Field; import java.util.List; +import java.util.Set; /** * A simple subclass of ScrollView that doesn't dispatch measure and layout to its children and has @@ -359,6 +363,19 @@ protected void onDetachedFromWindow() { } } + @Override + public @Nullable View focusSearch(View focused, @FocusRealDirection int direction) { + if (ReactNativeFeatureFlags.enableCustomFocusSearchOnClippedElementsAndroid()) { + @Nullable View nextfocusableView = findNextFocusableView(this, focused, direction, false); + + if (nextfocusableView != null) { + return nextfocusableView; + } + } + + return super.focusSearch(focused, direction); + } + /** * Since ReactScrollView handles layout changes on JS side, it does not call super.onlayout due to * which mIsLayoutDirty flag in ScrollView remains true and prevents scrolling to child when @@ -528,6 +545,11 @@ public boolean getRemoveClippedSubviews() { @Override public void updateClippingRect() { + updateClippingRect(null); + } + + @Override + public void updateClippingRect(@Nullable Set excludedViewsSet) { if (!mRemoveClippedSubviews) { return; } @@ -539,7 +561,7 @@ public void updateClippingRect() { ReactClippingViewGroupHelper.calculateClippingRect(this, mClippingRect); View contentView = getContentView(); if (contentView instanceof ReactClippingViewGroup) { - ((ReactClippingViewGroup) contentView).updateClippingRect(); + ((ReactClippingViewGroup) contentView).updateClippingRect(excludedViewsSet); } } finally { Systrace.endSection(Systrace.TRACE_TAG_REACT); diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewHelper.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewHelper.kt index 1e920bc36c10..9714d323aa2e 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewHelper.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewHelper.kt @@ -11,15 +11,19 @@ import android.animation.Animator import android.animation.ValueAnimator import android.content.Context import android.graphics.Point +import android.view.FocusFinder import android.view.View import android.view.ViewGroup import android.widget.OverScroller +import androidx.core.view.ViewCompat.FocusRealDirection import com.facebook.common.logging.FLog import com.facebook.react.bridge.ReactContext import com.facebook.react.bridge.WritableMap import com.facebook.react.bridge.WritableNativeMap import com.facebook.react.common.ReactConstants +import com.facebook.react.fabric.FabricUIManager import com.facebook.react.uimanager.PixelUtil.toDIPFromPixel +import com.facebook.react.uimanager.ReactClippingViewGroup import com.facebook.react.uimanager.StateWrapper import com.facebook.react.uimanager.UIManagerHelper import com.facebook.react.uimanager.common.UIManagerType @@ -462,6 +466,70 @@ public object ReactScrollViewHelper { return Point(scroller.finalX, scroller.finalY) } + @JvmStatic + public fun findNextFocusableView( + host: ViewGroup, + focused: View, + @FocusRealDirection direction: Int, + horizontal: Boolean + ): View? { + val absDir = resolveAbsoluteDirection(direction, horizontal, host.getLayoutDirection()) + + /* + * Check if we can focus the next element in the absolute direction within the ScrollView this + * would mean the view is not clipped, if we can't, look into the shadow tree to find the next + * focusable element + */ + val ff = FocusFinder.getInstance() + val result = ff.findNextFocus(host, focused, absDir) + + if (result != null) { + return result + } + + if (host !is ReactClippingViewGroup) { + return null + } + + val uimanager = + UIManagerHelper.getUIManager(host.context as ReactContext, UIManagerType.FABRIC) + ?: return null + + val nextFocusableViewId = + (uimanager as FabricUIManager).findNextFocusableElement( + host.getChildAt(0).id, focused.id, absDir) ?: return null + + val ancestorIdList = + uimanager + .getRelativeAncestorList(host.getChildAt(0).id, nextFocusableViewId) + ?.toMutableSet() ?: return null + + ancestorIdList.add(nextFocusableViewId) + + host.updateClippingRect(ancestorIdList) + + return host.findViewById(nextFocusableViewId) + } + + @JvmStatic + public fun resolveAbsoluteDirection( + @FocusRealDirection direction: Int, + horizontal: Boolean, + layoutDirection: Int + ): Int { + val rtl: Boolean = layoutDirection == View.LAYOUT_DIRECTION_RTL + + return if (direction == View.FOCUS_FORWARD || direction == View.FOCUS_BACKWARD) { + if (horizontal) { + if ((direction == View.FOCUS_FORWARD) != rtl) View.FOCUS_RIGHT else View.FOCUS_LEFT + } else { + if (direction == View.FOCUS_FORWARD) View.FOCUS_DOWN else View.FOCUS_UP + } + } else { + direction + } + } + public interface ScrollListener { public fun onScroll( scrollView: ViewGroup?, diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.java index 2d7563395e6a..c2801edd45b9 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.java @@ -406,6 +406,11 @@ public void getClippingRect(Rect outClippingRect) { @Override public void updateClippingRect() { + updateClippingRect(null); + } + + @Override + public void updateClippingRect(@Nullable Set excludedViewsSet) { if (!mRemoveClippedSubviews) { return; } @@ -414,7 +419,7 @@ public void updateClippingRect() { Assertions.assertNotNull(mAllChildren); ReactClippingViewGroupHelper.calculateClippingRect(this, mClippingRect); - updateClippingToRect(mClippingRect); + updateClippingToRect(mClippingRect, excludedViewsSet); } @Override @@ -438,12 +443,16 @@ private boolean isChildRemovedWhileTransitioning(View child) { } private void updateClippingToRect(Rect clippingRect) { + updateClippingToRect(clippingRect, null); + } + + private void updateClippingToRect(Rect clippingRect, @Nullable Set excludedViewsSet) { Assertions.assertNotNull(mAllChildren); mInSubviewClippingLoop = true; int clippedSoFar = 0; for (int i = 0; i < mAllChildrenCount; i++) { try { - updateSubviewClipStatus(clippingRect, i, clippedSoFar); + updateSubviewClipStatus(clippingRect, i, clippedSoFar, excludedViewsSet); } catch (IndexOutOfBoundsException e) { int realClippedSoFar = 0; Set uniqueViews = new HashSet<>(); @@ -477,6 +486,11 @@ private void updateClippingToRect(Rect clippingRect) { } private void updateSubviewClipStatus(Rect clippingRect, int idx, int clippedSoFar) { + updateSubviewClipStatus(clippingRect, idx, clippedSoFar, null); + } + + private void updateSubviewClipStatus( + Rect clippingRect, int idx, int clippedSoFar, @Nullable Set excludedViewsSet) { UiThreadUtil.assertOnUiThread(); View child = Assertions.assertNotNull(mAllChildren)[idx]; @@ -492,14 +506,19 @@ private void updateSubviewClipStatus(Rect clippingRect, int idx, int clippedSoFa // it won't be size and located properly. Animation animation = child.getAnimation(); boolean isAnimating = animation != null && !animation.hasEnded(); + boolean shouldSkipView = excludedViewsSet != null && excludedViewsSet.contains(child.getId()); // We don't want to clip a view that is currently focused at that might break focus navigation - if (!intersects && !isViewClipped(child, idx) && !isAnimating && child != getFocusedChild()) { + if (!intersects + && !isViewClipped(child, idx) + && !isAnimating + && child != getFocusedChild() + && !shouldSkipView) { setViewClipped(child, true); // We can try saving on invalidate call here as the view that we remove is out of visible area // therefore invalidation is not necessary. removeViewInLayout(child); needUpdateClippingRecursive = true; - } else if (intersects && isViewClipped(child, idx)) { + } else if ((shouldSkipView || intersects) && isViewClipped(child, idx)) { int adjustedIdx = idx - clippedSoFar; Assertions.assertCondition(adjustedIdx >= 0); setViewClipped(child, false); @@ -514,7 +533,7 @@ private void updateSubviewClipStatus(Rect clippingRect, int idx, int clippedSoFa if (child instanceof ReactClippingViewGroup) { ReactClippingViewGroup clippingChild = (ReactClippingViewGroup) child; if (clippingChild.getRemoveClippedSubviews()) { - clippingChild.updateClippingRect(); + clippingChild.updateClippingRect(excludedViewsSet); } } } diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricUIManagerBinding.cpp b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricUIManagerBinding.cpp index e3224ffac393..2abaa0a06b70 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricUIManagerBinding.cpp +++ b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricUIManagerBinding.cpp @@ -11,6 +11,7 @@ #include "ComponentFactory.h" #include "EventBeatManager.h" #include "FabricMountingManager.h" +#include "FocusOrderingHelper.h" #include #include @@ -195,6 +196,105 @@ void FabricUIManagerBinding::startSurface( } } +jint FabricUIManagerBinding::findNextFocusableElement( + jint parentTag, + jint focusedTag, + jint direction) { + ShadowNode::Shared nextNode; + + std::optional focusDirection = + FocusOrderingHelper::resolveFocusDirection(direction); + + if (!focusDirection.has_value()) { + return -1; + } + + std::shared_ptr uimanager = getScheduler()->getUIManager(); + + ShadowNode::Shared parentShadowNode = + uimanager->findShadowNodeByTag_DEPRECATED(parentTag); + + if (parentShadowNode == nullptr) { + return -1; + } + + ShadowNode::Shared focusedShadowNode = + FocusOrderingHelper::findShadowNodeByTagRecursively( + parentShadowNode, focusedTag); + + if (focusedShadowNode == nullptr) { + return -1; + } + + LayoutMetrics childLayoutMetrics = uimanager->getRelativeLayoutMetrics( + *focusedShadowNode, parentShadowNode.get(), {.includeTransform = true}); + + Rect sourceRect = childLayoutMetrics.frame; + + /* + * Traverse the tree recursively to find the next focusable element in the + * given direction + */ + std::optional nextRect = std::nullopt; + FocusOrderingHelper::traverseAndUpdateNextFocusableElement( + parentShadowNode, + focusedShadowNode, + parentShadowNode, + focusDirection.value(), + *uimanager, + sourceRect, + nextRect, + nextNode); + + if (nextNode == nullptr) { + return -1; + } + + return nextNode->getTag(); +} + +jintArray FabricUIManagerBinding::getRelativeAncestorList( + jint rootTag, + jint childTag) { + JNIEnv* env = jni::Environment::current(); + + std::shared_ptr uimanager = getScheduler()->getUIManager(); + + ShadowNode::Shared childShadowNode = + uimanager->findShadowNodeByTag_DEPRECATED(childTag); + ShadowNode::Shared rootShadowNode = + uimanager->findShadowNodeByTag_DEPRECATED(rootTag); + + if (childShadowNode == nullptr || rootShadowNode == nullptr) { + return nullptr; + } + + ShadowNode::AncestorList ancestorList = + childShadowNode->getFamily().getAncestors(*rootShadowNode); + + if (ancestorList.empty() || ancestorList.size() < 2) { + return nullptr; + } + + // ignore the first ancestor as it is the rootShadowNode itself + std::vector ancestorTags; + for (auto it = std::next(ancestorList.begin()); it != ancestorList.end(); + ++it) { + auto& ancestor = *it; + if (ancestor.first.get().getTraits().check( + ShadowNodeTraits::Trait::FormsStackingContext)) { + ancestorTags.push_back(ancestor.first.get().getTag()); + } + } + + jintArray result = env->NewIntArray(static_cast(ancestorTags.size())); + + env->SetIntArrayRegion( + result, 0, static_cast(ancestorTags.size()), ancestorTags.data()); + + return result; +} + // Used by non-bridgeless+Fabric void FabricUIManagerBinding::startSurfaceWithConstraints( jint surfaceId, @@ -667,6 +767,12 @@ void FabricUIManagerBinding::registerNatives() { makeNativeMethod( "stopSurfaceWithSurfaceHandler", FabricUIManagerBinding::stopSurfaceWithSurfaceHandler), + makeNativeMethod( + "findNextFocusableElement", + FabricUIManagerBinding::findNextFocusableElement), + makeNativeMethod( + "getRelativeAncestorList", + FabricUIManagerBinding::getRelativeAncestorList), }); } diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricUIManagerBinding.h b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricUIManagerBinding.h index c3c404c99908..4cd9f26cd051 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricUIManagerBinding.h +++ b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricUIManagerBinding.h @@ -132,6 +132,11 @@ class FabricUIManagerBinding : public jni::HybridClass, void reportMount(SurfaceId surfaceId); + jint + findNextFocusableElement(jint parentTag, jint focusedTag, jint direction); + + jintArray getRelativeAncestorList(jint rootTag, jint childTag); + void uninstallFabricUIManager(); // Private member variables diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FocusOrderingHelper.cpp b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FocusOrderingHelper.cpp new file mode 100644 index 000000000000..e30b1e2c89a0 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FocusOrderingHelper.cpp @@ -0,0 +1,186 @@ +/* + * 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. + */ + +#include "FocusOrderingHelper.h" +#include +#include + +namespace facebook::react { + +int majorAxisDistanceRaw( + FocusDirection focusDirection, + Rect source, + Rect dest) { + switch (focusDirection) { + case FocusDirection::FocusLeft: + return static_cast( + source.origin.x - (dest.origin.x + dest.size.width)); + case FocusDirection::FocusRight: + return static_cast( + dest.origin.x - (source.origin.x + source.size.width)); + case FocusDirection::FocusUp: + return static_cast( + source.origin.y - (dest.origin.y + dest.size.height)); + case FocusDirection::FocusDown: + return static_cast( + dest.origin.y - (source.origin.y + source.size.height)); + } +} + +int majorAxisDistance(FocusDirection focusDirection, Rect source, Rect dest) { + return std::max(0, majorAxisDistanceRaw(focusDirection, source, dest)); +} + +int minorAxisDistance(FocusDirection direction, Rect source, Rect dest) { + switch (direction) { + case FocusDirection::FocusLeft: + case FocusDirection::FocusRight: + // the distance between the center verticals + return static_cast(abs((source.getMidY() - dest.getMidY()))); + case FocusDirection::FocusUp: + case FocusDirection::FocusDown: + // the distance between the center horizontals + return static_cast(abs((source.getMidX() - dest.getMidX()))); + } +} + +// 13 is a magic number that comes from Android's implementation. We opt to use +// this to get the same focus ordering as Android. See: +// https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/view/FocusFinder.java;l=547 +int getWeightedDistanceFor(int majorAxisDistance, int minorAxisDistance) { + return 13 * majorAxisDistance * majorAxisDistance + + minorAxisDistance * minorAxisDistance; +} + +// Make sure dest rect is actually on the direction of focus +bool isCandidate(Rect source, Rect dest, FocusDirection focusDirection) { + switch (focusDirection) { + case FocusDirection::FocusLeft: + return ((source.origin.x + source.size.width) > + (dest.origin.x + dest.size.width) || + source.origin.x >= (dest.origin.x + dest.size.width)) && + source.origin.x > dest.origin.x; + case FocusDirection::FocusRight: + return (source.origin.x < dest.origin.x || + (source.origin.x + source.size.width) <= dest.origin.x) && + (source.origin.x + source.size.width) < + (dest.origin.x + dest.size.width); + case FocusDirection::FocusUp: + return ((source.origin.y + source.size.height) > + (dest.origin.y + dest.size.height) || + source.origin.y >= (dest.origin.y + dest.size.height)) && + source.origin.y > dest.origin.y; + case FocusDirection::FocusDown: + return (source.origin.y < dest.origin.y || + (source.origin.y + source.size.height) <= dest.origin.y) && + ((source.origin.y + source.size.height) < + (dest.origin.y + dest.size.height)); + } +} + +bool isBetterCandidate( + FocusDirection focusDirection, + Rect source, + Rect current, + Rect candidate) { + if (!isCandidate(source, candidate, focusDirection)) { + return false; + } + + int candidateWeightedDistance = getWeightedDistanceFor( + majorAxisDistance(focusDirection, source, candidate), + minorAxisDistance(focusDirection, source, candidate)); + + int currentWeightedDistance = getWeightedDistanceFor( + majorAxisDistance(focusDirection, source, current), + minorAxisDistance(focusDirection, source, current)); + + return candidateWeightedDistance < currentWeightedDistance; +} + +void FocusOrderingHelper::traverseAndUpdateNextFocusableElement( + const ShadowNode::Shared& parentShadowNode, + const ShadowNode::Shared& focusedShadowNode, + const ShadowNode::Shared& currNode, + FocusDirection focusDirection, + const UIManager& uimanager, + Rect sourceRect, + std::optional& nextRect, + ShadowNode::Shared& nextNode) { + const auto* props = + dynamic_cast(currNode->getProps().get()); + + // We only care about focusable elements since only they can be both + // focused and present in the hierarchy + if (currNode->getTraits().check(ShadowNodeTraits::Trait::KeyboardFocusable) || + (props != nullptr && + (props->focusable || props->accessible || props->hasTVPreferredFocus))) { + LayoutMetrics nodeLayoutMetrics = uimanager.getRelativeLayoutMetrics( + *currNode, parentShadowNode.get(), {.includeTransform = true}); + + if (nextRect == std::nullopt && + isCandidate(sourceRect, nodeLayoutMetrics.frame, focusDirection)) { + nextNode = currNode; + nextRect = nodeLayoutMetrics.frame; + } else if ( + nextRect != std::nullopt && + isBetterCandidate( + focusDirection, + sourceRect, + nextRect.value(), + nodeLayoutMetrics.frame)) { + nextNode = currNode; + nextRect = nodeLayoutMetrics.frame; + } + } + + for (auto& child : currNode->getChildren()) { + if (child->getTraits().check(ShadowNodeTraits::Trait::RootNodeKind)) { + continue; + } + + traverseAndUpdateNextFocusableElement( + parentShadowNode, + focusedShadowNode, + child, + focusDirection, + uimanager, + sourceRect, + nextRect, + nextNode); + }; +}; + +ShadowNode::Shared FocusOrderingHelper::findShadowNodeByTagRecursively( + const ShadowNode::Shared& parentShadowNode, + Tag tag) { + if (parentShadowNode->getTag() == tag) { + return parentShadowNode; + } + + for (auto& shadowNode : parentShadowNode->getChildren()) { + if (auto result = findShadowNodeByTagRecursively(shadowNode, tag)) { + return result; + } + } + + return nullptr; +} + +std::optional FocusOrderingHelper::resolveFocusDirection( + int direction) { + switch (static_cast(direction)) { + case FocusDirection::FocusDown: + case FocusDirection::FocusUp: + case FocusDirection::FocusRight: + case FocusDirection::FocusLeft: + return static_cast(direction); + } + + return std::nullopt; +} +} // namespace facebook::react diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FocusOrderingHelper.h b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FocusOrderingHelper.h new file mode 100644 index 000000000000..97eeba7d1ea4 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FocusOrderingHelper.h @@ -0,0 +1,38 @@ +/* + * 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. + */ + +#include +#include "FabricUIManagerBinding.h" + +namespace facebook::react { + +enum class FocusDirection { + FocusDown = 0, + FocusUp = 1, + FocusRight = 2, + FocusLeft = 3, +}; + +class FocusOrderingHelper { + public: + static void traverseAndUpdateNextFocusableElement( + const ShadowNode::Shared& parentShadowNode, + const ShadowNode::Shared& focusedShadowNode, + const ShadowNode::Shared& currNode, + FocusDirection focusDirection, + const UIManager& uimanager, + Rect sourceRect, + std::optional& nextRect, + ShadowNode::Shared& nextNode); + + static ShadowNode::Shared findShadowNodeByTagRecursively( + const ShadowNode::Shared& parentShadowNode, + Tag tag); + + static std::optional resolveFocusDirection(int direction); +}; +} // namespace facebook::react diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp index bb56df18b8f2..912e26f71f32 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp +++ b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp @@ -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<<37e6ac5e48af0510ba8268e682379b95>> + * @generated SignedSource<<0c7f406931f3e997cd62713d82644d2c>> */ /** @@ -93,6 +93,12 @@ class ReactNativeFeatureFlagsJavaProvider return method(javaProvider_); } + bool enableCustomFocusSearchOnClippedElementsAndroid() override { + static const auto method = + getReactNativeFeatureFlagsProviderJavaClass()->getMethod("enableCustomFocusSearchOnClippedElementsAndroid"); + return method(javaProvider_); + } + bool enableEagerRootViewAttachment() override { static const auto method = getReactNativeFeatureFlagsProviderJavaClass()->getMethod("enableEagerRootViewAttachment"); @@ -334,6 +340,11 @@ bool JReactNativeFeatureFlagsCxxInterop::enableCppPropsIteratorSetter( return ReactNativeFeatureFlags::enableCppPropsIteratorSetter(); } +bool JReactNativeFeatureFlagsCxxInterop::enableCustomFocusSearchOnClippedElementsAndroid( + facebook::jni::alias_ref /*unused*/) { + return ReactNativeFeatureFlags::enableCustomFocusSearchOnClippedElementsAndroid(); +} + bool JReactNativeFeatureFlagsCxxInterop::enableEagerRootViewAttachment( facebook::jni::alias_ref /*unused*/) { return ReactNativeFeatureFlags::enableEagerRootViewAttachment(); @@ -552,6 +563,9 @@ void JReactNativeFeatureFlagsCxxInterop::registerNatives() { makeNativeMethod( "enableCppPropsIteratorSetter", JReactNativeFeatureFlagsCxxInterop::enableCppPropsIteratorSetter), + makeNativeMethod( + "enableCustomFocusSearchOnClippedElementsAndroid", + JReactNativeFeatureFlagsCxxInterop::enableCustomFocusSearchOnClippedElementsAndroid), makeNativeMethod( "enableEagerRootViewAttachment", JReactNativeFeatureFlagsCxxInterop::enableEagerRootViewAttachment), diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h index f7f7e291e446..0d05aa82c606 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h +++ b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h @@ -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<> + * @generated SignedSource<<379e678b19c92671bf3bc70b57878cd3>> */ /** @@ -57,6 +57,9 @@ class JReactNativeFeatureFlagsCxxInterop static bool enableCppPropsIteratorSetter( facebook::jni::alias_ref); + static bool enableCustomFocusSearchOnClippedElementsAndroid( + facebook::jni::alias_ref); + static bool enableEagerRootViewAttachment( facebook::jni::alias_ref); diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp index 9587fecf8959..3c86c50a23cd 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp @@ -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<> + * @generated SignedSource<<087abd9e230584def7f1e912189fc973>> */ /** @@ -62,6 +62,10 @@ bool ReactNativeFeatureFlags::enableCppPropsIteratorSetter() { return getAccessor().enableCppPropsIteratorSetter(); } +bool ReactNativeFeatureFlags::enableCustomFocusSearchOnClippedElementsAndroid() { + return getAccessor().enableCustomFocusSearchOnClippedElementsAndroid(); +} + bool ReactNativeFeatureFlags::enableEagerRootViewAttachment() { return getAccessor().enableEagerRootViewAttachment(); } diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h index 699755b3b3ed..8d311a09ea6e 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h @@ -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<<4555ff8492c6f12e623f8e5b0cd9f833>> + * @generated SignedSource<<03e7ce4178eea7965f85ebf81f9ab237>> */ /** @@ -84,6 +84,11 @@ class ReactNativeFeatureFlags { */ RN_EXPORT static bool enableCppPropsIteratorSetter(); + /** + * This enables the fabric implementation of focus search so that we can focus clipped elements + */ + RN_EXPORT static bool enableCustomFocusSearchOnClippedElementsAndroid(); + /** * Feature flag to configure eager attachment of the root view/initialisation of the JS code. */ diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp index 7a631ab76db5..b283f444ec28 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp @@ -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<> + * @generated SignedSource<> */ /** @@ -191,6 +191,24 @@ bool ReactNativeFeatureFlagsAccessor::enableCppPropsIteratorSetter() { return flagValue.value(); } +bool ReactNativeFeatureFlagsAccessor::enableCustomFocusSearchOnClippedElementsAndroid() { + auto flagValue = enableCustomFocusSearchOnClippedElementsAndroid_.load(); + + if (!flagValue.has_value()) { + // This block is not exclusive but it is not necessary. + // If multiple threads try to initialize the feature flag, we would only + // be accessing the provider multiple times but the end state of this + // instance and the returned flag value would be the same. + + markFlagAsAccessed(9, "enableCustomFocusSearchOnClippedElementsAndroid"); + + flagValue = currentProvider_->enableCustomFocusSearchOnClippedElementsAndroid(); + enableCustomFocusSearchOnClippedElementsAndroid_ = flagValue; + } + + return flagValue.value(); +} + bool ReactNativeFeatureFlagsAccessor::enableEagerRootViewAttachment() { auto flagValue = enableEagerRootViewAttachment_.load(); @@ -200,7 +218,7 @@ bool ReactNativeFeatureFlagsAccessor::enableEagerRootViewAttachment() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(9, "enableEagerRootViewAttachment"); + markFlagAsAccessed(10, "enableEagerRootViewAttachment"); flagValue = currentProvider_->enableEagerRootViewAttachment(); enableEagerRootViewAttachment_ = flagValue; @@ -218,7 +236,7 @@ bool ReactNativeFeatureFlagsAccessor::enableFabricLogs() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(10, "enableFabricLogs"); + markFlagAsAccessed(11, "enableFabricLogs"); flagValue = currentProvider_->enableFabricLogs(); enableFabricLogs_ = flagValue; @@ -236,7 +254,7 @@ bool ReactNativeFeatureFlagsAccessor::enableFabricRenderer() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(11, "enableFabricRenderer"); + markFlagAsAccessed(12, "enableFabricRenderer"); flagValue = currentProvider_->enableFabricRenderer(); enableFabricRenderer_ = flagValue; @@ -254,7 +272,7 @@ bool ReactNativeFeatureFlagsAccessor::enableFontScaleChangesUpdatingLayout() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(12, "enableFontScaleChangesUpdatingLayout"); + markFlagAsAccessed(13, "enableFontScaleChangesUpdatingLayout"); flagValue = currentProvider_->enableFontScaleChangesUpdatingLayout(); enableFontScaleChangesUpdatingLayout_ = flagValue; @@ -272,7 +290,7 @@ bool ReactNativeFeatureFlagsAccessor::enableIOSViewClipToPaddingBox() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(13, "enableIOSViewClipToPaddingBox"); + markFlagAsAccessed(14, "enableIOSViewClipToPaddingBox"); flagValue = currentProvider_->enableIOSViewClipToPaddingBox(); enableIOSViewClipToPaddingBox_ = flagValue; @@ -290,7 +308,7 @@ bool ReactNativeFeatureFlagsAccessor::enableJSRuntimeGCOnMemoryPressureOnIOS() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(14, "enableJSRuntimeGCOnMemoryPressureOnIOS"); + markFlagAsAccessed(15, "enableJSRuntimeGCOnMemoryPressureOnIOS"); flagValue = currentProvider_->enableJSRuntimeGCOnMemoryPressureOnIOS(); enableJSRuntimeGCOnMemoryPressureOnIOS_ = flagValue; @@ -308,7 +326,7 @@ bool ReactNativeFeatureFlagsAccessor::enableLayoutAnimationsOnAndroid() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(15, "enableLayoutAnimationsOnAndroid"); + markFlagAsAccessed(16, "enableLayoutAnimationsOnAndroid"); flagValue = currentProvider_->enableLayoutAnimationsOnAndroid(); enableLayoutAnimationsOnAndroid_ = flagValue; @@ -326,7 +344,7 @@ bool ReactNativeFeatureFlagsAccessor::enableLayoutAnimationsOnIOS() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(16, "enableLayoutAnimationsOnIOS"); + markFlagAsAccessed(17, "enableLayoutAnimationsOnIOS"); flagValue = currentProvider_->enableLayoutAnimationsOnIOS(); enableLayoutAnimationsOnIOS_ = flagValue; @@ -344,7 +362,7 @@ bool ReactNativeFeatureFlagsAccessor::enableMainQueueModulesOnIOS() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(17, "enableMainQueueModulesOnIOS"); + markFlagAsAccessed(18, "enableMainQueueModulesOnIOS"); flagValue = currentProvider_->enableMainQueueModulesOnIOS(); enableMainQueueModulesOnIOS_ = flagValue; @@ -362,7 +380,7 @@ bool ReactNativeFeatureFlagsAccessor::enableNativeCSSParsing() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(18, "enableNativeCSSParsing"); + markFlagAsAccessed(19, "enableNativeCSSParsing"); flagValue = currentProvider_->enableNativeCSSParsing(); enableNativeCSSParsing_ = flagValue; @@ -380,7 +398,7 @@ bool ReactNativeFeatureFlagsAccessor::enableNewBackgroundAndBorderDrawables() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(19, "enableNewBackgroundAndBorderDrawables"); + markFlagAsAccessed(20, "enableNewBackgroundAndBorderDrawables"); flagValue = currentProvider_->enableNewBackgroundAndBorderDrawables(); enableNewBackgroundAndBorderDrawables_ = flagValue; @@ -398,7 +416,7 @@ bool ReactNativeFeatureFlagsAccessor::enablePropsUpdateReconciliationAndroid() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(20, "enablePropsUpdateReconciliationAndroid"); + markFlagAsAccessed(21, "enablePropsUpdateReconciliationAndroid"); flagValue = currentProvider_->enablePropsUpdateReconciliationAndroid(); enablePropsUpdateReconciliationAndroid_ = flagValue; @@ -416,7 +434,7 @@ bool ReactNativeFeatureFlagsAccessor::enableSynchronousStateUpdates() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(21, "enableSynchronousStateUpdates"); + markFlagAsAccessed(22, "enableSynchronousStateUpdates"); flagValue = currentProvider_->enableSynchronousStateUpdates(); enableSynchronousStateUpdates_ = flagValue; @@ -434,7 +452,7 @@ bool ReactNativeFeatureFlagsAccessor::enableViewCulling() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(22, "enableViewCulling"); + markFlagAsAccessed(23, "enableViewCulling"); flagValue = currentProvider_->enableViewCulling(); enableViewCulling_ = flagValue; @@ -452,7 +470,7 @@ bool ReactNativeFeatureFlagsAccessor::enableViewRecycling() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(23, "enableViewRecycling"); + markFlagAsAccessed(24, "enableViewRecycling"); flagValue = currentProvider_->enableViewRecycling(); enableViewRecycling_ = flagValue; @@ -470,7 +488,7 @@ bool ReactNativeFeatureFlagsAccessor::enableViewRecyclingForText() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(24, "enableViewRecyclingForText"); + markFlagAsAccessed(25, "enableViewRecyclingForText"); flagValue = currentProvider_->enableViewRecyclingForText(); enableViewRecyclingForText_ = flagValue; @@ -488,7 +506,7 @@ bool ReactNativeFeatureFlagsAccessor::enableViewRecyclingForView() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(25, "enableViewRecyclingForView"); + markFlagAsAccessed(26, "enableViewRecyclingForView"); flagValue = currentProvider_->enableViewRecyclingForView(); enableViewRecyclingForView_ = flagValue; @@ -506,7 +524,7 @@ bool ReactNativeFeatureFlagsAccessor::fixMappingOfEventPrioritiesBetweenFabricAn // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(26, "fixMappingOfEventPrioritiesBetweenFabricAndReact"); + markFlagAsAccessed(27, "fixMappingOfEventPrioritiesBetweenFabricAndReact"); flagValue = currentProvider_->fixMappingOfEventPrioritiesBetweenFabricAndReact(); fixMappingOfEventPrioritiesBetweenFabricAndReact_ = flagValue; @@ -524,7 +542,7 @@ bool ReactNativeFeatureFlagsAccessor::fuseboxEnabledRelease() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(27, "fuseboxEnabledRelease"); + markFlagAsAccessed(28, "fuseboxEnabledRelease"); flagValue = currentProvider_->fuseboxEnabledRelease(); fuseboxEnabledRelease_ = flagValue; @@ -542,7 +560,7 @@ bool ReactNativeFeatureFlagsAccessor::fuseboxNetworkInspectionEnabled() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(28, "fuseboxNetworkInspectionEnabled"); + markFlagAsAccessed(29, "fuseboxNetworkInspectionEnabled"); flagValue = currentProvider_->fuseboxNetworkInspectionEnabled(); fuseboxNetworkInspectionEnabled_ = flagValue; @@ -560,7 +578,7 @@ bool ReactNativeFeatureFlagsAccessor::removeTurboModuleManagerDelegateMutex() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(29, "removeTurboModuleManagerDelegateMutex"); + markFlagAsAccessed(30, "removeTurboModuleManagerDelegateMutex"); flagValue = currentProvider_->removeTurboModuleManagerDelegateMutex(); removeTurboModuleManagerDelegateMutex_ = flagValue; @@ -578,7 +596,7 @@ bool ReactNativeFeatureFlagsAccessor::traceTurboModulePromiseRejectionsOnAndroid // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(30, "traceTurboModulePromiseRejectionsOnAndroid"); + markFlagAsAccessed(31, "traceTurboModulePromiseRejectionsOnAndroid"); flagValue = currentProvider_->traceTurboModulePromiseRejectionsOnAndroid(); traceTurboModulePromiseRejectionsOnAndroid_ = flagValue; @@ -596,7 +614,7 @@ bool ReactNativeFeatureFlagsAccessor::updateRuntimeShadowNodeReferencesOnCommit( // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(31, "updateRuntimeShadowNodeReferencesOnCommit"); + markFlagAsAccessed(32, "updateRuntimeShadowNodeReferencesOnCommit"); flagValue = currentProvider_->updateRuntimeShadowNodeReferencesOnCommit(); updateRuntimeShadowNodeReferencesOnCommit_ = flagValue; @@ -614,7 +632,7 @@ bool ReactNativeFeatureFlagsAccessor::useAlwaysAvailableJSErrorHandling() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(32, "useAlwaysAvailableJSErrorHandling"); + markFlagAsAccessed(33, "useAlwaysAvailableJSErrorHandling"); flagValue = currentProvider_->useAlwaysAvailableJSErrorHandling(); useAlwaysAvailableJSErrorHandling_ = flagValue; @@ -632,7 +650,7 @@ bool ReactNativeFeatureFlagsAccessor::useEditTextStockAndroidFocusBehavior() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(33, "useEditTextStockAndroidFocusBehavior"); + markFlagAsAccessed(34, "useEditTextStockAndroidFocusBehavior"); flagValue = currentProvider_->useEditTextStockAndroidFocusBehavior(); useEditTextStockAndroidFocusBehavior_ = flagValue; @@ -650,7 +668,7 @@ bool ReactNativeFeatureFlagsAccessor::useFabricInterop() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(34, "useFabricInterop"); + markFlagAsAccessed(35, "useFabricInterop"); flagValue = currentProvider_->useFabricInterop(); useFabricInterop_ = flagValue; @@ -668,7 +686,7 @@ bool ReactNativeFeatureFlagsAccessor::useNativeViewConfigsInBridgelessMode() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(35, "useNativeViewConfigsInBridgelessMode"); + markFlagAsAccessed(36, "useNativeViewConfigsInBridgelessMode"); flagValue = currentProvider_->useNativeViewConfigsInBridgelessMode(); useNativeViewConfigsInBridgelessMode_ = flagValue; @@ -686,7 +704,7 @@ bool ReactNativeFeatureFlagsAccessor::useOptimizedEventBatchingOnAndroid() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(36, "useOptimizedEventBatchingOnAndroid"); + markFlagAsAccessed(37, "useOptimizedEventBatchingOnAndroid"); flagValue = currentProvider_->useOptimizedEventBatchingOnAndroid(); useOptimizedEventBatchingOnAndroid_ = flagValue; @@ -704,7 +722,7 @@ bool ReactNativeFeatureFlagsAccessor::useRawPropsJsiValue() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(37, "useRawPropsJsiValue"); + markFlagAsAccessed(38, "useRawPropsJsiValue"); flagValue = currentProvider_->useRawPropsJsiValue(); useRawPropsJsiValue_ = flagValue; @@ -722,7 +740,7 @@ bool ReactNativeFeatureFlagsAccessor::useShadowNodeStateOnClone() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(38, "useShadowNodeStateOnClone"); + markFlagAsAccessed(39, "useShadowNodeStateOnClone"); flagValue = currentProvider_->useShadowNodeStateOnClone(); useShadowNodeStateOnClone_ = flagValue; @@ -740,7 +758,7 @@ bool ReactNativeFeatureFlagsAccessor::useTurboModuleInterop() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(39, "useTurboModuleInterop"); + markFlagAsAccessed(40, "useTurboModuleInterop"); flagValue = currentProvider_->useTurboModuleInterop(); useTurboModuleInterop_ = flagValue; @@ -758,7 +776,7 @@ bool ReactNativeFeatureFlagsAccessor::useTurboModules() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(40, "useTurboModules"); + markFlagAsAccessed(41, "useTurboModules"); flagValue = currentProvider_->useTurboModules(); useTurboModules_ = flagValue; diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h index db3918aa04a2..5b3a3e47efa8 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h @@ -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<<73b019a5a5d3339603d120868286a09e>> + * @generated SignedSource<<9a6a9258b92a7adcc651bb240b2c06c6>> */ /** @@ -41,6 +41,7 @@ class ReactNativeFeatureFlagsAccessor { bool enableAccumulatedUpdatesInRawPropsAndroid(); bool enableBridgelessArchitecture(); bool enableCppPropsIteratorSetter(); + bool enableCustomFocusSearchOnClippedElementsAndroid(); bool enableEagerRootViewAttachment(); bool enableFabricLogs(); bool enableFabricRenderer(); @@ -84,7 +85,7 @@ class ReactNativeFeatureFlagsAccessor { std::unique_ptr currentProvider_; bool wasOverridden_; - std::array, 41> accessedFeatureFlags_; + std::array, 42> accessedFeatureFlags_; std::atomic> commonTestFlag_; std::atomic> animatedShouldSignalBatch_; @@ -95,6 +96,7 @@ class ReactNativeFeatureFlagsAccessor { std::atomic> enableAccumulatedUpdatesInRawPropsAndroid_; std::atomic> enableBridgelessArchitecture_; std::atomic> enableCppPropsIteratorSetter_; + std::atomic> enableCustomFocusSearchOnClippedElementsAndroid_; std::atomic> enableEagerRootViewAttachment_; std::atomic> enableFabricLogs_; std::atomic> enableFabricRenderer_; diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h index 9c1f9533a888..ecaede77643e 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h @@ -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<> + * @generated SignedSource<<8a34c1b5aa58edb37a32e5f9f40fafb9>> */ /** @@ -63,6 +63,10 @@ class ReactNativeFeatureFlagsDefaults : public ReactNativeFeatureFlagsProvider { return false; } + bool enableCustomFocusSearchOnClippedElementsAndroid() override { + return true; + } + bool enableEagerRootViewAttachment() override { return false; } diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h index 8db956b3ecbf..0371c91bc80c 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h @@ -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<> + * @generated SignedSource<> */ /** @@ -126,6 +126,15 @@ class ReactNativeFeatureFlagsDynamicProvider : public ReactNativeFeatureFlagsDef return ReactNativeFeatureFlagsDefaults::enableCppPropsIteratorSetter(); } + bool enableCustomFocusSearchOnClippedElementsAndroid() override { + auto value = values_["enableCustomFocusSearchOnClippedElementsAndroid"]; + if (!value.isNull()) { + return value.getBool(); + } + + return ReactNativeFeatureFlagsDefaults::enableCustomFocusSearchOnClippedElementsAndroid(); + } + bool enableEagerRootViewAttachment() override { auto value = values_["enableEagerRootViewAttachment"]; if (!value.isNull()) { diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h index f991a5c7806e..ede5157f881e 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h @@ -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<<3df15c4040fdee5261fd5442adc709fd>> + * @generated SignedSource<<3fa2d5f690687553254afdcec6f52391>> */ /** @@ -34,6 +34,7 @@ class ReactNativeFeatureFlagsProvider { virtual bool enableAccumulatedUpdatesInRawPropsAndroid() = 0; virtual bool enableBridgelessArchitecture() = 0; virtual bool enableCppPropsIteratorSetter() = 0; + virtual bool enableCustomFocusSearchOnClippedElementsAndroid() = 0; virtual bool enableEagerRootViewAttachment() = 0; virtual bool enableFabricLogs() = 0; virtual bool enableFabricRenderer() = 0; diff --git a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp index 74e631bd9ea5..19c363d4632b 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp +++ b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp @@ -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<<164ff02a57725e1ddbbb6ffa7e681b72>> + * @generated SignedSource<> */ /** @@ -89,6 +89,11 @@ bool NativeReactNativeFeatureFlags::enableCppPropsIteratorSetter( return ReactNativeFeatureFlags::enableCppPropsIteratorSetter(); } +bool NativeReactNativeFeatureFlags::enableCustomFocusSearchOnClippedElementsAndroid( + jsi::Runtime& /*runtime*/) { + return ReactNativeFeatureFlags::enableCustomFocusSearchOnClippedElementsAndroid(); +} + bool NativeReactNativeFeatureFlags::enableEagerRootViewAttachment( jsi::Runtime& /*runtime*/) { return ReactNativeFeatureFlags::enableEagerRootViewAttachment(); diff --git a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h index 47f342fe19ea..ec08dea5d809 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h +++ b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h @@ -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<> + * @generated SignedSource<<84e2bb6d0ee95ff95c84de29eb09ad31>> */ /** @@ -55,6 +55,8 @@ class NativeReactNativeFeatureFlags bool enableCppPropsIteratorSetter(jsi::Runtime& runtime); + bool enableCustomFocusSearchOnClippedElementsAndroid(jsi::Runtime& runtime); + bool enableEagerRootViewAttachment(jsi::Runtime& runtime); bool enableFabricLogs(jsi::Runtime& runtime); diff --git a/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphShadowNode.cpp b/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphShadowNode.cpp index a39bc18c1eb3..cc1b1a447bad 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphShadowNode.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphShadowNode.cpp @@ -26,6 +26,22 @@ using Content = ParagraphShadowNode::Content; const char ParagraphComponentName[] = "Paragraph"; +void ParagraphShadowNode::initialize() noexcept { +#ifdef ANDROID + if (getConcreteProps().isSelectable) { + traits_.set(ShadowNodeTraits::Trait::KeyboardFocusable); + } +#endif +} + +ParagraphShadowNode::ParagraphShadowNode( + const ShadowNodeFragment& fragment, + const ShadowNodeFamily::Shared& family, + ShadowNodeTraits traits) + : ConcreteViewShadowNode(fragment, family, traits) { + initialize(); +} + ParagraphShadowNode::ParagraphShadowNode( const ShadowNode& sourceShadowNode, const ShadowNodeFragment& fragment) @@ -49,6 +65,7 @@ ParagraphShadowNode::ParagraphShadowNode( // to stop Yoga from traversing it. cleanLayout(); } + initialize(); } const Content& ParagraphShadowNode::getContent( diff --git a/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphShadowNode.h b/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphShadowNode.h index 224299bc255c..91e11e40fb64 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphShadowNode.h +++ b/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphShadowNode.h @@ -34,6 +34,11 @@ class ParagraphShadowNode final : public ConcreteViewShadowNode< public: using ConcreteViewShadowNode::ConcreteViewShadowNode; + ParagraphShadowNode( + const ShadowNodeFragment& fragment, + const ShadowNodeFamily::Shared& family, + ShadowNodeTraits traits); + ParagraphShadowNode( const ShadowNode& sourceShadowNode, const ShadowNodeFragment& fragment); @@ -83,6 +88,7 @@ class ParagraphShadowNode final : public ConcreteViewShadowNode< }; private: + void initialize() noexcept; /* * Builds (if needed) and returns a reference to a `Content` object. */ diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/ConcreteViewShadowNode.h b/packages/react-native/ReactCommon/react/renderer/components/view/ConcreteViewShadowNode.h index 84c855c10592..38ac9ea6d05b 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/ConcreteViewShadowNode.h +++ b/packages/react-native/ReactCommon/react/renderer/components/view/ConcreteViewShadowNode.h @@ -7,6 +7,7 @@ #pragma once +#include #include #include #include @@ -15,6 +16,7 @@ #include #include #include +#include namespace facebook::react { @@ -28,6 +30,7 @@ template < typename ViewPropsT = ViewProps, typename ViewEventEmitterT = ViewEventEmitter, typename StateDataT = StateData> + requires(std::is_base_of_v) class ConcreteViewShadowNode : public ConcreteShadowNode< concreteComponentName, YogaLayoutableShadowNode, @@ -114,6 +117,16 @@ class ConcreteViewShadowNode : public ConcreteShadowNode< } else { BaseShadowNode::orderIndex_ = 0; } + + bool isKeyboardFocusable = + HostPlatformViewTraitsInitializer::isKeyboardFocusable(props) || + props.accessible; + + if (isKeyboardFocusable) { + BaseShadowNode::traits_.set(ShadowNodeTraits::Trait::KeyboardFocusable); + } else { + BaseShadowNode::traits_.unset(ShadowNodeTraits::Trait::KeyboardFocusable); + } } }; diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/platform/android/react/renderer/components/view/HostPlatformViewTraitsInitializer.h b/packages/react-native/ReactCommon/react/renderer/components/view/platform/android/react/renderer/components/view/HostPlatformViewTraitsInitializer.h index 4860fdfd7942..23297fec74b3 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/platform/android/react/renderer/components/view/HostPlatformViewTraitsInitializer.h +++ b/packages/react-native/ReactCommon/react/renderer/components/view/platform/android/react/renderer/components/view/HostPlatformViewTraitsInitializer.h @@ -24,4 +24,8 @@ inline bool formsView(const ViewProps& viewProps) { viewProps.renderToHardwareTextureAndroid; } +inline bool isKeyboardFocusable(const ViewProps& viewProps) { + return (viewProps.focusable || viewProps.hasTVPreferredFocus); +} + } // namespace facebook::react::HostPlatformViewTraitsInitializer diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/platform/cxx/react/renderer/components/view/HostPlatformViewTraitsInitializer.h b/packages/react-native/ReactCommon/react/renderer/components/view/platform/cxx/react/renderer/components/view/HostPlatformViewTraitsInitializer.h index f40277e3b035..88debe19dffb 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/platform/cxx/react/renderer/components/view/HostPlatformViewTraitsInitializer.h +++ b/packages/react-native/ReactCommon/react/renderer/components/view/platform/cxx/react/renderer/components/view/HostPlatformViewTraitsInitializer.h @@ -20,4 +20,8 @@ inline bool formsView(const ViewProps& props) { return false; } +inline bool isKeyboardFocusable(const ViewProps& /*props*/) { + return false; +} + } // namespace facebook::react::HostPlatformViewTraitsInitializer diff --git a/packages/react-native/ReactCommon/react/renderer/core/ShadowNodeTraits.h b/packages/react-native/ReactCommon/react/renderer/core/ShadowNodeTraits.h index 841a3d7e3a7f..aaa45fe5b9d7 100644 --- a/packages/react-native/ReactCommon/react/renderer/core/ShadowNodeTraits.h +++ b/packages/react-native/ReactCommon/react/renderer/core/ShadowNodeTraits.h @@ -78,6 +78,9 @@ class ShadowNodeTraits { // Forces the node not to form a host view. ForceFlattenView = 1 << 11, + + // Indicates if the node is keyboard focusable. + KeyboardFocusable = 1 << 12, }; /* diff --git a/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js b/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js index 00ee0c1d9b6a..cd4daa618b8c 100644 --- a/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js +++ b/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js @@ -145,6 +145,16 @@ const definitions: FeatureFlagDefinitions = { }, ossReleaseStage: 'none', }, + enableCustomFocusSearchOnClippedElementsAndroid: { + defaultValue: true, + metadata: { + description: + 'This enables the fabric implementation of focus search so that we can focus clipped elements', + expectedReleaseValue: true, + purpose: 'operational', + }, + ossReleaseStage: 'none', + }, enableEagerRootViewAttachment: { defaultValue: false, metadata: { diff --git a/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js b/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js index 86ff07cc2d2f..f1c279f756b6 100644 --- a/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js +++ b/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js @@ -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<<3687de0645766928a3aa53412c01f70c>> + * @generated SignedSource<> * @flow strict */ @@ -55,6 +55,7 @@ export type ReactNativeFeatureFlags = $ReadOnly<{ enableAccumulatedUpdatesInRawPropsAndroid: Getter, enableBridgelessArchitecture: Getter, enableCppPropsIteratorSetter: Getter, + enableCustomFocusSearchOnClippedElementsAndroid: Getter, enableEagerRootViewAttachment: Getter, enableFabricLogs: Getter, enableFabricRenderer: Getter, @@ -189,6 +190,10 @@ export const enableBridgelessArchitecture: Getter = createNativeFlagGet * Enable prop iterator setter-style construction of Props in C++ (this flag is not used in Java). */ export const enableCppPropsIteratorSetter: Getter = createNativeFlagGetter('enableCppPropsIteratorSetter', false); +/** + * This enables the fabric implementation of focus search so that we can focus clipped elements + */ +export const enableCustomFocusSearchOnClippedElementsAndroid: Getter = createNativeFlagGetter('enableCustomFocusSearchOnClippedElementsAndroid', true); /** * Feature flag to configure eager attachment of the root view/initialisation of the JS code. */ diff --git a/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js b/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js index 01ccabecba64..cba232645529 100644 --- a/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js +++ b/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js @@ -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<<2453ef4a88dff4e0151b7bce52ee9d04>> + * @generated SignedSource<> * @flow strict */ @@ -33,6 +33,7 @@ export interface Spec extends TurboModule { +enableAccumulatedUpdatesInRawPropsAndroid?: () => boolean; +enableBridgelessArchitecture?: () => boolean; +enableCppPropsIteratorSetter?: () => boolean; + +enableCustomFocusSearchOnClippedElementsAndroid?: () => boolean; +enableEagerRootViewAttachment?: () => boolean; +enableFabricLogs?: () => boolean; +enableFabricRenderer?: () => boolean;