fix(chrome): focus follows the caret to the field it types into

Compose for wasm never focuses the semantics node carrying the testTag.
It proxies keystrokes through a hidden 1px backing input that is a
sibling of the a11y tree, so the node the runner tapped never held focus
and confirmFocus refused to type into every Compose text field.

Focus is re-attributed to the smallest editable whose box holds the
caret's centre. Centre-point rather than full containment because the
caret's height comes from the text style and the field's from its layout
box, so a taller font would silently drop back to refusing.
This commit is contained in:
pj committed 2026-08-18 00:00:24 +05:30
1 parent 176b495245
commit 5fc91fdca1
3 files changed
+173

No files matched your search

@@ -0,0 +1,80 @@
<!doctype html>
<html id="page">
<head id="page-head">
<meta id="page-charset" charset="utf-8" />
<title id="page-title">compose backing input</title>
</head>
<body id="page-body">
<!-- Shaped like Compose for Web: keystrokes go through a 1px transparent
input pinned to the caret, a sibling of the accessibility tree rather
than a node in it, so DOM focus never lands on the semantics element
carrying the test tag. The custom properties are declared on the
container and read by the input, exactly as Compose writes them. -->
<div id="app"></div>
<script id="page-script">
const root = document.getElementById("app").attachShadow({ mode: "open" });
root.innerHTML = `
<div
id="caret-holder"
style="
position: absolute;
top: 0px;
left: 0px;
--compose-internal-web-backing-input-left: 0;
--compose-internal-web-backing-input-top: 0;
--compose-internal-web-backing-input-width: 1;
--compose-internal-web-backing-input-height: 17.578125;
"
>
<input
id="caret-input"
style="
position: absolute;
top: calc(var(--compose-internal-web-backing-input-top) * 1px);
left: calc(var(--compose-internal-web-backing-input-left) * 1px);
width: calc(var(--compose-internal-web-backing-input-width) * 1px);
height: calc(var(--compose-internal-web-backing-input-height) * 1px);
padding: 0;
color: transparent;
background: transparent;
caret-color: transparent;
border: none;
outline: none;
z-index: -1;
"
/>
</div>
<div id="a11y-root" role="presentation">
<div
id="EmailField"
role="textbox"
contenteditable="true"
style="position: absolute; left: 34px; top: 78px; width: 688px; height: 18px;"
></div>
<div
id="PasswordField"
role="textbox"
contenteditable="true"
style="position: absolute; left: 34px; top: 158px; width: 688px; height: 18px;"
></div>
</div>`;
const holder = root.getElementById("caret-holder");
const caret = root.getElementById("caret-input");
for (const field of root.querySelectorAll('[role="textbox"]')) {
field.addEventListener("mousedown", (event) => {
event.preventDefault();
const box = field.getBoundingClientRect();
holder.style.setProperty(
"--compose-internal-web-backing-input-left",
String(Math.round(event.clientX)),
);
holder.style.setProperty(
"--compose-internal-web-backing-input-top",
String(Math.round(box.top)),
);
caret.focus();
});
}
</script>
</body>
</html>