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:
parent
4fd613438c
commit
f5f788652e
3 changed files with 38 additions and 15 deletions
|
|
@ -184,24 +184,29 @@
|
||||||
return () => window.removeEventListener("keydown", onKey, true);
|
return () => window.removeEventListener("keydown", onKey, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
// ---- Document-level active-pane detector --------------------------------
|
// ---- Active-pane detector via active-element polling --------------------
|
||||||
// xterm.js calls `stopPropagation` on pointerdown inside terminals, so a
|
// Tried (and failed in WebView2): per-leaf onpointerdown (xterm blocks
|
||||||
// per-leaf `onpointerdown` never fires for body clicks. A document-level
|
// propagation), document-capture pointerdown (Webview2 only delivers the
|
||||||
// CAPTURE-phase listener fires before xterm.js can intercept, then finds
|
// first one then nothing), document-capture focusin (also silently fails),
|
||||||
// the nearest `data-leaf-id` ancestor to know which pane was clicked.
|
// xterm.js term.onFocus (no such API), textarea focus listener (race).
|
||||||
// Toolbar buttons also pass through (they're outside the xterm container,
|
//
|
||||||
// their own onclick still fires in the bubble phase afterwards).
|
// 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(() => {
|
$effect(() => {
|
||||||
function onAnyPointerDown(e: PointerEvent) {
|
let lastLeafId: string | null = null;
|
||||||
const t = e.target as Element | null;
|
const interval = window.setInterval(() => {
|
||||||
if (!t) return;
|
const el = document.activeElement;
|
||||||
const leafEl = t.closest("[data-leaf-id]");
|
if (!el) return;
|
||||||
|
const leafEl = el.closest("[data-leaf-id]");
|
||||||
if (!leafEl) return;
|
if (!leafEl) return;
|
||||||
const id = leafEl.getAttribute("data-leaf-id");
|
const id = leafEl.getAttribute("data-leaf-id");
|
||||||
if (id) orch.setActive(id);
|
if (id && id !== lastLeafId) {
|
||||||
}
|
lastLeafId = id;
|
||||||
document.addEventListener("pointerdown", onAnyPointerDown, true);
|
orch.setActive(id);
|
||||||
return () => document.removeEventListener("pointerdown", onAnyPointerDown, true);
|
}
|
||||||
|
}, 250);
|
||||||
|
return () => clearInterval(interval);
|
||||||
});
|
});
|
||||||
|
|
||||||
// ---- preset layouts ------------------------------------------------------
|
// ---- preset layouts ------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@
|
||||||
onSpawn = undefined,
|
onSpawn = undefined,
|
||||||
onInput = undefined,
|
onInput = undefined,
|
||||||
onDataReceived = undefined,
|
onDataReceived = undefined,
|
||||||
|
onFocus = undefined,
|
||||||
focusTrigger = 0,
|
focusTrigger = 0,
|
||||||
}: {
|
}: {
|
||||||
distro?: string;
|
distro?: string;
|
||||||
|
|
@ -31,6 +32,8 @@
|
||||||
onInput?: (dataB64: string) => void;
|
onInput?: (dataB64: string) => void;
|
||||||
/** Fired whenever output arrives from the PTY. Used for idle detection. */
|
/** Fired whenever output arrives from the PTY. Used for idle detection. */
|
||||||
onDataReceived?: () => void;
|
onDataReceived?: () => void;
|
||||||
|
/** Fired when xterm's textarea gains focus (i.e., user clicked here). */
|
||||||
|
onFocus?: () => void;
|
||||||
/** Increment to refocus the terminal programmatically (palette etc.). */
|
/** Increment to refocus the terminal programmatically (palette etc.). */
|
||||||
focusTrigger?: number;
|
focusTrigger?: number;
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
@ -112,6 +115,20 @@
|
||||||
onInput?.(b64);
|
onInput?.(b64);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// xterm.js's own focus event — fires when the hidden textarea gets focus
|
||||||
|
// (i.e., user clicked anywhere in the terminal). Most reliable signal
|
||||||
|
// for "user wants this pane active" — no DOM event traversal involved.
|
||||||
|
term.onSelectionChange(() => {}); // ensure the addon system is initialized; noop
|
||||||
|
if (typeof (term as unknown as { onFocus?: unknown }).onFocus === "function") {
|
||||||
|
(term as unknown as { onFocus: (cb: () => void) => void }).onFocus(() => {
|
||||||
|
onFocus?.();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Fallback: listen on the textarea element directly.
|
||||||
|
const ta = containerEl.querySelector(".xterm-helper-textarea");
|
||||||
|
if (ta) ta.addEventListener("focus", () => onFocus?.(), true);
|
||||||
|
}
|
||||||
|
|
||||||
// Re-fit on container resize; forward new size to the PTY.
|
// Re-fit on container resize; forward new size to the PTY.
|
||||||
ro = new ResizeObserver(() => {
|
ro = new ResizeObserver(() => {
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -214,6 +214,7 @@
|
||||||
}}
|
}}
|
||||||
onSpawn={onPaneSpawned}
|
onSpawn={onPaneSpawned}
|
||||||
onInput={onTerminalInput}
|
onInput={onTerminalInput}
|
||||||
|
onFocus={() => orch.setActive(leaf.id)}
|
||||||
{onDataReceived}
|
{onDataReceived}
|
||||||
{focusTrigger}
|
{focusTrigger}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue