fix(browser): keep Orca shortcuts working while an HTML document preview has focus (#41066)

* fix(browser): forward Orca shortcuts from focused HTML document previews

Workspace-document preview guests never installed the guest shortcut
forwarder that browser pages and plugin frames use, so once a preview
took focus Ctrl+Tab and the other window shortcuts stopped reaching
Orca. Install it in attachWorkspaceDocGuestPolicies, bound to the
preview's own host, with browserPage: false so browser-page chords stay
with the document, and dispose it with the rest of the doc policy.

* test(browser): pin Ctrl+Tab forwarding from a focused HTML document preview

* fix(browser): activate a document preview's split when it is clicked

A press inside the preview never reaches the host DOM, so forwarded
shortcuts such as Close acted on whichever split was active before.

* refactor(browser): read a document preview's page id from its guest policy

---------

Co-authored-by: agustin2606 <agustinvarela2606@gmail.com>
This commit is contained in:
Kelvin Amoaba
2026-11-11 07:40:31 +00:00
committed by GitHub
co-authored by agustin2606
parent e33a851d8e
commit 83ec1c09dd
3 changed files with 125 additions and 4 deletions
@@ -66,6 +66,12 @@ export function readDocPreviewGuestBoundGrantId(guest: Electron.WebContents): st
return previewGuests.get(guest)?.readBoundGrantId() ?? null
}
/** Browser page of the guest's live grant; null before the grant binds or once it is revoked. */
export function readDocPreviewGuestBoundPageId(guest: Electron.WebContents): string | null {
const grantId = readDocPreviewGuestBoundGrantId(guest)
return (grantId && getDocPreviewGrant(grantId)?.browserPageId) || null
}
/** URL of the guest's committed document; unlike getURL(), pushState cannot rewrite it. */
export function readDocPreviewGuestDocumentUrl(guest: Electron.WebContents): string {
return previewGuests.get(guest)?.readDocumentUrl() ?? ''
@@ -5,10 +5,11 @@ import { BrowserManagerGuestCleanup } from './browser-manager-guest-cleanup'
import { allowPageUnload } from '../../navigation/browser-guest-unload-policy'
import {
installDocPreviewGuestPolicy,
readDocPreviewGuestBoundGrantId
readDocPreviewGuestBoundPageId
} from '../../../document-preview/doc-preview-guest-policy'
import { getDocPreviewGrant } from '../../../document-preview/doc-preview-grant-registry'
import { setupGuestEscapeForwarding } from '../../input/shortcuts/browser-guest-escape-forwarding'
import { setupGuestShortcutForwarding } from '../../input/shortcuts/browser-guest-shortcut-forwarding'
import { forwardGuestMousePress } from '../../input/browser-guest-mouse-press'
import { installGuestDevToolsCloseShortcut } from '../../devtools/browser-guest-devtools-close-shortcut'
export abstract class BrowserManagerGuestPolicy extends BrowserManagerGuestCleanup {
@@ -92,15 +93,37 @@ export abstract class BrowserManagerGuestPolicy extends BrowserManagerGuestClean
host: Electron.WebContents
): void {
const disposeDocPolicy = installDocPreviewGuestPolicy(guest, host)
const resolveHost = (): Electron.WebContents | null => (host.isDestroyed() ? null : host)
const disposeEscapeForwarding = setupGuestEscapeForwarding({
guest,
resolveTarget: () => {
const grantId = readDocPreviewGuestBoundGrantId(guest)
const browserPageId = grantId ? getDocPreviewGrant(grantId)?.browserPageId : undefined
const browserPageId = readDocPreviewGuestBoundPageId(guest)
return browserPageId ? { browserPageId, renderer: host } : null
},
hasActiveGrabOp: (browserPageId) => this.hasActiveGrabOp(browserPageId)
})
// Why: a focused preview swallows keys, so Orca's window shortcuts (Ctrl+Tab, close tab) are
// forwarded to its own host as for plugin frames; browser-page chords stay with the document.
const disposeShortcutForwarding = setupGuestShortcutForwarding({
browserTabId: `workspace-doc:${guest.id}`,
guest,
resolveRenderer: resolveHost,
getKeybindings: () => this.settingsResolver?.().keybindings,
browserPage: false
})
// Why: a press in the preview never reaches the host DOM, so its split stays inactive and
// forwarded shortcuts like Close would act on another split (#22144). Presses only: previews
// have no history listener for the side buttons a release would send.
const reportPress = (_event: Electron.Event, mouse: Electron.MouseInputEvent): void => {
if (mouse.type !== 'mouseDown') {
return
}
const browserPageId = readDocPreviewGuestBoundPageId(guest)
if (browserPageId) {
forwardGuestMousePress(guest, browserPageId, resolveHost, mouse)
}
}
guest.on('before-mouse-event', reportPress)
const handleDestroyed = (): void => {
this.cleanupGuestPolicyAttachment(guest.id)
}
@@ -109,9 +132,11 @@ export abstract class BrowserManagerGuestPolicy extends BrowserManagerGuestClean
this.policyCleanupByGuestId.set(guest.id, () => {
disposeDocPolicy()
disposeEscapeForwarding()
disposeShortcutForwarding()
try {
guest.off('destroyed', handleDestroyed)
guest.off('will-prevent-unload', allowPageUnload)
guest.off('before-mouse-event', reportPress)
} catch {
// guest may already be destroyed
}
@@ -0,0 +1,90 @@
import { afterEach, describe, expect, it, vi, type Mock } from 'vitest'
vi.mock('electron', () => ({
app: { getPath: () => '/downloads' },
BrowserWindow: { fromWebContents: () => null },
clipboard: { writeText: vi.fn() },
shell: { openExternal: vi.fn() },
Menu: { buildFromTemplate: vi.fn() },
screen: { getCursorScreenPoint: () => ({ x: 0, y: 0 }) },
webContents: { fromId: () => null }
}))
vi.mock('../../popups/presentation/popup-origin-bar-window', () => ({
openPopupWithOriginBar: vi.fn()
}))
import { buildDocPreviewUrl } from '../../../../../shared/document-preview/doc-preview-scheme'
import {
mintDocPreviewGrant,
revokeAllDocPreviewGrants
} from '../../../document-preview/doc-preview-grant-registry'
import {
DestroyedGuestTestContents,
DestroyedGuestTestManager
} from './browser-manager-destroyed-guest-test-fixture'
describe('workspace document guest input', () => {
const guests: DestroyedGuestTestContents[] = []
function attachDocumentGuest(): {
guest: DestroyedGuestTestContents
send: Mock
} {
const grant = mintDocPreviewGrant({
owner: {
kind: 'runtime',
environmentId: 'env-1',
worktreeSelector: 'id:worktree-1',
worktreeRoot: '/repo'
},
root: '/repo',
entryRelativePath: 'page.html',
browserPageId: 'doc-page-1'
})
const url = buildDocPreviewUrl(grant.id, grant.entryRelativePath)
const guest = Object.assign(new DestroyedGuestTestContents(7), { getURL: () => url })
const send = vi.fn()
const host = Object.assign(new DestroyedGuestTestContents(5001), { send })
new DestroyedGuestTestManager().attachGuestPolicies(guest.asWebContents(), null, {
profile: 'workspace-doc',
host: host.asWebContents()
})
guests.push(guest)
return { guest, send }
}
afterEach(() => {
for (const guest of guests.splice(0)) {
guest.destroy()
}
revokeAllDocPreviewGrants()
})
it('forwards Ctrl+Tab from a focused document preview to its host window', () => {
const { guest, send } = attachDocumentGuest()
guest.emit(
'before-input-event',
{ preventDefault: vi.fn() },
{
type: 'keyDown',
key: 'Tab',
code: 'Tab',
control: true,
shift: false,
alt: false,
meta: false
}
)
expect(send.mock.calls).toEqual([['ui:ctrlTabKeyDown', { shiftKey: false }]])
})
it('reports a press in the preview so its split becomes active', () => {
const { guest, send } = attachDocumentGuest()
guest.emit('before-mouse-event', { preventDefault: vi.fn() }, { type: 'mouseDown', x: 1, y: 1 })
expect(send.mock.calls).toEqual([
['ui:browserPagePointerDown', { browserPageId: 'doc-page-1' }]
])
})
})