From e4eb8b6190c5874b9effc8774c5caefcc9769666 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Wed, 1 Jul 2026 08:10:05 -0700 Subject: [PATCH] FN-7363: prevent mobile task long-press text selection Prevent Board task long-press gestures from selecting card text while preserving editing controls. - Disable native text selection and WebKit touch callouts on non-editing task cards. - Prevent touch/pen pointer-down defaults before the long-press context-menu timer starts. - Cover the mobile selection behavior with component and CSS regression tests and document the Board behavior. Files changed: .changeset/fn-7363-mobile-board-text-selection.md | 7 ++++ docs/dashboard-guide.md | 5 +-- packages/dashboard/app/components/TaskCard.css | 17 ++++++++++ packages/dashboard/app/components/TaskCard.tsx | 5 +++ .../app/components/__tests__/TaskCard.test.tsx | 39 ++++++++++++++++++++++ .../app/components/__tests__/board-mobile.test.tsx | 24 +++++++++++++ 6 files changed, 95 insertions(+), 2 deletions(-) Fusion-Task-Id: FN-7363 Fusion-Task-Lineage: c683ab3b-2caa-4b45-a455-0cc163fd52cc Co-authored-by: Fusion (runfusion.ai) --- .../fn-7363-mobile-board-text-selection.md | 7 ++++ docs/dashboard-guide.md | 5 ++- .../dashboard/app/components/TaskCard.css | 17 ++++++++ .../dashboard/app/components/TaskCard.tsx | 5 +++ .../components/__tests__/TaskCard.test.tsx | 39 +++++++++++++++++++ .../__tests__/board-mobile.test.tsx | 24 ++++++++++++ 6 files changed, 95 insertions(+), 2 deletions(-) create mode 100644 .changeset/fn-7363-mobile-board-text-selection.md diff --git a/.changeset/fn-7363-mobile-board-text-selection.md b/.changeset/fn-7363-mobile-board-text-selection.md new file mode 100644 index 0000000000..c6132d5f22 --- /dev/null +++ b/.changeset/fn-7363-mobile-board-text-selection.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Prevent mobile Board task long-press menus from selecting card text. +category: fix +dev: Suppresses WebKit/native selection for non-editing TaskCard surfaces while preserving edit textareas. diff --git a/docs/dashboard-guide.md b/docs/dashboard-guide.md index 2a42e0aa25..e3a8eda670 100644 --- a/docs/dashboard-guide.md +++ b/docs/dashboard-guide.md @@ -139,11 +139,12 @@ Features: - GitHub provenance marker on task cards imported from GitHub (`sourceType: github_import`), shown in the footer with other external-source metadata - Task card header meta badges group priority, fast mode, agent-created provenance, workflow identity, and elapsed/created-time chips into one wrapping row; agent labels prefer `sourceMetadata.agentName` over raw agent IDs - Task detail surfaces show the selected/effective workflow identity near the task's workflow controls so individual cards remain understandable when Board is in **All workflows** or another aggregate/mixed context. -- Board task cards support a context menu from right-click, keyboard context menu / Shift+F10, or touch long-press for detail-aligned lifecycle actions without changing normal card clicks. The menu opens as an independent overlay so it stays visible beyond the card or column edge while remaining clamped to the viewport. Selecting an action applies that exact action once and dismisses the menu. Completed card context menus include **Refine**, which opens the existing task-detail refinement feedback modal for the same task. +- Board task cards support a context menu from right-click, keyboard context menu / Shift+F10, or touch long-press for detail-aligned lifecycle actions without changing normal card clicks. The menu opens as an independent overlay so it stays visible beyond the card or column edge while remaining clamped to the viewport. On mobile, long-press opens that menu without selecting card text or showing native copy/paste callouts. Selecting an action applies that exact action once and dismisses the menu. Completed card context menus include **Refine**, which opens the existing task-detail refinement feedback modal for the same task. +FNXC:TaskContextMenu 2026-07-01-00:00: Board card context menus must behave like independent overlays because Board columns intentionally clip and scroll their bodies for kanban containment. +FNXC:TaskCardMobileSelection 2026-07-01-00:00: Mobile Board long-press is a task-action gesture, not a text-selection gesture; document that the native selection/copy callout is suppressed while normal card clicks and edit textareas keep their behavior. --> diff --git a/packages/dashboard/app/components/TaskCard.css b/packages/dashboard/app/components/TaskCard.css index 14c01eea58..88aedfc63e 100644 --- a/packages/dashboard/app/components/TaskCard.css +++ b/packages/dashboard/app/components/TaskCard.css @@ -50,10 +50,24 @@ FNXC:TaskCardMobilePan 2026-06-13-19:51: Mobile board cards must allow horizontal kanban panning from every visible card surface, not only from gaps or progress-count text. The global mobile touch-action reset applies to descendants, and browsers intersect touch-action along the touched element's ancestor chain, so the whole non-editing card subtree must opt back into pan-x. + +FNXC:TaskCardMobileSelection 2026-07-01-00:00: +Mobile long-press opens the Board task context menu, so non-editing card text must not trigger native text selection or iOS copy callouts. Keep this scoped to the shared `.card` subtree so normal and worktree-grouped Board cards are covered while edit controls opt back into selectable text. */ .card:not(.card-editing), .card:not(.card-editing) * { touch-action: pan-x pan-y; + user-select: none; + -webkit-user-select: none; + -webkit-touch-callout: none; +} + +.card.card-editing, +.card.card-editing *, +.card :is(input, textarea, select, [contenteditable="true"]) { + user-select: text; + -webkit-user-select: text; + -webkit-touch-callout: default; } /* Agent-active glow: animated border glow when an agent is actively working on a task. @@ -1205,6 +1219,9 @@ executing. These map 1:1 to the unified progress status so the dot color encodes padding: 6px 8px; background: transparent; border: 1px solid transparent; + user-select: text; + -webkit-user-select: text; + -webkit-touch-callout: default; color: var(--text); font-size: 0.8125rem; font-family: inherit; diff --git a/packages/dashboard/app/components/TaskCard.tsx b/packages/dashboard/app/components/TaskCard.tsx index fe54cd2826..dae57fb25e 100644 --- a/packages/dashboard/app/components/TaskCard.tsx +++ b/packages/dashboard/app/components/TaskCard.tsx @@ -2019,6 +2019,11 @@ function TaskCardComponent({ const handlePointerDown = useCallback((e: React.PointerEvent) => { if (!hasContextMenuActions || e.pointerType === "mouse" || isInteractiveTarget(e.target)) return; + /* + FNXC:TaskCardMobileSelection 2026-07-01-00:00: + Touch/pen long-press is reserved for the Board task context menu. Prevent the native selection/copy callout before the timer starts while leaving mouse right-click, keyboard menu access, and editable descendants on their normal paths. + */ + e.preventDefault(); clearLongPressTimer(); longPressStartRef.current = { x: e.clientX, y: e.clientY, pointerId: e.pointerId }; longPressTimerRef.current = setTimeout(() => { diff --git a/packages/dashboard/app/components/__tests__/TaskCard.test.tsx b/packages/dashboard/app/components/__tests__/TaskCard.test.tsx index 38cd237020..b3c0c92b64 100644 --- a/packages/dashboard/app/components/__tests__/TaskCard.test.tsx +++ b/packages/dashboard/app/components/__tests__/TaskCard.test.tsx @@ -461,6 +461,45 @@ describe("TaskCard", () => { } }); + it("suppresses native text selection when touch long-press opens the board card context menu", async () => { + vi.useFakeTimers(); + const cleanupGeometry = mockBoardContextMenuGeometry(); + const onOpenDetail = vi.fn(); + const onPauseTask = vi.fn(async () => makeTask({ paused: true })); + try { + render( +
+
+ +
+
, + ); + + const title = screen.getByText("Long selectable title"); + const pointerDownWasNotCanceled = fireEvent.pointerDown(title, { + pointerType: "touch", + pointerId: 9, + clientX: 180, + clientY: 160, + cancelable: true, + }); + expect(pointerDownWasNotCanceled).toBe(false); + + act(() => vi.advanceTimersByTime(550)); + + expectBoardContextMenuPortaled(); + expect(onOpenDetail).not.toHaveBeenCalled(); + fireEvent.pointerUp(title, { pointerType: "touch", pointerId: 9, clientX: 180, clientY: 160 }); + } finally { + cleanupGeometry(); + } + }); + it("cancels board card long-press when touch moves before the delay", () => { vi.useFakeTimers(); render( diff --git a/packages/dashboard/app/components/__tests__/board-mobile.test.tsx b/packages/dashboard/app/components/__tests__/board-mobile.test.tsx index 8b4656ec3b..1a22f61910 100644 --- a/packages/dashboard/app/components/__tests__/board-mobile.test.tsx +++ b/packages/dashboard/app/components/__tests__/board-mobile.test.tsx @@ -245,6 +245,30 @@ describe("Board and Column mobile CSS", () => { }); describe("TaskCard mobile", () => { + it("suppresses native selection and iOS callouts for non-editing Board card text", () => { + const css = loadAllAppCss(); + + // The shared `.card` selector covers both Column-rendered cards and WorktreeGroup-rendered cards. + expectRuleToContain(css, ".card:not(.card-editing)", "touch-action: pan-x pan-y;"); + expectRuleToContain(css, ".card:not(.card-editing)", "user-select: none;"); + expectRuleToContain(css, ".card:not(.card-editing)", "-webkit-user-select: none;"); + expectRuleToContain(css, ".card:not(.card-editing)", "-webkit-touch-callout: none;"); + expectRuleToContain(css, ".card:not(.card-editing) *", "touch-action: pan-x pan-y;"); + expectRuleToContain(css, ".card:not(.card-editing) *", "user-select: none;"); + expectRuleToContain(css, ".card:not(.card-editing) *", "-webkit-user-select: none;"); + expectRuleToContain(css, ".card:not(.card-editing) *", "-webkit-touch-callout: none;"); + }); + + it("keeps TaskCard edit textarea selectable while non-editing cards suppress selection", () => { + const css = loadAllAppCss(); + + expectRuleToContain(css, ".card-edit-desc-textarea", "user-select: text;"); + expectRuleToContain(css, ".card-edit-desc-textarea", "-webkit-user-select: text;"); + expectRuleToContain(css, ".card-edit-desc-textarea", "-webkit-touch-callout: default;"); + expectRuleToContain(css, ".card.card-editing", "user-select: text;"); + expectRuleToContain(css, ".card :is(input, textarea, select, [contenteditable=\"true\"])", "user-select: text;"); + }); + it("sets .card-archive-btn opacity: 1 in the mobile media block", () => { const css = loadAllAppCss(); const mobileSection = getMainMobileSection(css);