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 @@ -62,6 +62,7 @@ import com.facebook.react.views.scroll.ReactScrollViewHelper.SNAP_ALIGNMENT_DISA
import com.facebook.react.views.scroll.ReactScrollViewHelper.SNAP_ALIGNMENT_END
import com.facebook.react.views.scroll.ReactScrollViewHelper.SNAP_ALIGNMENT_START
import com.facebook.react.views.scroll.ReactScrollViewHelper.findNextFocusableView
import com.facebook.react.views.scroll.ReactScrollViewHelper.isSelfOrDescendant
import com.facebook.react.views.view.ImportantForInteractionHelper
import com.facebook.systrace.Systrace
import kotlin.math.abs
Expand Down Expand Up @@ -912,7 +913,7 @@ constructor(context: Context, private val fpsListener: FpsListener? = null) :
if (ReactNativeFeatureFlags.enableCustomFocusSearchOnClippedElementsAndroid()) {
// If we can find the next focus and it is a child of this view, return it, else it means we
// are leaving the scroll view and we should try to find a clipped element
if (nextFocus != null && findViewById<View>(nextFocus.id) != null) {
if (nextFocus != null && isSelfOrDescendant(this, nextFocus)) {
return nextFocus
}
val nextFocusableView = findNextFocusableView(this, focused, direction)
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<<37e2e57a137f899bb94e78bd265de562>>
* @generated SignedSource<<3cd05abbef2823a5f07515fe17ce394c>>
*/

/**
Expand Down Expand Up @@ -64,6 +64,7 @@ import com.facebook.react.views.scroll.ReactScrollViewHelper.SNAP_ALIGNMENT_DISA
import com.facebook.react.views.scroll.ReactScrollViewHelper.SNAP_ALIGNMENT_END
import com.facebook.react.views.scroll.ReactScrollViewHelper.SNAP_ALIGNMENT_START
import com.facebook.react.views.scroll.ReactScrollViewHelper.findNextFocusableView
import com.facebook.react.views.scroll.ReactScrollViewHelper.isSelfOrDescendant
import com.facebook.react.views.view.ImportantForInteractionHelper
import com.facebook.systrace.Systrace
import kotlin.math.abs
Expand Down Expand Up @@ -465,7 +466,7 @@ constructor(context: Context, private val fpsListener: FpsListener? = null) :
val nextFocus = super.focusSearch(focused, direction)

if (ReactNativeFeatureFlags.enableCustomFocusSearchOnClippedElementsAndroid()) {
if (nextFocus != null && findViewById<View>(nextFocus.id) != null) {
if (nextFocus != null && isSelfOrDescendant(this, nextFocus)) {
return nextFocus
}
val nextFocusableView = findNextFocusableView(this, focused, direction)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ import com.facebook.react.views.scroll.ReactScrollViewHelper.SNAP_ALIGNMENT_DISA
import com.facebook.react.views.scroll.ReactScrollViewHelper.SNAP_ALIGNMENT_END
import com.facebook.react.views.scroll.ReactScrollViewHelper.SNAP_ALIGNMENT_START
import com.facebook.react.views.scroll.ReactScrollViewHelper.findNextFocusableView
import com.facebook.react.views.scroll.ReactScrollViewHelper.isSelfOrDescendant
import com.facebook.react.views.view.ImportantForInteractionHelper
import com.facebook.systrace.Systrace
import kotlin.math.abs
Expand Down Expand Up @@ -457,7 +458,7 @@ constructor(context: Context, private val fpsListener: FpsListener? = null) :
val nextFocus = super.focusSearch(focused, direction)

if (ReactNativeFeatureFlags.enableCustomFocusSearchOnClippedElementsAndroid()) {
if (nextFocus != null && findViewById<View>(nextFocus.id) != null) {
if (nextFocus != null && isSelfOrDescendant(this, nextFocus)) {
return nextFocus
}
val nextFocusableView = findNextFocusableView(this, focused, direction)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -597,6 +597,23 @@ public object ReactScrollViewHelper {
return host.findViewById(nextFocusableViewId)
}

/**
* Returns whether [view] is [host] or one of its descendants. Unlike looking the view up by id,
* this also holds for views without an id, such as the `AndroidComposeView` that a `ComposeView`
* adds as its child.
*/
@JvmStatic
internal fun isSelfOrDescendant(host: ViewGroup, view: View): Boolean {
var current: View? = view
while (current != null) {
if (current === host) {
return true
}
current = current.parent as? View
}
return false
}

@JvmStatic
public fun resolveAbsoluteDirection(
@FocusRealDirection direction: Int,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* 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.scroll

import android.view.View
import android.widget.FrameLayout
import org.assertj.core.api.Assertions.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.RuntimeEnvironment

/** Verifies the containment check used by the scroll views' keyboard focus search. */
@RunWith(RobolectricTestRunner::class)
class ReactScrollViewHelperFocusTest {

private lateinit var host: FrameLayout
private lateinit var content: FrameLayout

@Before
fun setup() {
val context = RuntimeEnvironment.getApplication()
host = FrameLayout(context)
content = FrameLayout(context)
host.addView(content)
}

@Test
fun hostItselfIsIncluded() {
assertThat(ReactScrollViewHelper.isSelfOrDescendant(host, host)).isTrue()
}

@Test
fun descendantWithIdIsIncluded() {
val child = View(host.context).apply { id = 42 }
content.addView(child)

assertThat(ReactScrollViewHelper.isSelfOrDescendant(host, child)).isTrue()
}

@Test
fun descendantWithoutIdIsIncluded() {
// For example the AndroidComposeView that a ComposeView adds as its child.
val child = View(host.context)
content.addView(child)

assertThat(child.id).isEqualTo(View.NO_ID)
assertThat(ReactScrollViewHelper.isSelfOrDescendant(host, child)).isTrue()
}

@Test
fun viewOutsideHostIsExcludedEvenIfIdMatchesADescendant() {
content.addView(View(host.context).apply { id = 42 })
val parent = FrameLayout(host.context)
parent.addView(host)
val outside = View(host.context).apply { id = 42 }
parent.addView(outside)

assertThat(ReactScrollViewHelper.isSelfOrDescendant(host, outside)).isFalse()
}

@Test
fun detachedViewIsExcluded() {
assertThat(ReactScrollViewHelper.isSelfOrDescendant(host, View(host.context))).isFalse()
}
}
Loading