Skip to content

Commit

Permalink
CfW: implement conditional preventDefault calls for Key Events (#1124)
Browse files Browse the repository at this point in the history
Introduce new method `override fun onKeyboardEventWithResult(event:
SkikoKeyboardEvent): Boolean` to know whether Compose processed a key
event or not.
If Compose processed a key event, then we call preventDefault.
  • Loading branch information
eymar authored and igordmn committed Mar 4, 2024
1 parent bb51450 commit 5df429f
Show file tree
Hide file tree
Showing 5 changed files with 138 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,8 @@ open class AndroidXComposeMultiplatformExtensionImpl @Inject constructor(
browser {
testTask(Action<KotlinJsTest> {
it.useKarma {
useChromeHeadless()
//useChromeHeadless() // can't use headless for some integration tests
useChrome()
useConfigDirectory(
project.rootProject.projectDir.resolve("mpp/karma.config.d/wasm")
)
Expand Down
2 changes: 2 additions & 0 deletions compose/ui/ui/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -302,9 +302,11 @@ if(AndroidXComposePlugin.isMultiplatformEnabled(project)) {
dependsOn(skikoTest)
}
jsTest {
kotlin.srcDir("src/webTest/kotlin")
dependsOn(skikoTest)
}
wasmJsTest {
kotlin.srcDir("src/webTest/kotlin")
dependsOn(skikoTest)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,16 +53,20 @@ internal class ComposeLayer(
// Should be set to an actual value by ComposeWindow implementation
private var density = Density(1f)

private inner class ComponentImpl : SkikoView {
private inner class ComponentImpl : SkikoViewExtended {
override val input = this@ComposeLayer.input

override fun onRender(canvas: Canvas, width: Int, height: Int, nanoTime: Long) {
scene.render(canvas.asComposeCanvas(), nanoTime)
}

override fun onKeyboardEvent(event: SkikoKeyboardEvent) {
if (isDisposed) return
scene.sendKeyEvent(KeyEvent(event))
onKeyboardEventWithResult(event)
}

override fun onKeyboardEventWithResult(event: SkikoKeyboardEvent): Boolean {
if (isDisposed) return false
return scene.sendKeyEvent(KeyEvent(event))
}

override fun onPointerEvent(event: SkikoPointerEvent) {
Expand Down Expand Up @@ -116,7 +120,7 @@ internal class ComposeLayer(
}
}

private val view = ComponentImpl()
internal val view: SkikoViewExtended = ComponentImpl()

init {
layer.skikoView = view
Expand Down Expand Up @@ -183,4 +187,8 @@ internal fun SkikoPointerEvent.getScrollDelta(): Offset {
}?.let {
Offset(it.deltaX.toFloat(), it.deltaY.toFloat())
} ?: Offset.Zero
}

internal interface SkikoViewExtended : SkikoView {
fun onKeyboardEventWithResult(event: SkikoKeyboardEvent): Boolean
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import androidx.compose.ui.events.toSkikoScrollEvent
import androidx.compose.ui.input.pointer.BrowserCursor
import androidx.compose.ui.input.pointer.PointerIcon
import androidx.compose.ui.native.ComposeLayer
import androidx.compose.ui.native.SkikoViewExtended
import androidx.compose.ui.platform.JSTextInputService
import androidx.compose.ui.platform.PlatformContext
import androidx.compose.ui.platform.ViewConfiguration
Expand All @@ -44,7 +45,6 @@ import kotlinx.coroutines.isActive
import org.jetbrains.skiko.SkiaLayer
import org.jetbrains.skiko.SkikoKeyboardEventKind
import org.jetbrains.skiko.SkikoPointerEventKind
import org.jetbrains.skiko.SkikoView
import org.w3c.dom.HTMLCanvasElement
import org.w3c.dom.HTMLStyleElement
import org.w3c.dom.HTMLTitleElement
Expand Down Expand Up @@ -95,10 +95,11 @@ private class ComposeWindow(

val canvas = document.getElementById(canvasId) as HTMLCanvasElement

private fun <T : Event> HTMLCanvasElement.addTypedEvent(type: String, handler: (event: T, skikoView: SkikoView) -> Unit) {
addEventListener(type, { event ->
layer.layer?.skikoView?.let { skikoView -> handler(event as T, skikoView) }
})
private fun <T : Event> HTMLCanvasElement.addTypedEvent(
type: String,
handler: (event: T, skikoView: SkikoViewExtended) -> Unit
) {
this.addEventListener(type, { event -> handler(event as T, layer.view) })
}

private fun initEvents(canvas: HTMLCanvasElement) {
Expand Down Expand Up @@ -159,13 +160,13 @@ private class ComposeWindow(
})

canvas.addTypedEvent<KeyboardEvent>("keydown") { event, skikoView ->
event.preventDefault()
skikoView.onKeyboardEvent(event.toSkikoEvent(SkikoKeyboardEventKind.DOWN))
val processed = skikoView.onKeyboardEventWithResult(event.toSkikoEvent(SkikoKeyboardEventKind.DOWN))
if (processed) event.preventDefault()
}

canvas.addTypedEvent<KeyboardEvent>("keyup") { event, skikoView ->
event.preventDefault()
skikoView.onKeyboardEvent(event.toSkikoEvent(SkikoKeyboardEventKind.UP))
val processed = skikoView.onKeyboardEventWithResult(event.toSkikoEvent(SkikoKeyboardEventKind.UP))
if (processed) event.preventDefault()
}
}

Expand Down
112 changes: 112 additions & 0 deletions compose/ui/ui/src/webTest/kotlin/CanvasBasedWindowTests.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/*
* Copyright 2024 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.
*/

import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.TextField
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import kotlin.test.Test
import kotlinx.browser.document
import org.w3c.dom.HTMLCanvasElement
import androidx.compose.ui.window.*
import kotlin.test.AfterTest
import kotlin.test.assertEquals
import kotlin.test.assertTrue
import kotlin.test.assertFalse
import org.w3c.dom.events.KeyboardEvent
import org.w3c.dom.events.KeyboardEventInit

class CanvasBasedWindowTests {

private val canvasId = "canvas1"

@AfterTest
fun cleanup() {
document.getElementById(canvasId)?.remove()
}

@Test
fun canCreate() {
val canvasElement = document.createElement("canvas") as HTMLCanvasElement
canvasElement.setAttribute("id", canvasId)
document.body!!.appendChild(canvasElement)
CanvasBasedWindow(canvasElementId = canvasId) { }
}

@Test
fun testPreventDefault() {
val canvasElement = document.createElement("canvas") as HTMLCanvasElement
canvasElement.setAttribute("id", canvasId)
document.body!!.appendChild(canvasElement)

val fr = FocusRequester()
var changedValue = ""
CanvasBasedWindow(canvasElementId = canvasId) {
TextField(
value = "",
onValueChange = { changedValue = it },
modifier = Modifier.fillMaxSize().focusRequester(fr)
)
SideEffect {
fr.requestFocus()
}
}

val stack = mutableListOf<Boolean>()
canvasElement.addEventListener("keydown", { event ->
stack.add(event.defaultPrevented)
})

// dispatchEvent synchronously invokes all the listeners
canvasElement.dispatchEvent(createCopyKeyboardEvent())
assertEquals(1, stack.size)
assertTrue(stack.last())

canvasElement.dispatchEvent(createTypedEvent())
assertEquals(2, stack.size)
assertTrue( stack.last())
assertEquals("c", changedValue)

canvasElement.dispatchEvent(createEventShouldNotBePrevented())
assertEquals(3, stack.size)
assertFalse(stack.last())
}
}

internal external interface KeyboardEventInitExtended : KeyboardEventInit {
var keyCode: Int?
}

internal fun KeyboardEventInit.keyDownEvent() = KeyboardEvent("keydown", this)
internal fun KeyboardEventInit.withKeyCode() = (this as KeyboardEventInitExtended).apply {
keyCode = key!!.uppercase().first().code
}

internal fun createCopyKeyboardEvent(): KeyboardEvent =
KeyboardEventInit(key = "c", code = "KeyC", ctrlKey = true, metaKey = true, cancelable = true)
.withKeyCode()
.keyDownEvent()

internal fun createTypedEvent(): KeyboardEvent =
KeyboardEventInit(key = "c", code = "KeyC", cancelable = true)
.withKeyCode()
.keyDownEvent()

internal fun createEventShouldNotBePrevented(): KeyboardEvent =
KeyboardEventInit(ctrlKey = true, cancelable = true)
.keyDownEvent()

0 comments on commit 5df429f

Please sign in to comment.