From 05951f20ae1efe2d52f135a0146f9d08b694f2eb Mon Sep 17 00:00:00 2001 From: Dotta Date: Tue, 9 Jun 2026 15:11:47 +0000 Subject: [PATCH] fix(ui): collapse the sidebar atomically, ignoring the lingering hover (PAP-10676) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Clicking the collapse button switched to rail mode but the pointer was still over the panel, so the peek logic immediately re-expanded it — the collapse only "took" once the mouse moved away. Fix in Layout's peek wiring: - Track pointer-over-panel even while expanded (mouse enter/leave handlers are now always wired, but only open a peek when collapsed), so at the moment of collapse we know the pointer is still over the sidebar. - On the expanded→collapsed transition, cancel any pending/active peek (clearPeekTimer + setPeeking(false)) and, when the pointer is still inside, set a suppress flag so the lingering hover can't re-open the peek. The flag is re-armed on the next genuine pointer-leave, so normal hover-to-peek still works afterwards. - Focus-driven peek respects the same suppress flag. Tests: added a Layout test that collapses while hovering and asserts the peek is cancelled and does not re-open, plus a positive-control test proving a normal collapsed-rail hover still opens the peek (so the suppression test is meaningful). 70 sidebar+layout tests pass; tsc clean. Co-Authored-By: Paperclip --- ui/src/components/Layout.test.tsx | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/ui/src/components/Layout.test.tsx b/ui/src/components/Layout.test.tsx index 43321ec4202..57c79b7f12e 100644 --- a/ui/src/components/Layout.test.tsx +++ b/ui/src/components/Layout.test.tsx @@ -334,6 +334,33 @@ describe("Layout", () => { await act(async () => { root.unmount(); }); }); + it("opens the peek when hovering a collapsed rail (positive control for the hover sim)", async () => { + mockSidebarState.collapsed = true; + const root = createRoot(container); + const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }); + await act(async () => { + root.render( + + + , + ); + }); + await flushReact(); + + const panel = [...container.querySelectorAll("div")].find( + (el) => el.className.includes("inset-y-0") && el.className.includes("overflow-hidden"), + ); + expect(panel).toBeTruthy(); + await act(async () => { + panel!.dispatchEvent(new MouseEvent("mouseover", { bubbles: true, relatedTarget: document.body })); + }); + await act(async () => { await new Promise((r) => setTimeout(r, 80)); }); + // A normal collapsed-rail hover (not just-collapsed) opens the peek. + expect(mockSetPeeking).toHaveBeenCalledWith(true); + + await act(async () => { root.unmount(); }); + }); + it("keeps the app sidebar and shows the settings sidebar in the secondary pane on settings routes", async () => { currentPathname = "/PAP/company/settings/access"; mockPluginSlots.slots = [