diff --git a/compose/foundation/foundation/src/jsWasmMain/kotlin/androidx/compose/foundation/text/TouchMode.js.kt b/compose/foundation/foundation/src/jsWasmMain/kotlin/androidx/compose/foundation/text/TouchMode.js.kt
index f7bc041d323e4..4861c5bcb65b6 100644
--- a/compose/foundation/foundation/src/jsWasmMain/kotlin/androidx/compose/foundation/text/TouchMode.js.kt
+++ b/compose/foundation/foundation/src/jsWasmMain/kotlin/androidx/compose/foundation/text/TouchMode.js.kt
@@ -16,4 +16,4 @@
package androidx.compose.foundation.text
-internal actual val isInTouchMode = false
+internal actual val isInTouchMode = true
diff --git a/compose/mpp/demo/src/jsMain/resources/index.html b/compose/mpp/demo/src/jsMain/resources/index.html
index eda2871422713..9e7f6b9b4a7cf 100644
--- a/compose/mpp/demo/src/jsMain/resources/index.html
+++ b/compose/mpp/demo/src/jsMain/resources/index.html
@@ -2,14 +2,13 @@
-
+
compose multiplatform web demo
- compose multiplatform web demo
diff --git a/compose/ui/ui/src/jsMain/kotlin/androidx/compose/ui/platform/getVisualViewport.js.kt b/compose/ui/ui/src/jsMain/kotlin/androidx/compose/ui/platform/getVisualViewport.js.kt
new file mode 100644
index 0000000000000..bd54e2593b16f
--- /dev/null
+++ b/compose/ui/ui/src/jsMain/kotlin/androidx/compose/ui/platform/getVisualViewport.js.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2023 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.platform
+
+internal actual fun getVisualViewport(): VisualViewport? {
+ return js("window.visualViewport") as? VisualViewport
+}
\ No newline at end of file
diff --git a/compose/ui/ui/src/jsNativeMain/kotlin/androidx/compose/ui/native/ComposeLayer.jsNative.kt b/compose/ui/ui/src/jsNativeMain/kotlin/androidx/compose/ui/native/ComposeLayer.jsNative.kt
index 9d6953f8d2a54..31f34503e197a 100644
--- a/compose/ui/ui/src/jsNativeMain/kotlin/androidx/compose/ui/native/ComposeLayer.jsNative.kt
+++ b/compose/ui/ui/src/jsNativeMain/kotlin/androidx/compose/ui/native/ComposeLayer.jsNative.kt
@@ -22,6 +22,7 @@ import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.asComposeCanvas
import androidx.compose.ui.input.InputMode
import androidx.compose.ui.input.key.KeyEvent
+import androidx.compose.ui.input.pointer.PointerButton
import androidx.compose.ui.input.pointer.PointerId
import androidx.compose.ui.input.pointer.PointerType
import androidx.compose.ui.input.pointer.toCompose
@@ -73,6 +74,9 @@ internal class ComposeLayer(
onPointerEventWithMultitouch(event)
} else {
// macos and desktop`s web don't work properly when using onPointerEventWithMultitouch
+ if (scene.platformContext.inputModeManager.inputMode != InputMode.Keyboard) {
+ scene.platformContext.inputModeManager.requestInputMode(InputMode.Keyboard)
+ }
onPointerEventNoMultitouch(event)
}
}
diff --git a/compose/ui/ui/src/jsWasmMain/kotlin/androidx/compose/ui/native/Actuals.js.kt b/compose/ui/ui/src/jsWasmMain/kotlin/androidx/compose/ui/native/Actuals.js.kt
index 787f30dd88465..e84c179316306 100644
--- a/compose/ui/ui/src/jsWasmMain/kotlin/androidx/compose/ui/native/Actuals.js.kt
+++ b/compose/ui/ui/src/jsWasmMain/kotlin/androidx/compose/ui/native/Actuals.js.kt
@@ -15,4 +15,4 @@
*/
package androidx.compose.ui.native
-internal actual val supportsMultitouch: Boolean get() = false
+internal actual val supportsMultitouch: Boolean get() = true
diff --git a/compose/ui/ui/src/jsWasmMain/kotlin/androidx/compose/ui/platform/ImeMobileKeyboardListener.kt b/compose/ui/ui/src/jsWasmMain/kotlin/androidx/compose/ui/platform/ImeMobileKeyboardListener.kt
new file mode 100644
index 0000000000000..2bbbf0892e6ef
--- /dev/null
+++ b/compose/ui/ui/src/jsWasmMain/kotlin/androidx/compose/ui/platform/ImeMobileKeyboardListener.kt
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2023 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.platform
+
+import kotlinx.browser.window
+import org.w3c.dom.events.*
+
+internal interface ImeMobileKeyboardListener {
+ fun onShow(callback: ()->Unit)
+ fun onHide(callback: ()->Unit)
+}
+
+internal class ImeKeyboardListenerImpl: ImeMobileKeyboardListener {
+ private var callbackOnShow = {}
+ private var callbackOnHide = {}
+
+ init {
+ val visualViewport = getVisualViewport()
+
+ if (visualViewport != null) {
+ val viewportVsClientHeightRatio = 0.75
+
+ visualViewport.addEventListener("resize", { event ->
+ val target = event.target as? VisualViewport ?: return@addEventListener
+ if (
+ (target.height * target.scale) / window.screen.height <
+ viewportVsClientHeightRatio
+ ) {
+ callbackOnShow()
+ } else {
+ callbackOnHide()
+ }
+ }, false)
+ }
+ }
+
+ override fun onShow(callback: () -> Unit) {
+ callbackOnShow = callback
+ }
+
+ override fun onHide(callback: () -> Unit) {
+ callbackOnHide = callback
+ }
+}
+
+abstract external class VisualViewport : EventTarget {
+ val offsetLeft: Double
+ val offsetTop: Double
+
+ val pageLeft: Double
+ val pageTop: Double
+
+ val width: Double
+ val height: Double
+
+ val scale: Double
+
+ val onresize: (Event) -> Unit
+ val onscroll: (Event) -> Unit
+ val onscrollend: (Event) -> Unit
+}
\ No newline at end of file
diff --git a/compose/ui/ui/src/jsWasmMain/kotlin/androidx/compose/ui/platform/ImeTextInputService.js.kt b/compose/ui/ui/src/jsWasmMain/kotlin/androidx/compose/ui/platform/ImeTextInputService.js.kt
new file mode 100644
index 0000000000000..7ae9c87e9410d
--- /dev/null
+++ b/compose/ui/ui/src/jsWasmMain/kotlin/androidx/compose/ui/platform/ImeTextInputService.js.kt
@@ -0,0 +1,254 @@
+/*
+ * Copyright 2023 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.platform
+
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.text.input.CommitTextCommand
+import androidx.compose.ui.text.input.DeleteAllCommand
+import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.text.input.SetSelectionCommand
+import androidx.compose.ui.text.input.TextFieldValue
+import androidx.compose.ui.unit.Density
+import kotlinx.browser.document
+import org.w3c.dom.HTMLTextAreaElement
+import org.w3c.dom.asList
+import org.w3c.dom.events.*
+import org.w3c.dom.events.KeyboardEvent
+import org.w3c.dom.events.MouseEvent
+
+internal open class ImeTextInputService(
+ private val canvasId: String,
+ private val density: Density
+) {
+ private val inputId = "compose-software-input-$canvasId"
+
+ private val imeKeyboardListener = ImeKeyboardListenerImpl()
+
+ private var composeInput: JSTextInputService.CurrentInput? = null
+
+ private var currentHtmlInput: HTMLTextAreaElement? = null
+
+ private fun createHtmlInput(): HTMLTextAreaElement {
+ val htmlInput = document.createElement("textarea") as HTMLTextAreaElement
+
+ // handle ActionKey(Enter)
+ val keyHandler: (Event) -> Unit = keyHandler@{ event ->
+ event as KeyboardEvent
+ if (event.key == "Enter" && event.type == "keydown") {
+ runImeActionIfRequired()
+ }
+ }
+
+ htmlInput.apply {
+ setAttribute("autocorrect", "off")
+ setAttribute("autocomplete", "off")
+ setAttribute("autocapitalize", "off")
+ setAttribute("spellcheck", "false")
+ setAttribute("readonly", "true")
+ className = inputId
+ id = inputId
+ style.apply {
+ setProperty("position", "absolute")
+ setProperty("user-select", "none")
+ setProperty("forced-color-adjust", "none")
+ setProperty("white-space", "pre-wrap")
+ setProperty("align-content", "center")
+ setProperty("top", "0px")
+ setProperty("left", "0px")
+ setProperty("padding", "0px")
+ setProperty("opacity", "0")
+ setProperty("color", "transparent")
+ setProperty("background", "transparent")
+ setProperty("caret-color", "transparent")
+ setProperty("outline", "none")
+ setProperty("border", "none")
+ setProperty("resize", "none")
+ setProperty("text-shadow", "none")
+ }
+ // disable native context menu
+ val eventHandler: (MouseEvent) -> Any = eventHandler@{ event ->
+ event.preventDefault()
+ event.stopPropagation()
+ event.stopImmediatePropagation()
+ return@eventHandler false
+ }
+
+ oncontextmenu = eventHandler as ((MouseEvent) -> Unit)
+ addEventListener("keyup", keyHandler, false)
+ addEventListener("keydown", keyHandler, false)
+ addEventListener("input", eventHandler@{ event ->
+ val el = (event.target as HTMLTextAreaElement)
+ val text = el.value
+ val cursorPosition = el.selectionEnd
+ sendImeValueToCompose(text, cursorPosition)
+ }, false)
+ }
+ document.body?.appendChild(htmlInput)
+
+ return htmlInput
+ }
+
+ private fun createOrGetHtmlInput(): HTMLTextAreaElement {
+ // Use the same input to prevent flashing.
+ return (currentHtmlInput ?: createHtmlInput()) as HTMLTextAreaElement
+ }
+
+ fun clear() {
+ // console.log("clear")
+ composeInput = null
+ currentHtmlInput = null
+ document.getElementsByClassName(inputId).asList().forEach {
+ it as HTMLTextAreaElement
+ document.body?.removeChild(it)
+ }
+ }
+
+ fun showSoftwareKeyboard() {
+ // Safari accepts the focus event only inside a touch event handler.
+ // if event from js call, it's not will work
+ composeInput?.let { composeInput ->
+ val htmlInput = createOrGetHtmlInput()
+
+ val inputMode = when (composeInput.imeOptions.keyboardType) {
+ KeyboardType.Text -> "text"
+ KeyboardType.Ascii -> "text"
+ KeyboardType.Number -> "number"
+ KeyboardType.Phone -> "tel"
+ KeyboardType.Uri -> "url"
+ KeyboardType.Email -> "email"
+ KeyboardType.Password -> "password"
+ KeyboardType.NumberPassword -> "number"
+ KeyboardType.Decimal -> "decimal"
+ else -> "text"
+ }
+ val enterKeyHint = when (composeInput.imeOptions.imeAction) {
+ ImeAction.Default -> "enter"
+ ImeAction.None -> "enter"
+ ImeAction.Done -> "done"
+ ImeAction.Go -> "go"
+ ImeAction.Next -> "next"
+ ImeAction.Previous -> "previous"
+ ImeAction.Search -> "search"
+ ImeAction.Send -> "send"
+ else -> "enter"
+ }
+ val start = composeInput.value.selection.start ?: htmlInput.value.length - 1
+ val end = composeInput.value.selection.start ?: htmlInput.value.length - 1
+
+ htmlInput.setAttribute("inputmode", inputMode)
+ htmlInput.setAttribute("enterkeyhint", enterKeyHint)
+ htmlInput.value = composeInput.value.text
+
+ htmlInput.setSelectionRange(start, end)
+ currentHtmlInput = htmlInput
+ imeKeyboardListener.onHide {
+ clear()
+ }
+ }
+ }
+
+ fun hideSoftwareKeyboard() {
+ clear()
+ }
+
+ fun updateState(newValue: TextFieldValue) {
+ currentHtmlInput?.let { it ->
+ it.value = newValue.text
+ it.setSelectionRange(newValue.selection.start, newValue.selection.end)
+ }
+ }
+
+ fun updatePosition(rect: Rect) {
+ val scale = density.density
+ document.getElementById(canvasId)?.getBoundingClientRect()?.let { offset ->
+ val offsetX = offset.left.toFloat().coerceAtLeast(0f) + (rect.left / scale)
+ val offsetY = offset.top.toFloat().coerceAtLeast(0f) + (rect.top / scale)
+
+ currentHtmlInput?.let { html ->
+
+ html.style.apply {
+ setProperty("left", "${offsetX}px")
+ setProperty("top", "${offsetY}px")
+ }
+
+ val hasFocus = html == document.activeElement
+
+ if (!hasFocus) {
+ html.removeAttribute("readonly")
+ html.focus()
+ }
+ }
+ }
+ }
+
+ private fun sendImeValueToCompose(text: String, newCursorPosition: Int? = null) {
+ composeInput?.let { input ->
+ val value = if (text == "\n") {
+ ""
+ } else {
+ text
+ }
+
+ if (newCursorPosition != null) {
+ input.onEditCommand(
+ listOf(
+ DeleteAllCommand(),
+ CommitTextCommand(value, 1),
+ SetSelectionCommand(newCursorPosition, newCursorPosition)
+ )
+ )
+ } else {
+ input.onEditCommand(
+ listOf(
+ CommitTextCommand(value, 1)
+ )
+ )
+ }
+ }
+ }
+
+ private fun imeActionRequired(): Boolean =
+ composeInput?.imeOptions?.run {
+ singleLine || (
+ imeAction != ImeAction.None
+ && imeAction != ImeAction.Default
+ && imeAction != ImeAction.Search
+ )
+ } ?: false
+
+ private fun runImeActionIfRequired(): Boolean {
+ val currentImeOptions = composeInput?.imeOptions
+ val currentImeActionHandler = composeInput?.onImeActionPerformed
+ val imeAction = currentImeOptions?.imeAction ?: return false
+ val imeActionHandler = currentImeActionHandler ?: return false
+ if (!imeActionRequired()) {
+ return false
+ }
+ if (imeAction == ImeAction.Default) {
+ imeActionHandler(ImeAction.Done)
+ } else {
+ imeActionHandler(imeAction)
+ }
+ return true
+ }
+
+ fun setInput(input: JSTextInputService.CurrentInput?) {
+ composeInput = input
+ }
+}
+
diff --git a/compose/ui/ui/src/jsWasmMain/kotlin/androidx/compose/ui/platform/JSTextInputService.js.kt b/compose/ui/ui/src/jsWasmMain/kotlin/androidx/compose/ui/platform/JSTextInputService.js.kt
index d1ad2425ac6ec..a0d7ce67cb036 100644
--- a/compose/ui/ui/src/jsWasmMain/kotlin/androidx/compose/ui/platform/JSTextInputService.js.kt
+++ b/compose/ui/ui/src/jsWasmMain/kotlin/androidx/compose/ui/platform/JSTextInputService.js.kt
@@ -16,6 +16,8 @@
package androidx.compose.ui.platform
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.input.InputMode
import androidx.compose.ui.text.input.CommitTextCommand
import androidx.compose.ui.text.input.EditCommand
import androidx.compose.ui.text.input.ImeAction
@@ -25,11 +27,16 @@ import androidx.compose.ui.text.input.TextFieldValue
import org.jetbrains.skiko.SkikoInput
import org.jetbrains.skiko.SkikoInputEvent
-internal class JSTextInputService : PlatformTextInputService {
+internal class JSTextInputService(
+ private val jsInputModeManager: DefaultInputModeManager,
+ private val imeTextInputService: ImeTextInputService
+) : PlatformTextInputService {
data class CurrentInput(
var value: TextFieldValue,
+ var imeOptions: ImeOptions,
val onEditCommand: ((List) -> Unit),
+ var onImeActionPerformed: (ImeAction) -> Unit
)
private var currentInput: CurrentInput? = null
@@ -42,26 +49,47 @@ internal class JSTextInputService : PlatformTextInputService {
) {
currentInput = CurrentInput(
value,
- onEditCommand
+ imeOptions,
+ onEditCommand,
+ onImeActionPerformed
)
- showSoftwareKeyboard()
+
+ if (jsInputModeManager.inputMode == InputMode.Touch) {
+ imeTextInputService.setInput(currentInput)
+ showSoftwareKeyboard()
+ }
}
override fun stopInput() {
currentInput = null
+ imeTextInputService.clear()
}
override fun showSoftwareKeyboard() {
- println("TODO showSoftwareKeyboard in JS")
+ if (jsInputModeManager.inputMode == InputMode.Touch) {
+ imeTextInputService.setInput(currentInput)
+ imeTextInputService.showSoftwareKeyboard()
+ }
}
override fun hideSoftwareKeyboard() {
- println("TODO showSoftwareKeyboard in JS")
+ if (jsInputModeManager.inputMode == InputMode.Touch) {
+ imeTextInputService.hideSoftwareKeyboard()
+ }
+ }
+
+ override fun notifyFocusedRect(rect: Rect) {
+ if (jsInputModeManager.inputMode == InputMode.Touch) {
+ imeTextInputService.updatePosition(rect)
+ }
}
override fun updateState(oldValue: TextFieldValue?, newValue: TextFieldValue) {
currentInput?.let { input ->
input.value = newValue
+ if (jsInputModeManager.inputMode == InputMode.Touch) {
+ imeTextInputService.updateState(newValue)
+ }
}
}
diff --git a/compose/ui/ui/src/jsWasmMain/kotlin/androidx/compose/ui/platform/getVisualViewport.kt b/compose/ui/ui/src/jsWasmMain/kotlin/androidx/compose/ui/platform/getVisualViewport.kt
new file mode 100644
index 0000000000000..b3d5da4de782c
--- /dev/null
+++ b/compose/ui/ui/src/jsWasmMain/kotlin/androidx/compose/ui/platform/getVisualViewport.kt
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2023 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.platform
+
+
+internal expect fun getVisualViewport(): VisualViewport?
\ No newline at end of file
diff --git a/compose/ui/ui/src/wasmJsMain/kotlin/androidx/compose/ui/platform/getVisualViewport.wasmJs.kt b/compose/ui/ui/src/wasmJsMain/kotlin/androidx/compose/ui/platform/getVisualViewport.wasmJs.kt
new file mode 100644
index 0000000000000..5da21958fcce5
--- /dev/null
+++ b/compose/ui/ui/src/wasmJsMain/kotlin/androidx/compose/ui/platform/getVisualViewport.wasmJs.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2023 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.platform
+
+@JsFun("""
+ () => window.visualViewport
+""")
+internal actual external fun getVisualViewport(): VisualViewport?
\ No newline at end of file