From 87aab438dc2a3afbe6437119434141428a9598c1 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Sun, 12 Jul 2026 22:59:52 -0700 Subject: [PATCH] FN-7922: fix jerky tablet drag on headerless floating-window handles Ensures every draggable modal surface (terminal, artifacts viewer, and other FloatingWindow delegates) keeps touch-action: none on its drag handle so tablet touch-drag is captured by FloatingWindow's pointermove stream instead of being intersected by page pan, and backs the contract with tests. - Add touch-action: none to .artifacts-gallery-viewer-header, the headerless FloatingWindow's delegated drag handle, matching the other movable modal handles. - Add a FloatingWindow test asserting touch-action: none is present across every tablet movable-modal drag handle (terminal, artifacts, workflow editor, automation, mission interview, PR create, file browser, right dock, new task modal, quick chat FAB) and absent from broad tablet pan-y overrides. - Add a FloatingWindow test exercising the captured tablet touch-drag path for a headerless delegated handle (pointerdown/move/up with pointerType: "touch"). - Fix TerminalModal drag test to send pointerType: "touch" on pointerdown/move/up so it exercises the same tablet touch-drag contract. - Add changeset for the tablet drag fix. Files changed: .changeset/fn-7922-tablet-modal-drag.md | 7 ++ .../dashboard/app/components/ArtifactsGallery.css | 5 ++ .../components/__tests__/FloatingWindow.test.tsx | 76 +++++++++++++++++++++- .../components/__tests__/TerminalModal.test.tsx | 6 +- 4 files changed, 90 insertions(+), 4 deletions(-) Fusion-Task-Id: FN-7922 Fusion-Task-Lineage: 0d63ced5-faa7-4362-a42d-884b4c889ebf Co-authored-by: Fusion (runfusion.ai) --- .changeset/fn-7922-tablet-modal-drag.md | 7 ++ .../app/components/ArtifactsGallery.css | 5 ++ .../__tests__/FloatingWindow.test.tsx | 76 ++++++++++++++++++- .../__tests__/TerminalModal.test.tsx | 6 +- 4 files changed, 90 insertions(+), 4 deletions(-) create mode 100644 .changeset/fn-7922-tablet-modal-drag.md diff --git a/.changeset/fn-7922-tablet-modal-drag.md b/.changeset/fn-7922-tablet-modal-drag.md new file mode 100644 index 0000000000..ec02994bd8 --- /dev/null +++ b/.changeset/fn-7922-tablet-modal-drag.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Fix jerky tablet drag for the terminal and other movable modals. +category: fix +dev: Reassert drag-handle `touch-action: none` coverage for headerless floating-window delegates and test the tablet touch-drag contract across movable modal surfaces. diff --git a/packages/dashboard/app/components/ArtifactsGallery.css b/packages/dashboard/app/components/ArtifactsGallery.css index 58d7f595cf..45c88a67ac 100644 --- a/packages/dashboard/app/components/ArtifactsGallery.css +++ b/packages/dashboard/app/components/ArtifactsGallery.css @@ -369,11 +369,16 @@ Viewers live inside the shared FloatingWindow (draggable by the viewer header, r padding: var(--space-md); } +/* +FNXC:ArtifactsGallery 2026-07-12-21:10: +The artifacts viewer is a headerless FloatingWindow, so this header is the delegated drag handle on tablet and coarse-pointer floating layouts. Keep its effective `touch-action: none` so a single-finger drag is delivered through FloatingWindow's captured pointermove stream instead of being intersected back into page pan; the mobile sheet override below still disables the desktop drag affordance at <=768px. +*/ .artifacts-gallery-viewer-header { display: flex; align-items: center; gap: var(--space-sm); cursor: grab; + touch-action: none; } .artifacts-gallery-viewer-header:active { diff --git a/packages/dashboard/app/components/__tests__/FloatingWindow.test.tsx b/packages/dashboard/app/components/__tests__/FloatingWindow.test.tsx index eb5af921ec..3bb15ccfff 100644 --- a/packages/dashboard/app/components/__tests__/FloatingWindow.test.tsx +++ b/packages/dashboard/app/components/__tests__/FloatingWindow.test.tsx @@ -1,11 +1,12 @@ import { render, screen, fireEvent } from "@testing-library/react"; import { readFileSync } from "node:fs"; import { beforeEach, describe, expect, it, vi } from "vitest"; -import { loadAllAppCss } from "../../test/cssFixture"; +import { loadAllAppCss, loadStylesCss } from "../../test/cssFixture"; import { FloatingWindow } from "../FloatingWindow"; const floatingWindowCss = readFileSync("app/components/FloatingWindow.css", "utf8"); const allAppCss = loadAllAppCss(); +const stylesCss = loadStylesCss(); function cssRuleFor(css: string, selector: string): string { const start = css.indexOf(`${selector} {`); @@ -14,6 +15,15 @@ function cssRuleFor(css: string, selector: string): string { return css.slice(start, end); } +function cssRuleContaining(css: string, selector: string, declaration: string): string { + const escaped = selector.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/\\ /g, "\\s+"); + const matches = css.matchAll(new RegExp(`${escaped}\\s*\\{[^}]*\\}`, "g")); + for (const match of matches) { + if (match[0].includes(declaration)) return match[0]; + } + return ""; +} + /* FNXC:FloatingWindow 2026-06-22-20:45: Contract tests for the reusable non-blocking floating window: @@ -83,6 +93,35 @@ describe("FloatingWindow", () => { } }); + it("keeps every tablet movable-modal drag handle on the explicit touch-action none contract", () => { + const tabletStylesStart = stylesCss.indexOf("@media (min-width: 769px) and (max-width: 1024px)"); + const mobileStylesStart = stylesCss.indexOf("@media (max-width: 768px)", tabletStylesStart); + expect(tabletStylesStart).toBeGreaterThan(-1); + expect(mobileStylesStart).toBeGreaterThan(tabletStylesStart); + + const tabletBlock = stylesCss.slice(tabletStylesStart, mobileStylesStart); + expect(tabletBlock).not.toContain("* {"); + expect(tabletBlock).not.toContain("touch-action: pan-y;"); + + for (const selector of [ + ".floating-window__header", + ".floating-window--headerless .task-detail-content--embedded > .modal-header", + ".chat-view--floating .view-header", + ".floating-window--workflow-editor .wf-editor-header", + ".floating-window--automation .automation-modal__drag-handle", + ".floating-window--mission-interview .mission-interview-modal__drag-handle", + ".floating-window--pr-create .pr-create-modal__drag-handle", + ".file-browser-modal-header", + ".artifacts-gallery-viewer-header", + ".terminal-header--draggable", + ".right-dock-expand-modal__header--draggable", + ".new-task-modal__header--draggable", + ".quick-chat-fab", + ]) { + expect(cssRuleContaining(allAppCss, selector, "touch-action: none;"), selector).toContain("touch-action: none;"); + } + }); + it("moves a visible-header window through the captured touch drag path", () => { render( { } }); + it("moves a headerless delegated handle through the captured tablet touch drag path", () => { + render( + {}} + hideHeader + dragHandleSelector=".artifacts-gallery-viewer-header" + className="artifacts-gallery-window" + defaultSize={{ width: 320, height: 240 }} + defaultPosition={{ x: 90, y: 110 }} + minSize={{ width: 240, height: 180 }} + > +
Artifacts header
+
+ + ); + + const panel = screen.getByTestId("floating-window-artifacts-delegate"); + const delegatedHeader = screen.getByText("Artifacts header"); + const setPointerCapture = vi.fn(); + const releasePointerCapture = vi.fn(); + Object.defineProperty(panel, "setPointerCapture", { configurable: true, value: setPointerCapture }); + Object.defineProperty(panel, "releasePointerCapture", { configurable: true, value: releasePointerCapture }); + + fireEvent.pointerDown(delegatedHeader, { pointerId: 23, pointerType: "touch", clientX: 120, clientY: 140 }); + fireEvent.pointerMove(panel, { pointerId: 23, pointerType: "touch", clientX: 150, clientY: 170 }); + fireEvent.pointerUp(panel, { pointerId: 23, pointerType: "touch", clientX: 150, clientY: 170 }); + + expect(setPointerCapture).toHaveBeenCalledWith(23); + expect(releasePointerCapture).toHaveBeenCalledWith(23); + expect(panel.style.left).toBe("120px"); + expect(panel.style.top).toBe("140px"); + }); + it("scopes mobile sheet sizing and hidden resize handles to task-detail pop-outs", () => { expect(floatingWindowCss).toContain("FNXC:MobileTaskPopups 2026-06-29-00:00"); expect(floatingWindowCss).toContain(".floating-window--task-detail {"); diff --git a/packages/dashboard/app/components/__tests__/TerminalModal.test.tsx b/packages/dashboard/app/components/__tests__/TerminalModal.test.tsx index 7080b8ee64..60d0629a77 100644 --- a/packages/dashboard/app/components/__tests__/TerminalModal.test.tsx +++ b/packages/dashboard/app/components/__tests__/TerminalModal.test.tsx @@ -1089,9 +1089,9 @@ describe("TerminalModal", () => { const header = modal.querySelector(".terminal-header") as HTMLElement & { setPointerCapture: (pointerId: number) => void; releasePointerCapture: (pointerId: number) => void }; header.setPointerCapture = vi.fn(); header.releasePointerCapture = vi.fn(); - fireEvent.pointerDown(header, { pointerId: 2, clientX: 100, clientY: 100 }); - fireEvent.pointerMove(header, { pointerId: 2, clientX: 125, clientY: 135 }); - fireEvent.pointerUp(header, { pointerId: 2 }); + fireEvent.pointerDown(header, { pointerId: 2, pointerType: "touch", clientX: 100, clientY: 100 }); + fireEvent.pointerMove(header, { pointerId: 2, pointerType: "touch", clientX: 125, clientY: 135 }); + fireEvent.pointerUp(header, { pointerId: 2, pointerType: "touch" }); await waitFor(() => { expect(window.localStorage.getItem(`fusion:terminal-float-pos-${projectId}`)).toBeTruthy();