From 688eaa5716da407c930c91e4689936e7442a6115 Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 27 Jul 2026 10:14:54 +0200 Subject: [PATCH] fix(preview): keep the panel out of the tab order while closed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause of the "Shift+Tab opens the preview" report on both Windows and macOS: xterm.js's evaluateKeyboardEvent deliberately leaves Shift+Tab uncancelled (cancel: false only on that branch — plain Tab, Enter, and Escape all set it true) so app-level Shift+Tab chords, like Claude Code's own plan-mode toggle, still reach the pty. That also means the browser's native reverse-tab-order focus navigation runs uninterrupted. The file-preview panel stays in the DOM (translated off-screen) rather than display:none so its slide-in CSS transition can animate, which left its Copy/Edit/Close buttons real, always-focusable elements sitting right before the terminal panes in DOM order — exactly where Shift+Tab from the terminal would land. Confirmed live: before this fix, real Shift+Tab moved focus onto #file-preview-close; after, it skips straight to the next earlier focusable element (the window's own close button). Buttons now start tabindex="-1" and only become tab-reachable while the panel is actually open, so Shift+Tab from the terminal can never land on them (and can't scroll the off-screen panel into view either). Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01GHgSVKPsaWRfuJMo1rc2AN --- index.html | 6 +++--- src/main.ts | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/index.html b/index.html index 9c5dfda..c8cfa56 100644 --- a/index.html +++ b/index.html @@ -123,13 +123,13 @@

- - -
diff --git a/src/main.ts b/src/main.ts index 7175c3a..4e9c20b 100644 --- a/src/main.ts +++ b/src/main.ts @@ -3428,6 +3428,23 @@ function showLinkToast(text: string) { const filePreviewModal = $("#file-preview-modal"); const filePreviewTitle = $("#file-preview-title"); const filePreviewBody = $("#file-preview-body"); +// The panel stays in the DOM (translated off-screen) rather than +// display:none, so its CSS slide transition can animate — but that also +// keeps its buttons in the page's normal tab order while "closed". xterm.js +// deliberately leaves Shift+Tab uncancelled (so app-level Shift+Tab chords +// like Claude Code's plan-mode toggle still reach the pty), which means the +// browser's native reverse-tab-order focus navigation runs — and can land +// right here, scrolling the off-screen panel into view. tabindex="-1" while +// closed removes it from that path entirely; restored while open so the +// panel itself is still keyboard-navigable. +const filePreviewFocusables = [ + $("#file-preview-copy"), + $("#file-preview-edit"), + $("#file-preview-close"), +]; +function setFilePreviewFocusable(focusable: boolean) { + for (const el of filePreviewFocusables) el.tabIndex = focusable ? 0 : -1; +} // Tracks the live Three.js scene/renderer for the currently open model // preview (if any) so it can be torn down before the next preview replaces @@ -3440,6 +3457,7 @@ function disposeModelPreview() { function closeFilePreview() { filePreviewModal.classList.remove("open"); + setFilePreviewFocusable(false); disposeModelPreview(); setPreviewEditing(false); // Release state immediately rather than leaving the last-viewed file @@ -3569,6 +3587,7 @@ async function openFilePreview(raw: string, cwd: string | null) { filePreviewBody.className = "file-preview-body plain"; filePreviewBody.textContent = "…"; filePreviewModal.classList.add("open"); + setFilePreviewFocusable(true); // Move focus off the terminal and onto the panel: xterm's own keydown // handler treats Escape as a key it must own (cancels the browser event // unconditionally), which stops it from ever reaching our document-level