Skip to content

Commit 310bfb3

Browse files
authored
Cancel touch event if active backing DOM field is detected (JetBrains#2996)
Following changes are introduced: - we treat stylus as touch event rather then mouse one - somer redundant code was removed - additional touchevent introduced that checks whether whether an active backing dom input is detected - in that case we preventDefault ## Testing `./gradlew testWeb` + manual ## Release Notes ### Fixes - Web - [CMP-10079](https://youtrack.jetbrains.com/issue/CMP-10079) [Web] Mobile. iOS 26.4 only. The virtual keyboard jumps after each tap
1 parent 2016446 commit 310bfb3

4 files changed

Lines changed: 93 additions & 68 deletions

File tree

compose/mpp/demo/src/webMain/kotlin/androidx/compose/mpp/demo/components/DragAndDropExample.web.kt

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,9 @@ import androidx.compose.animation.core.tween
5858
import androidx.compose.foundation.clickable
5959
import androidx.compose.material3.Button
6060
import androidx.compose.material3.Text
61+
import androidx.compose.material3.TextField
6162
import androidx.compose.ui.graphics.drawscope.Stroke
63+
import androidx.compose.ui.text.input.TextFieldValue
6264

6365
@Composable
6466
@OptIn(ExperimentalComposeUiApi::class)
@@ -121,6 +123,29 @@ actual fun DragAndDropExample() {
121123
Text("Clickable and Draggable", color = Color.White)
122124
}
123125

126+
var textFieldValue by remember { mutableStateOf(TextFieldValue("Drag me")) }
127+
128+
Box(
129+
modifier = Modifier
130+
.align(Alignment.BottomCenter)
131+
.padding(bottom = 16.dp)
132+
.dragAndDropSource { _: Offset ->
133+
val dataTransfer = createDataTransfer()
134+
dataTransfer.setData("text/plain", textFieldValue.text)
135+
DragAndDropTransferData(dataTransfer)
136+
}
137+
.background(Color(0xFFE0E0E0))
138+
.border(BorderStroke(1.dp, Color.DarkGray))
139+
.padding(8.dp),
140+
contentAlignment = Alignment.Center
141+
) {
142+
TextField(
143+
value = textFieldValue,
144+
onValueChange = { textFieldValue = it },
145+
label = { Text("Draggable text input") }
146+
)
147+
}
148+
124149
val dragAndDropTarget = remember {
125150
object: DragAndDropTarget {
126151
override fun onStarted(event: DragAndDropEvent) {

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

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ import androidx.compose.ui.text.input.EditCommand
2222
import androidx.compose.ui.text.input.ImeOptions
2323
import androidx.compose.ui.text.input.TextFieldValue
2424
import kotlin.js.js
25-
import kotlinx.browser.document
2625
import kotlinx.browser.window
2726
import org.w3c.dom.HTMLElement
2827

@@ -70,9 +69,7 @@ internal class BackingDomInput(
7069
// and https://youtrack.jetbrains.com/issue/CMP-7836/
7170
backingElement.focus()
7271
window.requestAnimationFrame {
73-
if (document.activeElement != backingElement) {
74-
backingElement.focus()
75-
}
72+
backingElement.focus()
7673
}
7774
}
7875

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

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,9 +82,6 @@ internal class DomInputStrategy(
8282
private val tabKeyCode = Key.Tab.keyCode.toInt()
8383

8484
private fun initEvents() {
85-
htmlInput.addEventListener("blur", { evt ->
86-
// TODO: any actions here?
87-
})
8885

8986
htmlInput.addEventListener("keydown", { evt ->
9087
nativeInputEventsProcessor.registerEvent(evt as KeyboardEvent)

compose/ui/ui/src/webMain/kotlin/androidx/compose/ui/window/ComposeWindowInternal.web.kt

Lines changed: 67 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,6 @@ import androidx.compose.ui.viewinterop.TrackInteropPlacementContainer
8484
import androidx.compose.ui.viewinterop.WebInteropContainer
8585
import androidx.lifecycle.Lifecycle
8686
import androidx.lifecycle.enableSavedStateHandles
87-
import kotlin.coroutines.coroutineContext
8887
import kotlin.js.ExperimentalWasmJsInterop
8988
import kotlin.js.JsArray
9089
import kotlin.js.js
@@ -99,9 +98,7 @@ import kotlinx.coroutines.channels.Channel
9998
import kotlinx.coroutines.channels.Channel.Factory.CONFLATED
10099
import kotlinx.coroutines.coroutineScope
101100
import kotlinx.coroutines.flow.Flow
102-
import kotlinx.coroutines.flow.flow
103101
import kotlinx.coroutines.flow.receiveAsFlow
104-
import kotlinx.coroutines.isActive
105102
import org.jetbrains.skiko.SkiaLayer
106103
import org.jetbrains.skiko.SkikoRenderDelegate
107104
import org.jetbrains.skiko.hostOs
@@ -115,6 +112,7 @@ import org.w3c.dom.HTMLTextAreaElement
115112
import org.w3c.dom.LOADING
116113
import org.w3c.dom.MediaQueryListEvent
117114
import org.w3c.dom.Node
115+
import org.w3c.dom.TouchEvent
118116
import org.w3c.dom.events.Event
119117
import org.w3c.dom.events.EventTarget
120118
import org.w3c.dom.events.FocusEvent
@@ -135,19 +133,6 @@ internal interface ComposeWindowState {
135133
fun dispose() {
136134
globalEvents.dispose()
137135
}
138-
139-
companion object {
140-
fun createFromLambda(lambda: suspend () -> IntSize): ComposeWindowState {
141-
return object : ComposeWindowState {
142-
override val globalEvents = EventTargetListener(window)
143-
override fun sizeFlow(): Flow<IntSize> = flow {
144-
while (coroutineContext.isActive) {
145-
emit(lambda())
146-
}
147-
}
148-
}
149-
}
150-
}
151136
}
152137

153138
private sealed interface KeyboardModeState {
@@ -402,6 +387,17 @@ internal class ComposeWindow(
402387
addTypedEvent<PointerEvent>(name, passive = false) { onPointerEvent(it) }
403388
}
404389

390+
addTypedEvent<TouchEvent>("touchstart") { evt ->
391+
// in most cases we don't care about touches since in Compose we do not process them at all
392+
// there's one case however when we need to cancel them - it's when we are focussed in a DOM backing field
393+
// see https://youtrack.jetbrains.com/issue/CMP-10079
394+
395+
val backingInput = (platformContext.textInputService as WebTextInputService).getBackingInput()
396+
if (backingInput?.isFocused() == true) {
397+
evt.preventDefault()
398+
}
399+
}
400+
405401
addTypedEvent<WheelEvent>("wheel", passive = false) { event ->
406402
onWheelEvent(event)
407403
}
@@ -564,9 +560,43 @@ internal class ComposeWindow(
564560
private fun onPointerEvent(event: PointerEvent) {
565561
val eventType = event.getPointerEventType()
566562
var result: PointerEventResult? = null
567-
val isTouchEvent = isTouchEvent(event)
568563

569-
if (isTouchEvent) {
564+
if (isMouseEvent(event)) {
565+
keyboardModeState = KeyboardModeState.Hardware
566+
567+
// validate event before sending it further - see
568+
// https://youtrack.jetbrains.com/issue/CMP-8430/Sequence-of-Move-PointerInputEvents-cancel-out-press-PointerInputEvent-under-certain-conditions
569+
570+
var isValidEvent = true
571+
when (eventType) {
572+
PointerEventType.Press -> {
573+
actualActivePointerButtons = event.composeButtons
574+
}
575+
PointerEventType.Release -> {
576+
actualActivePointerButtons = null
577+
}
578+
PointerEventType.Move -> {
579+
isValidEvent = actualActivePointerButtons == null || actualActivePointerButtons == event.composeButtons
580+
}
581+
}
582+
583+
if (!isValidEvent) return
584+
585+
scene.sendPointerEvent(
586+
eventType = eventType,
587+
position = event.offset,
588+
timeMillis = event.timeStamp.toInt().toLong(),
589+
buttons = event.composeButtons,
590+
keyboardModifiers = PointerKeyboardModifiers(
591+
isCtrlPressed = event.ctrlKey,
592+
isMetaPressed = event.metaKey,
593+
isAltPressed = event.altKey,
594+
isShiftPressed = event.shiftKey,
595+
),
596+
nativeEvent = event,
597+
button = event.composeButton,
598+
)
599+
} else {
570600
if (eventType == PointerEventType.Enter || eventType == PointerEventType.Exit) {
571601
//Enter and Exit events have no sense for touches (Firefox and Safari send them)
572602
return
@@ -652,48 +682,7 @@ internal class ComposeWindow(
652682

653683
if (result != null && result.anyChangeConsumed && event.cancelable) {
654684
event.preventDefault()
655-
656-
// Since we call preventDefault, the browser will not focus the canvas automatically,
657-
// but it should be focused to receive key events.
658-
if (!canvasFocused && !isTouchEvent && eventType == PointerEventType.Press) {
659-
canvas.focus()
660-
}
661-
}
662-
} else {
663-
keyboardModeState = KeyboardModeState.Hardware
664-
665-
// validate event before sending it further - see
666-
// https://youtrack.jetbrains.com/issue/CMP-8430/Sequence-of-Move-PointerInputEvents-cancel-out-press-PointerInputEvent-under-certain-conditions
667-
668-
var isValidEvent = true
669-
when (eventType) {
670-
PointerEventType.Press -> {
671-
actualActivePointerButtons = event.composeButtons
672-
}
673-
PointerEventType.Release -> {
674-
actualActivePointerButtons = null
675-
}
676-
PointerEventType.Move -> {
677-
isValidEvent = actualActivePointerButtons == null || actualActivePointerButtons == event.composeButtons
678-
}
679685
}
680-
681-
if (!isValidEvent) return
682-
683-
result = scene.sendPointerEvent(
684-
eventType = eventType,
685-
position = event.offset,
686-
timeMillis = event.timeStamp.toInt().toLong(),
687-
buttons = event.composeButtons,
688-
keyboardModifiers = PointerKeyboardModifiers(
689-
isCtrlPressed = event.ctrlKey,
690-
isMetaPressed = event.metaKey,
691-
isAltPressed = event.altKey,
692-
isShiftPressed = event.shiftKey,
693-
),
694-
nativeEvent = event,
695-
button = event.composeButton,
696-
)
697686
}
698687
}
699688

@@ -826,7 +815,7 @@ private fun clipTargetElement(canvas: HTMLCanvasElement): HTMLTextAreaElement {
826815

827816
// strings checks are faster on a JS side
828817
// language=js
829-
private fun isTouchEvent(event: PointerEvent): Boolean = js("event.pointerType === 'touch'")
818+
private fun isMouseEvent(event: PointerEvent): Boolean = js("event.pointerType === 'mouse'")
830819

831820
// strings checks are faster on a JS side
832821
// language=js
@@ -858,4 +847,21 @@ private fun PointerEvent.getPointerEventType(): PointerEventType =
858847
PointerEventType.Enter.value -> PointerEventType.Enter
859848
PointerEventType.Exit.value -> PointerEventType.Exit
860849
else -> PointerEventType.Unknown
861-
}
850+
}
851+
852+
private fun Element.isFocused(): Boolean {
853+
val activeElement = when {
854+
document.activeElement?.shadowRoot != null -> (document.activeElement?.shadowRoot as? ShadowRootExt)?.activeElement
855+
else -> document.activeElement
856+
}
857+
858+
if (activeElement == null) {
859+
return false
860+
}
861+
862+
return activeElement == this
863+
}
864+
865+
private external interface ShadowRootExt {
866+
val activeElement: Element?
867+
}

0 commit comments

Comments
 (0)