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

+30
View File
@@ -649,6 +649,36 @@ func (d *Driver) Hierarchy(ctx context.Context) (string, error) {
while (focusedElement && focusedElement.shadowRoot && focusedElement.shadowRoot.activeElement) {
focusedElement = focusedElement.shadowRoot.activeElement;
}
// Descending is still not enough on Compose for Web: it takes keystrokes on a
// 1px transparent input pinned to the caret, and that input is a SIBLING of
// the accessibility tree rather than a node in it. DOM focus therefore never
// reaches the semantics element carrying the test tag, so confirmFocus in
// internal/runner/runner.go saw an unnamed element hold focus after every
// focus tap and refused to type. Compose declares the caret's box in these
// custom properties, which the input inherits from the container that
// positions it, so the field being typed into is the innermost editable box
// that caret sits in.
const CARET_ORIGIN_PROPERTY = '--compose-internal-web-backing-input-left';
function fieldBehindTheCaret(caretInput) {
if (!caretInput || caretInput.tagName !== 'INPUT') return null;
if (!getComputedStyle(caretInput).getPropertyValue(CARET_ORIGIN_PROPERTY).trim()) return null;
const caret = caretInput.getBoundingClientRect();
const x = (caret.left + caret.right) / 2;
const y = (caret.top + caret.bottom) / 2;
let field = null;
let fieldArea = Infinity;
for (const candidate of editableSet) {
if (candidate === caretInput) continue;
const box = candidate.getBoundingClientRect();
const area = box.width * box.height;
if (area <= 0 || area >= fieldArea) continue;
if (x < box.left || x > box.right || y < box.top || y > box.bottom) continue;
field = candidate;
fieldArea = area;
}
return field;
}
focusedElement = fieldBehindTheCaret(focusedElement) || focusedElement;
function buildTree(el, isRoot) {
const rect = el.getBoundingClientRect();
// Every attribute the markup wrote, keyed as written, which is what attrs