FN-7873: fix mobile floating-window header drag being intersected by page pan-y lockdown
Restores reliable single-finger dragging of movable FloatingWindow headers on mobile by reasserting touch-action: none at the mobile breakpoint, and adds regression coverage for the touch drag path. - Reassert touch-action: none on movable FloatingWindow headers within the mobile media query, excluding full-screen sheet variants (chat, task-detail, workflow-editor, automation, mission-interview, file-browser, pr-create, artifacts-gallery), so the drag-handle contract isn't overridden by the global pan-y lockdown on mobile. - Add a test verifying the movable mobile drag-handle selector keeps touch-action: none alongside the other opted-out draggable selectors (right-dock-expand-modal header, terminal header). - Add a test exercising the captured pointermove drag path (pointerdown/move/up with pointer capture) confirming the window repositions correctly on touch input. - Add a patch changeset for @runfusion/fusion documenting the fix. Files changed: .changeset/fn-7873-mobile-modal-header-drag.md | 7 +++ packages/dashboard/app/components/FloatingWindow.css | 8 +++ packages/dashboard/app/components/__tests__/FloatingWindow.test.tsx | 59 ++++++++++++++++++++++ 3 files changed, 74 insertions(+) Fusion-Task-Id: FN-7873 Fusion-Task-Lineage: 96f2b199-549e-4cab-a42b-05946a7bae86 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-7873-mobile-modal-header-drag.md
Normal file
7
.changeset/fn-7873-mobile-modal-header-drag.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
summary: Restore reliable mobile header dragging for movable floating modals.
|
||||||
|
category: fix
|
||||||
|
dev: Reasserts the FloatingWindow mobile touch-action contract and covers touch pointer dragging.
|
||||||
@@ -99,6 +99,14 @@ FNXC:ChatModal 2026-06-22-14:49:
|
|||||||
On mobile/narrow app viewports, opening Quick Chat should present the full Chat modal as a full-screen sheet instead of a small draggable desktop window. Scope this to the chat FloatingWindow and override the inline desktop geometry only at the mobile breakpoint; desktop pop-out behavior remains movable/resizable.
|
On mobile/narrow app viewports, opening Quick Chat should present the full Chat modal as a full-screen sheet instead of a small draggable desktop window. Scope this to the chat FloatingWindow and override the inline desktop geometry only at the mobile breakpoint; desktop pop-out behavior remains movable/resizable.
|
||||||
*/
|
*/
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
|
/*
|
||||||
|
FNXC:FloatingWindow 2026-07-12-17:35:
|
||||||
|
Mobile keeps the global `styles.css` pan-y lockdown so the dashboard cannot drift, but movable FloatingWindow headers must still resolve to an effective `touch-action: none`. Reassert the drag-handle contract at the mobile breakpoint, excluding full-screen sheet variants, so a single-finger header drag stays on the captured pointermove stream instead of being intersected back into page pan by the ancestor chain. Desktop drag/resize and mobile sheet variants are unchanged.
|
||||||
|
*/
|
||||||
|
.floating-window:not(.floating-window--chat):not(.floating-window--task-detail):not(.floating-window--workflow-editor):not(.floating-window--automation):not(.floating-window--mission-interview):not(.floating-window--file-browser):not(.floating-window--pr-create):not(.artifacts-gallery-window) .floating-window__header {
|
||||||
|
touch-action: none;
|
||||||
|
}
|
||||||
|
|
||||||
.floating-window--chat {
|
.floating-window--chat {
|
||||||
inset: 0 !important;
|
inset: 0 !important;
|
||||||
width: 100vw !important;
|
width: 100vw !important;
|
||||||
|
|||||||
@@ -1,9 +1,18 @@
|
|||||||
import { render, screen, fireEvent } from "@testing-library/react";
|
import { render, screen, fireEvent } from "@testing-library/react";
|
||||||
import { readFileSync } from "node:fs";
|
import { readFileSync } from "node:fs";
|
||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { loadAllAppCss } from "../../test/cssFixture";
|
||||||
import { FloatingWindow } from "../FloatingWindow";
|
import { FloatingWindow } from "../FloatingWindow";
|
||||||
|
|
||||||
const floatingWindowCss = readFileSync("app/components/FloatingWindow.css", "utf8");
|
const floatingWindowCss = readFileSync("app/components/FloatingWindow.css", "utf8");
|
||||||
|
const allAppCss = loadAllAppCss();
|
||||||
|
|
||||||
|
function cssRuleFor(css: string, selector: string): string {
|
||||||
|
const start = css.indexOf(`${selector} {`);
|
||||||
|
if (start === -1) return "";
|
||||||
|
const end = css.indexOf("}", start);
|
||||||
|
return css.slice(start, end);
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
FNXC:FloatingWindow 2026-06-22-20:45:
|
FNXC:FloatingWindow 2026-06-22-20:45:
|
||||||
@@ -56,6 +65,56 @@ describe("FloatingWindow", () => {
|
|||||||
expect(floatingWindowCss).not.toContain("var(--shadow-xl)");
|
expect(floatingWindowCss).not.toContain("var(--shadow-xl)");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("keeps movable mobile drag handles opted out of the pan-y touch lockdown", () => {
|
||||||
|
expect(allAppCss).toContain("html,");
|
||||||
|
expect(allAppCss).toContain("body {");
|
||||||
|
expect(allAppCss).toContain("touch-action: pan-y;");
|
||||||
|
expect(allAppCss).toContain("* {");
|
||||||
|
expect(allAppCss).toContain("#root {");
|
||||||
|
|
||||||
|
const movableFloatingWindowSelector = ".floating-window:not(.floating-window--chat):not(.floating-window--task-detail):not(.floating-window--workflow-editor):not(.floating-window--automation):not(.floating-window--mission-interview):not(.floating-window--file-browser):not(.floating-window--pr-create):not(.artifacts-gallery-window) .floating-window__header";
|
||||||
|
expect(cssRuleFor(floatingWindowCss, movableFloatingWindowSelector)).toContain("touch-action: none;");
|
||||||
|
|
||||||
|
for (const selector of [
|
||||||
|
".right-dock-expand-modal__header--draggable",
|
||||||
|
".terminal-header--draggable",
|
||||||
|
]) {
|
||||||
|
expect(cssRuleFor(allAppCss, selector)).toContain("touch-action: none;");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("moves a visible-header window through the captured touch drag path", () => {
|
||||||
|
render(
|
||||||
|
<FloatingWindow
|
||||||
|
windowKey="touch-drag"
|
||||||
|
title="A very long movable floating window title that still starts drag from the ellipsized title text"
|
||||||
|
onClose={() => {}}
|
||||||
|
defaultSize={{ width: 320, height: 240 }}
|
||||||
|
defaultPosition={{ x: 80, y: 90 }}
|
||||||
|
minSize={{ width: 240, height: 180 }}
|
||||||
|
>
|
||||||
|
<div>touch drag body</div>
|
||||||
|
</FloatingWindow>
|
||||||
|
);
|
||||||
|
|
||||||
|
const panel = screen.getByTestId("floating-window-touch-drag");
|
||||||
|
const header = screen.getByTestId("floating-window-drag-handle-touch-drag");
|
||||||
|
const titleText = screen.getByText(/very long movable floating window title/i);
|
||||||
|
const setPointerCapture = vi.fn();
|
||||||
|
const releasePointerCapture = vi.fn();
|
||||||
|
Object.defineProperty(header, "setPointerCapture", { configurable: true, value: setPointerCapture });
|
||||||
|
Object.defineProperty(header, "releasePointerCapture", { configurable: true, value: releasePointerCapture });
|
||||||
|
|
||||||
|
fireEvent.pointerDown(titleText, { pointerId: 17, pointerType: "touch", clientX: 100, clientY: 120 });
|
||||||
|
fireEvent.pointerMove(header, { pointerId: 17, pointerType: "touch", clientX: 140, clientY: 150 });
|
||||||
|
fireEvent.pointerUp(header, { pointerId: 17, pointerType: "touch", clientX: 140, clientY: 150 });
|
||||||
|
|
||||||
|
expect(setPointerCapture).toHaveBeenCalledWith(17);
|
||||||
|
expect(releasePointerCapture).toHaveBeenCalledWith(17);
|
||||||
|
expect(panel.style.left).toBe("120px");
|
||||||
|
expect(panel.style.top).toBe("120px");
|
||||||
|
});
|
||||||
|
|
||||||
it("can hide generic chrome and delegate dragging to a child header", () => {
|
it("can hide generic chrome and delegate dragging to a child header", () => {
|
||||||
render(
|
render(
|
||||||
<FloatingWindow
|
<FloatingWindow
|
||||||
|
|||||||
Reference in New Issue
Block a user