diff --git a/apps/web/e2e/help-tip.spec.ts b/apps/web/e2e/help-tip.spec.ts new file mode 100644 index 000000000..8b8c8cb1b --- /dev/null +++ b/apps/web/e2e/help-tip.spec.ts @@ -0,0 +1,36 @@ +import { expect, test } from "@playwright/test"; + +import { expectManagementBoundary, openConsole, writes } from "./console"; + +test.afterEach(async ({ request }) => expectManagementBoundary(request)); + +for (const activation of ["keyboard", "pinned", "hover"] as const) { + test(`Escape dismisses ${activation} help before closing an edited node`, async ({ page, request }) => { + await openConsole(page, request, "nodes?id=node-local"); + const open = page.getByRole("button", { name: "Edit node", exact: true }); + await open.click(); + const edit = page.getByRole("dialog", { name: "Edit node" }); + const name = edit.getByLabel("Name", { exact: true }); + await name.fill("Unsubmitted node name"); + + const help = edit.getByRole("button", { name: "Help", exact: true }); + if (activation === "keyboard") await page.keyboard.press("Tab"); + else if (activation === "hover") await help.hover(); + else await help.click(); + const focused = activation === "hover" ? name : help; + await expect(focused).toBeFocused(); + await expect(page.locator(".help-tip-popover")).toBeVisible(); + + await page.keyboard.press("Escape"); + await expect(page.locator(".help-tip-popover")).toHaveCount(0); + await expect(edit).toBeVisible(); + await expect(name).toHaveValue("Unsubmitted node name"); + await expect(focused).toBeFocused(); + await expect(help).toHaveAttribute("aria-expanded", "false"); + + await page.keyboard.press("Escape"); + await expect(edit).toBeHidden(); + await expect(open).toBeFocused(); + expect(await writes(request)).toEqual([]); + }); +} diff --git a/apps/web/src/components/console-ui.tsx b/apps/web/src/components/console-ui.tsx index f821148f1..81ae49669 100644 --- a/apps/web/src/components/console-ui.tsx +++ b/apps/web/src/components/console-ui.tsx @@ -41,9 +41,17 @@ export function HelpTip({ children, label, id: fixedId }: { children: ReactNode; useEffect(() => { if (!position) return; const close = () => hide(); + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key !== "Escape") return; + event.stopPropagation(); + hide(); + }; + // A hovered tip can be open while focus remains in a dialog's input. + document.addEventListener("keydown", handleKeyDown, true); window.addEventListener("scroll", close, true); window.addEventListener("resize", close); return () => { + document.removeEventListener("keydown", handleKeyDown, true); window.removeEventListener("scroll", close, true); window.removeEventListener("resize", close); }; @@ -63,7 +71,6 @@ export function HelpTip({ children, label, id: fixedId }: { children: ReactNode; onFocus={show} onBlur={hide} onClick={() => { if (position && pinned) hide(); else { show(); setPinned(true); } }} - onKeyDown={(event) => { if (event.key === "Escape") hide(); }} >