Fix browser keyboard lifecycle regressions

This commit is contained in:
Mohamed Boudra
2026-07-14 14:27:21 +00:00
parent 52994373f7
commit 6aa0daaf22
5 changed files with 57 additions and 2 deletions

View File

@@ -17,6 +17,7 @@ import { resolveKeyboardFocusScope } from "@/keyboard/focus-scope";
import {
buildBrowserShortcutPolicy,
parseBrowserShortcutInput,
shouldPublishBrowserShortcutPolicy,
} from "@/keyboard/browser-shortcuts";
import type { KeyboardFocusScope, KeyboardShortcutPayload } from "@/keyboard/actions";
import {
@@ -220,6 +221,7 @@ export function useKeyboardShortcuts({
browserFocusRestoreElement?: HTMLElement | null;
}) => {
const store = useKeyboardShortcutsStore.getState();
const previousChordState = chordStateRef.current;
const result = resolveKeyboardShortcut({
event: input.event,
context: {
@@ -241,7 +243,13 @@ export function useKeyboardShortcuts({
});
chordStateRef.current = result.nextChordState;
if ("browserId" in input.event) {
if (
shouldPublishBrowserShortcutPolicy({
isBrowserInput: "browserId" in input.event,
previousChordState,
nextChordState: result.nextChordState,
})
) {
publishBrowserShortcutPolicy(result.nextChordState);
}

View File

@@ -1,5 +1,9 @@
import { describe, expect, it } from "vitest";
import { buildBrowserShortcutPolicy, parseBrowserShortcutInput } from "./browser-shortcuts";
import {
buildBrowserShortcutPolicy,
parseBrowserShortcutInput,
shouldPublishBrowserShortcutPolicy,
} from "./browser-shortcuts";
import { buildEffectiveBindings, resolveKeyboardShortcut } from "./keyboard-shortcuts";
describe("buildBrowserShortcutPolicy", () => {
@@ -171,6 +175,18 @@ describe("buildBrowserShortcutPolicy", () => {
});
});
describe("shouldPublishBrowserShortcutPolicy", () => {
it("restores the initial browser policy when a host key resets a pending chord", () => {
expect(
shouldPublishBrowserShortcutPolicy({
isBrowserInput: false,
previousChordState: { candidateIndices: [1], step: 1, timeoutId: null },
nextChordState: { candidateIndices: [], step: 0, timeoutId: null },
}),
).toBe(true);
});
});
describe("parseBrowserShortcutInput", () => {
it("normalizes browser shortcut input without losing its identity", () => {
expect(

View File

@@ -29,6 +29,16 @@ interface BrowserShortcutPolicyInput {
isDesktop: boolean;
}
export function shouldPublishBrowserShortcutPolicy(input: {
isBrowserInput: boolean;
nextChordState: ChordState;
previousChordState: ChordState;
}): boolean {
return (
input.isBrowserInput || (input.previousChordState.step > 0 && input.nextChordState.step === 0)
);
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}

View File

@@ -230,6 +230,24 @@ describe("BrowserKeyboard", () => {
expect(guest.sent).toHaveLength(3);
});
test("ignores guest lifecycle events after the host is destroyed", () => {
const { attach, keyboard } = createBrowserKeyboard();
const guest = new FakeBrowserContents(71);
const host = new FakeBrowserContents(72);
attach({ browserId: "browser-a", contents: guest, hostContents: host });
keyboard.publish(host.id, { prefixes: [] });
host.destroy();
guest.domReady();
expect(guest.sent).toEqual([
{
channel: "paseo:browser-keyboard-policy",
payload: { browserId: "browser-a", prefixes: [] },
},
]);
});
test("owns reserved shortcuts and leaves plain guest input contained", () => {
const { attach } = createBrowserKeyboard();
const guest = new FakeBrowserContents(81);

View File

@@ -203,6 +203,9 @@ export class BrowserKeyboard {
if (this.attachedGuestsByWebContentsId.get(webContentsId) !== guest) {
return null;
}
if (guest.hostContents.isDestroyed()) {
return null;
}
const registration = this.browserRegistry.getRegistrationForWebContents(webContentsId);
return registration?.hostWebContentsId === guest.hostContents.id ? registration : null;
}