Merge "Improve focus handling using the HierarchicalFocusCoordinator" into androidx-main
diff --git a/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/SwipeToDismissBoxTest.kt b/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/SwipeToDismissBoxTest.kt
index dfcca4b..956835f 100644
--- a/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/SwipeToDismissBoxTest.kt
+++ b/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/SwipeToDismissBoxTest.kt
@@ -18,6 +18,7 @@
 
 import androidx.compose.foundation.ScrollState
 import androidx.compose.foundation.clickable
+import androidx.compose.foundation.focusable
 import androidx.compose.foundation.horizontalScroll
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Box
@@ -36,6 +37,8 @@
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.focus.onFocusChanged
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.platform.testTag
@@ -53,6 +56,8 @@
 import androidx.compose.ui.test.swipeRight
 import java.lang.Math.sin
 import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
 import org.junit.Rule
 import org.junit.Test
 
@@ -419,6 +424,58 @@
         }
     }
 
+    @OptIn(ExperimentalWearFoundationApi::class)
+    @Test
+    fun partial_swipe_maintains_focus() {
+        var focusedBackground by mutableStateOf(false)
+        var focusedContent by mutableStateOf(false)
+
+        rule.setContent {
+            val state = rememberSwipeToDismissBoxState()
+            SwipeToDismissBox(
+                state = state,
+                modifier = Modifier.testTag(TEST_TAG),
+            ) { isBackground ->
+                if (isBackground) {
+                    val focusRequester = rememberActiveFocusRequester()
+                    BasicText(
+                        BACKGROUND_MESSAGE,
+                        Modifier
+                            .onFocusChanged { focusedBackground = it.isFocused }
+                            .focusRequester(focusRequester)
+                            .focusable())
+                } else {
+                    val focusRequester = rememberActiveFocusRequester()
+                    BasicText(
+                        CONTENT_MESSAGE,
+                        Modifier
+                            .onFocusChanged { focusedContent = it.isFocused }
+                            .focusRequester(focusRequester)
+                            .focusable())
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertTrue(focusedContent)
+            assertFalse(focusedBackground)
+        }
+
+        // Click down and drag across 1/4 of the screen to start a swipe,
+        // but don't release the finger, so that the screen can be inspected
+        // (note that swipeRight would release the finger and does not pause time midway).
+        rule.onNodeWithTag(TEST_TAG).performTouchInput {
+            down(Offset(x = 0f, y = height / 2f))
+            moveTo(Offset(x = width / 4f, y = height / 2f))
+        }
+
+        // We started showing the background, but focus hasn't changed.
+        rule.runOnIdle {
+            assertTrue(focusedContent)
+            assertFalse(focusedBackground)
+        }
+    }
+
     private fun testBothDirectionScroll(
         initialTouch: Long,
         duration: Long,
diff --git a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/SwipeToDismissBox.kt b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/SwipeToDismissBox.kt
index b776940..f4e9c56 100644
--- a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/SwipeToDismissBox.kt
+++ b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/SwipeToDismissBox.kt
@@ -203,13 +203,15 @@
                 if (!isBackground ||
                     (userSwipeEnabled && (state.swipeableState.offset?.roundToInt() ?: 0) > 0)
                 ) {
-                    Box(contentModifier) {
-                        // We use the repeat loop above and call content at this location
-                        // for both background and foreground so that any persistence
-                        // within the content composable has the same call stack which is used
-                        // as part of the hash identity for saveable state.
-                        content(isBackground)
-                        Box(modifier = scrimModifier)
+                    HierarchicalFocusCoordinator(requiresFocus = { !isBackground }) {
+                        Box(contentModifier) {
+                            // We use the repeat loop above and call content at this location
+                            // for both background and foreground so that any persistence
+                            // within the content composable has the same call stack which is used
+                            // as part of the hash identity for saveable state.
+                            content(isBackground)
+                            Box(modifier = scrimModifier)
+                        }
                     }
                 }
             }
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/DemoApp.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/DemoApp.kt
index 02fcfac..d06de43 100644
--- a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/DemoApp.kt
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/DemoApp.kt
@@ -43,6 +43,7 @@
 import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.unit.dp
+import androidx.wear.compose.foundation.ExperimentalWearFoundationApi
 import androidx.wear.compose.foundation.SwipeToDismissBox
 import androidx.wear.compose.foundation.SwipeToDismissBoxState
 import androidx.wear.compose.foundation.SwipeToDismissKeys
@@ -54,6 +55,7 @@
 import androidx.wear.compose.foundation.lazy.ScalingLazyListState
 import androidx.wear.compose.foundation.lazy.ScalingParams
 import androidx.wear.compose.foundation.lazy.rememberScalingLazyListState
+import androidx.wear.compose.foundation.rememberActiveFocusRequester
 import androidx.wear.compose.foundation.rememberSwipeToDismissBoxState
 import androidx.wear.compose.integration.demos.common.ActivityDemo
 import androidx.wear.compose.integration.demos.common.ComposableDemo
@@ -133,7 +135,9 @@
 ) {
     ScalingLazyColumnWithRSB(
         horizontalAlignment = Alignment.CenterHorizontally,
-        modifier = Modifier.fillMaxWidth().testTag(DemoListTag),
+        modifier = Modifier
+            .fillMaxWidth()
+            .testTag(DemoListTag),
     ) {
         item {
             ListHeader {
@@ -169,7 +173,9 @@
                     ) {
                         Text(
                             text = description,
-                            modifier = Modifier.fillMaxWidth().align(Alignment.Center),
+                            modifier = Modifier
+                                .fillMaxWidth()
+                                .align(Alignment.Center),
                             textAlign = TextAlign.Center
                         )
                     }
@@ -259,13 +265,15 @@
             true
         }.let {
             if (focusRequester != null) {
-                it.focusRequester(focusRequester)
+                it
+                    .focusRequester(focusRequester)
                     .focusable()
             } else it
         }
     }
 }
 
+@OptIn(ExperimentalWearFoundationApi::class)
 @Composable
 fun ScalingLazyColumnWithRSB(
     modifier: Modifier = Modifier,
@@ -284,7 +292,7 @@
     val flingBehavior = if (snap) ScalingLazyColumnDefaults.snapFlingBehavior(
         state = state
     ) else ScrollableDefaults.flingBehavior()
-    val focusRequester = remember { FocusRequester() }
+    val focusRequester = rememberActiveFocusRequester()
     ScalingLazyColumn(
         modifier = modifier.rsbScroll(
             scrollableState = state,
@@ -300,7 +308,4 @@
         autoCentering = autoCentering,
         content = content
     )
-    LaunchedEffect(Unit) {
-        focusRequester.requestFocus()
-    }
 }