Skip to content

Commit 6fd60ff

Browse files
authored
Update NativeInputEventsProcessor to handle deleteContentBackward when a browser performs 'Fast Delete' (JetBrains#2985)
Adjusted the logic: The browsers can perform 'Fast Delete' via deleteContentBackward with textRangeSize > 0. (For example, Chrome on Samsung A25) Fixes https://youtrack.jetbrains.com/issue/CMP-9966 ## Testing - Added a new test - This should be tested by QA ## Release Notes ### Fixes - Web - _(prerelease fix)_ Additional fix for handling Fast Delete in mobile browsers - https://youtrack.jetbrains.com/issue/CMP-9966
1 parent 193766a commit 6fd60ff

2 files changed

Lines changed: 59 additions & 18 deletions

File tree

compose/ui/ui/src/webMain/kotlin/androidx/compose/ui/platform/NativeInputEventsProcessor.kt

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -161,26 +161,29 @@ internal abstract class NativeInputEventsProcessor(
161161
private fun InputEventExt.process(currentTextFieldValue: TextFieldValue) {
162162
val editCommands = when (inputType) {
163163
"deleteContentBackward" -> buildList {
164-
// this means "deleteContentBackward" happened because of an earlier "keydown" event, so skipping it here
165-
if (lastProcessedKeydown?.isBackspace() == true) return@buildList
166-
167164
if (!currentTextFieldValue.selection.collapsed) {
168-
// Likely it's on mobile, where the Backspace has Unidentified key value.
169-
// When Compose TextField shows text selection,
170-
// a good UX for deleteContentBackward would be to emulate Backspace
171-
add(BackspaceCommand())
172-
} else {
165+
// If the lastProcessedKeydown was Backspace, then Compose must have already processed this.
166+
if (lastProcessedKeydown?.isBackspace() != true) {
167+
// If we got here, then it's likely one of the mobile browsers, where the Backspace has Unidentified key value.
168+
// Compose doesn't handle Unidentified keys - it does not have any context about them.
169+
// And here in `deleteContentBackward` we have this context.
170+
// When Compose TextField has text selection, a good UX for deleteContentBackward would be to emulate Backspace.
171+
add(BackspaceCommand())
172+
}
173+
} else { // Empty selection case.
173174
// This happens when an autocorrection is applied on mobile:
174175
// The system first tells us to delete the old text,
175176
// and then it would send the "insertText" event.
176177
if (textRangeSize > 0) {
177-
// deleteContentBackward can happen under very non-trivial circumstances,
178-
// for instance; when an input suggestion on Android Chrome is accepted,
179-
// the browser then deletes space after the word just to add space again
178+
// deleteContentBackward can happen under very non-trivial circumstances:
179+
// - for instance, when an input suggestion on Android Chrome is accepted,
180+
// the browser then deletes space after the word just to add space again;
181+
// - or when a browser performs Fast Delete;
180182
add(SetSelectionCommand(textRangeStart, textRangeEnd))
181183
add(BackspaceCommand())
182-
} else if (textRangeSize == 0) {
183-
// under specific circumstance previous symbol can be deleted while inputing new one
184+
} else if (textRangeSize == 0 && lastProcessedKeydown?.isBackspace() != true) {
185+
// We skip this branch if the lastProcessedKeydown is Backspace, because Compose must have already processed this.
186+
// Otherwise, under specific circumstance previous symbol can be deleted while inputting the new one
184187
// see https://youtrack.jetbrains.com/issue/CMP-8773
185188
add(BackspaceCommand())
186189
}

compose/ui/ui/src/webTest/kotlin/androidx/compose/ui/platform/NativeInputEventsProcessorTest.kt

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,7 @@ class NativeInputEventsProcessorTest {
279279
}
280280

281281
@Test
282-
fun when_Backspace_Pressed_deleteContentBackward_is_ignored() {
282+
fun when_NonCollapsedSelection_And_Backspace_Pressed_then_deleteContentBackward_is_ignored() {
283283
val communicator = MockComposeCommandCommunicator()
284284
val processor = TestNativeInputEventsProcessor(communicator)
285285

@@ -296,15 +296,16 @@ class NativeInputEventsProcessorTest {
296296
textRangeEnd = 4
297297
}
298298
)
299-
processor.manuallyRunCheckpoint(TextFieldValue("test"))
299+
processor.manuallyRunCheckpoint(TextFieldValue("test", selection = TextRange(3, 4)))
300300

301-
assertEquals(1, communicator.keyboardEvents.size)
302-
assertEquals(0, communicator.editCommands.size)
301+
assertEquals(1, communicator.keyboardEvents.size, "exactly one key event should be sent")
302+
assertEquals(0, communicator.editCommands.size, "editCommands should not be sent")
303303

304304
val sentKeyEvent = communicator.keyboardEvents[0]
305305
assertEquals(
306306
"Backspace",
307-
((sentKeyEvent.nativeKeyEvent as InternalKeyEvent).nativeEvent as KeyboardEvent).key
307+
((sentKeyEvent.nativeKeyEvent as InternalKeyEvent).nativeEvent as KeyboardEvent).key,
308+
"keyboardEvent for Backspace should be sent"
308309
)
309310
}
310311

@@ -675,6 +676,43 @@ class NativeInputEventsProcessorTest {
675676
assertEquals(0, communicator.editCommands.size)
676677
}
677678

679+
@Test
680+
fun testDeleteContentBackward_with_collapsed_selection_and_Backspace_key_pressed_same_frame() {
681+
val textFieldValue = TextFieldValue(
682+
text = "example text",
683+
selection = TextRange(12) // collapsed selection at the very end
684+
)
685+
686+
val communicator = MockComposeCommandCommunicator(textFieldValue)
687+
val processor = TestNativeInputEventsProcessor(communicator)
688+
689+
val backspaceEvent = keyEvent(
690+
key = "Backspace",
691+
code = "Backspace",
692+
type = "keydown"
693+
)
694+
processor.registerEvent(backspaceEvent)
695+
696+
processor.registerEvent(
697+
beforeInput("deleteContentBackward", "").asInputEventExt().apply {
698+
textRangeStart = 8
699+
textRangeEnd = 12
700+
},
701+
)
702+
703+
processor.manuallyRunCheckpoint(communicator.currentTextFieldValue())
704+
705+
assertEquals(1, communicator.keyboardEvents.size)
706+
assertEquals(2, communicator.editCommands.size)
707+
708+
val selectionCommand = communicator.editCommands[0]
709+
assertTrue(selectionCommand is SetSelectionCommand)
710+
assertEquals(8, selectionCommand.start)
711+
assertEquals(12, selectionCommand.end)
712+
713+
assertEquals("example ", communicator.currentTextFieldValue().text)
714+
}
715+
678716
@Test
679717
fun testDeleteContentBackward_with_Backspace_key_pressed_different_frame() {
680718
val communicator = MockComposeCommandCommunicator()

0 commit comments

Comments
 (0)