Skip to content

Commit 9cb59bf

Browse files
jnunemakerclaudemaria-rcks
authored
fix(web): mod+L focuses the browser address bar from a focused page (#22328)
Co-authored-by: Claude Opus 5.5 <[email protected]> Co-authored-by: maria-rcks <[email protected]>
1 parent 08a40a9 commit 9cb59bf

6 files changed

Lines changed: 82 additions & 6 deletions

File tree

‎apps/desktop/src/preview/Manager.test.ts‎

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -619,7 +619,8 @@ describe("PreviewManager", () => {
619619
withManager((manager) =>
620620
Effect.gen(function* () {
621621
const send = vi.fn();
622-
const host = { isDestroyed: () => false, send };
622+
const focus = vi.fn();
623+
const host = { isDestroyed: () => false, send, focus };
623624
const older = makeFaviconWebContents({ id: 42 });
624625
const newer = makeFaviconWebContents({ id: 43 });
625626
for (const tab of [older, newer]) Object.assign(tab.webContents, { hostWebContents: host });
@@ -648,6 +649,22 @@ describe("PreviewManager", () => {
648649
expect(preventDefault).toHaveBeenCalledOnce();
649650
expect(send).toHaveBeenCalledExactlyOnceWith("desktop:menu-action", "view.reopenClosed");
650651

652+
yield* manager.setForwardedShortcuts([
653+
{ command: "preview.focusUrl", shortcut: parseKeybindingShortcut("mod+l")! },
654+
]);
655+
beforeInput({ preventDefault } as never, { ...key, key: "l", shift: false } as never);
656+
expect(focus).toHaveBeenCalledOnce();
657+
expect(send).toHaveBeenLastCalledWith("desktop:menu-action", "preview.focusUrl");
658+
expect(focus.mock.invocationCallOrder[0]).toBeLessThan(send.mock.invocationCallOrder[1]!);
659+
beforeInput(
660+
{ preventDefault } as never,
661+
{ ...key, key: "l", shift: false, isAutoRepeat: true } as never,
662+
);
663+
expect(focus).toHaveBeenCalledOnce();
664+
expect(send).toHaveBeenCalledTimes(2);
665+
send.mockClear();
666+
yield* manager.setForwardedShortcuts([{ command: "view.reopenClosed", shortcut }]);
667+
651668
Object.assign(newer.webContents, { focusedFrame: null });
652669
const agentInput = newer.listeners.get("before-input-event")!;
653670
const agentPreventDefault = vi.fn();
@@ -663,7 +680,7 @@ describe("PreviewManager", () => {
663680
{ ...key, key: "T", shift: true } as never,
664681
);
665682
expect(agentPreventDefault).not.toHaveBeenCalled();
666-
expect(send).toHaveBeenCalledOnce();
683+
expect(send).not.toHaveBeenCalled();
667684
}),
668685
),
669686
);

‎apps/desktop/src/preview/Manager.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1478,7 +1478,8 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function
14781478
if (!input.isAutoRepeat) {
14791479
if (
14801480
forwarded.command === "workspace.focusNext" ||
1481-
forwarded.command === "workspace.focusPrevious"
1481+
forwarded.command === "workspace.focusPrevious" ||
1482+
forwarded.command === "preview.focusUrl"
14821483
) {
14831484
// DOM focus in the renderer cannot by itself release the guest's native keyboard focus.
14841485
host.focus();

‎apps/web/src/components/ReopenClosedViewShortcut.browser.test.tsx‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,4 +265,11 @@ describe("root reopen shortcut", () => {
265265
expect(useClosedViewStore.getState().entries).toEqual([]);
266266
},
267267
);
268+
it("forwards address-bar focus from a focused desktop page", async () => {
269+
setup();
270+
await using _shortcut = await renderShortcut();
271+
expect(state.setShortcuts).toHaveBeenLastCalledWith(
272+
expect.arrayContaining([expect.objectContaining({ command: "preview.focusUrl" })]),
273+
);
274+
});
268275
});

‎apps/web/src/components/ReopenClosedViewShortcut.tsx‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,13 @@ import {
4343
} from "../threadRoutes";
4444
import { toastManager } from "./ui/toast";
4545

46-
const PREVIEW_FORWARDED_LAYOUT_COMMANDS = [
46+
const PREVIEW_FORWARDED_COMMANDS = [
4747
"workspace.focusNext",
4848
"workspace.focusPrevious",
4949
"sidebar.toggle",
5050
"rightPanel.toggle",
5151
"rightPanel.toggleMaximized",
52+
"preview.focusUrl",
5253
] as const;
5354

5455
const isGlobalPullRequests = (ref: ScopedThreadRef) =>
@@ -205,7 +206,7 @@ export function ReopenClosedViewShortcut() {
205206
// Guest browser processes miss app keydown listeners; desktop forwards the chords as menu actions.
206207
const commands = [
207208
...(hasHistory ? (["view.reopenClosed"] as const) : []),
208-
...PREVIEW_FORWARDED_LAYOUT_COMMANDS,
209+
...PREVIEW_FORWARDED_COMMANDS,
209210
];
210211
void preview
211212
.setForwardedShortcuts(

‎apps/web/src/components/preview/PreviewView.browser.test.tsx‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,49 @@ describe("PreviewView", () => {
286286
expect(mocks.addImage).not.toHaveBeenCalled();
287287
});
288288

289+
it("focuses and selects the address from forwarded desktop actions only while visible", async () => {
290+
setup();
291+
using stack = new DisposableStack();
292+
const previousBridge = window.desktopBridge;
293+
stack.defer(() => {
294+
if (previousBridge) window.desktopBridge = previousBridge;
295+
else delete window.desktopBridge;
296+
});
297+
const listeners = new Set<(action: string) => void>();
298+
window.desktopBridge = {
299+
getWindowZoomFactor: () => 1,
300+
onMenuAction: (listener: (action: string) => void) => {
301+
listeners.add(listener);
302+
return () => {
303+
listeners.delete(listener);
304+
};
305+
},
306+
} as NonNullable<typeof window.desktopBridge>;
307+
await using screen = await render(view());
308+
const address = page.getByRole("textbox");
309+
const input = address.element() as HTMLInputElement;
310+
const forward = () =>
311+
act(() => {
312+
listeners.forEach((listener) => listener("preview.focusUrl"));
313+
});
314+
315+
await forward();
316+
await expect.element(address).toHaveFocus();
317+
expect(input.selectionStart).toBe(0);
318+
expect(input.selectionEnd).toBe(input.value.length);
319+
await act(() => input.blur());
320+
await forward();
321+
await expect.element(address).toHaveFocus();
322+
323+
await act(() => input.blur());
324+
await screen.rerender(
325+
<PreviewView threadRef={TEST_THREAD_REF} tabId="tab-1" visible={false} />,
326+
);
327+
await forward();
328+
await expect.element(address).not.toHaveFocus();
329+
expect(listeners.size).toBe(0);
330+
});
331+
289332
it("focuses the address bar each time the focus-url shortcut fires", async () => {
290333
setup();
291334
await using _screen = await render(view());

‎apps/web/src/components/preview/PreviewView.tsx‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -860,7 +860,14 @@ export function PreviewView({
860860
// oxlint-disable-next-line t3code/no-unreasoned-state-sync -- subscribes to preview actions while the view is visible
861861
useEffect(() => {
862862
if (!visible) return;
863-
return subscribePreviewAction((action) => onPreviewAction(action));
863+
const unsubscribeAction = subscribePreviewAction((action) => onPreviewAction(action));
864+
const unsubscribeMenu = window.desktopBridge?.onMenuAction((action) => {
865+
if (action === "preview.focusUrl") onPreviewAction("focus-url");
866+
});
867+
return () => {
868+
unsubscribeAction();
869+
unsubscribeMenu?.();
870+
};
864871
}, [visible]);
865872

866873
return (

0 commit comments

Comments
 (0)