diff --git a/packages/app/component-tests/browser-pane-restore.spec.ts b/packages/app/component-tests/browser-pane-restore.spec.ts index d1dffe25fd2..34d8ed60bb9 100644 --- a/packages/app/component-tests/browser-pane-restore.spec.ts +++ b/packages/app/component-tests/browser-pane-restore.spec.ts @@ -147,6 +147,44 @@ story("closes a blank browser tab from its address field without closing the ses await expect(root.getByTestId("session-state")).toHaveText("open") }) +story("toggles the element picker from the shortcut only inside the browser pane", async ({ page }) => { + const root = page.getByTestId("browser-region-fixture") + const outside = root.getByRole("button", { name: "Beta", exact: true }) + await expect(root.getByText("Registrations: 1", { exact: true })).toBeVisible() + await outside.click() + await expect(root.getByText("Registrations: 2", { exact: true })).toBeVisible() + + await root.getByRole("button", { name: "Blank page", exact: true }).click() + const inspect = root.getByRole("button", { name: "Select an element to comment on", exact: true }) + const address = root.getByRole("combobox", { name: "Browser address", exact: true }) + await expect(inspect).toBeDisabled() + await address.click() + await address.press("ControlOrMeta+Shift+c") + await expect(root.getByTestId("browser-inspects")).toHaveText("[]") + + await root.getByRole("button", { name: "First inventory", exact: true }).click() + await expect(inspect).toBeEnabled() + + await outside.focus() + await outside.press("ControlOrMeta+Shift+c") + await expect(inspect).toHaveAttribute("aria-pressed", "false") + await expect(root.getByTestId("browser-inspects")).toHaveText("[]") + + await address.click() + await expect(address).toBeFocused() + await address.press("ControlOrMeta+Shift+c") + await expect(inspect).toHaveAttribute("aria-pressed", "true") + await expect(root.getByTestId("browser-inspects")).toHaveText( + '[{"tabID":"tab_33333333-3333-3333-3333-333333333333","enabled":true}]', + ) + + await address.press("ControlOrMeta+Shift+c") + await expect(inspect).toHaveAttribute("aria-pressed", "false") + await expect(root.getByTestId("browser-inspects")).toHaveText( + '[{"tabID":"tab_33333333-3333-3333-3333-333333333333","enabled":true},{"tabID":"tab_33333333-3333-3333-3333-333333333333","enabled":false}]', + ) +}) + story("keeps a restored browser tab selected and undrawn until the desktop's first inventory", async ({ page }) => { const root = page.getByTestId("browser-region-fixture") const tabs = root.getByRole("tab") diff --git a/packages/gui-extensions/src/browser/chromium.ts b/packages/gui-extensions/src/browser/chromium.ts index 4e665fa8c87..e4d272cd574 100644 --- a/packages/gui-extensions/src/browser/chromium.ts +++ b/packages/gui-extensions/src/browser/chromium.ts @@ -7,6 +7,7 @@ import { createCdp, abortError, waitFor } from "./cdp" import { createBrowserFiles } from "./files" import { createDiagnostics } from "./diagnostics" import { loadIcon } from "./icon" +import { pageLetter } from "./keys" import { createProfiling, type Recording } from "./profiling" import type { BrowserNetwork } from "./network" import { @@ -192,7 +193,7 @@ export function createBrowserPage( if (input.alt || !(process.platform === "darwin" ? input.meta : input.control)) return // The same chord as Chromium DevTools' element picker. - if (input.shift && input.code === "KeyC") { + if (input.shift && pageLetter(input) === "c") { event.preventDefault() void toggleInspect(!inspecting) @@ -200,7 +201,7 @@ export function createBrowserPage( } // The system browser's address shortcut. The app's own focus moves to the address field. - if (!input.shift && input.code === "KeyL") { + if (!input.shift && pageLetter(input) === "l") { event.preventDefault() options.address?.() diff --git a/packages/gui-extensions/src/browser/i18n/en.ts b/packages/gui-extensions/src/browser/i18n/en.ts index 96437ad612f..39967cc6413 100644 --- a/packages/gui-extensions/src/browser/i18n/en.ts +++ b/packages/gui-extensions/src/browser/i18n/en.ts @@ -14,7 +14,6 @@ export default { "refused.credentials": "Addresses with a user name or password can't open here.", inspect: "Select an element to comment on", "inspect.active": "Click an element in the page to comment on it. Press Escape to cancel.", - "inspect.pageShortcut": "While the page has focus", "action.stop": "Stop", replaced: "Browser control moved to another desktop window.", unsupported: "This desktop app does not support the browser pane.", diff --git a/packages/gui-extensions/src/browser/keys.test.ts b/packages/gui-extensions/src/browser/keys.test.ts new file mode 100644 index 00000000000..94285bf52a7 --- /dev/null +++ b/packages/gui-extensions/src/browser/keys.test.ts @@ -0,0 +1,15 @@ +import { expect, test } from "bun:test" +import { pageLetter } from "./keys" + +test.each([ + { layout: "QWERTY", input: { key: "C", code: "KeyC" }, letter: "c" }, + // Dvorak types C on the physical I key; Chromium reports the physical key as the code. + { layout: "Dvorak", input: { key: "C", code: "KeyI" }, letter: "c" }, + { layout: "Dvorak", input: { key: "l", code: "KeyP" }, letter: "l" }, + // Dvorak's J sits where QWERTY has C, so it must not open the picker. + { layout: "Dvorak", input: { key: "J", code: "KeyC" }, letter: "j" }, + { layout: "Russian", input: { key: "\u0421", code: "KeyC" }, letter: "c" }, + { layout: "QWERTY", input: { key: "=", code: "Equal" }, letter: "=" }, +])("$layout $input.key on $input.code is $letter", (row) => { + expect(pageLetter(row.input)).toBe(row.letter) +}) diff --git a/packages/gui-extensions/src/browser/keys.ts b/packages/gui-extensions/src/browser/keys.ts new file mode 100644 index 00000000000..7aaecf9cc8a --- /dev/null +++ b/packages/gui-extensions/src/browser/keys.ts @@ -0,0 +1,11 @@ +/** + * The letter a page key press names, as the app's keybinds read it: the layout's character, so Dvorak's C is "c" + * although it sits on the physical I key. A layout that types no Latin letter falls back to the physical key. + */ +export function pageLetter(input: Pick) { + const key = input.key.toLowerCase() + + if (/^[a-z]$/.test(key) || !/^Key[A-Z]$/.test(input.code)) return key + + return input.code.slice(3).toLowerCase() +} diff --git a/packages/gui-extensions/src/browser/panel.fixture.tsx b/packages/gui-extensions/src/browser/panel.fixture.tsx index f44b6728464..d9fa7f46c3e 100644 --- a/packages/gui-extensions/src/browser/panel.fixture.tsx +++ b/packages/gui-extensions/src/browser/panel.fixture.tsx @@ -618,6 +618,8 @@ type RegionFixtureState = { blank: boolean /** Native browser tab close requests, by tab id. */ closes: string[] + /** Native element picker requests, by tab id and state. */ + inspects: { tabID: string; enabled: boolean }[] } // Component-test fixture: the real side region over the real browser and file extensions, with the host's @@ -645,6 +647,7 @@ export function mountBrowserRegion(input: RegionHost) { closed: false, blank: false, closes: [], + inspects: [], }) // The file tree's state, Changes or All files, as the file extension stores it. @@ -813,7 +816,12 @@ export function mountBrowserRegion(input: RegionHost) { listener({ binding: value.binding, event: { type: "state", state: { tabs: [], focusedTabID: null } } }), ) }, - inspect: async () => undefined, + inspect: async (value) => { + setStore("inspects", (items) => [...items, { tabID: value.tabID, enabled: value.enabled }]) + listeners.forEach((listener) => + listener({ binding: value.binding, event: { type: "inspect", tabID: value.tabID, active: value.enabled } }), + ) + }, highlight: async () => undefined, zoom: async () => undefined, site: async () => ({ cookies: 0 }), @@ -966,7 +974,14 @@ export function mountBrowserRegion(input: RegionHost) {

Restored side strip