From 794cbcef632761be6469bfd4649a8bdd11537c0c Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Sun, 31 May 2026 16:38:01 +0700 Subject: [PATCH] Fix scripts menu resizing after service launch Reanimated entering animations can leave the measured menu surface with an inline height. Release that fixed height after opening and when measured content changes so service rows can grow in place. --- .../e2e/workspace-scripts-menu-resize.spec.ts | 80 +++++++++++++++++++ .../app/src/components/ui/dropdown-menu.tsx | 68 +++++++++++++++- 2 files changed, 145 insertions(+), 3 deletions(-) create mode 100644 packages/app/e2e/workspace-scripts-menu-resize.spec.ts diff --git a/packages/app/e2e/workspace-scripts-menu-resize.spec.ts b/packages/app/e2e/workspace-scripts-menu-resize.spec.ts new file mode 100644 index 000000000..dec22a14c --- /dev/null +++ b/packages/app/e2e/workspace-scripts-menu-resize.spec.ts @@ -0,0 +1,80 @@ +import { test, expect } from "./fixtures"; +import { createTempGitRepo } from "./helpers/workspace"; +import { + connectWorkspaceSetupClient, + createWorkspaceThroughDaemon, + openWorkspaceScriptsMenu, + seedProjectForWorkspaceSetup, + startWorkspaceScriptFromMenu, + waitForWorkspaceSetupProgress, +} from "./helpers/workspace-setup"; +import { waitForWorkspaceTabsVisible } from "./helpers/workspace-tabs"; +import { getServerId } from "./helpers/server-id"; +import { buildHostWorkspaceRoute } from "../src/utils/host-routes"; + +test("scripts menu resizes when a service row grows after launch", async ({ page }) => { + const client = await connectWorkspaceSetupClient(); + const repo = await createTempGitRepo("script-menu-resize-", { + paseoConfig: { + worktree: { + setup: ["sh -c 'echo setup complete'"], + }, + scripts: { + web: { + type: "service", + command: + "node -e \"const http = require('http'); const s = http.createServer((q,r) => r.end('ok')); s.listen(process.env.PORT || 3000, () => console.log('listening on ' + s.address().port))\"", + }, + }, + }, + }); + + try { + await seedProjectForWorkspaceSetup(client, repo.path); + const completed = waitForWorkspaceSetupProgress( + client, + (payload) => payload.status === "completed" && payload.detail.log.includes("setup complete"), + ); + const workspace = await createWorkspaceThroughDaemon(client, { + cwd: repo.path, + worktreeSlug: `script-menu-resize-${Date.now()}`, + }); + await completed; + + await page.goto(buildHostWorkspaceRoute(getServerId(), workspace.id)); + await waitForWorkspaceTabsVisible(page); + await openWorkspaceScriptsMenu(page); + + const menu = page.getByTestId("workspace-scripts-menu"); + const before = await menu.evaluate((element) => { + const rect = element.getBoundingClientRect(); + return { + height: rect.height, + clientHeight: element.clientHeight, + scrollHeight: element.scrollHeight, + }; + }); + + await startWorkspaceScriptFromMenu(page, "web"); + await expect(menu).toContainText("localhost:", { timeout: 15_000 }); + + const after = await menu.evaluate((element) => { + const rect = element.getBoundingClientRect(); + const firstChild = element.firstElementChild; + const childRect = firstChild?.getBoundingClientRect(); + return { + height: rect.height, + clientHeight: element.clientHeight, + scrollHeight: element.scrollHeight, + childHeight: childRect?.height ?? 0, + }; + }); + + expect(after.height).toBeGreaterThan(before.height); + expect(after.scrollHeight).toBeLessThanOrEqual(after.clientHeight + 1); + expect(after.childHeight).toBeGreaterThan(before.height); + } finally { + await client.close(); + await repo.cleanup(); + } +}); diff --git a/packages/app/src/components/ui/dropdown-menu.tsx b/packages/app/src/components/ui/dropdown-menu.tsx index a51f92282..a50ea04fc 100644 --- a/packages/app/src/components/ui/dropdown-menu.tsx +++ b/packages/app/src/components/ui/dropdown-menu.tsx @@ -3,6 +3,7 @@ import { useCallback, useContext, useEffect, + useId, useMemo, useRef, useState, @@ -30,6 +31,7 @@ import { Check, CheckCircle } from "lucide-react-native"; import { FloatingScrollView, FloatingSurface } from "@/components/ui/floating"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { useWebScrollbarStyle } from "@/hooks/use-web-scrollbar-style"; +import { isWeb } from "@/constants/platform"; // Action status for menu items with loading/success feedback export type ActionStatus = "idle" | "pending" | "success"; @@ -178,10 +180,19 @@ function renderDropdownSurface(input: { scrollable: boolean; scrollViewportStyle: StyleProp; content: ReactElement; + surfaceNativeID: string; onExited: () => void; }): ReactElement { - const { frameStyle, testID, surfaceStyle, scrollable, scrollViewportStyle, content, onExited } = - input; + const { + frameStyle, + testID, + surfaceStyle, + scrollable, + scrollViewportStyle, + content, + surfaceNativeID, + onExited, + } = input; const body = scrollable ? ( { + if (!enabled) return undefined; + + // Reanimated web entering animations leave the measured menu surface with + // an inline height snapshot. Once the menu is open, height must return to + // content-sized so rows can grow in place (for example service script URLs). + const release = () => { + releaseFixedMenuHeight(surfaceNativeID); + if (typeof requestAnimationFrame === "function") { + requestAnimationFrame(() => releaseFixedMenuHeight(surfaceNativeID)); + } + }; + const timers: ReturnType[] = [ + setTimeout(release, CONTENT_ENTERING_DURATION_MS), + ]; + + if (contentSize) { + timers.push(setTimeout(release, 0)); + } + + return () => { + for (const timer of timers) clearTimeout(timer); + }; + }, [contentSize, enabled, surfaceNativeID]); +} + export function DropdownMenuContent({ children, side = "bottom", @@ -389,6 +443,7 @@ export function DropdownMenuContent({ const { open, setOpen, triggerRef, flushPendingSelect } = useDropdownMenuContext("DropdownMenuContent"); const [modalVisible, setModalVisible] = useState(false); + const surfaceNativeID = useId(); const webScrollbarStyle = useWebScrollbarStyle(); const [closing, setClosing] = useState(false); const [triggerRect, setTriggerRect] = useState(null); @@ -429,6 +484,12 @@ export function DropdownMenuContent({ } }, [flushPendingSelect, modalVisible, open]); + useReleaseFixedMenuHeight({ + contentSize, + enabled: modalVisible, + surfaceNativeID, + }); + const handleClose = useCallback(() => { setOpen(false); }, [setOpen]); @@ -575,6 +636,7 @@ export function DropdownMenuContent({ scrollable, scrollViewportStyle, content, + surfaceNativeID, onExited: () => setModalVisible(false), }) : null}