Skip to content

Commit eece07a

Browse files
committed
Fixed the focus handling between compose and wasmJs
1 parent e0d2903 commit eece07a

3 files changed

Lines changed: 76 additions & 23 deletions

File tree

webview-compose/src/wasmJsMain/kotlin/io/github/kdroidfilter/webview/web/HtmlView.kt

Lines changed: 59 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -47,31 +47,41 @@ fun HtmlView(
4747
val root: Node = document.body?.shadowRoot ?: document.body!!
4848
val density = LocalDensity.current.density
4949
val focusManager = LocalFocusManager.current
50+
val htmlViewFocusRequester = remember { FocusRequester() }
5051

5152
val componentInfo = remember { ComponentInfo<HTMLIFrameElement>() }
5253
val focusSwitcher = remember { FocusSwitcher(componentInfo, focusManager) }
5354
val eventsInitialized = remember { mutableStateOf(false) }
5455
val componentReady = remember { mutableStateOf(false) }
5556

5657
Box(
57-
modifier = modifier.onGloballyPositioned { coordinates ->
58-
val location = coordinates.positionInWindow().round()
59-
val size = coordinates.size
60-
if (componentReady.value) {
61-
val container = componentInfo.container as HTMLDivElement
62-
container.style.width = "${size.width / density}px"
63-
container.style.height = "${size.height / density}px"
64-
container.style.left = "${location.x / density}px"
65-
container.style.top = "${location.y / density}px"
58+
modifier = modifier
59+
.focusRequester(htmlViewFocusRequester)
60+
.onFocusChanged {
61+
if (it.isFocused) {
62+
element.value?.let(::requestFocus)
63+
}
64+
}
65+
.focusTarget()
66+
.onGloballyPositioned { coordinates ->
67+
val location = coordinates.positionInWindow().round()
68+
val size = coordinates.size
69+
if (componentReady.value) {
70+
val container = componentInfo.container as HTMLDivElement
71+
container.style.width = "${size.width / density}px"
72+
container.style.height = "${size.height / density}px"
73+
container.style.left = "${location.x / density}px"
74+
container.style.top = "${location.y / density}px"
75+
}
6676
}
67-
}
6877
) {
6978
focusSwitcher.Content()
7079
}
7180

7281
DisposableEffect(Unit) {
7382
componentInfo.container = document.createElement("div") as HTMLDivElement
7483
componentInfo.component = document.createElement("iframe") as HTMLIFrameElement
84+
componentInfo.component.tabIndex = 0
7585
componentReady.value = true
7686
val container = componentInfo.container as HTMLDivElement
7787

@@ -99,9 +109,38 @@ fun HtmlView(
99109
if (!eventsInitialized.value) {
100110
eventsInitialized.value = true
101111

112+
val syncFocusToIframe = {
113+
try {
114+
htmlViewFocusRequester.requestFocus()
115+
requestFocus(iframe)
116+
iframe.contentWindow?.focus()
117+
} catch (_: Throwable) {
118+
}
119+
}
120+
102121
val loadCallback: (Event) -> Unit = {
103122
state.loadingState = HtmlLoadingState.Finished()
104123

124+
try {
125+
registerDomListener(iframe.contentWindow, "focus") {
126+
htmlViewFocusRequester.requestFocus()
127+
}
128+
129+
registerDomListener(iframe.contentDocument, "focusin") {
130+
htmlViewFocusRequester.requestFocus()
131+
}
132+
133+
registerDomListener(iframe.contentDocument, "pointerdown") {
134+
syncFocusToIframe()
135+
}
136+
137+
registerDomListener(iframe.contentDocument, "mousedown") {
138+
syncFocusToIframe()
139+
}
140+
} catch (_: Throwable) {
141+
// Cross-origin iframe: cannot access contentDocument/contentWindow listeners
142+
}
143+
105144
when (val content = state.content) {
106145
is HtmlContent.Url -> {
107146
// Safe: use the URL requested, do not inspect iframe internals
@@ -137,14 +176,15 @@ fun HtmlView(
137176
)
138177
}
139178

140-
iframe.addEventListener(
141-
type = "load",
142-
callback = loadCallback
143-
)
144-
iframe.addEventListener(
145-
type = "error",
146-
callback = errorCallback
147-
)
179+
registerDomListener(iframe, "focus") {
180+
htmlViewFocusRequester.requestFocus()
181+
}
182+
183+
registerDomListener(iframe, "pointerdown") {
184+
syncFocusToIframe()
185+
}
186+
iframe.addEventListener("load", loadCallback)
187+
iframe.addEventListener("error", errorCallback)
148188

149189
scope.launch {
150190
navigator.handleNavigationEvents(iframe)
@@ -344,9 +384,7 @@ fun HtmlViewUrl(
344384
HtmlView(
345385
state = state,
346386
modifier = modifier,
347-
navigator = navigator,
348-
onCreated = {},
349-
onDispose = {},
387+
navigator = navigator
350388
)
351389
}
352390

webview-compose/src/wasmJsMain/kotlin/io/github/kdroidfilter/webview/web/JsInterop.kt

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
@file:OptIn(ExperimentalWasmJsInterop::class)
2-
32
package io.github.kdroidfilter.webview.web
43

54
import org.w3c.dom.Element
5+
import kotlin.js.JsAny
66

77
/**
88
* Evaluate JavaScript in the iframe context
@@ -46,3 +46,19 @@ fun addContentIdentifierJs(iframe: Element) {
4646
fun requestFocus(element: Element) {
4747
js("element.focus()")
4848
}
49+
50+
/**
51+
* Register a DOM listener without relying on Kotlin event casting.
52+
*/
53+
fun registerDomListener(target: JsAny?, type: String, callback: () -> Unit) {
54+
js(
55+
//language=javascript
56+
"""{
57+
if (target && typeof target.addEventListener === 'function') {
58+
target.addEventListener(type, function () {
59+
callback();
60+
});
61+
}
62+
}"""
63+
)
64+
}

webview-compose/src/wasmJsMain/kotlin/io/github/kdroidfilter/webview/web/WebView.wasmJs.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
@file:OptIn(ExperimentalWasmJsInterop::class)
2-
32
package io.github.kdroidfilter.webview.web
43

54
import androidx.compose.runtime.*

0 commit comments

Comments
 (0)