Merge "Rename NavRecord to NavEntry" into androidx-main
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/BasicTextFieldTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/BasicTextFieldTest.kt
index 78ac040..7f8b8f7 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/BasicTextFieldTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/BasicTextFieldTest.kt
@@ -16,6 +16,7 @@
 
 package androidx.compose.foundation.text.input
 
+import android.R
 import android.os.Build
 import android.text.InputType
 import android.text.SpannableStringBuilder
@@ -40,7 +41,10 @@
 import androidx.compose.foundation.text.TEST_FONT_FAMILY
 import androidx.compose.foundation.text.computeSizeForDefaultText
 import androidx.compose.foundation.text.input.TextFieldBuffer.ChangeList
+import androidx.compose.foundation.text.input.internal.TextLayoutState
+import androidx.compose.foundation.text.input.internal.TransformedTextFieldState
 import androidx.compose.foundation.text.input.internal.selection.FakeClipboard
+import androidx.compose.foundation.text.input.internal.selection.TextFieldSelectionState
 import androidx.compose.foundation.text.input.internal.setComposingRegion
 import androidx.compose.foundation.text.selection.fetchTextLayoutResult
 import androidx.compose.foundation.verticalScroll
@@ -127,6 +131,9 @@
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
 
 @OptIn(ExperimentalFoundationApi::class, ExperimentalTestApi::class)
 @LargeTest
@@ -1130,9 +1137,7 @@
 
         requestFocus(Tag)
 
-        inputMethodInterceptor.withInputConnection {
-            performContextMenuAction(android.R.id.selectAll)
-        }
+        inputMethodInterceptor.withInputConnection { performContextMenuAction(R.id.selectAll) }
 
         rule.runOnIdle {
             assertThat(state.selection).isEqualTo(TextRange(0, 5))
@@ -1152,7 +1157,7 @@
 
         requestFocus(Tag)
 
-        inputMethodInterceptor.withInputConnection { performContextMenuAction(android.R.id.cut) }
+        inputMethodInterceptor.withInputConnection { performContextMenuAction(R.id.cut) }
 
         rule.waitForIdle()
         assertThat(clipboard.getClipEntry()?.readText()).isEqualTo("He")
@@ -1171,7 +1176,7 @@
 
         requestFocus(Tag)
 
-        inputMethodInterceptor.withInputConnection { performContextMenuAction(android.R.id.copy) }
+        inputMethodInterceptor.withInputConnection { performContextMenuAction(R.id.copy) }
 
         rule.waitForIdle()
         assertThat(clipboard.getClipEntry()?.readText()).isEqualTo("He")
@@ -1189,7 +1194,7 @@
 
         requestFocus(Tag)
 
-        inputMethodInterceptor.withInputConnection { performContextMenuAction(android.R.id.paste) }
+        inputMethodInterceptor.withInputConnection { performContextMenuAction(R.id.paste) }
 
         rule.runOnIdle {
             assertThat(state.text.toString()).isEqualTo("Worldo")
@@ -1299,6 +1304,34 @@
     }
 
     @Test
+    fun textField_state_invokesAutofill() {
+        val mockLambda: () -> Unit = mock()
+        var density by mutableStateOf(Density(1f))
+
+        val manager =
+            TextFieldSelectionState(
+                    // other parameters not necessary to test autofill invocation
+                    textFieldState =
+                        TransformedTextFieldState(
+                            textFieldState = TextFieldState(),
+                            inputTransformation = null,
+                            codepointTransformation = null,
+                            outputTransformation = null
+                        ),
+                    textLayoutState = TextLayoutState(),
+                    density = density,
+                    enabled = true,
+                    readOnly = false,
+                    isFocused = false,
+                    isPassword = false
+                )
+                .apply { requestAutofillAction = mockLambda }
+
+        manager.autofill()
+        verify(mockLambda, times(1)).invoke()
+    }
+
+    @Test
     fun changingInputTransformation_doesNotRestartInput() {
         var inputTransformation by mutableStateOf(InputTransformation.maxLength(10))
         inputMethodInterceptor.setTextFieldTestContent {
diff --git a/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManagerTest.kt b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManagerTest.kt
index fb28556..9d8e33e 100644
--- a/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManagerTest.kt
+++ b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManagerTest.kt
@@ -23,7 +23,6 @@
 import androidx.compose.foundation.text.LegacyTextFieldState
 import androidx.compose.foundation.text.TextDelegate
 import androidx.compose.foundation.text.TextLayoutResultProxy
-import androidx.compose.ui.autofill.AutofillManager
 import androidx.compose.ui.focus.FocusRequester
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.geometry.Rect
@@ -98,7 +97,6 @@
     private val hapticFeedback = mock<HapticFeedback>()
     private val focusRequester = mock<FocusRequester>()
     private val multiParagraph = mock<MultiParagraph>()
-    private val autofillManager = mock<AutofillManager>()
 
     @Before
     fun setup() {
@@ -110,7 +108,6 @@
         manager.textToolbar = textToolbar
         manager.hapticFeedBack = hapticFeedback
         manager.focusRequester = focusRequester
-        manager.autofillManager = autofillManager
         manager.coroutineScope = null
 
         whenever(layoutResult.layoutInput)
@@ -370,10 +367,12 @@
     @Test
     fun autofill_selection_collapse() {
         manager.value = TextFieldValue(text = text, selection = TextRange(4, 4))
+        val mockLambda: () -> Unit = mock()
+        val manager = TextFieldSelectionManager().apply { requestAutofillAction = mockLambda }
 
         manager.autofill()
 
-        verify(autofillManager, times(1)).requestAutofillForActiveElement()
+        verify(mockLambda, times(1)).invoke()
         assertThat(state.handleState).isEqualTo(HandleState.None)
     }
 
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/BasicTextField.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/BasicTextField.kt
index 925ecfd..7e63192 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/BasicTextField.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/BasicTextField.kt
@@ -68,7 +68,6 @@
 import androidx.compose.ui.graphics.SolidColor
 import androidx.compose.ui.input.pointer.pointerHoverIcon
 import androidx.compose.ui.input.pointer.pointerInput
-import androidx.compose.ui.platform.LocalAutofillManager
 import androidx.compose.ui.platform.LocalClipboard
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.platform.LocalHapticFeedback
@@ -301,7 +300,6 @@
     val currentHapticFeedback = LocalHapticFeedback.current
     val currentClipboard = LocalClipboard.current
     val currentTextToolbar = LocalTextToolbar.current
-    val autofillManager = LocalAutofillManager.current
 
     val textToolbarHandler =
         remember(coroutineScope, currentTextToolbar) {
@@ -360,7 +358,6 @@
             enabled = enabled,
             readOnly = readOnly,
             isPassword = isPassword,
-            autofillManager = autofillManager,
             showTextToolbar = textToolbarHandler
         )
     }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt
index 14f8402..494f392 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt
@@ -27,6 +27,7 @@
 import androidx.compose.foundation.relocation.BringIntoViewRequester
 import androidx.compose.foundation.relocation.bringIntoViewRequester
 import androidx.compose.foundation.text.handwriting.stylusHandwriting
+import androidx.compose.foundation.text.input.internal.CoreTextFieldSemanticsModifier
 import androidx.compose.foundation.text.input.internal.createLegacyPlatformTextInputServiceAdapter
 import androidx.compose.foundation.text.input.internal.legacyTextInputAdapter
 import androidx.compose.foundation.text.selection.LocalTextSelectionColors
@@ -58,7 +59,6 @@
 import androidx.compose.runtime.snapshotFlow
 import androidx.compose.runtime.snapshots.Snapshot
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.autofill.ContentDataType
 import androidx.compose.ui.draw.drawBehind
 import androidx.compose.ui.focus.FocusManager
 import androidx.compose.ui.focus.FocusRequester
@@ -82,7 +82,6 @@
 import androidx.compose.ui.layout.MeasureResult
 import androidx.compose.ui.layout.MeasureScope
 import androidx.compose.ui.layout.onGloballyPositioned
-import androidx.compose.ui.platform.LocalAutofillManager
 import androidx.compose.ui.platform.LocalClipboard
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.platform.LocalFocusManager
@@ -92,33 +91,13 @@
 import androidx.compose.ui.platform.LocalTextToolbar
 import androidx.compose.ui.platform.LocalWindowInfo
 import androidx.compose.ui.platform.SoftwareKeyboardController
-import androidx.compose.ui.semantics.contentDataType
-import androidx.compose.ui.semantics.copyText
-import androidx.compose.ui.semantics.cutText
-import androidx.compose.ui.semantics.disabled
-import androidx.compose.ui.semantics.editableText
-import androidx.compose.ui.semantics.getTextLayoutResult
-import androidx.compose.ui.semantics.insertTextAtCursor
-import androidx.compose.ui.semantics.isEditable
-import androidx.compose.ui.semantics.onAutofillText
-import androidx.compose.ui.semantics.onClick
-import androidx.compose.ui.semantics.onImeAction
-import androidx.compose.ui.semantics.onLongClick
-import androidx.compose.ui.semantics.password
-import androidx.compose.ui.semantics.pasteText
 import androidx.compose.ui.semantics.semantics
-import androidx.compose.ui.semantics.setSelection
-import androidx.compose.ui.semantics.setText
-import androidx.compose.ui.semantics.textSelectionRange
 import androidx.compose.ui.text.AnnotatedString
 import androidx.compose.ui.text.TextLayoutResult
 import androidx.compose.ui.text.TextRange
 import androidx.compose.ui.text.TextStyle
 import androidx.compose.ui.text.font.FontFamily
-import androidx.compose.ui.text.input.CommitTextCommand
-import androidx.compose.ui.text.input.DeleteAllCommand
 import androidx.compose.ui.text.input.EditProcessor
-import androidx.compose.ui.text.input.FinishComposingTextCommand
 import androidx.compose.ui.text.input.ImeAction
 import androidx.compose.ui.text.input.ImeOptions
 import androidx.compose.ui.text.input.KeyboardType
@@ -316,7 +295,6 @@
     manager.coroutineScope = coroutineScope
     manager.textToolbar = LocalTextToolbar.current
     manager.hapticFeedBack = LocalHapticFeedback.current
-    manager.autofillManager = LocalAutofillManager.current
     manager.focusRequester = focusRequester
     manager.editable = !readOnly
     manager.enabled = enabled
@@ -474,151 +452,18 @@
 
     val isPassword = visualTransformation is PasswordVisualTransformation
     val semanticsModifier =
-        Modifier.semantics(true) {
-            // focused semantics are handled by Modifier.focusable()
-            this.editableText = transformedText.text
-            this.textSelectionRange = value.selection
-
-            // The developer will set `contentType`. CTF populates the other autofill-related
-            // semantics. And since we're in a TextField, set the `contentDataType` to be "Text".
-            this.contentDataType = ContentDataType.Text
-            onAutofillText { text ->
-                state.justAutofilled = true
-                state.autofillHighlightOn = true
-                handleTextUpdateFromSemantics(state, text.text, readOnly, enabled)
-                true
-            }
-
-            if (!enabled) this.disabled()
-            if (isPassword) this.password()
-            val editable = enabled && !readOnly
-            isEditable = editable
-            getTextLayoutResult {
-                if (state.layoutResult != null) {
-                    it.add(state.layoutResult!!.value)
-                    true
-                } else {
-                    false
-                }
-            }
-            if (editable) {
-                setText { text ->
-                    handleTextUpdateFromSemantics(state, text.text, readOnly, enabled)
-                    true
-                }
-
-                insertTextAtCursor { text ->
-                    if (readOnly || !enabled) return@insertTextAtCursor false
-
-                    // If the action is performed while in an active text editing session, treat
-                    // this like an IME command and update the text by going through the buffer.
-                    // This keeps the buffer state consistent if other IME commands are performed
-                    // before the next recomposition, and is used for the testing code path.
-                    state.inputSession?.let { session ->
-                        TextFieldDelegate.onEditCommand(
-                            // Finish composing text first because when the field is focused the IME
-                            // might
-                            // set composition.
-                            ops = listOf(FinishComposingTextCommand(), CommitTextCommand(text, 1)),
-                            editProcessor = state.processor,
-                            state.onValueChange,
-                            session
-                        )
-                    }
-                        ?: run {
-                            val newText =
-                                value.text.replaceRange(
-                                    value.selection.start,
-                                    value.selection.end,
-                                    text
-                                )
-                            val newCursor = TextRange(value.selection.start + text.length)
-                            state.onValueChange(TextFieldValue(newText, newCursor))
-                        }
-                    true
-                }
-            }
-
-            setSelection { selectionStart, selectionEnd, relativeToOriginalText ->
-                // in traversal mode we get selection from the `textSelectionRange` semantics which
-                // is
-                // selection in original text. In non-traversal mode selection comes from the
-                // Talkback
-                // and indices are relative to the transformed text
-                val start =
-                    if (relativeToOriginalText) {
-                        selectionStart
-                    } else {
-                        offsetMapping.transformedToOriginal(selectionStart)
-                    }
-                val end =
-                    if (relativeToOriginalText) {
-                        selectionEnd
-                    } else {
-                        offsetMapping.transformedToOriginal(selectionEnd)
-                    }
-
-                if (!enabled) {
-                    false
-                } else if (start == value.selection.start && end == value.selection.end) {
-                    false
-                } else if (
-                    minOf(start, end) >= 0 && maxOf(start, end) <= value.annotatedString.length
-                ) {
-                    // Do not show toolbar if it's a traversal mode (with the volume keys), or
-                    // if the cursor just moved to beginning or end.
-                    if (relativeToOriginalText || start == end) {
-                        manager.exitSelectionMode()
-                    } else {
-                        manager.enterSelectionMode()
-                    }
-                    state.onValueChange(
-                        TextFieldValue(value.annotatedString, TextRange(start, end))
-                    )
-                    true
-                } else {
-                    manager.exitSelectionMode()
-                    false
-                }
-            }
-            onImeAction(imeOptions.imeAction) {
-                // This will perform the appropriate default action if no handler has been
-                // specified, so
-                // as far as the platform is concerned, we always handle the action and never want
-                // to
-                // defer to the default _platform_ implementation.
-                state.onImeActionPerformed(imeOptions.imeAction)
-                true
-            }
-            onClick {
-                // according to the documentation, we still need to provide proper semantics actions
-                // even if the state is 'disabled'
-                tapToFocus(state, focusRequester, !readOnly)
-                true
-            }
-            onLongClick {
-                manager.enterSelectionMode()
-                true
-            }
-            if (!value.selection.collapsed && !isPassword) {
-                copyText {
-                    manager.copy()
-                    true
-                }
-                if (enabled && !readOnly) {
-                    cutText {
-                        manager.cut()
-                        true
-                    }
-                }
-            }
-            if (enabled && !readOnly) {
-                pasteText {
-                    manager.paste()
-                    true
-                }
-            }
-        }
+        CoreTextFieldSemanticsModifier(
+            transformedText,
+            value,
+            state,
+            readOnly,
+            enabled,
+            isPassword,
+            offsetMapping,
+            manager,
+            imeOptions,
+            focusRequester
+        )
 
     val showCursor = enabled && !readOnly && windowInfo.isWindowFocused && !state.hasHighlight()
     val cursorModifier = Modifier.cursor(state, value, offsetMapping, cursorBrush, showCursor)
@@ -884,30 +729,6 @@
     }
 }
 
-/**
- * In an active input session, semantics updates are handled just as user updates coming from the
- * IME. Otherwise the updates are directly applied on the current state.
- */
-private fun handleTextUpdateFromSemantics(
-    state: LegacyTextFieldState,
-    text: String,
-    readOnly: Boolean,
-    enabled: Boolean
-) {
-    if (readOnly || !enabled) return
-
-    // If the action is performed while in an active text editing session, treat this
-    // like an IME command and update the text by going through the buffer.
-    state.inputSession?.let { session ->
-        TextFieldDelegate.onEditCommand(
-            ops = listOf(DeleteAllCommand(), CommitTextCommand(text, 1)),
-            editProcessor = state.processor,
-            state.onValueChange,
-            session
-        )
-    } ?: run { state.onValueChange(TextFieldValue(text, TextRange(text.length))) }
-}
-
 internal class LegacyTextFieldState(
     var textDelegate: TextDelegate,
     val recomposeScope: RecomposeScope,
@@ -1108,7 +929,7 @@
 }
 
 /** Request focus on tap. If already focused, makes sure the keyboard is requested. */
-private fun tapToFocus(
+internal fun tapToFocus(
     state: LegacyTextFieldState,
     focusRequester: FocusRequester,
     allowKeyboard: Boolean
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/CoreTextFieldSemanticsModifier.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/CoreTextFieldSemanticsModifier.kt
new file mode 100644
index 0000000..a5fe111
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/CoreTextFieldSemanticsModifier.kt
@@ -0,0 +1,334 @@
+/*
+ * Copyright 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.text.input.internal
+
+import androidx.compose.foundation.text.LegacyTextFieldState
+import androidx.compose.foundation.text.TextFieldDelegate
+import androidx.compose.foundation.text.selection.TextFieldSelectionManager
+import androidx.compose.foundation.text.tapToFocus
+import androidx.compose.ui.autofill.ContentDataType
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.node.DelegatingNode
+import androidx.compose.ui.node.ModifierNodeElement
+import androidx.compose.ui.node.SemanticsModifierNode
+import androidx.compose.ui.node.invalidateSemantics
+import androidx.compose.ui.node.requestAutofill
+import androidx.compose.ui.platform.InspectorInfo
+import androidx.compose.ui.semantics.SemanticsPropertyReceiver
+import androidx.compose.ui.semantics.contentDataType
+import androidx.compose.ui.semantics.copyText
+import androidx.compose.ui.semantics.cutText
+import androidx.compose.ui.semantics.disabled
+import androidx.compose.ui.semantics.editableText
+import androidx.compose.ui.semantics.getTextLayoutResult
+import androidx.compose.ui.semantics.insertTextAtCursor
+import androidx.compose.ui.semantics.isEditable
+import androidx.compose.ui.semantics.onAutofillText
+import androidx.compose.ui.semantics.onClick
+import androidx.compose.ui.semantics.onImeAction
+import androidx.compose.ui.semantics.onLongClick
+import androidx.compose.ui.semantics.password
+import androidx.compose.ui.semantics.pasteText
+import androidx.compose.ui.semantics.setSelection
+import androidx.compose.ui.semantics.setText
+import androidx.compose.ui.semantics.textSelectionRange
+import androidx.compose.ui.text.TextRange
+import androidx.compose.ui.text.input.CommitTextCommand
+import androidx.compose.ui.text.input.DeleteAllCommand
+import androidx.compose.ui.text.input.FinishComposingTextCommand
+import androidx.compose.ui.text.input.ImeOptions
+import androidx.compose.ui.text.input.OffsetMapping
+import androidx.compose.ui.text.input.TextFieldValue
+import androidx.compose.ui.text.input.TransformedText
+
+internal data class CoreTextFieldSemanticsModifier(
+    val transformedText: TransformedText,
+    val value: TextFieldValue,
+    val state: LegacyTextFieldState,
+    val readOnly: Boolean,
+    val enabled: Boolean,
+    val isPassword: Boolean,
+    val offsetMapping: OffsetMapping,
+    val manager: TextFieldSelectionManager,
+    val imeOptions: ImeOptions,
+    val focusRequester: FocusRequester
+) : ModifierNodeElement<CoreTextFieldSemanticsModifierNode>() {
+    override fun create(): CoreTextFieldSemanticsModifierNode =
+        CoreTextFieldSemanticsModifierNode(
+            transformedText = transformedText,
+            value = value,
+            state = state,
+            readOnly = readOnly,
+            enabled = enabled,
+            isPassword = isPassword,
+            offsetMapping = offsetMapping,
+            manager = manager,
+            imeOptions = imeOptions,
+            focusRequester = focusRequester
+        )
+
+    override fun update(node: CoreTextFieldSemanticsModifierNode) {
+        node.updateNodeSemantics(
+            transformedText = transformedText,
+            value = value,
+            state = state,
+            readOnly = readOnly,
+            enabled = enabled,
+            isPassword = isPassword,
+            offsetMapping = offsetMapping,
+            manager = manager,
+            imeOptions = imeOptions,
+            focusRequester = focusRequester
+        )
+    }
+
+    override fun InspectorInfo.inspectableProperties() {
+        // Show nothing in the inspector.
+    }
+}
+
+internal class CoreTextFieldSemanticsModifierNode(
+    var transformedText: TransformedText,
+    var value: TextFieldValue,
+    var state: LegacyTextFieldState,
+    var readOnly: Boolean,
+    var enabled: Boolean,
+    var isPassword: Boolean,
+    var offsetMapping: OffsetMapping,
+    var manager: TextFieldSelectionManager,
+    var imeOptions: ImeOptions,
+    var focusRequester: FocusRequester
+) : DelegatingNode(), SemanticsModifierNode {
+    init {
+        manager.requestAutofillAction = { requestAutofill() }
+    }
+
+    override val shouldMergeDescendantSemantics: Boolean
+        get() = true
+
+    override fun SemanticsPropertyReceiver.applySemantics() {
+        this.editableText = transformedText.text
+        this.textSelectionRange = value.selection
+
+        // The developer will set `contentType`. CTF populates the other autofill-related
+        // semantics. And since we're in a TextField, set the `contentDataType` to be "Text".
+        this.contentDataType = ContentDataType.Text
+        onAutofillText { text ->
+            state.justAutofilled = true
+            state.autofillHighlightOn = true
+            handleTextUpdateFromSemantics(state, text.text, readOnly, enabled)
+            true
+        }
+
+        if (!enabled) this.disabled()
+        if (isPassword) this.password()
+        val editable = enabled && !readOnly
+        isEditable = editable
+        getTextLayoutResult {
+            if (state.layoutResult != null) {
+                it.add(state.layoutResult!!.value)
+                true
+            } else {
+                false
+            }
+        }
+
+        if (editable) {
+            setText { text ->
+                handleTextUpdateFromSemantics(state, text.text, readOnly, enabled)
+                true
+            }
+
+            insertTextAtCursor { text ->
+                if (readOnly || !enabled) return@insertTextAtCursor false
+
+                // If the action is performed while in an active text editing session, treat
+                // this like an IME command and update the text by going through the buffer.
+                // This keeps the buffer state consistent if other IME commands are performed
+                // before the next recomposition, and is used for the testing code path.
+                state.inputSession?.let { session ->
+                    TextFieldDelegate.onEditCommand(
+                        // Finish composing text first because when the field is focused the IME
+                        // might
+                        // set composition.
+                        ops = listOf(FinishComposingTextCommand(), CommitTextCommand(text, 1)),
+                        editProcessor = state.processor,
+                        state.onValueChange,
+                        session
+                    )
+                }
+                    ?: run {
+                        val newText =
+                            value.text.replaceRange(
+                                value.selection.start,
+                                value.selection.end,
+                                text
+                            )
+                        val newCursor = TextRange(value.selection.start + text.length)
+                        state.onValueChange(TextFieldValue(newText, newCursor))
+                    }
+                true
+            }
+        }
+
+        setSelection { selectionStart, selectionEnd, relativeToOriginalText ->
+            // in traversal mode we get selection from the `textSelectionRange` semantics which
+            // is selection in original text. In non-traversal mode selection comes from the
+            // Talkback and indices are relative to the transformed text
+            val start =
+                if (relativeToOriginalText) {
+                    selectionStart
+                } else {
+                    offsetMapping.transformedToOriginal(selectionStart)
+                }
+            val end =
+                if (relativeToOriginalText) {
+                    selectionEnd
+                } else {
+                    offsetMapping.transformedToOriginal(selectionEnd)
+                }
+
+            if (!enabled) {
+                false
+            } else if (start == value.selection.start && end == value.selection.end) {
+                false
+            } else if (
+                minOf(start, end) >= 0 && maxOf(start, end) <= value.annotatedString.length
+            ) {
+                // Do not show toolbar if it's a traversal mode (with the volume keys), or
+                // if the cursor just moved to beginning or end.
+                if (relativeToOriginalText || start == end) {
+                    manager.exitSelectionMode()
+                } else {
+                    manager.enterSelectionMode()
+                }
+                state.onValueChange(TextFieldValue(value.annotatedString, TextRange(start, end)))
+                true
+            } else {
+                manager.exitSelectionMode()
+                false
+            }
+        }
+        onImeAction(imeOptions.imeAction) {
+            // This will perform the appropriate default action if no handler has been
+            // specified, so
+            // as far as the platform is concerned, we always handle the action and never want
+            // to
+            // defer to the default _platform_ implementation.
+            state.onImeActionPerformed(imeOptions.imeAction)
+            true
+        }
+        onClick {
+            // according to the documentation, we still need to provide proper semantics actions
+            // even if the state is 'disabled'
+            tapToFocus(state, focusRequester, !readOnly)
+            true
+        }
+        onLongClick {
+            manager.enterSelectionMode()
+            true
+        }
+        if (!value.selection.collapsed && !isPassword) {
+            copyText {
+                manager.copy()
+                true
+            }
+            if (enabled && !readOnly) {
+                cutText {
+                    manager.cut()
+                    true
+                }
+            }
+        }
+        if (enabled && !readOnly) {
+            pasteText {
+                manager.paste()
+                true
+            }
+        }
+    }
+
+    fun updateNodeSemantics(
+        transformedText: TransformedText,
+        value: TextFieldValue,
+        state: LegacyTextFieldState,
+        readOnly: Boolean,
+        enabled: Boolean,
+        isPassword: Boolean,
+        offsetMapping: OffsetMapping,
+        manager: TextFieldSelectionManager,
+        imeOptions: ImeOptions,
+        focusRequester: FocusRequester
+    ) {
+        // Find the diff: current previous and new values before updating current.
+        val previousEditable = this.enabled && !this.readOnly
+        val previousEnabled = this.enabled
+        val previousIsPassword = this.isPassword
+        val previousImeOptions = this.imeOptions
+        val previousManager = this.manager
+        val editable = enabled && !readOnly
+
+        // Apply the diff.
+        this.transformedText = transformedText
+        this.value = value
+        this.state = state
+        this.readOnly = readOnly
+        this.enabled = enabled
+        this.offsetMapping = offsetMapping
+        this.manager = manager
+        this.imeOptions = imeOptions
+        this.focusRequester = focusRequester
+
+        if (
+            enabled != previousEnabled ||
+                editable != previousEditable ||
+                imeOptions != previousImeOptions ||
+                isPassword != previousIsPassword ||
+                !value.selection.collapsed
+        ) {
+            invalidateSemantics()
+        }
+
+        if (manager != previousManager) {
+            manager.requestAutofillAction = { requestAutofill() }
+        }
+    }
+
+    /**
+     * In an active input session, semantics updates are handled just as user updates coming from
+     * the IME. Otherwise the updates are directly applied on the current state.
+     */
+    private fun handleTextUpdateFromSemantics(
+        state: LegacyTextFieldState,
+        text: String,
+        readOnly: Boolean,
+        enabled: Boolean
+    ) {
+        if (readOnly || !enabled) return
+
+        // If the action is performed while in an active text editing session, treat this
+        // like an IME command and update the text by going through the buffer.
+        state.inputSession?.let { session ->
+            TextFieldDelegate.onEditCommand(
+                ops = listOf(DeleteAllCommand(), CommitTextCommand(text, 1)),
+                editProcessor = state.processor,
+                state.onValueChange,
+                session
+            )
+        } ?: run { state.onValueChange(TextFieldValue(text, TextRange(text.length))) }
+    }
+}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldDecoratorModifier.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldDecoratorModifier.kt
index c9eb589..d7810f9 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldDecoratorModifier.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldDecoratorModifier.kt
@@ -66,6 +66,7 @@
 import androidx.compose.ui.node.currentValueOf
 import androidx.compose.ui.node.invalidateSemantics
 import androidx.compose.ui.node.observeReads
+import androidx.compose.ui.node.requestAutofill
 import androidx.compose.ui.platform.InspectorInfo
 import androidx.compose.ui.platform.LocalFocusManager
 import androidx.compose.ui.platform.LocalSoftwareKeyboardController
@@ -202,8 +203,9 @@
     ObserverModifierNode,
     LayoutAwareModifierNode {
 
-    private val editable
-        get() = enabled && !readOnly
+    init {
+        textFieldSelectionState.requestAutofillAction = { requestAutofill() }
+    }
 
     private val pointerInputNode =
         delegate(
@@ -430,9 +432,7 @@
         stylusHandwritingTrigger: MutableSharedFlow<Unit>?
     ) {
         // Find the diff: current previous and new values before updating current.
-        val previousEditable = this.editable
-        val editable = enabled && !readOnly
-
+        val previousEditable = this.enabled && !this.readOnly
         val previousEnabled = this.enabled
         val previousTextFieldState = this.textFieldState
         val previousKeyboardOptions = this.keyboardOptions
@@ -440,6 +440,7 @@
         val previousInteractionSource = this.interactionSource
         val previousIsPassword = this.isPassword
         val previousStylusHandwritingTrigger = this.stylusHandwritingTrigger
+        val editable = enabled && !readOnly
 
         // Apply the diff.
         this.textFieldState = textFieldState
@@ -487,6 +488,7 @@
                 textFieldSelectionState.receiveContentConfiguration =
                     receiveContentConfigurationProvider
             }
+            textFieldSelectionState.requestAutofillAction = { requestAutofill() }
         }
 
         if (interactionSource != previousInteractionSource) {
@@ -507,7 +509,8 @@
         if (!enabled) disabled()
         if (isPassword) password()
 
-        isEditable = [email protected]
+        val editable = enabled && !readOnly
+        isEditable = editable
 
         // The developer will set `contentType`. TF populates the other autofill-related
         // semantics. And since we're in a TextField, set the `contentDataType` to be "Text".
@@ -630,6 +633,7 @@
         isElementFocused = focusState.isFocused
         onFocusChange()
 
+        val editable = enabled && !readOnly
         if (focusState.isFocused) {
             // Deselect when losing focus even if readonly.
             if (editable) {
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldSelectionState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldSelectionState.kt
index 5baa1a8..0a6c258 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldSelectionState.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldSelectionState.kt
@@ -66,7 +66,6 @@
 import androidx.compose.runtime.setValue
 import androidx.compose.runtime.snapshotFlow
 import androidx.compose.runtime.snapshots.Snapshot
-import androidx.compose.ui.autofill.AutofillManager
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.geometry.Rect
 import androidx.compose.ui.geometry.isSpecified
@@ -115,9 +114,6 @@
     var isFocused: Boolean,
     private var isPassword: Boolean,
 ) {
-    /** [AutofillManager] to perform Autofill. */
-    private var autofillManager: AutofillManager? = null
-
     /** [HapticFeedback] handle to perform haptic feedback. */
     private var hapticFeedBack: HapticFeedback? = null
 
@@ -130,6 +126,9 @@
     /** Whether user is interacting with the UI in touch mode. */
     var isInTouchMode: Boolean by mutableStateOf(true)
 
+    /** The action to invoke when autofill is requested in text toolbar. */
+    var requestAutofillAction: (() -> Unit)? = null
+
     /**
      * Reduced [ReceiveContentConfiguration] from the attached modifier node hierarchy. This value
      * is set by [TextFieldDecoratorModifierNode].
@@ -353,8 +352,7 @@
         density: Density,
         enabled: Boolean,
         readOnly: Boolean,
-        isPassword: Boolean,
-        autofillManager: AutofillManager?
+        isPassword: Boolean
     ) {
         if (!enabled) {
             hideTextToolbar()
@@ -366,7 +364,6 @@
         this.enabled = enabled
         this.readOnly = readOnly
         this.isPassword = isPassword
-        this.autofillManager = autofillManager
     }
 
     /** Implements the complete set of gestures supported by the cursor handle. */
@@ -443,7 +440,6 @@
 
         clipboard = null
         hapticFeedBack = null
-        autofillManager = null
     }
 
     /**
@@ -1395,7 +1391,7 @@
      * Inserts credentials (if there exist any that match this field type) into the text field.
      */
     fun autofill() {
-        autofillManager?.requestAutofillForActiveElement()
+        requestAutofillAction?.invoke()
     }
 
     fun deselect() {
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManager.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManager.kt
index 674b87a..6f75364 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManager.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManager.kt
@@ -38,7 +38,6 @@
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.autofill.AutofillManager
 import androidx.compose.ui.focus.FocusRequester
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.geometry.Rect
@@ -100,8 +99,8 @@
      */
     internal var visualTransformation: VisualTransformation = VisualTransformation.None
 
-    /** [AutofillManager] to perform clipboard features. */
-    internal var autofillManager: AutofillManager? = null
+    /** The action to invoke when autofill is requested in text toolbar. */
+    internal var requestAutofillAction: (() -> Unit)? = null
 
     /** [Clipboard] to perform clipboard features. */
     internal var clipboard: Clipboard? = null
@@ -705,7 +704,7 @@
     }
 
     internal fun autofill() {
-        autofillManager?.requestAutofillForActiveElement()
+        requestAutofillAction?.invoke()
     }
 
     internal fun getHandlePosition(isStartHandle: Boolean): Offset {
diff --git a/compose/ui/ui/api/current.txt b/compose/ui/ui/api/current.txt
index 5a884ad..2b88ca8 100644
--- a/compose/ui/ui/api/current.txt
+++ b/compose/ui/ui/api/current.txt
@@ -228,7 +228,6 @@
   public abstract class AutofillManager {
     method public abstract void cancel();
     method public abstract void commit();
-    method public abstract void requestAutofillForActiveElement();
   }
 
   public final class AutofillNode {
@@ -2860,6 +2859,7 @@
 
   public final class DelegatableNodeKt {
     method public static void invalidateSubtree(androidx.compose.ui.node.DelegatableNode);
+    method public static void requestAutofill(androidx.compose.ui.node.DelegatableNode);
     method public static androidx.compose.ui.unit.Density requireDensity(androidx.compose.ui.node.DelegatableNode);
     method public static androidx.compose.ui.graphics.GraphicsContext requireGraphicsContext(androidx.compose.ui.node.DelegatableNode);
     method public static androidx.compose.ui.layout.LayoutCoordinates requireLayoutCoordinates(androidx.compose.ui.node.DelegatableNode);
diff --git a/compose/ui/ui/api/restricted_current.txt b/compose/ui/ui/api/restricted_current.txt
index 2c0d712..8b114f0 100644
--- a/compose/ui/ui/api/restricted_current.txt
+++ b/compose/ui/ui/api/restricted_current.txt
@@ -228,7 +228,6 @@
   public abstract class AutofillManager {
     method public abstract void cancel();
     method public abstract void commit();
-    method public abstract void requestAutofillForActiveElement();
   }
 
   public final class AutofillNode {
@@ -2914,6 +2913,7 @@
 
   public final class DelegatableNodeKt {
     method public static void invalidateSubtree(androidx.compose.ui.node.DelegatableNode);
+    method public static void requestAutofill(androidx.compose.ui.node.DelegatableNode);
     method public static androidx.compose.ui.unit.Density requireDensity(androidx.compose.ui.node.DelegatableNode);
     method public static androidx.compose.ui.graphics.GraphicsContext requireGraphicsContext(androidx.compose.ui.node.DelegatableNode);
     method public static androidx.compose.ui.layout.LayoutCoordinates requireLayoutCoordinates(androidx.compose.ui.node.DelegatableNode);
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/AndroidAutofillManagerTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/AndroidAutofillManagerTest.kt
index d6ce34a..25dcd50 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/AndroidAutofillManagerTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/AndroidAutofillManagerTest.kt
@@ -42,11 +42,14 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.alpha
 import androidx.compose.ui.focus.FocusManager
-import androidx.compose.ui.focus.focusProperties
+import androidx.compose.ui.node.SemanticsModifierNode
+import androidx.compose.ui.node.elementOf
+import androidx.compose.ui.node.requestAutofill
 import androidx.compose.ui.platform.LocalAutofillManager
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.platform.LocalFocusManager
 import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.semantics.SemanticsPropertyReceiver
 import androidx.compose.ui.semantics.contentDataType
 import androidx.compose.ui.semantics.contentDescription
 import androidx.compose.ui.semantics.contentType
@@ -713,68 +716,6 @@
 
     @Test
     @SmallTest
-    @SdkSuppress(minSdkVersion = 26)
-    fun autofillManager_requestAutofillAfterFocus() {
-        val am: PlatformAutofillManager = mock()
-        val contextMenuTag = "menu_tag"
-        var autofillManager: AutofillManager?
-
-        rule.setContent {
-            autofillManager = LocalAutofillManager.current
-            (autofillManager as AndroidAutofillManager).platformAutofillManager = am
-            Box(
-                modifier =
-                    Modifier.semantics {
-                            testTag = contextMenuTag
-                            onAutofillText { true }
-                        }
-                        .focusProperties { canFocus = true }
-                        .clickable { autofillManager?.requestAutofillForActiveElement() }
-                        .size(height, width)
-            )
-        }
-
-        // `requestAutofill` is always called after an element is focused
-        rule.onNodeWithTag(contextMenuTag).requestFocus()
-        rule.runOnIdle { verify(am).notifyViewEntered(any(), any(), any()) }
-
-        // then `requestAutofill` is called on that same previously focused element
-        rule.onNodeWithTag(contextMenuTag).performClick()
-        rule.runOnIdle { verify(am).requestAutofill(any(), any(), any()) }
-    }
-
-    @Test
-    @SmallTest
-    @SdkSuppress(minSdkVersion = 26)
-    fun autofillManager_notAutofillable_doesNotrequestAutofillAfterFocus() {
-        val am: PlatformAutofillManager = mock()
-        val contextMenuTag = "menu_tag"
-        var autofillManager: AutofillManager?
-
-        rule.setContent {
-            autofillManager = LocalAutofillManager.current
-            (autofillManager as AndroidAutofillManager).platformAutofillManager = am
-            Box(
-                modifier =
-                    Modifier.semantics { testTag = contextMenuTag }
-                        .focusProperties { canFocus = true }
-                        .clickable { autofillManager?.requestAutofillForActiveElement() }
-                        .size(height, width)
-            )
-        }
-        clearInvocations(am)
-
-        // `requestAutofill` is always called after an element is focused
-        rule.onNodeWithTag(contextMenuTag).requestFocus()
-        rule.runOnIdle { verifyZeroInteractions(am) }
-
-        // then `requestAutofill` is called on that same previously focused element
-        rule.onNodeWithTag(contextMenuTag).performClick()
-        rule.runOnIdle { verifyNoMoreInteractions(am) }
-    }
-
-    @Test
-    @SmallTest
     fun autofillManager_lazyColumnScroll_callsCommit() {
         lateinit var state: LazyListState
         lateinit var coroutineScope: CoroutineScope
@@ -866,4 +807,35 @@
         // A column disappearing will call commit
         rule.runOnIdle { verify(am).commit() }
     }
+
+    @Test
+    @SmallTest
+    @SdkSuppress(minSdkVersion = 26)
+    fun autofillManager_requestAutofill() {
+        val am: PlatformAutofillManager = mock()
+        val semanticsModifier = TestSemanticsModifier { testTag = "TestTag" }
+        var autofillManager: AutofillManager?
+
+        rule.setContent {
+            autofillManager = LocalAutofillManager.current
+            (autofillManager as AndroidAutofillManager).platformAutofillManager = am
+            Box(Modifier.elementOf(semanticsModifier))
+        }
+
+        // Act
+        rule.runOnIdle { semanticsModifier.requestAutofill() }
+
+        // Assert
+        rule.runOnIdle { verify(am).requestAutofill(any(), any(), any()) }
+    }
+
+    private class TestSemanticsModifier(
+        private val onApplySemantics: SemanticsPropertyReceiver.() -> Unit
+    ) : SemanticsModifierNode, Modifier.Node() {
+
+        override fun SemanticsPropertyReceiver.applySemantics() {
+            contentType = ContentType.Username
+            onApplySemantics.invoke(this)
+        }
+    }
 }
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/HitPathTrackerTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/HitPathTrackerTest.kt
index 84314e3e..1d90f0b 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/HitPathTrackerTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/HitPathTrackerTest.kt
@@ -3510,6 +3510,10 @@
 
     override fun requestFocus(): Boolean = false
 
+    override fun requestAutofill(node: LayoutNode) {
+        TODO("Not yet implemented")
+    }
+
     override fun measureAndLayout(sendPointerUpdate: Boolean) {}
 
     override fun measureAndLayout(layoutNode: LayoutNode, constraints: Constraints) {}
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessorTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessorTest.kt
index 3f9150a..ffb3fec 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessorTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessorTest.kt
@@ -2846,6 +2846,10 @@
 
     override fun requestFocus(): Boolean = false
 
+    override fun requestAutofill(node: LayoutNode) {
+        TODO("Not yet implemented")
+    }
+
     override val rootForTest: RootForTest
         get() = TODO("Not yet implemented")
 
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/Helpers.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/Helpers.kt
index 76aa7ce..4322a1c 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/Helpers.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/Helpers.kt
@@ -281,6 +281,10 @@
 
     override fun requestFocus() = TODO("Not yet implemented")
 
+    override fun requestAutofill(node: LayoutNode) {
+        TODO("Not yet implemented")
+    }
+
     override fun onSemanticsChange() {}
 
     override fun getFocusDirection(keyEvent: KeyEvent) = TODO("Not yet implemented")
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/NodeChainTester.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/NodeChainTester.kt
index a36713d..5d5e649 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/NodeChainTester.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/NodeChainTester.kt
@@ -521,6 +521,10 @@
 
     override fun requestFocus(): Boolean = false
 
+    override fun requestAutofill(node: LayoutNode) {
+        TODO("Not yet implemented")
+    }
+
     override fun measureAndLayout(sendPointerUpdate: Boolean) {}
 
     override fun measureAndLayout(layoutNode: LayoutNode, constraints: Constraints) {}
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofillManager.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofillManager.android.kt
index 69f31cc..4d49938 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofillManager.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofillManager.android.kt
@@ -77,19 +77,6 @@
         platformAutofillManager.cancel()
     }
 
-    // This will be used to request autofill when
-    // `AutofillManager.requestAutofillForActiveElement()` is called (e.g. from the text toolbar).
-    private var previouslyFocusedId = -1
-
-    override fun requestAutofillForActiveElement() {
-        if (previouslyFocusedId <= 0) return
-
-        rectManager.rects.withRect(previouslyFocusedId) { left, top, right, bottom ->
-            reusableRect.set(left, top, right, bottom)
-            platformAutofillManager.requestAutofill(view, previouslyFocusedId, reusableRect)
-        }
-    }
-
     override fun onFocusChanged(
         previous: FocusTargetModifierNode?,
         current: FocusTargetModifierNode?
@@ -105,7 +92,6 @@
                 rectManager.rects.withRect(semanticsId) { l, t, r, b ->
                     platformAutofillManager.notifyViewEntered(view, semanticsId, Rect(l, t, r, b))
                 }
-                previouslyFocusedId = semanticsId
             }
         }
     }
@@ -138,7 +124,6 @@
             val previousFocus = prevConfig?.getOrNull(SemanticsProperties.Focused)
             val currFocus = config?.getOrNull(SemanticsProperties.Focused)
             if (previousFocus != true && currFocus == true && config.isAutofillable()) {
-                previouslyFocusedId = semanticsId
                 rectManager.rects.withRect(semanticsId) { l, t, r, b ->
                     platformAutofillManager.notifyViewEntered(view, semanticsId, Rect(l, t, r, b))
                 }
@@ -236,6 +221,13 @@
     private var currentlyDisplayedIDs = MutableIntSet()
     private var pendingChangesToDisplayedIds = false
 
+    internal fun requestAutofill(semanticsInfo: SemanticsInfo) {
+        rectManager.rects.withRect(semanticsInfo.semanticsId) { left, top, right, bottom ->
+            reusableRect.set(left, top, right, bottom)
+            platformAutofillManager.requestAutofill(view, semanticsInfo.semanticsId, reusableRect)
+        }
+    }
+
     internal fun onPostAttach(semanticsInfo: SemanticsInfo) {
         if (semanticsInfo.semanticsConfiguration?.isRelatedToAutoCommit() == true) {
             currentlyDisplayedIDs.add(semanticsInfo.semanticsId)
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
index c059f5d..7da948c 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
@@ -1247,6 +1247,13 @@
         }
     }
 
+    override fun requestAutofill(node: LayoutNode) {
+        @OptIn(ExperimentalComposeUiApi::class)
+        if (autofillSupported() && ComposeUiFlags.isSemanticAutofillEnabled) {
+            _autofillManager?.requestAutofill(node)
+        }
+    }
+
     fun requestClearInvalidObservations() {
         observationClearRequested = true
     }
diff --git a/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt
index f13b1f1..b88e67e 100644
--- a/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt
+++ b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt
@@ -2448,6 +2448,10 @@
 
     override fun requestFocus(): Boolean = false
 
+    override fun requestAutofill(node: LayoutNode) {
+        TODO("Not yet implemented")
+    }
+
     override fun measureAndLayout(sendPointerUpdate: Boolean) {}
 
     override fun measureAndLayout(layoutNode: LayoutNode, constraints: Constraints) {}
diff --git a/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/ModifierLocalConsumerEntityTest.kt b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/ModifierLocalConsumerEntityTest.kt
index 4fdd4ea..de1b8d6 100644
--- a/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/ModifierLocalConsumerEntityTest.kt
+++ b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/ModifierLocalConsumerEntityTest.kt
@@ -446,6 +446,10 @@
 
         override fun requestFocus() = TODO("Not yet implemented")
 
+        override fun requestAutofill(node: LayoutNode) {
+            TODO("Not yet implemented")
+        }
+
         override fun measureAndLayout(sendPointerUpdate: Boolean) = TODO("Not yet implemented")
 
         override fun measureAndLayout(layoutNode: LayoutNode, constraints: Constraints) {
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/autofill/AutofillManager.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/autofill/AutofillManager.kt
index 2a6163b..f5ebdc7 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/autofill/AutofillManager.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/autofill/AutofillManager.kt
@@ -42,14 +42,4 @@
      * without processing any information entered in the autofillable field.
      */
     abstract fun cancel()
-
-    /**
-     * Request autofill for previously focused element.
-     *
-     * This may have no effect, and it is not required that any autofill service will be notified.
-     *
-     * Any component that can be autofilled may call this when it is active to request an autofill
-     * services response.
-     */
-    abstract fun requestAutofillForActiveElement()
 }
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatableNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatableNode.kt
index 08505aa..8b45c3f 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatableNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatableNode.kt
@@ -332,6 +332,13 @@
     checkPreconditionNotNull(requireLayoutNode().owner) { "This node does not have an owner." }
 
 /**
+ * Requests autofill for the LayoutNode that this [DelegatableNode] is attached to. If the node does
+ * not have any autofill semantic properties set, then the request still may be sent to the Autofill
+ * service, but no response is expected.
+ */
+fun DelegatableNode.requestAutofill() = requireLayoutNode().requestAutofill()
+
+/**
  * Returns the current [Density] of the LayoutNode that this [DelegatableNode] is attached to. If
  * the node is not attached, this function will throw an [IllegalStateException].
  */
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
index 5f60779..d751280 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
@@ -402,6 +402,14 @@
 
     private var isSemanticsInvalidated = false
 
+    internal fun requestAutofill() {
+        // Ignore calls while semantics are being applied (b/378114177).
+        if (isCurrentlyCalculatingSemanticsConfiguration) return
+
+        val owner = requireOwner()
+        owner.requestAutofill(this)
+    }
+
     internal fun invalidateSemantics() {
         // Ignore calls to invalidate Semantics while semantics are being applied (b/378114177).
         if (isCurrentlyCalculatingSemanticsConfiguration) return
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/Owner.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/Owner.kt
index a75bf87..ee7342a 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/Owner.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/Owner.kt
@@ -245,6 +245,9 @@
      */
     fun requestFocus(): Boolean
 
+    /** Ask the system to request autofill values to this owner. */
+    fun requestAutofill(node: LayoutNode)
+
     /**
      * Iterates through all LayoutNodes that have requested layout and measures and lays them out.
      * If [sendPointerUpdate] is `true` then a simulated PointerEvent may be sent to update pointer