Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[web] Process keydown and keyup keys for identified keys from virtual keyboard #1380

Merged
merged 7 commits into from
Jun 11, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@ package androidx.compose.ui.platform

import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.KeyEvent
import androidx.compose.ui.input.key.toComposeEvent
import androidx.compose.ui.text.input.CommitTextCommand
import androidx.compose.ui.text.input.EditCommand
import androidx.compose.ui.text.input.ImeAction
Expand All @@ -29,6 +27,7 @@ import androidx.compose.ui.text.input.SetComposingTextCommand
import androidx.compose.ui.text.input.TextFieldValue
import kotlinx.browser.document
import org.w3c.dom.HTMLTextAreaElement
import org.w3c.dom.events.Event
import org.w3c.dom.events.KeyboardEvent
import org.w3c.dom.events.KeyboardEventInit

Expand All @@ -41,10 +40,19 @@ internal class BackingTextArea(
private val imeOptions: ImeOptions,
private val onEditCommand: (List<EditCommand>) -> Unit,
private val onImeActionPerformed: (ImeAction) -> Unit,
private val sendKey: (evt: KeyEvent) -> Unit
private val processKeyboardEvent: (KeyboardEvent) -> Unit
) {
private val textArea: HTMLTextAreaElement = createHtmlInput()

private fun processIdentifiedEvent(evt: Event) {
if (evt !is KeyboardEvent) return
// TODO: In theory nothing stops us from passing Unidentified keys but this yet to be investigated:
// First, this way we will pass (and attempt to process) "dummy" KeyboardEvents that were designed not to have physical representation at all
// Second, we need more tests on keyboard in general before doing this anyways
if (evt.key == "Unidentified") return
processKeyboardEvent(evt)
}

private fun createHtmlInput(): HTMLTextAreaElement {
val htmlInput = document.createElement("textarea") as HTMLTextAreaElement

Expand Down Expand Up @@ -100,6 +108,14 @@ internal class BackingTextArea(
setProperty("text-shadow", "none")
}

htmlInput.addEventListener("keydown", { evt ->
processIdentifiedEvent(evt)
})

htmlInput.addEventListener("keyup", { evt ->
processIdentifiedEvent(evt)
})

htmlInput.addEventListener("input", { evt ->
evt.preventDefault()
evt as InputEventExtended
Expand All @@ -118,23 +134,15 @@ internal class BackingTextArea(

"insertText" -> {
val data = evt.data ?: return@addEventListener
if (data.length == 1) {
sendKey(
KeyboardEvent(
"keydown", KeyboardEventInit(key = data)
).toComposeEvent()
)
} else if (data.length > 1) {
onEditCommand(listOf(CommitTextCommand(data, 1)))
}
onEditCommand(listOf(CommitTextCommand(data, 1)))
}

"deleteContentBackward" -> {
sendKey(
processKeyboardEvent(
KeyboardEvent(
"keydown",
KeyboardEventInit(key = "Backspace", code = "Backspace").withKeyCode(Key.Backspace)
).toComposeEvent()
)
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,12 @@ internal class WebImeInputService(parentInputService: InputAwareInputService) :
onImeActionPerformed: (ImeAction) -> Unit
) {
backingTextArea =
BackingTextArea(imeOptions, onEditCommand, onImeActionPerformed, sendKey = { evt ->
sendKeyEvent(evt)
})
BackingTextArea(
imeOptions,
onEditCommand,
onImeActionPerformed,
processKeyboardEvent = this@WebImeInputService::processKeyboardEvent
)
backingTextArea?.register()

showSoftwareKeyboard()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,11 @@ import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.ImeOptions
import androidx.compose.ui.text.input.PlatformTextInputService
import androidx.compose.ui.text.input.TextFieldValue
import org.w3c.dom.events.KeyboardEvent

internal interface InputAwareInputService {
fun getOffset(rect: Rect): Offset
fun sendKeyEvent(event: KeyEvent)
fun processKeyboardEvent(keyboardEvent: KeyboardEvent)
fun isVirtualKeyboard(): Boolean
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,8 +184,8 @@ internal class ComposeWindow(
return Offset(offsetX, offsetY)
}

override fun sendKeyEvent(event: KeyEvent) {
layer.onKeyboardEvent(event)
override fun processKeyboardEvent(keyboardEvent: KeyboardEvent) {
this@ComposeWindow.processKeyboardEvent(keyboardEvent)
}
}

Expand Down Expand Up @@ -218,6 +218,11 @@ internal class ComposeWindow(
canvasEvents.addDisposableEvent(type) { event -> handler(event as T) }
}

private fun processKeyboardEvent(keyboardEvent: KeyboardEvent) {
val processed = layer.onKeyboardEvent(keyboardEvent.toComposeEvent())
if (processed) keyboardEvent.preventDefault()
}

private fun initEvents(canvas: HTMLCanvasElement) {
var offset = Offset.Zero

Expand Down Expand Up @@ -275,13 +280,11 @@ internal class ComposeWindow(
})

addTypedEvent<KeyboardEvent>("keydown") { event ->
val processed = layer.onKeyboardEvent(event.toComposeEvent())
if (processed) event.preventDefault()
processKeyboardEvent(event)
}

addTypedEvent<KeyboardEvent>("keyup") { event ->
val processed = layer.onKeyboardEvent(event.toComposeEvent())
if (processed) event.preventDefault()
processKeyboardEvent(event)
}

state.globalEvents.addDisposableEvent("focus") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,6 @@ internal interface OnCanvasTests {

fun commonAfterTest() {
document.getElementById(canvasId)?.remove()
val childNodes = document.body!!.childNodes.asList()
childNodes.forEach {
document.body!!.removeChild(it)
}
}

fun assertApproximatelyEqual(expected: Float, actual: Float, tolerance: Float = 1f) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ class SelectionContainerTests : OnCanvasTests {
fun setup() {
// Because AfterTest is fixed only in kotlin 2.0
// https://youtrack.jetbrains.com/issue/KT-61888
document.getElementById(canvasId)?.remove()
commonAfterTest()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tests here failed on CI.
Could this change be a reason of the test failure?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

although locally all the tests pass. weird :(

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tried to rerun the CI, but same failure.

for some reason I think commonAfterTest might be a reason.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let me just return everything as it was and see how it goes
the reason might be parallelization or whatever

}

fun HTMLCanvasElement.doClick() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* 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.
*/

package androidx.compose.ui.events

import org.w3c.dom.events.Event

internal external interface InputEventInit {
val data: String
val inputType: String
}

internal fun InputEventInit(inputType: String, data: String): InputEventInit = js("({data, inputType})")

internal external class InputEvent(type: String, options: InputEventInit) : Event
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* 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.
*/

package androidx.compose.ui.events

import org.w3c.dom.events.Event

internal external class MouseEvent(type: String) : Event
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* 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.
*/

package androidx.compose.ui.events

import org.w3c.dom.events.Event

internal external interface TouchEventInit

internal external class TouchEvent(type: String, initParams: TouchEventInit) : Event
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,12 @@
* limitations under the License.
*/

package androidx.compose.ui.input
package androidx.compose.ui.events

import androidx.compose.ui.input.key.Key
import org.w3c.dom.events.KeyboardEvent
import org.w3c.dom.events.KeyboardEventInit
import org.w3c.dom.events.MouseEvent

internal external interface KeyboardEventInitExtended : KeyboardEventInit {
var keyCode: Int?
Expand Down Expand Up @@ -54,4 +55,15 @@ internal fun keyDownEvent(
internal fun keyDownEventUnprevented(): KeyboardEvent =
KeyboardEventInit(ctrlKey = true, cancelable = true, key = "Control")
.withKeyCode(Key.CtrlLeft.keyCode.toInt())
.keyDownEvent()
.keyDownEvent()

private fun DummyTouchEventInit(): TouchEventInit = js("({ changedTouches: [new Touch({identifier: 0, target: document})] })")

internal fun createTouchEvent(type: String): TouchEvent {
return TouchEvent(type, DummyTouchEventInit())
}

internal fun createMouseEvent(type: String): MouseEvent {
return MouseEvent(type)
}

Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package androidx.compose.ui.input

import androidx.compose.foundation.text.isTypedEvent
import androidx.compose.ui.events.keyDownEvent
import androidx.compose.ui.input.key.toComposeEvent
import kotlin.test.Test
import kotlin.test.assertFalse
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package androidx.compose.ui.input

import androidx.compose.ui.events.keyDownEvent
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.toComposeEvent
Expand Down
Loading
Loading