From 2a2d1b636d951b98c6508880a1292189ddd573dd Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Sat, 31 Oct 2026 20:28:02 +0800 Subject: [PATCH] fix(workbench): restore focus on a second Alt and keep closed menus closed --- src/workbench/MainMenu.interaction.test.tsx | 39 +++++++++++++++++++++ src/workbench/MainMenu.tsx | 15 ++++++-- 2 files changed, 51 insertions(+), 3 deletions(-) diff --git a/src/workbench/MainMenu.interaction.test.tsx b/src/workbench/MainMenu.interaction.test.tsx index f2b98b5fe..0d003e88b 100644 --- a/src/workbench/MainMenu.interaction.test.tsx +++ b/src/workbench/MainMenu.interaction.test.tsx @@ -156,6 +156,45 @@ describe("main menu presentation", () => { expect(editor).toHaveFocus(); }); + it("returns focus on a second lone Alt", async () => { + setStyle("merged"); + render( + <> + + + , + ); + await screen.findByRole("menubar"); + const editor = screen.getByRole("button", { name: "Editor" }); + editor.focus(); + alt(); + expect(titles().getByRole("menuitem", { name: "File" })).toHaveFocus(); + alt(); + expect(editor).toHaveFocus(); + alt(); + fireEvent.keyDown(document.activeElement!, { key: "Escape" }); + expect(editor).toHaveFocus(); + }); + + it("does not reopen a title's menu after the titles collapse and return", async () => { + setStyle("merged"); + render(); + await screen.findByRole("menubar"); + fireEvent.click(titles().getByRole("menuitem", { name: "Edit" })); + expect(await screen.findByRole("menu", { name: "Edit" })).toBeInTheDocument(); + act(() => { + available = 100; + resize(); + }); + expect(screen.queryByRole("menu")).toBeNull(); + act(() => { + available = 500; + resize(); + }); + expect(screen.getByRole("menubar")).toBeInTheDocument(); + expect(screen.queryByRole("menu")).toBeNull(); + }); + it("falls back to a hamburger popup when narrow and restores titles when space returns", async () => { available = 100; setStyle("merged"); diff --git a/src/workbench/MainMenu.tsx b/src/workbench/MainMenu.tsx index 608dd56ed..f9adf94a3 100644 --- a/src/workbench/MainMenu.tsx +++ b/src/workbench/MainMenu.tsx @@ -122,7 +122,13 @@ function MainMenuBar({ merged, onExpandedChange }: MainMenuProps & { merged: boo // Measuring the allocated slot includes all neighbouring toolbar/caption controls. useLayoutEffect(() => { if (!merged || !slot.current || !measure.current) return; - const update = () => setFits(menus.length > 0 && slot.current!.clientWidth >= measure.current!.scrollWidth); + const update = () => { + const next = menus.length > 0 && slot.current!.clientWidth >= measure.current!.scrollWidth; + setFits(next); + // Each presentation's menu closes with it, and must not reopen when it returns. + if (next) setExpanded(false); + else setOpen(null); + }; const observer = new ResizeObserver(update); observer.observe(slot.current); observer.observe(measure.current); @@ -161,8 +167,11 @@ function MainMenuBar({ merged, onExpandedChange }: MainMenuProps & { merged: boo close(); if (persistent) focusTitle(focused); } else if (persistent) { - previousFocus.current = document.activeElement as HTMLElement; - focusTitle(0); + if (bar?.contains(document.activeElement)) previousFocus.current?.focus(); + else { + previousFocus.current = document.activeElement as HTMLElement; + focusTitle(0); + } } else void expand(); }), );