Clickable and focusable can now be focused in non-touch modes

Also adds focus state support in ripple and Material components.

Bug: b/202856230
Fixes: b/152525426
Fixes: b/163725615
Test: Screenshot tests / modifier tests
Relnote: "Modifier.clickable and Modifier.toggleable can now be focused by default when using a keyboard. Ripple has been updated to support focus state, so components such as Button will now appear focused when necessary."
Change-Id: I2161d9c5e4ef4d039fb8f90cb43a94f0f15c0795
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ClickableTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ClickableTest.kt
index 2f8b9d6..745c482 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ClickableTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ClickableTest.kt
@@ -20,6 +20,7 @@
 import androidx.compose.foundation.gestures.detectTapGestures
 import androidx.compose.foundation.gestures.draggable
 import androidx.compose.foundation.gestures.rememberDraggableState
+import androidx.compose.foundation.interaction.FocusInteraction
 import androidx.compose.foundation.interaction.HoverInteraction
 import androidx.compose.foundation.interaction.Interaction
 import androidx.compose.foundation.interaction.MutableInteractionSource
@@ -29,16 +30,25 @@
 import androidx.compose.foundation.layout.requiredWidth
 import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.text.BasicText
+import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.runtime.setValue
+import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.clipToBounds
+import androidx.compose.ui.focus.FocusManager
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
 import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.input.InputMode
+import androidx.compose.ui.input.InputModeManager
 import androidx.compose.ui.input.pointer.pointerInput
 import androidx.compose.ui.platform.InspectableValue
+import androidx.compose.ui.platform.LocalFocusManager
+import androidx.compose.ui.platform.LocalInputModeManager
 import androidx.compose.ui.platform.isDebugInspectorInfoEnabled
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.semantics.SemanticsActions
@@ -434,7 +444,7 @@
     fun clickableTest_interactionSource() {
         val interactionSource = MutableInteractionSource()
 
-        var scope: CoroutineScope? = null
+        lateinit var scope: CoroutineScope
 
         rule.mainClock.autoAdvance = false
 
@@ -455,7 +465,7 @@
 
         val interactions = mutableListOf<Interaction>()
 
-        scope!!.launch {
+        scope.launch {
             interactionSource.interactions.collect { interactions.add(it) }
         }
 
@@ -499,7 +509,7 @@
     fun clickableTest_interactionSource_immediateRelease() {
         val interactionSource = MutableInteractionSource()
 
-        var scope: CoroutineScope? = null
+        lateinit var scope: CoroutineScope
 
         rule.mainClock.autoAdvance = false
 
@@ -520,7 +530,7 @@
 
         val interactions = mutableListOf<Interaction>()
 
-        scope!!.launch {
+        scope.launch {
             interactionSource.interactions.collect { interactions.add(it) }
         }
 
@@ -549,7 +559,7 @@
     fun clickableTest_interactionSource_immediateCancel() {
         val interactionSource = MutableInteractionSource()
 
-        var scope: CoroutineScope? = null
+        lateinit var scope: CoroutineScope
 
         rule.mainClock.autoAdvance = false
 
@@ -570,7 +580,7 @@
 
         val interactions = mutableListOf<Interaction>()
 
-        scope!!.launch {
+        scope.launch {
             interactionSource.interactions.collect { interactions.add(it) }
         }
 
@@ -595,7 +605,7 @@
     fun clickableTest_interactionSource_immediateDrag() {
         val interactionSource = MutableInteractionSource()
 
-        var scope: CoroutineScope? = null
+        lateinit var scope: CoroutineScope
 
         rule.mainClock.autoAdvance = false
 
@@ -620,7 +630,7 @@
 
         val interactions = mutableListOf<Interaction>()
 
-        scope!!.launch {
+        scope.launch {
             interactionSource.interactions.collect { interactions.add(it) }
         }
 
@@ -646,7 +656,7 @@
     fun clickableTest_interactionSource_dragAfterTimeout() {
         val interactionSource = MutableInteractionSource()
 
-        var scope: CoroutineScope? = null
+        lateinit var scope: CoroutineScope
 
         rule.mainClock.autoAdvance = false
 
@@ -671,7 +681,7 @@
 
         val interactions = mutableListOf<Interaction>()
 
-        scope!!.launch {
+        scope.launch {
             interactionSource.interactions.collect { interactions.add(it) }
         }
 
@@ -710,7 +720,7 @@
     fun clickableTest_interactionSource_cancelledGesture() {
         val interactionSource = MutableInteractionSource()
 
-        var scope: CoroutineScope? = null
+        lateinit var scope: CoroutineScope
 
         rule.mainClock.autoAdvance = false
 
@@ -731,7 +741,7 @@
 
         val interactions = mutableListOf<Interaction>()
 
-        scope!!.launch {
+        scope.launch {
             interactionSource.interactions.collect { interactions.add(it) }
         }
 
@@ -766,7 +776,7 @@
         val interactionSource = MutableInteractionSource()
         var emitClickableText by mutableStateOf(true)
 
-        var scope: CoroutineScope? = null
+        lateinit var scope: CoroutineScope
 
         rule.mainClock.autoAdvance = false
 
@@ -789,7 +799,7 @@
 
         val interactions = mutableListOf<Interaction>()
 
-        scope!!.launch {
+        scope.launch {
             interactionSource.interactions.collect { interactions.add(it) }
         }
 
@@ -828,7 +838,7 @@
     fun clickableTest_interactionSource_hover() {
         val interactionSource = MutableInteractionSource()
 
-        var scope: CoroutineScope? = null
+        lateinit var scope: CoroutineScope
 
         rule.setContent {
             scope = rememberCoroutineScope()
@@ -847,7 +857,7 @@
 
         val interactions = mutableListOf<Interaction>()
 
-        scope!!.launch {
+        scope.launch {
             interactionSource.interactions.collect { interactions.add(it) }
         }
 
@@ -881,7 +891,7 @@
     fun clickableTest_interactionSource_hover_and_press() {
         val interactionSource = MutableInteractionSource()
 
-        var scope: CoroutineScope? = null
+        lateinit var scope: CoroutineScope
 
         rule.setContent {
             scope = rememberCoroutineScope()
@@ -900,7 +910,7 @@
 
         val interactions = mutableListOf<Interaction>()
 
-        scope!!.launch {
+        scope.launch {
             interactionSource.interactions.collect { interactions.add(it) }
         }
 
@@ -928,6 +938,123 @@
         }
     }
 
+    @Test
+    fun clickableTest_interactionSource_focus_inTouchMode() {
+        val interactionSource = MutableInteractionSource()
+
+        lateinit var scope: CoroutineScope
+
+        val focusRequester = FocusRequester()
+
+        rule.setContent {
+            scope = rememberCoroutineScope()
+            Box {
+                BasicText(
+                    "ClickableText",
+                    modifier = Modifier
+                        .testTag("myClickable")
+                        .focusRequester(focusRequester)
+                        .combinedClickable(
+                            interactionSource = interactionSource,
+                            indication = null
+                        ) {}
+                )
+            }
+        }
+
+        val interactions = mutableListOf<Interaction>()
+
+        scope.launch {
+            interactionSource.interactions.collect { interactions.add(it) }
+        }
+
+        rule.runOnIdle {
+            assertThat(interactions).isEmpty()
+        }
+
+        rule.runOnIdle {
+            focusRequester.requestFocus()
+        }
+
+        // Touch mode by default, so we shouldn't be focused
+        rule.runOnIdle {
+            assertThat(interactions).isEmpty()
+        }
+    }
+
+    @Test
+    fun clickableTest_interactionSource_focus_inKeyboardMode() {
+        val interactionSource = MutableInteractionSource()
+
+        lateinit var scope: CoroutineScope
+
+        val focusRequester = FocusRequester()
+
+        lateinit var focusManager: FocusManager
+
+        val keyboardInputModeManager = object : InputModeManager {
+            override val inputMode = InputMode.Keyboard
+
+            @OptIn(ExperimentalComposeUiApi::class)
+            override fun requestInputMode(inputMode: InputMode) = true
+        }
+
+        rule.setContent {
+            scope = rememberCoroutineScope()
+            focusManager = LocalFocusManager.current
+            CompositionLocalProvider(LocalInputModeManager provides keyboardInputModeManager) {
+                Box {
+                    BasicText(
+                        "ClickableText",
+                        modifier = Modifier
+                            .testTag("myClickable")
+                            .focusRequester(focusRequester)
+                            .combinedClickable(
+                                interactionSource = interactionSource,
+                                indication = null
+                            ) {}
+                    )
+                }
+            }
+        }
+
+        val interactions = mutableListOf<Interaction>()
+
+        scope.launch {
+            interactionSource.interactions.collect { interactions.add(it) }
+        }
+
+        rule.runOnIdle {
+            assertThat(interactions).isEmpty()
+        }
+
+        rule.runOnIdle {
+            focusRequester.requestFocus()
+        }
+
+        // Keyboard mode, so we should now be focused and see an interaction
+        rule.runOnIdle {
+            assertThat(interactions).hasSize(1)
+            assertThat(interactions.first()).isInstanceOf(FocusInteraction.Focus::class.java)
+        }
+
+        rule.runOnIdle {
+            focusManager.clearFocus()
+        }
+
+        rule.runOnIdle {
+            assertThat(interactions).hasSize(2)
+            assertThat(interactions.first()).isInstanceOf(FocusInteraction.Focus::class.java)
+            assertThat(interactions[1])
+                .isInstanceOf(FocusInteraction.Unfocus::class.java)
+            assertThat((interactions[1] as FocusInteraction.Unfocus).focus)
+                .isEqualTo(interactions[0])
+        }
+    }
+
+    // TODO: b/202871171 - add test for changing between keyboard mode and touch mode, making sure
+    // it resets existing focus
+
     /**
      * Regression test for b/186223077
      *
@@ -945,7 +1072,7 @@
         // Simulate the long click causing a recomposition, and changing the lambda instance
         onLongClick = initialLongClick
 
-        var scope: CoroutineScope? = null
+        lateinit var scope: CoroutineScope
 
         rule.mainClock.autoAdvance = false
 
@@ -967,7 +1094,7 @@
 
         val interactions = mutableListOf<Interaction>()
 
-        scope!!.launch {
+        scope.launch {
             interactionSource.interactions.collect { interactions.add(it) }
         }
 
@@ -1021,7 +1148,7 @@
         // Simulate the long click causing a recomposition, and changing the lambda to be null
         onLongClick = initialLongClick
 
-        var scope: CoroutineScope? = null
+        lateinit var scope: CoroutineScope
 
         rule.mainClock.autoAdvance = false
 
@@ -1043,7 +1170,7 @@
 
         val interactions = mutableListOf<Interaction>()
 
-        scope!!.launch {
+        scope.launch {
             interactionSource.interactions.collect { interactions.add(it) }
         }
 
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/SelectableTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/SelectableTest.kt
index b74d3a8..cf783e1 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/SelectableTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/SelectableTest.kt
@@ -16,6 +16,7 @@
 
 package androidx.compose.foundation
 
+import androidx.compose.foundation.interaction.FocusInteraction
 import androidx.compose.foundation.interaction.HoverInteraction
 import androidx.compose.foundation.interaction.Interaction
 import androidx.compose.foundation.interaction.MutableInteractionSource
@@ -23,15 +24,24 @@
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.selection.selectable
 import androidx.compose.foundation.text.BasicText
+import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.runtime.setValue
 import androidx.compose.testutils.first
+import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusManager
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
 import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.input.InputMode
+import androidx.compose.ui.input.InputModeManager
 import androidx.compose.ui.platform.InspectableValue
+import androidx.compose.ui.platform.LocalFocusManager
+import androidx.compose.ui.platform.LocalInputModeManager
 import androidx.compose.ui.platform.isDebugInspectorInfoEnabled
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.semantics.SemanticsProperties
@@ -180,7 +190,7 @@
     fun selectableTest_interactionSource() {
         val interactionSource = MutableInteractionSource()
 
-        var scope: CoroutineScope? = null
+        lateinit var scope: CoroutineScope
 
         rule.mainClock.autoAdvance = false
 
@@ -202,7 +212,7 @@
 
         val interactions = mutableListOf<Interaction>()
 
-        scope!!.launch {
+        scope.launch {
             interactionSource.interactions.collect { interactions.add(it) }
         }
 
@@ -238,7 +248,7 @@
         val interactionSource = MutableInteractionSource()
         var emitSelectableText by mutableStateOf(true)
 
-        var scope: CoroutineScope? = null
+        lateinit var scope: CoroutineScope
 
         rule.mainClock.autoAdvance = false
 
@@ -262,7 +272,7 @@
 
         val interactions = mutableListOf<Interaction>()
 
-        scope!!.launch {
+        scope.launch {
             interactionSource.interactions.collect { interactions.add(it) }
         }
 
@@ -302,7 +312,7 @@
     fun selectableTest_interactionSource_hover() {
         val interactionSource = MutableInteractionSource()
 
-        var scope: CoroutineScope? = null
+        lateinit var scope: CoroutineScope
 
         rule.setContent {
             scope = rememberCoroutineScope()
@@ -322,7 +332,7 @@
 
         val interactions = mutableListOf<Interaction>()
 
-        scope!!.launch {
+        scope.launch {
             interactionSource.interactions.collect { interactions.add(it) }
         }
 
@@ -352,6 +362,127 @@
     }
 
     @Test
+    fun selectableTest_interactionSource_focus_inTouchMode() {
+        val interactionSource = MutableInteractionSource()
+
+        lateinit var scope: CoroutineScope
+
+        val focusRequester = FocusRequester()
+
+        rule.setContent {
+            scope = rememberCoroutineScope()
+            Box {
+                Box(
+                    Modifier
+                        .focusRequester(focusRequester)
+                        .selectable(
+                            selected = true,
+                            interactionSource = interactionSource,
+                            indication = null,
+                            onClick = {}
+                        )
+                ) {
+                    BasicText("SelectableText")
+                }
+            }
+        }
+
+        val interactions = mutableListOf<Interaction>()
+
+        scope.launch {
+            interactionSource.interactions.collect { interactions.add(it) }
+        }
+
+        rule.runOnIdle {
+            assertThat(interactions).isEmpty()
+        }
+
+        rule.runOnIdle {
+            focusRequester.requestFocus()
+        }
+
+        // Touch mode by default, so we shouldn't be focused
+        rule.runOnIdle {
+            assertThat(interactions).isEmpty()
+        }
+    }
+
+    @Test
+    fun selectableTest_interactionSource_focus_inKeyboardMode() {
+        val interactionSource = MutableInteractionSource()
+
+        lateinit var scope: CoroutineScope
+
+        val focusRequester = FocusRequester()
+
+        lateinit var focusManager: FocusManager
+
+        val keyboardInputModeManager = object : InputModeManager {
+            override val inputMode = InputMode.Keyboard
+
+            @OptIn(ExperimentalComposeUiApi::class)
+            override fun requestInputMode(inputMode: InputMode) = true
+        }
+
+        rule.setContent {
+            scope = rememberCoroutineScope()
+            focusManager = LocalFocusManager.current
+            CompositionLocalProvider(LocalInputModeManager provides keyboardInputModeManager) {
+                Box {
+                    Box(
+                        Modifier
+                            .focusRequester(focusRequester)
+                            .selectable(
+                                selected = true,
+                                interactionSource = interactionSource,
+                                indication = null,
+                                onClick = {}
+                            )
+                    ) {
+                        BasicText("SelectableText")
+                    }
+                }
+            }
+        }
+
+        val interactions = mutableListOf<Interaction>()
+
+        scope.launch {
+            interactionSource.interactions.collect { interactions.add(it) }
+        }
+
+        rule.runOnIdle {
+            assertThat(interactions).isEmpty()
+        }
+
+        rule.runOnIdle {
+            focusRequester.requestFocus()
+        }
+
+        // Keyboard mode, so we should now be focused and see an interaction
+        rule.runOnIdle {
+            assertThat(interactions).hasSize(1)
+            assertThat(interactions.first()).isInstanceOf(FocusInteraction.Focus::class.java)
+        }
+
+        rule.runOnIdle {
+            focusManager.clearFocus()
+        }
+
+        rule.runOnIdle {
+            assertThat(interactions).hasSize(2)
+            assertThat(interactions.first()).isInstanceOf(FocusInteraction.Focus::class.java)
+            assertThat(interactions[1])
+                .isInstanceOf(FocusInteraction.Unfocus::class.java)
+            assertThat((interactions[1] as FocusInteraction.Unfocus).focus)
+                .isEqualTo(interactions[0])
+        }
+    }
+
+    // TODO: b/202871171 - add test for changing between keyboard mode and touch mode, making sure
+    // it resets existing focus
+
+    @Test
     fun selectableTest_testInspectorValue_noIndication() {
         rule.setContent {
             val modifier = Modifier.selectable(false) {} as InspectableValue
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ToggleableTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ToggleableTest.kt
index 3de6204..a01e6a7 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ToggleableTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ToggleableTest.kt
@@ -16,6 +16,7 @@
 
 package androidx.compose.foundation
 
+import androidx.compose.foundation.interaction.FocusInteraction
 import androidx.compose.foundation.interaction.HoverInteraction
 import androidx.compose.foundation.interaction.Interaction
 import androidx.compose.foundation.interaction.MutableInteractionSource
@@ -27,15 +28,24 @@
 import androidx.compose.foundation.selection.toggleable
 import androidx.compose.foundation.selection.triStateToggleable
 import androidx.compose.foundation.text.BasicText
+import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.runtime.setValue
 import androidx.compose.testutils.first
+import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusManager
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
 import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.input.InputMode
+import androidx.compose.ui.input.InputModeManager
 import androidx.compose.ui.platform.InspectableValue
+import androidx.compose.ui.platform.LocalFocusManager
+import androidx.compose.ui.platform.LocalInputModeManager
 import androidx.compose.ui.platform.isDebugInspectorInfoEnabled
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.semantics.SemanticsProperties
@@ -275,7 +285,7 @@
     fun toggleableTest_interactionSource() {
         val interactionSource = MutableInteractionSource()
 
-        var scope: CoroutineScope? = null
+        lateinit var scope: CoroutineScope
 
         rule.mainClock.autoAdvance = false
 
@@ -297,7 +307,7 @@
 
         val interactions = mutableListOf<Interaction>()
 
-        scope!!.launch {
+        scope.launch {
             interactionSource.interactions.collect { interactions.add(it) }
         }
 
@@ -333,7 +343,7 @@
         val interactionSource = MutableInteractionSource()
         var emitToggleableText by mutableStateOf(true)
 
-        var scope: CoroutineScope? = null
+        lateinit var scope: CoroutineScope
 
         rule.mainClock.autoAdvance = false
 
@@ -357,7 +367,7 @@
 
         val interactions = mutableListOf<Interaction>()
 
-        scope!!.launch {
+        scope.launch {
             interactionSource.interactions.collect { interactions.add(it) }
         }
 
@@ -397,7 +407,7 @@
     fun toggleableTest_interactionSource_hover() {
         val interactionSource = MutableInteractionSource()
 
-        var scope: CoroutineScope? = null
+        lateinit var scope: CoroutineScope
 
         rule.setContent {
             scope = rememberCoroutineScope()
@@ -417,7 +427,7 @@
 
         val interactions = mutableListOf<Interaction>()
 
-        scope!!.launch {
+        scope.launch {
             interactionSource.interactions.collect { interactions.add(it) }
         }
 
@@ -447,6 +457,127 @@
     }
 
     @Test
+    fun toggleableTest_interactionSource_focus_inTouchMode() {
+        val interactionSource = MutableInteractionSource()
+
+        lateinit var scope: CoroutineScope
+
+        val focusRequester = FocusRequester()
+
+        rule.setContent {
+            scope = rememberCoroutineScope()
+            Box {
+                Box(
+                    Modifier
+                        .focusRequester(focusRequester)
+                        .toggleable(
+                            value = true,
+                            interactionSource = interactionSource,
+                            indication = null,
+                            onValueChange = {}
+                        )
+                ) {
+                    BasicText("ToggleableText")
+                }
+            }
+        }
+
+        val interactions = mutableListOf<Interaction>()
+
+        scope.launch {
+            interactionSource.interactions.collect { interactions.add(it) }
+        }
+
+        rule.runOnIdle {
+            assertThat(interactions).isEmpty()
+        }
+
+        rule.runOnIdle {
+            focusRequester.requestFocus()
+        }
+
+        // Touch mode by default, so we shouldn't be focused
+        rule.runOnIdle {
+            assertThat(interactions).isEmpty()
+        }
+    }
+
+    @Test
+    fun toggleableTest_interactionSource_focus_inKeyboardMode() {
+        val interactionSource = MutableInteractionSource()
+
+        lateinit var scope: CoroutineScope
+
+        val focusRequester = FocusRequester()
+
+        lateinit var focusManager: FocusManager
+
+        val keyboardInputModeManager = object : InputModeManager {
+            override val inputMode = InputMode.Keyboard
+
+            @OptIn(ExperimentalComposeUiApi::class)
+            override fun requestInputMode(inputMode: InputMode) = true
+        }
+
+        rule.setContent {
+            scope = rememberCoroutineScope()
+            focusManager = LocalFocusManager.current
+            CompositionLocalProvider(LocalInputModeManager provides keyboardInputModeManager) {
+                Box {
+                    Box(
+                        Modifier
+                            .focusRequester(focusRequester)
+                            .toggleable(
+                                value = true,
+                                interactionSource = interactionSource,
+                                indication = null,
+                                onValueChange = {}
+                            )
+                    ) {
+                        BasicText("ToggleableText")
+                    }
+                }
+            }
+        }
+
+        val interactions = mutableListOf<Interaction>()
+
+        scope.launch {
+            interactionSource.interactions.collect { interactions.add(it) }
+        }
+
+        rule.runOnIdle {
+            assertThat(interactions).isEmpty()
+        }
+
+        rule.runOnIdle {
+            focusRequester.requestFocus()
+        }
+
+        // Keyboard mode, so we should now be focused and see an interaction
+        rule.runOnIdle {
+            assertThat(interactions).hasSize(1)
+            assertThat(interactions.first()).isInstanceOf(FocusInteraction.Focus::class.java)
+        }
+
+        rule.runOnIdle {
+            focusManager.clearFocus()
+        }
+
+        rule.runOnIdle {
+            assertThat(interactions).hasSize(2)
+            assertThat(interactions.first()).isInstanceOf(FocusInteraction.Focus::class.java)
+            assertThat(interactions[1])
+                .isInstanceOf(FocusInteraction.Unfocus::class.java)
+            assertThat((interactions[1] as FocusInteraction.Unfocus).focus)
+                .isEqualTo(interactions[0])
+        }
+    }
+
+    // TODO: b/202871171 - add test for changing between keyboard mode and touch mode, making sure
+    // it resets existing focus
+
+    @Test
     fun toggleableText_testInspectorValue_noIndication() {
         rule.setContent {
             val modifier = Modifier.toggleable(value = true, onValueChange = {}) as InspectableValue
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Clickable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Clickable.kt
index e752677..83b26bf 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Clickable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Clickable.kt
@@ -394,8 +394,6 @@
  */
 internal expect val TapIndicationDelay: Long
 
-@Composable
-@Suppress("ComposableModifierFactory")
 internal fun Modifier.genericClickableWithoutGesture(
     gestureModifiers: Modifier,
     interactionSource: MutableInteractionSource,
@@ -426,6 +424,7 @@
     return this
         .then(semanticModifier)
         .indication(interactionSource, indication)
-        .hoverable(interactionSource = interactionSource)
+        .hoverable(enabled = enabled, interactionSource = interactionSource)
+        .focusableInNonTouchMode(enabled = enabled, interactionSource = interactionSource)
         .then(gestureModifiers)
 }
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Focusable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Focusable.kt
index 18786ba..582e52c 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Focusable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Focusable.kt
@@ -27,10 +27,13 @@
 import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.composed
+import androidx.compose.ui.focus.focusProperties
 import androidx.compose.ui.focus.focusTarget
 import androidx.compose.ui.focus.onFocusChanged
+import androidx.compose.ui.input.InputMode
 import androidx.compose.ui.layout.RelocationRequester
 import androidx.compose.ui.layout.relocationRequester
+import androidx.compose.ui.platform.LocalInputModeManager
 import androidx.compose.ui.platform.debugInspectorInfo
 import androidx.compose.ui.semantics.focused
 import androidx.compose.ui.semantics.semantics
@@ -122,3 +125,25 @@
         Modifier
     }
 }
+
+// TODO: b/202856230 - consider either making this / a similar API public, or add a parameter to
+//  focusable to configure this behavior.
+/**
+ * [focusable] but only when not in touch mode - when [LocalInputModeManager] is
+ * not [InputMode.Touch]
+ */
+internal fun Modifier.focusableInNonTouchMode(
+    enabled: Boolean,
+    interactionSource: MutableInteractionSource?
+) = composed(
+    inspectorInfo = debugInspectorInfo {
+        name = "focusableInNonTouchMode"
+        properties["enabled"] = enabled
+        properties["interactionSource"] = interactionSource
+    }
+) {
+    val inputModeManager = LocalInputModeManager.current
+    Modifier
+        .focusProperties { canFocus = inputModeManager.inputMode != InputMode.Touch }
+        .focusable(enabled, interactionSource)
+}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Indication.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Indication.kt
index d85462d..e15bfe4 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Indication.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Indication.kt
@@ -17,6 +17,7 @@
 package androidx.compose.foundation
 
 import androidx.compose.foundation.interaction.InteractionSource
+import androidx.compose.foundation.interaction.collectIsFocusedAsState
 import androidx.compose.foundation.interaction.collectIsHoveredAsState
 import androidx.compose.foundation.interaction.collectIsPressedAsState
 import androidx.compose.runtime.Composable
@@ -149,13 +150,14 @@
 
     private class DefaultDebugIndicationInstance(
         private val isPressed: State<Boolean>,
-        private val isHovered: State<Boolean>
+        private val isHovered: State<Boolean>,
+        private val isFocused: State<Boolean>,
     ) : IndicationInstance {
         override fun ContentDrawScope.drawIndication() {
             drawContent()
             if (isPressed.value) {
                 drawRect(color = Color.Black.copy(alpha = 0.3f), size = size)
-            } else if (isHovered.value) {
+            } else if (isHovered.value || isFocused.value) {
                 drawRect(color = Color.Black.copy(alpha = 0.1f), size = size)
             }
         }
@@ -165,8 +167,9 @@
     override fun rememberUpdatedInstance(interactionSource: InteractionSource): IndicationInstance {
         val isPressed = interactionSource.collectIsPressedAsState()
         val isHovered = interactionSource.collectIsHoveredAsState()
+        val isFocused = interactionSource.collectIsFocusedAsState()
         return remember(interactionSource) {
-            DefaultDebugIndicationInstance(isPressed, isHovered)
+            DefaultDebugIndicationInstance(isPressed, isHovered, isFocused)
         }
     }
 }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/selection/Toggleable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/selection/Toggleable.kt
index 8d79b16..d954661 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/selection/Toggleable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/selection/Toggleable.kt
@@ -19,6 +19,7 @@
 import androidx.compose.foundation.Indication
 import androidx.compose.foundation.PressedInteractionSourceDisposableEffect
 import androidx.compose.foundation.LocalIndication
+import androidx.compose.foundation.focusableInNonTouchMode
 import androidx.compose.foundation.gestures.detectTapAndPress
 import androidx.compose.foundation.handlePressInteraction
 import androidx.compose.foundation.hoverable
@@ -269,6 +270,7 @@
     this
         .then(semantics)
         .indication(interactionSource, indication)
-        .hoverable(interactionSource = interactionSource)
+        .hoverable(enabled = enabled, interactionSource = interactionSource)
+        .focusableInNonTouchMode(enabled = enabled, interactionSource = interactionSource)
         .then(gestures)
 }
\ No newline at end of file
diff --git a/compose/material/material-ripple/src/commonMain/kotlin/androidx/compose/material/ripple/Ripple.kt b/compose/material/material-ripple/src/commonMain/kotlin/androidx/compose/material/ripple/Ripple.kt
index 1ae18ca..e6ca169 100644
--- a/compose/material/material-ripple/src/commonMain/kotlin/androidx/compose/material/ripple/Ripple.kt
+++ b/compose/material/material-ripple/src/commonMain/kotlin/androidx/compose/material/ripple/Ripple.kt
@@ -23,6 +23,7 @@
 import androidx.compose.foundation.Indication
 import androidx.compose.foundation.IndicationInstance
 import androidx.compose.foundation.interaction.DragInteraction
+import androidx.compose.foundation.interaction.FocusInteraction
 import androidx.compose.foundation.interaction.HoverInteraction
 import androidx.compose.foundation.interaction.Interaction
 import androidx.compose.foundation.interaction.InteractionSource
@@ -247,7 +248,6 @@
     private var currentInteraction: Interaction? = null
 
     fun handleInteraction(interaction: Interaction, scope: CoroutineScope) {
-        // TODO: handle focus states
         when (interaction) {
             is HoverInteraction.Enter -> {
                 interactions.add(interaction)
@@ -255,6 +255,12 @@
             is HoverInteraction.Exit -> {
                 interactions.remove(interaction.enter)
             }
+            is FocusInteraction.Focus -> {
+                interactions.add(interaction)
+            }
+            is FocusInteraction.Unfocus -> {
+                interactions.remove(interaction.focus)
+            }
             is DragInteraction.Start -> {
                 interactions.add(interaction)
             }
@@ -274,6 +280,7 @@
             if (newInteraction != null) {
                 val targetAlpha = when (interaction) {
                     is HoverInteraction.Enter -> rippleAlpha.value.hoveredAlpha
+                    is FocusInteraction.Focus -> rippleAlpha.value.focusedAlpha
                     is DragInteraction.Start -> rippleAlpha.value.draggedAlpha
                     else -> 0f
                 }
@@ -319,26 +326,24 @@
 /**
  * @return the [AnimationSpec] used when transitioning to [interaction], either from a previous
  * state, or no state.
- *
- * TODO: handle focus states
  */
 private fun incomingStateLayerAnimationSpecFor(interaction: Interaction): AnimationSpec<Float> {
     return when (interaction) {
-        is DragInteraction.Start -> TweenSpec(durationMillis = 45, easing = LinearEasing)
         is HoverInteraction.Enter -> DefaultTweenSpec
+        is FocusInteraction.Focus -> TweenSpec(durationMillis = 45, easing = LinearEasing)
+        is DragInteraction.Start -> TweenSpec(durationMillis = 45, easing = LinearEasing)
         else -> DefaultTweenSpec
     }
 }
 
 /**
  * @return the [AnimationSpec] used when transitioning away from [interaction], to no state.
- *
- * TODO: handle focus states
  */
 private fun outgoingStateLayerAnimationSpecFor(interaction: Interaction?): AnimationSpec<Float> {
     return when (interaction) {
-        is DragInteraction.Start -> TweenSpec(durationMillis = 150, easing = LinearEasing)
         is HoverInteraction.Enter -> DefaultTweenSpec
+        is FocusInteraction.Focus -> DefaultTweenSpec
+        is DragInteraction.Start -> TweenSpec(durationMillis = 150, easing = LinearEasing)
         else -> DefaultTweenSpec
     }
 }
diff --git a/compose/material/material-ripple/src/commonMain/kotlin/androidx/compose/material/ripple/RippleTheme.kt b/compose/material/material-ripple/src/commonMain/kotlin/androidx/compose/material/ripple/RippleTheme.kt
index 09118d6..3197721 100644
--- a/compose/material/material-ripple/src/commonMain/kotlin/androidx/compose/material/ripple/RippleTheme.kt
+++ b/compose/material/material-ripple/src/commonMain/kotlin/androidx/compose/material/ripple/RippleTheme.kt
@@ -104,7 +104,7 @@
  * RippleAlpha defines the alpha of the ripple / state layer for different [Interaction]s.
  *
  * @property draggedAlpha the alpha used when the ripple is dragged
- * @property focusedAlpha not currently supported
+ * @property focusedAlpha the alpha used when the ripple is focused
  * @property hoveredAlpha the alpha used when the ripple is hovered
  * @property pressedAlpha the alpha used when the ripple is pressed
  */
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ButtonScreenshotTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ButtonScreenshotTest.kt
index 58a3991..c3f82d1 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ButtonScreenshotTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ButtonScreenshotTest.kt
@@ -21,6 +21,9 @@
 import androidx.compose.foundation.layout.wrapContentSize
 import androidx.compose.testutils.assertAgainstGolden
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusProperties
+import androidx.compose.ui.focus.focusRequester
 import androidx.compose.ui.test.ExperimentalTestApi
 import androidx.compose.ui.test.captureToImage
 import androidx.compose.ui.test.hasClickAction
@@ -121,4 +124,32 @@
             .captureToImage()
             .assertAgainstGolden(screenshotRule, "button_hover")
     }
+
+    @Test
+    fun focus() {
+        val focusRequester = FocusRequester()
+
+        rule.setMaterialContent {
+            Box(Modifier.requiredSize(200.dp, 100.dp).wrapContentSize()) {
+                Button(
+                    onClick = { },
+                    modifier = Modifier
+                        // Normally this is only focusable in non-touch mode, so let's force it to
+                        // always be focusable so we can test how it appears
+                        .focusProperties { canFocus = true }
+                        .focusRequester(focusRequester)
+                ) { }
+            }
+        }
+
+        rule.runOnIdle {
+            focusRequester.requestFocus()
+        }
+
+        rule.waitForIdle()
+
+        rule.onRoot()
+            .captureToImage()
+            .assertAgainstGolden(screenshotRule, "button_focus")
+    }
 }
\ No newline at end of file
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/CheckboxScreenshotTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/CheckboxScreenshotTest.kt
index 0d8bbba..b133192 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/CheckboxScreenshotTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/CheckboxScreenshotTest.kt
@@ -22,6 +22,9 @@
 import androidx.compose.testutils.assertAgainstGolden
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusProperties
+import androidx.compose.ui.focus.focusRequester
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.state.ToggleableState
 import androidx.compose.ui.test.ExperimentalTestApi
@@ -226,6 +229,33 @@
         assertToggeableAgainstGolden("checkbox_hover")
     }
 
+    @Test
+    fun checkBoxTest_focus() {
+        val focusRequester = FocusRequester()
+
+        rule.setMaterialContent {
+            Box(wrap.testTag(wrapperTestTag)) {
+                Checkbox(
+                    modifier = wrap
+                        // Normally this is only focusable in non-touch mode, so let's force it to
+                        // always be focusable so we can test how it appears
+                        .focusProperties { canFocus = true }
+                        .focusRequester(focusRequester),
+                    checked = true,
+                    onCheckedChange = { }
+                )
+            }
+        }
+
+        rule.runOnIdle {
+            focusRequester.requestFocus()
+        }
+
+        rule.waitForIdle()
+
+        assertToggeableAgainstGolden("checkbox_focus")
+    }
+
     private fun assertToggeableAgainstGolden(goldenName: String) {
         // TODO: replace with find(isToggeable()) after b/157687898 is fixed
         rule.onNodeWithTag(wrapperTestTag)
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/FloatingActionButtonScreenshotTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/FloatingActionButtonScreenshotTest.kt
index 08cac4f..f8efa44 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/FloatingActionButtonScreenshotTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/FloatingActionButtonScreenshotTest.kt
@@ -23,6 +23,9 @@
 import androidx.compose.material.icons.filled.Favorite
 import androidx.compose.testutils.assertAgainstGolden
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusProperties
+import androidx.compose.ui.focus.focusRequester
 import androidx.compose.ui.test.ExperimentalTestApi
 import androidx.compose.ui.test.captureToImage
 import androidx.compose.ui.test.hasClickAction
@@ -142,4 +145,34 @@
             .captureToImage()
             .assertAgainstGolden(screenshotRule, "fab_hover")
     }
+
+    @Test
+    fun focus() {
+        val focusRequester = FocusRequester()
+
+        rule.setMaterialContent {
+            Box(Modifier.requiredSize(100.dp, 100.dp).wrapContentSize()) {
+                FloatingActionButton(
+                    onClick = { },
+                    modifier = Modifier
+                        // Normally this is only focusable in non-touch mode, so let's force it to
+                        // always be focusable so we can test how it appears
+                        .focusProperties { canFocus = true }
+                        .focusRequester(focusRequester)
+                ) {
+                    Icon(Icons.Filled.Favorite, contentDescription = null)
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            focusRequester.requestFocus()
+        }
+
+        rule.waitForIdle()
+
+        rule.onRoot()
+            .captureToImage()
+            .assertAgainstGolden(screenshotRule, "fab_focus")
+    }
 }
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/MaterialRippleThemeTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/MaterialRippleThemeTest.kt
index d1509f7..50e5533 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/MaterialRippleThemeTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/MaterialRippleThemeTest.kt
@@ -26,6 +26,7 @@
 import androidx.compose.foundation.interaction.MutableInteractionSource
 import androidx.compose.foundation.interaction.PressInteraction
 import androidx.compose.foundation.indication
+import androidx.compose.foundation.interaction.FocusInteraction
 import androidx.compose.foundation.interaction.HoverInteraction
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.fillMaxSize
@@ -129,6 +130,28 @@
     }
 
     @Test
+    fun bounded_lightTheme_highLuminance_focused() {
+        val interactionSource = MutableInteractionSource()
+
+        val contentColor = Color.White
+
+        val scope = rule.setRippleContent(
+            interactionSource = interactionSource,
+            bounded = true,
+            lightTheme = true,
+            contentColor = contentColor
+        )
+
+        assertRippleMatches(
+            scope,
+            interactionSource,
+            FocusInteraction.Focus(),
+            "ripple_bounded_light_highluminance_focused",
+            calculateResultingRippleColor(contentColor, rippleOpacity = 0.24f)
+        )
+    }
+
+    @Test
     fun bounded_lightTheme_highLuminance_dragged() {
         val interactionSource = MutableInteractionSource()
 
@@ -195,6 +218,28 @@
     }
 
     @Test
+    fun bounded_lightTheme_lowLuminance_focused() {
+        val interactionSource = MutableInteractionSource()
+
+        val contentColor = Color.Black
+
+        val scope = rule.setRippleContent(
+            interactionSource = interactionSource,
+            bounded = true,
+            lightTheme = true,
+            contentColor = contentColor
+        )
+
+        assertRippleMatches(
+            scope,
+            interactionSource,
+            FocusInteraction.Focus(),
+            "ripple_bounded_light_lowluminance_focused",
+            calculateResultingRippleColor(contentColor, rippleOpacity = 0.12f)
+        )
+    }
+
+    @Test
     fun bounded_lightTheme_lowLuminance_dragged() {
         val interactionSource = MutableInteractionSource()
 
@@ -261,6 +306,28 @@
     }
 
     @Test
+    fun bounded_darkTheme_highLuminance_focused() {
+        val interactionSource = MutableInteractionSource()
+
+        val contentColor = Color.White
+
+        val scope = rule.setRippleContent(
+            interactionSource = interactionSource,
+            bounded = true,
+            lightTheme = false,
+            contentColor = contentColor
+        )
+
+        assertRippleMatches(
+            scope,
+            interactionSource,
+            FocusInteraction.Focus(),
+            "ripple_bounded_dark_highluminance_focused",
+            calculateResultingRippleColor(contentColor, rippleOpacity = 0.12f)
+        )
+    }
+
+    @Test
     fun bounded_darkTheme_highLuminance_dragged() {
         val interactionSource = MutableInteractionSource()
 
@@ -329,6 +396,29 @@
     }
 
     @Test
+    fun bounded_darkTheme_lowLuminance_focused() {
+        val interactionSource = MutableInteractionSource()
+
+        val contentColor = Color.Black
+
+        val scope = rule.setRippleContent(
+            interactionSource = interactionSource,
+            bounded = true,
+            lightTheme = false,
+            contentColor = contentColor
+        )
+
+        assertRippleMatches(
+            scope,
+            interactionSource,
+            FocusInteraction.Focus(),
+            "ripple_bounded_dark_lowluminance_focused",
+            // Low luminance content in dark theme should use a white ripple by default
+            calculateResultingRippleColor(Color.White, rippleOpacity = 0.12f)
+        )
+    }
+
+    @Test
     fun bounded_darkTheme_lowLuminance_dragged() {
         val interactionSource = MutableInteractionSource()
 
@@ -396,6 +486,28 @@
     }
 
     @Test
+    fun unbounded_lightTheme_highLuminance_focused() {
+        val interactionSource = MutableInteractionSource()
+
+        val contentColor = Color.White
+
+        val scope = rule.setRippleContent(
+            interactionSource = interactionSource,
+            bounded = false,
+            lightTheme = true,
+            contentColor = contentColor
+        )
+
+        assertRippleMatches(
+            scope,
+            interactionSource,
+            FocusInteraction.Focus(),
+            "ripple_unbounded_light_highluminance_focused",
+            calculateResultingRippleColor(contentColor, rippleOpacity = 0.24f)
+        )
+    }
+
+    @Test
     fun unbounded_lightTheme_highLuminance_dragged() {
         val interactionSource = MutableInteractionSource()
 
@@ -462,6 +574,28 @@
     }
 
     @Test
+    fun unbounded_lightTheme_lowLuminance_focused() {
+        val interactionSource = MutableInteractionSource()
+
+        val contentColor = Color.Black
+
+        val scope = rule.setRippleContent(
+            interactionSource = interactionSource,
+            bounded = false,
+            lightTheme = true,
+            contentColor = contentColor
+        )
+
+        assertRippleMatches(
+            scope,
+            interactionSource,
+            FocusInteraction.Focus(),
+            "ripple_unbounded_light_lowluminance_focused",
+            calculateResultingRippleColor(contentColor, rippleOpacity = 0.12f)
+        )
+    }
+
+    @Test
     fun unbounded_lightTheme_lowLuminance_dragged() {
         val interactionSource = MutableInteractionSource()
 
@@ -528,6 +662,28 @@
     }
 
     @Test
+    fun unbounded_darkTheme_highLuminance_focused() {
+        val interactionSource = MutableInteractionSource()
+
+        val contentColor = Color.White
+
+        val scope = rule.setRippleContent(
+            interactionSource = interactionSource,
+            bounded = false,
+            lightTheme = false,
+            contentColor = contentColor
+        )
+
+        assertRippleMatches(
+            scope,
+            interactionSource,
+            FocusInteraction.Focus(),
+            "ripple_unbounded_dark_highluminance_focused",
+            calculateResultingRippleColor(contentColor, rippleOpacity = 0.12f)
+        )
+    }
+
+    @Test
     fun unbounded_darkTheme_highLuminance_dragged() {
         val interactionSource = MutableInteractionSource()
 
@@ -596,6 +752,29 @@
     }
 
     @Test
+    fun unbounded_darkTheme_lowLuminance_focused() {
+        val interactionSource = MutableInteractionSource()
+
+        val contentColor = Color.Black
+
+        val scope = rule.setRippleContent(
+            interactionSource = interactionSource,
+            bounded = false,
+            lightTheme = false,
+            contentColor = contentColor
+        )
+
+        assertRippleMatches(
+            scope,
+            interactionSource,
+            FocusInteraction.Focus(),
+            "ripple_unbounded_dark_lowluminance_focused",
+            // Low luminance content in dark theme should use a white ripple by default
+            calculateResultingRippleColor(Color.White, rippleOpacity = 0.12f)
+        )
+    }
+
+    @Test
     fun unbounded_darkTheme_lowLuminance_dragged() {
         val interactionSource = MutableInteractionSource()
 
@@ -721,6 +900,57 @@
     }
 
     @Test
+    fun customRippleTheme_focused() {
+        val interactionSource = MutableInteractionSource()
+
+        val contentColor = Color.Black
+
+        val rippleColor = Color.Red
+        val expectedAlpha = 0.5f
+        val rippleAlpha = RippleAlpha(expectedAlpha, expectedAlpha, expectedAlpha, expectedAlpha)
+
+        val rippleTheme = object : RippleTheme {
+            @Composable
+            override fun defaultColor() = rippleColor
+
+            @Composable
+            override fun rippleAlpha() = rippleAlpha
+        }
+
+        var scope: CoroutineScope? = null
+
+        rule.setContent {
+            scope = rememberCoroutineScope()
+            MaterialTheme {
+                CompositionLocalProvider(LocalRippleTheme provides rippleTheme) {
+                    Surface(contentColor = contentColor) {
+                        Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
+                            RippleBoxWithBackground(
+                                interactionSource,
+                                rememberRipple(),
+                                bounded = true
+                            )
+                        }
+                    }
+                }
+            }
+        }
+
+        val expectedColor = calculateResultingRippleColor(
+            rippleColor,
+            rippleOpacity = expectedAlpha
+        )
+
+        assertRippleMatches(
+            scope!!,
+            interactionSource,
+            FocusInteraction.Focus(),
+            "ripple_customtheme_focused",
+            expectedColor
+        )
+    }
+
+    @Test
     fun customRippleTheme_dragged() {
         val interactionSource = MutableInteractionSource()
 
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/RadioButtonScreenshotTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/RadioButtonScreenshotTest.kt
index 99fe9e3..c34f5cc 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/RadioButtonScreenshotTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/RadioButtonScreenshotTest.kt
@@ -24,6 +24,9 @@
 import androidx.compose.testutils.assertAgainstGolden
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusProperties
+import androidx.compose.ui.focus.focusRequester
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.test.ExperimentalTestApi
 import androidx.compose.ui.test.captureToImage
@@ -114,6 +117,31 @@
     }
 
     @Test
+    fun radioButtonTest_focused() {
+        val focusRequester = FocusRequester()
+
+        rule.setMaterialContent {
+            Box(wrap.testTag(wrapperTestTag)) {
+                RadioButton(
+                    selected = false,
+                    onClick = {},
+                    modifier = Modifier
+                        // Normally this is only focusable in non-touch mode, so let's force it to
+                        // always be focusable so we can test how it appears
+                        .focusProperties { canFocus = true }
+                        .focusRequester(focusRequester)
+                )
+            }
+        }
+
+        rule.runOnIdle {
+            focusRequester.requestFocus()
+        }
+
+        assertSelectableAgainstGolden("radioButton_focused")
+    }
+
+    @Test
     fun radioButtonTest_disabled_selected() {
         rule.setMaterialContent {
             Box(wrap.testTag(wrapperTestTag)) {
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SwitchScreenshotTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SwitchScreenshotTest.kt
index ad24ccf..59ce852 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SwitchScreenshotTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SwitchScreenshotTest.kt
@@ -26,6 +26,9 @@
 import androidx.compose.testutils.assertAgainstGolden
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusProperties
+import androidx.compose.ui.focus.focusRequester
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.platform.LocalLayoutDirection
 import androidx.compose.ui.platform.testTag
@@ -255,6 +258,33 @@
         assertToggeableAgainstGolden("switch_hover")
     }
 
+    @Test
+    fun switchTest_focus() {
+        val focusRequester = FocusRequester()
+
+        rule.setMaterialContent {
+            Box(wrapperModifier) {
+                Switch(
+                    checked = true,
+                    onCheckedChange = { },
+                    modifier = Modifier
+                        // Normally this is only focusable in non-touch mode, so let's force it to
+                        // always be focusable so we can test how it appears
+                        .focusProperties { canFocus = true }
+                        .focusRequester(focusRequester)
+                )
+            }
+        }
+
+        rule.runOnIdle {
+            focusRequester.requestFocus()
+        }
+
+        rule.waitForIdle()
+
+        assertToggeableAgainstGolden("switch_focus")
+    }
+
     private fun assertToggeableAgainstGolden(goldenName: String) {
         rule.onNodeWithTag(wrapperTestTag)
             .captureToImage()
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Button.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Button.kt
index 47e85a9..c866582 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Button.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Button.kt
@@ -19,6 +19,7 @@
 import androidx.compose.animation.core.Animatable
 import androidx.compose.animation.core.VectorConverter
 import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.interaction.FocusInteraction
 import androidx.compose.foundation.interaction.HoverInteraction
 import androidx.compose.foundation.interaction.Interaction
 import androidx.compose.foundation.interaction.InteractionSource
@@ -328,7 +329,6 @@
      */
     val IconSpacing = 8.dp
 
-    // TODO: b/152525426 add support for focused states
     /**
      * Creates a [ButtonElevation] that will animate between the provided values according to the
      * Material specification for a [Button].
@@ -363,7 +363,7 @@
      * is pressed.
      * @param disabledElevation the elevation to use when the [Button] is not enabled.
      * @param hoveredElevation the elevation to use when the [Button] is enabled and is hovered.
-     * @param focusedElevation not currently supported.
+     * @param focusedElevation the elevation to use when the [Button] is enabled and is focused.
      */
     @Suppress("UNUSED_PARAMETER")
     @Composable
@@ -374,12 +374,19 @@
         hoveredElevation: Dp = 4.dp,
         focusedElevation: Dp = 4.dp,
     ): ButtonElevation {
-        return remember(defaultElevation, pressedElevation, disabledElevation, hoveredElevation) {
+        return remember(
+            defaultElevation,
+            pressedElevation,
+            disabledElevation,
+            hoveredElevation,
+            focusedElevation
+        ) {
             DefaultButtonElevation(
                 defaultElevation = defaultElevation,
                 pressedElevation = pressedElevation,
                 disabledElevation = disabledElevation,
                 hoveredElevation = hoveredElevation,
+                focusedElevation = focusedElevation
             )
         }
     }
@@ -491,6 +498,7 @@
     private val pressedElevation: Dp,
     private val disabledElevation: Dp,
     private val hoveredElevation: Dp,
+    private val focusedElevation: Dp,
 ) : ButtonElevation {
     @Composable
     override fun elevation(enabled: Boolean, interactionSource: InteractionSource): State<Dp> {
@@ -504,6 +512,12 @@
                     is HoverInteraction.Exit -> {
                         interactions.remove(interaction.enter)
                     }
+                    is FocusInteraction.Focus -> {
+                        interactions.add(interaction)
+                    }
+                    is FocusInteraction.Unfocus -> {
+                        interactions.remove(interaction.focus)
+                    }
                     is PressInteraction.Press -> {
                         interactions.add(interaction)
                     }
@@ -525,6 +539,7 @@
             when (interaction) {
                 is PressInteraction.Press -> pressedElevation
                 is HoverInteraction.Enter -> hoveredElevation
+                is FocusInteraction.Focus -> focusedElevation
                 else -> defaultElevation
             }
         }
@@ -541,6 +556,7 @@
                 val lastInteraction = when (animatable.targetValue) {
                     pressedElevation -> PressInteraction.Press(Offset.Zero)
                     hoveredElevation -> HoverInteraction.Enter()
+                    focusedElevation -> FocusInteraction.Focus()
                     else -> null
                 }
                 animatable.animateElevation(
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Elevation.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Elevation.kt
index 56e3198..bb9c2f4 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Elevation.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Elevation.kt
@@ -22,6 +22,7 @@
 import androidx.compose.animation.core.FastOutSlowInEasing
 import androidx.compose.animation.core.TweenSpec
 import androidx.compose.foundation.interaction.DragInteraction
+import androidx.compose.foundation.interaction.FocusInteraction
 import androidx.compose.foundation.interaction.HoverInteraction
 import androidx.compose.foundation.interaction.Interaction
 import androidx.compose.foundation.interaction.PressInteraction
@@ -81,6 +82,7 @@
             is PressInteraction.Press -> DefaultIncomingSpec
             is DragInteraction.Start -> DefaultIncomingSpec
             is HoverInteraction.Enter -> DefaultIncomingSpec
+            is FocusInteraction.Focus -> DefaultIncomingSpec
             else -> null
         }
     }
@@ -96,6 +98,7 @@
             is PressInteraction.Press -> DefaultOutgoingSpec
             is DragInteraction.Start -> DefaultOutgoingSpec
             is HoverInteraction.Enter -> HoveredOutgoingSpec
+            is FocusInteraction.Focus -> DefaultOutgoingSpec
             else -> null
         }
     }
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/FloatingActionButton.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/FloatingActionButton.kt
index ef23afe..59d7247 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/FloatingActionButton.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/FloatingActionButton.kt
@@ -18,6 +18,7 @@
 
 import androidx.compose.animation.core.Animatable
 import androidx.compose.animation.core.VectorConverter
+import androidx.compose.foundation.interaction.FocusInteraction
 import androidx.compose.foundation.interaction.HoverInteraction
 import androidx.compose.foundation.interaction.Interaction
 import androidx.compose.foundation.interaction.InteractionSource
@@ -206,7 +207,6 @@
  * Contains the default values used by [FloatingActionButton]
  */
 object FloatingActionButtonDefaults {
-    // TODO: b/152525426 add support for focused states
     /**
      * Creates a [FloatingActionButtonElevation] that will animate between the provided values
      * according to the Material specification.
@@ -238,7 +238,8 @@
      * pressed.
      * @param hoveredElevation the elevation to use when the [FloatingActionButton] is
      * hovered.
-     * @param focusedElevation not currently supported.
+     * @param focusedElevation the elevation to use when the [FloatingActionButton] is
+     * focused.
      */
     @Suppress("UNUSED_PARAMETER")
     @Composable
@@ -248,11 +249,12 @@
         hoveredElevation: Dp = 8.dp,
         focusedElevation: Dp = 8.dp,
     ): FloatingActionButtonElevation {
-        return remember(defaultElevation, pressedElevation, hoveredElevation) {
+        return remember(defaultElevation, pressedElevation, hoveredElevation, focusedElevation) {
             DefaultFloatingActionButtonElevation(
                 defaultElevation = defaultElevation,
                 pressedElevation = pressedElevation,
                 hoveredElevation = hoveredElevation,
+                focusedElevation = focusedElevation
             )
         }
     }
@@ -265,7 +267,8 @@
 private class DefaultFloatingActionButtonElevation(
     private val defaultElevation: Dp,
     private val pressedElevation: Dp,
-    private val hoveredElevation: Dp
+    private val hoveredElevation: Dp,
+    private val focusedElevation: Dp
 ) : FloatingActionButtonElevation {
     @Composable
     override fun elevation(interactionSource: InteractionSource): State<Dp> {
@@ -279,6 +282,12 @@
                     is HoverInteraction.Exit -> {
                         interactions.remove(interaction.enter)
                     }
+                    is FocusInteraction.Focus -> {
+                        interactions.add(interaction)
+                    }
+                    is FocusInteraction.Unfocus -> {
+                        interactions.remove(interaction.focus)
+                    }
                     is PressInteraction.Press -> {
                         interactions.add(interaction)
                     }
@@ -297,6 +306,7 @@
         val target = when (interaction) {
             is PressInteraction.Press -> pressedElevation
             is HoverInteraction.Enter -> hoveredElevation
+            is FocusInteraction.Focus -> focusedElevation
             else -> defaultElevation
         }
 
@@ -306,6 +316,7 @@
             val lastInteraction = when (animatable.targetValue) {
                 pressedElevation -> PressInteraction.Press(Offset.Zero)
                 hoveredElevation -> HoverInteraction.Enter()
+                focusedElevation -> FocusInteraction.Focus()
                 else -> null
             }
             animatable.animateElevation(
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/PrintToStringTest.kt b/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/PrintToStringTest.kt
index fada0e7..183c479 100644
--- a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/PrintToStringTest.kt
+++ b/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/PrintToStringTest.kt
@@ -130,6 +130,7 @@
                     | [Disabled]
                     |  |-Node #X at (l=X, t=X, r=X, b=X)px
                     |    Role = 'Button'
+                    |    Focused = 'false'
                     |    Text = '[Button]'
                     |    Actions = [OnClick, GetTextLayoutResult]
                     |    MergeDescendants = 'true'