From 34946011a1f925f6676e744edfc79671aef88c3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Flor=C3=AAncio?= Date: Fri, 24 Jul 2026 12:08:07 +0100 Subject: [PATCH] fix(web): restore sidebar task title overflow (#1913) --- .../task/task-sidebar-scroll-area.tsx | 6 ++- .../task/sidebar-scroll-preservation.spec.ts | 51 +++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/apps/web/components/task/task-sidebar-scroll-area.tsx b/apps/web/components/task/task-sidebar-scroll-area.tsx index 9f0986e467..e92da02ab0 100644 --- a/apps/web/components/task/task-sidebar-scroll-area.tsx +++ b/apps/web/components/task/task-sidebar-scroll-area.tsx @@ -43,7 +43,11 @@ export function TaskSidebarScrollArea({ children }: { children: ReactNode }) { className="task-sidebar-scroll-root min-h-0 flex-1" viewportProps={{ ref: scrollRef, - className: "task-sidebar-scroll", + // Radix uses an intrinsic-width table wrapper so generic scroll areas + // can grow horizontally. Sidebar rows must stay viewport-width instead: + // otherwise a long title pushes its actions beyond the visible edge and + // never overflows its own ScrollOnOverflow container. + className: "task-sidebar-scroll [&>div]:!block [&>div]:!min-w-0 [&>div]:!w-full", "data-can-scroll-down": canScrollDown, "data-testid": "task-sidebar-scroll", }} diff --git a/apps/web/e2e/tests/task/sidebar-scroll-preservation.spec.ts b/apps/web/e2e/tests/task/sidebar-scroll-preservation.spec.ts index f236b1bde5..9973b9ca29 100644 --- a/apps/web/e2e/tests/task/sidebar-scroll-preservation.spec.ts +++ b/apps/web/e2e/tests/task/sidebar-scroll-preservation.spec.ts @@ -138,6 +138,57 @@ test.describe("sidebar scrolling", () => { await expect(session.sidebar.locator(OVERLAY_SCROLLBAR_SELECTOR)).toHaveCount(0); }); + test("keeps long task titles and hover actions inside the sidebar", async ({ + testPage, + apiClient, + seedData, + }) => { + const longTitle = + "Long sidebar title that should scroll on hover and leave room for task actions ".repeat(4); + const task = await apiClient.createTask(seedData.workspaceId, longTitle, { + workflow_id: seedData.workflowId, + workflow_step_id: seedData.startStepId, + repository_ids: [seedData.repositoryId], + }); + + await testPage.goto(`/t/${task.id}`); + const session = new SessionPage(testPage); + await session.waitForLoad(); + + const scrollContainer = session.sidebar.getByTestId("task-sidebar-scroll"); + const taskRow = session.sidebar.getByTestId("sidebar-task-item").filter({ hasText: longTitle }); + const titleText = taskRow.getByText(longTitle, { exact: true }); + const titleViewport = titleText.locator(".."); + const actions = taskRow.getByRole("button", { name: "Task actions" }); + + await expect(taskRow).toBeVisible(); + const overflow = await titleViewport.evaluate((element) => ({ + clientWidth: element.clientWidth, + scrollWidth: element.scrollWidth, + })); + expect(overflow.scrollWidth).toBeGreaterThan(overflow.clientWidth); + + await titleViewport.hover(); + await expect + .poll(() => titleText.evaluate((element) => element.style.transform)) + .toMatch(/^translateX\(-/); + await expect(actions.locator("..")).toHaveCSS("opacity", "1"); + await expect(actions).toBeInViewport(); + + const [containerBox, rowBox, titleBox, actionBox] = await Promise.all([ + scrollContainer.boundingBox(), + taskRow.boundingBox(), + titleViewport.boundingBox(), + actions.boundingBox(), + ]); + if (!containerBox || !rowBox || !titleBox || !actionBox) { + throw new Error("Long-title sidebar row has no layout box"); + } + expect(rowBox.x + rowBox.width).toBeLessThanOrEqual(containerBox.x + containerBox.width + 1); + expect(rowBox.x + rowBox.width - (actionBox.x + actionBox.width)).toBeGreaterThanOrEqual(11); + expect(actionBox.x - (titleBox.x + titleBox.width)).toBeGreaterThanOrEqual(7); + }); + test("clicking another task does not reset the sidebar scroll position", async ({ testPage, apiClient,