diff --git a/compose/ui/ui/src/webMain/kotlin/androidx/compose/ui/platform/NativeInputEventsProcessor.kt b/compose/ui/ui/src/webMain/kotlin/androidx/compose/ui/platform/NativeInputEventsProcessor.kt index 052a708e74af7..e07ef7421a111 100644 --- a/compose/ui/ui/src/webMain/kotlin/androidx/compose/ui/platform/NativeInputEventsProcessor.kt +++ b/compose/ui/ui/src/webMain/kotlin/androidx/compose/ui/platform/NativeInputEventsProcessor.kt @@ -22,6 +22,7 @@ import androidx.compose.ui.text.TextLayoutResult import androidx.compose.ui.text.input.BackspaceCommand import androidx.compose.ui.text.input.CommitTextCommand import androidx.compose.ui.text.input.DeleteSurroundingTextCommand +import androidx.compose.ui.text.input.EditCommand import androidx.compose.ui.text.input.OffsetMapping import androidx.compose.ui.text.input.SetComposingTextCommand import androidx.compose.ui.text.input.SetSelectionCommand @@ -156,6 +157,15 @@ internal abstract class NativeInputEventsProcessor( collectedEvents.clear() } + private fun InputEventExt.createDeleteWordCommand(): EditCommand? { + val keydown = lastProcessedKeydown ?: return null + if (!keydown.isBackspace() || !keydown.repeat) return null + val layoutResult = composeSender.currentTextLayoutResult() ?: return null + + val offset = layoutResult.getPrevWordOffset(textRangeEnd) + return DeleteSurroundingTextCommand((textRangeEnd - offset).coerceAtLeast(0), 0) + } + private fun InputEventExt.process(currentTextFieldValue: TextFieldValue) { val editCommands = when (inputType) { "deleteContentBackward" -> buildList { @@ -168,7 +178,7 @@ internal abstract class NativeInputEventsProcessor( // When Compose TextField has text selection, a good UX for deleteContentBackward would be to emulate Backspace. add(BackspaceCommand()) } - } else { // Empty selection case. + } else { // This happens when an autocorrection is applied on mobile: // The system first tells us to delete the old text, // and then it would send the "insertText" event. @@ -184,25 +194,16 @@ internal abstract class NativeInputEventsProcessor( // Otherwise, under specific circumstance previous symbol can be deleted while inputting the new one // see https://youtrack.jetbrains.com/issue/CMP-8773 add(BackspaceCommand()) + } else { + createDeleteWordCommand()?.let { add(it) } } } } "deleteWordBackward" -> buildList { - if (lastProcessedKeydown?.isBackspace() != true) return@buildList - - // This would mean event was triggered by long press on mobile device (iOS) - if (lastProcessedKeydown?.repeat == true) { - val layoutResult = composeSender.currentTextLayoutResult() ?: return@buildList - - - val offset = layoutResult.getPrevWordOffset(textRangeEnd) - val deleteCommand = DeleteSurroundingTextCommand((textRangeEnd - offset).coerceAtLeast(0), 0) - add(deleteCommand) - } + createDeleteWordCommand()?.let { add(it) } } - "insertReplacementText" -> buildList { if (data == null) return@buildList if (textRangeSize > 0) { diff --git a/compose/ui/ui/src/webTest/kotlin/androidx/compose/ui/input/DeleteWordBackwardTests.kt b/compose/ui/ui/src/webTest/kotlin/androidx/compose/ui/input/DeleteWordBackwardTests.kt index b27fe83516cfa..635b9e63f014f 100644 --- a/compose/ui/ui/src/webTest/kotlin/androidx/compose/ui/input/DeleteWordBackwardTests.kt +++ b/compose/ui/ui/src/webTest/kotlin/androidx/compose/ui/input/DeleteWordBackwardTests.kt @@ -27,7 +27,7 @@ import kotlin.test.Test class DeleteWordBackwardTests : TextFieldTestSpec, BasicTextFieldWithValue { - fun sendPhysicalDeleteWordBackward() { + private fun sendPhysicalDeleteWordBackward() { sendToHtmlInput( keyEvent( key = "Backspace", @@ -39,7 +39,7 @@ class DeleteWordBackwardTests : TextFieldTestSpec, BasicTextFieldWithValue { ) } - fun sendVirtualDeleteWordBackward() { + private fun sendVirtualDeleteWordBackward() { sendToHtmlInput( keyEvent( key = "Backspace", @@ -51,6 +51,18 @@ class DeleteWordBackwardTests : TextFieldTestSpec, BasicTextFieldWithValue { ) } + private fun sendVirtualFastDeleteAsContentBackward() { + sendToHtmlInput( + keyEvent( + key = "Backspace", + code = "Backspace", + type = "keydown", + repeat = true, + ), + beforeInput("deleteContentBackward", null) + ) + } + @Test fun deletePrevWordVirtualMiddle() = runApplicationTest { @@ -191,4 +203,19 @@ class DeleteWordBackwardTests : TextFieldTestSpec, BasicTextFieldWithValue { textFieldValue.awaitAndAssertTextEquals("천천히 ") } + @Test + fun deletePrevWordVirtualMiddle_viaDeleteContentBackward_CMP_10086() = runApplicationTest { + val textFieldValue = createApplicationWithHolder( + "here we go again!!!", + initialSelection = TextRange(14, 14) + ) + awaitAnimationFrame() + + sendVirtualFastDeleteAsContentBackward() + textFieldValue.awaitAndAssertTextEquals( + "here go again!!!", + "deleteContentBackward with repeating Backspace should behave as deleteWordBackward (CMP-10086)" + ) + } + } \ No newline at end of file