From 5e83ca0edd2b935fac1e3b7940583974aa9f3706 Mon Sep 17 00:00:00 2001 From: OrcaWin Date: Mon, 12 Oct 2026 16:58:22 -0700 Subject: [PATCH] fix(lint): move the doc preview guest-focus handoff into its own hook (#28110) HtmlDocPreview.tsx reached 403 lines on main (#27608 + #27875), over the 400-line limit for .tsx, so lint and the pre-commit hook fail on any branch that merges main. The focus handoff effect moves unchanged into use-doc-preview-guest-focus.ts. Co-authored-by: Orca Worker --- .../workspace-doc/HtmlDocPreview.tsx | 62 ++-------------- .../use-doc-preview-guest-focus.ts | 74 +++++++++++++++++++ 2 files changed, 82 insertions(+), 54 deletions(-) create mode 100644 src/renderer/src/components/browser-pane/workspace-doc/use-doc-preview-guest-focus.ts diff --git a/src/renderer/src/components/browser-pane/workspace-doc/HtmlDocPreview.tsx b/src/renderer/src/components/browser-pane/workspace-doc/HtmlDocPreview.tsx index 927dc0e8117..f197a6af72b 100644 --- a/src/renderer/src/components/browser-pane/workspace-doc/HtmlDocPreview.tsx +++ b/src/renderer/src/components/browser-pane/workspace-doc/HtmlDocPreview.tsx @@ -34,12 +34,11 @@ import { import { DocPreviewToolbar } from './doc-preview-toolbar' import { buildDocPreviewShareControl } from './doc-preview-artifact-share' import { useDocPreviewWebviewHistory } from './doc-preview-webview-history' +import { useDocPreviewGuestFocus } from './use-doc-preview-guest-focus' import { useDocPreviewGuestTools } from './use-doc-preview-guest-tools' type PreviewState = 'loading' | 'ready' | 'unavailable' -/** Frames a preview keeps offering focus to a guest that is still attaching. */ -const GUEST_FOCUS_FRAMES = 10 const MAX_ASSET_FAILURES = 50 export function HtmlDocPreview({ @@ -301,58 +300,13 @@ export function HtmlDocPreview({ .recordWorkspaceDocVisit({ kind: 'workspace-doc', worktreeId, filePath }, null) }, [filePath, worktreeId]) - // Why the guest is handed focus: a preview has no address bar to make the usual handoff, so a - // surfaced document would otherwise look active while its keyboard and link input land elsewhere. - useEffect(() => { - if (!holdsGuestFocus || state !== 'ready') { - return - } - let frameId = 0 - let attempts = 0 - let claimedOnly = false - const focusGuest = (): void => { - const webview = webviewRef.current - attempts += 1 - // Why a re-offer yields: it is a handoff for focus nothing else wanted, and the reader - // pressing a tab lands here first. Taking it back would fight them for the keyboard, which - // is what shut the tab strip while a preview was open. - if ( - claimedOnly && - document.activeElement !== document.body && - document.activeElement !== webview - ) { - return - } - try { - webview?.focus() - } catch { - // Why swallowed: WebViewElement.focus() reads null internals once the guest is destroyed. - return - } - // Why retried: the guest takes focus only once it is attached and laid out, a frame or two - // after it reports ready. - if (document.activeElement !== webview && attempts < GUEST_FOCUS_FRAMES) { - frameId = window.requestAnimationFrame(focusGuest) - } - } - const offerFocus = (yieldToOtherClaims: boolean): void => { - window.cancelAnimationFrame(frameId) - attempts = 0 - claimedOnly = yieldToOtherClaims - frameId = window.requestAnimationFrame(focusGuest) - } - // Why assertive: the reader just made this preview their surface, so the handoff is the point. - offerFocus(false) - // Why re-offered on the window's own focus: another app taking the front takes focus out of the - // guest, and coming back puts it on the embedder. Nothing hands it on, so the route out of the - // preview would stay shut until something remounted it. - const reofferFocus = (): void => offerFocus(true) - window.addEventListener('focus', reofferFocus) - return () => { - window.removeEventListener('focus', reofferFocus) - window.cancelAnimationFrame(frameId) - } - }, [holdsGuestFocus, previewId, remintCount, state]) + useDocPreviewGuestFocus({ + webviewRef, + holdsGuestFocus, + ready: state === 'ready', + previewId, + remintCount + }) // Why: a grant is pinned to the owner ids resolved when it was minted, so after a pairing or // SSH reconnect the old one reads nothing and reloading the guest would just refetch the diff --git a/src/renderer/src/components/browser-pane/workspace-doc/use-doc-preview-guest-focus.ts b/src/renderer/src/components/browser-pane/workspace-doc/use-doc-preview-guest-focus.ts new file mode 100644 index 00000000000..ae1edee0641 --- /dev/null +++ b/src/renderer/src/components/browser-pane/workspace-doc/use-doc-preview-guest-focus.ts @@ -0,0 +1,74 @@ +import { useEffect, type MutableRefObject } from 'react' + +/** Frames a preview keeps offering focus to a guest that is still attaching. */ +const GUEST_FOCUS_FRAMES = 10 + +/** + * Hands a ready preview's guest the keyboard. A preview has no address bar to make the usual + * handoff, so a surfaced document would otherwise look active while its keyboard and link input + * land elsewhere. `remintCount` re-runs the handoff when a re-mint replaces the guest. + */ +export function useDocPreviewGuestFocus({ + webviewRef, + holdsGuestFocus, + ready, + previewId, + remintCount +}: { + webviewRef: MutableRefObject + holdsGuestFocus: boolean + ready: boolean + previewId: string + remintCount: number +}): void { + useEffect(() => { + if (!holdsGuestFocus || !ready) { + return + } + let frameId = 0 + let attempts = 0 + let claimedOnly = false + const focusGuest = (): void => { + const webview = webviewRef.current + attempts += 1 + // Why a re-offer yields: it is a handoff for focus nothing else wanted, and the reader + // pressing a tab lands here first. Taking it back would fight them for the keyboard, which + // is what shut the tab strip while a preview was open. + if ( + claimedOnly && + document.activeElement !== document.body && + document.activeElement !== webview + ) { + return + } + try { + webview?.focus() + } catch { + // Why swallowed: WebViewElement.focus() reads null internals once the guest is destroyed. + return + } + // Why retried: the guest takes focus only once it is attached and laid out, a frame or two + // after it reports ready. + if (document.activeElement !== webview && attempts < GUEST_FOCUS_FRAMES) { + frameId = window.requestAnimationFrame(focusGuest) + } + } + const offerFocus = (yieldToOtherClaims: boolean): void => { + window.cancelAnimationFrame(frameId) + attempts = 0 + claimedOnly = yieldToOtherClaims + frameId = window.requestAnimationFrame(focusGuest) + } + // Why assertive: the reader just made this preview their surface, so the handoff is the point. + offerFocus(false) + // Why re-offered on the window's own focus: another app taking the front takes focus out of the + // guest, and coming back puts it on the embedder. Nothing hands it on, so the route out of the + // preview would stay shut until something remounted it. + const reofferFocus = (): void => offerFocus(true) + window.addEventListener('focus', reofferFocus) + return () => { + window.removeEventListener('focus', reofferFocus) + window.cancelAnimationFrame(frameId) + } + }, [holdsGuestFocus, previewId, ready, remintCount, webviewRef]) +}