Call onFocusChanged when a focused composable is removed

Bug: 274798780
Test: ./gradlew compose:ui:ui:cC -P android.testInstrumentationRunnerArguments.package=androidx.compose.ui.focus.FocusEventCountTest
Test: ./gradlew compose:ui:ui:cC -P android.testInstrumentationRunnerArguments.package=androidx.compose.ui.focus.FocusTargetAttachDetachTest
(cherry picked from https://android-review.googlesource.com/q/commit:509a6f2e320979ab0357ccbd687ea7d2d6f4db6e)
Merged-In: I5fa68637d9d3e8f055cac37dde561e7fc44224c9
Change-Id: I5fa68637d9d3e8f055cac37dde561e7fc44224c9
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusEventCountTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusEventCountTest.kt
index 7c6cc56..7d49b02 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusEventCountTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusEventCountTest.kt
@@ -172,6 +172,34 @@
     }
 
     @Test
+    fun removingActiveComposable_onFocusEventIsCalledWithDefaultValue() {
+        // Arrange.
+        val focusStates = mutableListOf<FocusState>()
+        val focusRequester = FocusRequester()
+        var showBox by mutableStateOf(true)
+        rule.setFocusableContent {
+            if (showBox) {
+                Box(
+                    modifier = Modifier
+                        .onFocusEvent { focusStates.add(it) }
+                        .focusRequester(focusRequester)
+                        .focusTarget()
+                )
+            }
+        }
+        rule.runOnIdle {
+            focusRequester.requestFocus()
+            focusStates.clear()
+        }
+
+        // Act.
+        rule.runOnIdle { showBox = false }
+
+        // Assert.
+        rule.runOnIdle { assertThat(focusStates).isExactly(Inactive) }
+    }
+
+    @Test
     fun removingActiveFocusNode_onFocusEventIsCalledTwice() {
         // Arrange.
         val focusStates = mutableListOf<FocusState>()
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusTargetAttachDetachTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusTargetAttachDetachTest.kt
index 10b7e0e..525d640 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusTargetAttachDetachTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusTargetAttachDetachTest.kt
@@ -165,6 +165,67 @@
     }
 
     @Test
+    fun removedActiveFocusTargetAndFocusChanged_triggersOnFocusEvent() {
+        // Arrange.
+        lateinit var focusState: FocusState
+        val focusRequester = FocusRequester()
+        var optionalModifiers by mutableStateOf(true)
+        rule.setFocusableContent {
+            Box(
+                modifier = Modifier
+                    .focusRequester(focusRequester)
+                    .then(
+                        if (optionalModifiers) {
+                            Modifier
+                                .onFocusEvent { focusState = it }
+                                .focusTarget()
+                        } else {
+                            Modifier
+                        }
+                    )
+            )
+        }
+        rule.runOnIdle {
+            focusRequester.requestFocus()
+            assertThat(focusState.isFocused).isTrue()
+        }
+
+        // Act.
+        rule.runOnIdle { optionalModifiers = false }
+
+        // Assert.
+        rule.runOnIdle { assertThat(focusState.isFocused).isFalse() }
+    }
+
+    @Test
+    fun removedActiveComposable_doesNotTriggerOnFocusEvent() {
+        // Arrange.
+        lateinit var focusState: FocusState
+        val focusRequester = FocusRequester()
+        var optionalBox by mutableStateOf(true)
+        rule.setFocusableContent {
+            if (optionalBox) {
+                Box(
+                    modifier = Modifier
+                        .focusRequester(focusRequester)
+                        .onFocusEvent { focusState = it }
+                        .focusTarget()
+                )
+            }
+        }
+        rule.runOnIdle {
+            focusRequester.requestFocus()
+            assertThat(focusState.isFocused).isTrue()
+        }
+
+        // Act.
+        rule.runOnIdle { optionalBox = false }
+
+        // Assert.
+        rule.runOnIdle { assertThat(focusState.isFocused).isFalse() }
+    }
+
+    @Test
     fun removedCapturedFocusTarget_pointsToNextFocusTarget() {
         // Arrange.
         lateinit var focusState: FocusState
@@ -308,6 +369,45 @@
     }
 
     @Test
+    fun removedActiveComposable_clearsFocusFromAllParents() {
+        // Arrange.
+        lateinit var focusState: FocusState
+        lateinit var parentFocusState: FocusState
+        val focusRequester = FocusRequester()
+        var optionalBox by mutableStateOf(true)
+        rule.setFocusableContent {
+            Box(
+                modifier = Modifier
+                    .onFocusChanged { parentFocusState = it }
+                    .focusTarget()
+            ) {
+                if (optionalBox) {
+                    Box(
+                        modifier = Modifier
+                            .onFocusChanged { focusState = it }
+                            .focusRequester(focusRequester)
+                            .focusTarget()
+                    )
+                }
+            }
+        }
+        rule.runOnIdle {
+            focusRequester.requestFocus()
+            assertThat(focusState.hasFocus).isTrue()
+            assertThat(parentFocusState.hasFocus).isTrue()
+        }
+
+        // Act.
+        rule.runOnIdle { optionalBox = false }
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(focusState.isFocused).isFalse()
+            assertThat(parentFocusState.isFocused).isFalse()
+        }
+    }
+
+    @Test
     fun removedDeactivatedParentFocusTarget_pointsToNextFocusTarget() {
         // Arrange.
         lateinit var focusState: FocusState
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusInvalidationManager.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusInvalidationManager.kt
index 8421e1b..6217e96 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusInvalidationManager.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusInvalidationManager.kt
@@ -73,7 +73,13 @@
         focusEventNodes.forEach { focusEventNode ->
             // When focus nodes are removed, the corresponding focus events are scheduled for
             // invalidation. If the focus event was also removed, we don't need to invalidate it.
-            if (!focusEventNode.node.isAttached) return@forEach
+            // We call onFocusEvent with the default value, just to make it easier for the user,
+            // so that they don't have to keep track of whether they caused a focused item to be
+            // removed (Which would cause it to lose focus).
+            if (!focusEventNode.node.isAttached) {
+                focusEventNode.onFocusEvent(Inactive)
+                return@forEach
+            }
 
             var requiresUpdate = true
             var aggregatedNode = false