diff --git a/apps/web/src/components/files/FileBrowserPanel.browser.test.tsx b/apps/web/src/components/files/FileBrowserPanel.browser.test.tsx index 75e55cc46f4c4..1c0e1bc914bed 100644 --- a/apps/web/src/components/files/FileBrowserPanel.browser.test.tsx +++ b/apps/web/src/components/files/FileBrowserPanel.browser.test.tsx @@ -1,7 +1,9 @@ import { EnvironmentId, type ProjectEntry } from "@t3tools/contracts"; import { describe, expect, it, vi } from "vite-plus/test"; +import { page, userEvent } from "vite-plus/test/browser"; import { COMPOSER_MENTION_DRAG_TYPE } from "~/components/chat/composerMentionDrag"; +import { dismissContextMenu } from "~/contextMenuFallback"; import { render } from "../../../test/render"; import FileBrowserPanel from "./FileBrowserPanel"; @@ -43,6 +45,43 @@ function findTreeRow(path: string): HTMLElement | null { } describe("FileBrowserPanel", () => { + it("copies with the pointer and returns to the row after keyboard dismissal", async () => { + await using _screen = await render( + {}} + workspaceMutationId={null} + />, + ); + using _menu = { [Symbol.dispose]: () => dismissContextMenu() }; + using clipboard = vi.spyOn(navigator.clipboard, "writeText").mockResolvedValue(undefined); + await vi.waitFor(() => expect(findTreeRow("README.md")).not.toBeNull()); + findTreeRow("README.md")!.dispatchEvent( + new MouseEvent("contextmenu", { + bubbles: true, + cancelable: true, + composed: true, + button: 2, + }), + ); + const action = page.getByRole("menuitem", { name: "Copy mention", exact: true }); + await expect.element(action).toBeVisible(); + await userEvent.click(action); + await vi.waitFor(() => expect(clipboard).toHaveBeenCalledWith("[README.md](README.md)")); + + const row = findTreeRow("README.md")!; + await userEvent.keyboard("{Tab}"); + row.focus(); + await userEvent.keyboard("{Shift>}{F10}{/Shift}"); + await expect.element(action).toHaveFocus(); + await userEvent.keyboard("{Escape}"); + await vi.waitFor(() => expect((row.getRootNode() as ShadowRoot).activeElement).toBe(row)); + }); + it("dragging a file row carries a composer mention without opening it, and dragend releases it", async () => { const onOpenFile = vi.fn(); await using _screen = await render( diff --git a/apps/web/src/components/files/FileBrowserPanel.tsx b/apps/web/src/components/files/FileBrowserPanel.tsx index 147ed7614c704..b02baf31b0acf 100644 --- a/apps/web/src/components/files/FileBrowserPanel.tsx +++ b/apps/web/src/components/files/FileBrowserPanel.tsx @@ -192,6 +192,8 @@ export default function FileBrowserPanel({ : { x: anchorRect.left, y: anchorRect.bottom }; const fileTarget = { environmentId, filePath: relativePath, workspaceRoot: cwd }; const fileMenuItems = fileContextMenu.buildItems(fileTarget); + // The app menu owns focus; the tree's outside-click handler would dismiss it before click. + context.close({ restoreFocus: !pointerIsFresh }); void settlePromise(async () => { const clicked = await api.contextMenu.show( [ @@ -246,7 +248,6 @@ export default function FileBrowserPanel({ } } }).then((result) => { - context.close(); if (!AsyncResult.isSuccess(result)) throw Cause.squash(result.cause); }); }, diff --git a/apps/web/src/contextMenuFallback.ts b/apps/web/src/contextMenuFallback.ts index b63fc5b3ff7a2..a3e760ddc58ab 100644 --- a/apps/web/src/contextMenuFallback.ts +++ b/apps/web/src/contextMenuFallback.ts @@ -245,8 +245,11 @@ export function showContextMenuFallback( ): Promise { dismissContextMenu(); return new Promise((resolve) => { - const previouslyFocusedElement = - document.activeElement instanceof HTMLElement ? document.activeElement : null; + let focusedElement = document.activeElement; + while (focusedElement?.shadowRoot?.activeElement) { + focusedElement = focusedElement.shadowRoot.activeElement; + } + const previouslyFocusedElement = focusedElement instanceof HTMLElement ? focusedElement : null; const menuStack: HTMLDivElement[] = []; const submenuTriggerStack: Array = []; let isDisposed = false;