Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package androidx.compose.mpp.demo

import androidx.compose.ui.platform.ClipEntry
import kotlinx.browser.document
import org.w3c.dom.HTMLDivElement

expect suspend fun ClipEntry?.getPlainText(): String?

Expand Down Expand Up @@ -52,7 +53,8 @@ internal fun setupBackingTextAreaDebugHints() {
}
""".trimIndent()

val shadowRoot = document.getElementById("composeApplication")?.shadowRoot!!
val container = document.getElementById("composeApplication") as HTMLDivElement
val shadowRoot = (container.firstChild?.firstChild as HTMLDivElement).shadowRoot!!

shadowRoot.prepend(shadowRootStyle)
shadowRoot.appendChild(document.createElement("div").apply {
Expand Down
4 changes: 3 additions & 1 deletion compose/ui/ui/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,9 @@ if(AndroidXComposePlugin.isMultiplatformEnabled(project)) {
nativeTest {
dependsOn(skikoTest)
}
webTest {}
webTest {
dependsOn(skikoTest)
}
jsTest {
dependsOn(webTest)
dependsOn(skikoTest)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ internal abstract class WebInteropElementHolder<T : HTMLElement>(

override fun changeInteropViewIndex(root: InteropViewGroup, index: Int) {
val referenceNode = root.htmlElement.children.item(index)
if (referenceNode === group.htmlElement) return

root.htmlElement.insertBefore(group.htmlElement, referenceNode)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package androidx.compose.ui.window
import androidx.compose.runtime.Composable
import androidx.compose.ui.ExperimentalComposeUiApi
import kotlinx.browser.document
import kotlinx.dom.clear
import org.w3c.dom.Element
import org.w3c.dom.HTMLCanvasElement
import org.w3c.dom.HTMLDivElement
Expand Down Expand Up @@ -62,35 +63,47 @@ fun ComposeViewport(
* Creates the composition in HTML canvas created in parent container identified by [viewportContainer] Element.
* This size of canvas is adjusted with the size of the container
*
* The current hierarchy:
* <viewportContainer.shadowDom>
* <app root>
* <container>
* <positioning_container>
* <shadow_container.shadow>
* <style/>
* <app_container>
* <canvas/>
* <interop elements container/>
* <a11y elements root/>
* </app root>
* </viewportContainer.shadowDom>
* <a11y_container/>
* </app_container>
* </shadow_container>
* <interopContainer/>
* <positioning_container/>
* </container>
*
* Note: The viewportContainer will be cleared on composition creation.
*/
@ExperimentalComposeUiApi
fun ComposeViewport(
viewportContainer: Element,
configure: ComposeViewportConfiguration.() -> Unit = {},
content: @Composable () -> Unit = { }
) = onSkikoReady {
val canvas = document.createElement("canvas") as HTMLCanvasElement
canvas.setAttribute("tabindex", "0")
canvas.setAttribute("role", "generic")
canvas.style.outline = "none" // Fixes https://youtrack.jetbrains.com/issue/CMP-9040
viewportContainer.clear()

// Create a common container (parent html element) for canvas and the interop container
// to position at the same place - the interop container is position at 0,0 relative to <canvas>.
// Create a common positioning container (parent html element) for shadow and the interop containers
// to position at the same place - the interop container is position at 0,0 relative to the shadow.
// It simplifies the positioning of the interop views in the container.
val layerRoot = document.createElement("div") as HTMLElement
layerRoot.style.apply {
val positioningContainer = document.createElement("div") as HTMLDivElement
positioningContainer.style.apply {
position = "relative"
}
viewportContainer.appendChild(positioningContainer)

//shadow container
val shadowContainer = document.createElement("div") as HTMLDivElement
shadowContainer.style.apply {
position = "relative"
}
positioningContainer.appendChild(shadowContainer)

val shadowRoot = viewportContainer.attachShadow(ShadowRootInit(ShadowRootMode.OPEN))
//shadow
val shadowRoot = shadowContainer.attachShadow(ShadowRootInit(ShadowRootMode.OPEN))
val shadowRootStyle = document.createElement("style")
shadowRootStyle.textContent = """
:host {
Expand All @@ -101,38 +114,50 @@ fun ComposeViewport(
position: relative;
}
""".trimIndent()
shadowRoot.appendChild(shadowRootStyle)

shadowRoot.append(shadowRootStyle, layerRoot)
layerRoot.appendChild(canvas)

val interopContainerElement = document.createElement("div") as HTMLDivElement
layerRoot.appendChild(interopContainerElement)

interopContainerElement.style.apply {
position = "absolute"
top = "0"
left = "0"
//app container (canvas + a11y)
val appContainer = document.createElement("div") as HTMLElement
appContainer.style.apply {
position = "relative"
}
shadowRoot.appendChild(appContainer)

val configuration = ComposeViewportConfiguration().apply(configure)
//canvas
val canvas = document.createElement("canvas") as HTMLCanvasElement
canvas.setAttribute("tabindex", "0")
canvas.setAttribute("role", "generic")
canvas.style.outline = "none" // Fixes https://youtrack.jetbrains.com/issue/CMP-9040
appContainer.appendChild(canvas)

//a11y container
val configuration = ComposeViewportConfiguration().apply(configure)
val a11yContainerElement = if (configuration.isA11YEnabled) {
(document.createElement("div") as HTMLDivElement).also { a11yContainer ->
layerRoot.appendChild(a11yContainer)
a11yContainer.style.apply {
position = "absolute"
top = "0"
left = "0"
}
appContainer.appendChild(a11yContainer)
}
} else {
null
}

//interop container
val interopContainerElement = document.createElement("div") as HTMLDivElement
interopContainerElement.style.apply {
position = "absolute"
top = "0"
left = "0"
}
positioningContainer.appendChild(interopContainerElement)

ComposeWindow(
canvas = canvas,
rootNode = shadowRoot,
layerRoot = layerRoot,
layerRoot = appContainer,
interopContainerElement = interopContainerElement,
a11yContainerElement = a11yContainerElement,
content = content,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,7 @@ internal class ComposeWindow(
get() = activeTouchOffset

override val backingDomInputContainer: HTMLElement
get() = interopContainerElement
get() = layerRoot

override fun getNewGeometryForBackingInput(rect: Rect): DpRect {
val dpRect = rect.toDpRect(density)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.yield
import org.w3c.dom.Element
import org.w3c.dom.HTMLCanvasElement
import org.w3c.dom.HTMLDivElement
import org.w3c.dom.HTMLElement
import org.w3c.dom.ShadowRoot
import org.w3c.dom.events.Event
Expand Down Expand Up @@ -76,26 +77,26 @@ internal interface OnCanvasTests {
(getContainer() as CanReplaceChildren).replaceChildren()
}

/*
<container>
<positioning_container>
<shadow_container.shadow>
<style/>
<app_container>
<canvas/>
<a11y_container/>
</app_container>
</shadow_container>
<interopContainer/>
<positioning_container/>
</container>
*/
private fun getContainer() = document.getElementById(containerId) ?: error("failed to get canvas with id ${containerId}")

private fun getAppRoot() = getShadowRoot().children[1] as HTMLElement

fun getA11YContainer(): HTMLElement? {
return if (getAppRoot().children.length < 3) {
null
} else {
// The expected order is: canvas, interop container <div>, a11y container <div>
getAppRoot().children[2] as HTMLElement
}
}

fun getShadowRoot(): ExtendedShadowRoot =
(getContainer().shadowRoot as? ExtendedShadowRoot) ?: error("failed to get shadowRoot")

fun getCanvas(): HTMLCanvasElement {
val canvas = (getShadowRoot().querySelector("canvas") as? HTMLCanvasElement) ?: error("failed to get canvas")
return canvas
}
private fun getPositioningContainer() = getContainer().children[0] ?: error("failed to get positioning container")
fun getShadowRoot() = (getPositioningContainer().children[0]?.shadowRoot as? ExtendedShadowRoot) ?: error("failed to get shadowRoot")
private fun getAppRoot() = getShadowRoot().children[1] as? HTMLElement ?: error("failed to get app root")
fun getCanvas() = getAppRoot().children[0] as? HTMLCanvasElement ?: error("failed to get canvas")
fun getA11YContainer() = getAppRoot().children[1] as? HTMLDivElement

suspend fun createComposeWindow(
configure: ComposeViewportConfiguration.() -> Unit = {},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -424,9 +424,8 @@ class CfWA11YTest : OnCanvasTests {
val appContainer = getCanvas().parentElement as HTMLElement

assertTrue(appContainer.isConnected)
assertEquals(2, appContainer.children.length)
assertEquals(1, appContainer.children.length)
assertEquals(getCanvas(), appContainer.children[0])
assertEquals("DIV", appContainer.children[1]!!.tagName) // interop container
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import androidx.compose.ui.unit.dp
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertNotEquals
import kotlin.test.assertNotNull
import kotlin.test.assertNull
import kotlin.test.assertTrue
Expand Down Expand Up @@ -64,13 +65,13 @@ class WebInteropTest : OnCanvasTests {
}


var div = getShadowRoot().getElementById(divId) as HTMLDivElement?
var div = document.getElementById(divId) as HTMLDivElement?
assertNull(div)

showDiv.value = true
awaitIdle()

div = getShadowRoot().getElementById(divId) as HTMLDivElement?
div = document.getElementById(divId) as HTMLDivElement?
assertNotNull(div)
assertTrue(div.isConnected)
assertEquals("Text1", div.innerText)
Expand Down Expand Up @@ -166,16 +167,17 @@ class WebInteropTest : OnCanvasTests {

@Test
fun hitPath() = runApplicationTest {
val divId = "interop_div"
createComposeWindow {
Box(modifier = Modifier.size(100.dp), contentAlignment = Alignment.Center) {
TestInteropView(Modifier.size(30.dp), "div")
TestInteropView(Modifier.size(30.dp), divId)
}
}
awaitIdle()

assertEquals("CANVAS", getShadowRoot().elementFromPoint(10.0, 10.0).tagName)
assertEquals("DIV", getShadowRoot().elementFromPoint(50.0, 50.0).tagName)
assertEquals("CANVAS", getShadowRoot().elementFromPoint(90.0, 90.0).tagName)
assertNotEquals(divId, document.elementFromPoint(10.0, 10.0)?.id)
assertEquals(divId, document.elementFromPoint(50.0, 50.0)?.id)
assertNotEquals(divId, document.elementFromPoint(90.0, 90.0)?.id)
}
}

Expand Down
Loading