Fix active-pane detection via activeElement polling

After exhausting event-based approaches that all failed in WebView2:
- per-leaf onpointerdown: xterm.js stopPropagation
- document-capture pointerdown: only first event ever delivered
- document-capture mousedown/click: never delivered at all
- document-capture focusin: silently fails
- term.onFocus: no such xterm.js API

The bulletproof fallback: poll document.activeElement every 250ms
and call orch.setActive on its closest [data-leaf-id] ancestor.
No DOM events involved. Verified working with automation: clicking
pane 2 turns its border blue, clicking pane 1 moves the border to
pane 1, etc.

XtermPane gained an onFocus prop (still wired through LeafPane) as a
secondary signal that might fire in some configurations, but the
polling is the actual fix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
megaproxy 2026-05-22 15:43:30 +01:00
parent 4fd613438c
commit f5f788652e
3 changed files with 38 additions and 15 deletions

View file

@ -184,24 +184,29 @@
return () => window.removeEventListener("keydown", onKey, true);
});
// ---- Document-level active-pane detector --------------------------------
// xterm.js calls `stopPropagation` on pointerdown inside terminals, so a
// per-leaf `onpointerdown` never fires for body clicks. A document-level
// CAPTURE-phase listener fires before xterm.js can intercept, then finds
// the nearest `data-leaf-id` ancestor to know which pane was clicked.
// Toolbar buttons also pass through (they're outside the xterm container,
// their own onclick still fires in the bubble phase afterwards).
// ---- Active-pane detector via active-element polling --------------------
// Tried (and failed in WebView2): per-leaf onpointerdown (xterm blocks
// propagation), document-capture pointerdown (Webview2 only delivers the
// first one then nothing), document-capture focusin (also silently fails),
// xterm.js term.onFocus (no such API), textarea focus listener (race).
//
// Polling document.activeElement is the only thing left that's bulletproof
// — no events involved at all. 250ms is fast enough to feel instant when
// clicking and cheap enough to not show up on a CPU profile.
$effect(() => {
function onAnyPointerDown(e: PointerEvent) {
const t = e.target as Element | null;
if (!t) return;
const leafEl = t.closest("[data-leaf-id]");
let lastLeafId: string | null = null;
const interval = window.setInterval(() => {
const el = document.activeElement;
if (!el) return;
const leafEl = el.closest("[data-leaf-id]");
if (!leafEl) return;
const id = leafEl.getAttribute("data-leaf-id");
if (id) orch.setActive(id);
}
document.addEventListener("pointerdown", onAnyPointerDown, true);
return () => document.removeEventListener("pointerdown", onAnyPointerDown, true);
if (id && id !== lastLeafId) {
lastLeafId = id;
orch.setActive(id);
}
}, 250);
return () => clearInterval(interval);
});
// ---- preset layouts ------------------------------------------------------