diff --git a/.changeset/fn-067-board-safe-surface-pan.md b/.changeset/fn-067-board-safe-surface-pan.md new file mode 100644 index 0000000000..e6f91c68f7 --- /dev/null +++ b/.changeset/fn-067-board-safe-surface-pan.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Restore desktop Board dragging from safe empty-column surfaces. +category: fix +dev: Keeps mobile Board scrolling and column snapping unchanged while retaining no edge auto-scroll. diff --git a/docs/dashboard-guide.md b/docs/dashboard-guide.md index 723473964a..a9e4c7dda0 100644 --- a/docs/dashboard-guide.md +++ b/docs/dashboard-guide.md @@ -326,8 +326,8 @@ Features: - Move a task from its context menu: right-click on desktop, use the overflow button or keyboard Context Menu/Shift+F10, or long-press on touch. When several legal destinations exist, open the grouped **Move to** submenu; a single destination remains a direct action. This changes task movement only—subtasks still use drag-to-reorder. - -- On desktop, click-hold and drag the bare Board background to pan columns by actual pointer movement. Text, task cards, and controls retain their normal selection and interaction behavior; approaching an edge never auto-scrolls. Native wheel, trackpad, scrollbar, and keyboard scrolling remain available. Phones continue to use native touch scrolling and mobile-only column snapping. + +- On desktop and tablet, click-hold and drag a safe Board surface, including empty-column text, to pan columns by actual pointer movement. Task cards, controls, editable content, and native-draggable targets retain their normal behavior; approaching an edge never auto-scrolls. Native wheel, trackpad, scrollbar, and keyboard scrolling remain available. Phones continue to use unchanged native touch scrolling and mobile-only column snapping. - Search/filter tasks (including working-branch and base-branch dropdown filters with explicit **No working branch** / **No base branch** options) - Working-branch and base-branch filter selections are persisted per project and restored across refresh/navigation - Column visibility controls diff --git a/packages/dashboard/app/components/Board.css b/packages/dashboard/app/components/Board.css index 1195cd8a01..4ddca5810a 100644 --- a/packages/dashboard/app/components/Board.css +++ b/packages/dashboard/app/components/Board.css @@ -45,9 +45,9 @@ The board needs a neutral first-paint shell whenever workflow lanes are enabled } /* -FNXC:BoardNavigation 2026-08-20-02:44: -Feedback appears only after a qualifying direct-root desktop drag crosses horizontal intent. It -cannot suppress selection for text, cards, controls, the loading skeleton, or mobile touch. +FNXC:BoardNavigation 2026-08-20-04:47: +Feedback begins only after an eligible desktop/tablet safe-surface drag crosses horizontal intent. +It never activates for excluded cards or controls, the unbound skeleton, or the disabled mobile path. */ .board.board-workflow-columns.is-mouse-panning { cursor: grabbing; diff --git a/packages/dashboard/app/components/Board.tsx b/packages/dashboard/app/components/Board.tsx index 1dfcf9284b..7b12a9141e 100644 --- a/packages/dashboard/app/components/Board.tsx +++ b/packages/dashboard/app/components/Board.tsx @@ -225,20 +225,20 @@ export function Board({ tasks, projectId, maxConcurrent, showWorktreeGrouping, o boardRef.current = element; setBoardElement((current) => current === element ? current : element); }, []); + const viewportMode = useViewportMode(); useColumnScrollSnap(boardElement, { mobileOnly: true }); /* - FNXC:BoardNavigation 2026-08-20-02:44: - Selected and All-workflows Board roots share one bounded mouse-pan hook, while the loading - skeleton remains unbound and mobile touch remains owned by the separate snap hook. Direct-root - drags alone can pan; this wiring must not turn cards, text, or controls into navigation surfaces. + FNXC:BoardNavigation 2026-08-20-04:47: + Selected and All-workflows live roots share safe-surface primary-mouse panning outside mobile; + empty-column text may pan, while cards and controls remain native. The skeleton stays unbound, + and the separate mobile-only snap owner plus mobile styles remain unchanged. */ - const { isPanning: isBoardMousePanning, ...boardMousePanBindings } = useBoardMousePan(boardElement); + const { isPanning: isBoardMousePanning, ...boardMousePanBindings } = useBoardMousePan(boardElement, viewportMode !== "mobile"); const boardClassName = `board board-workflow-columns${isBoardMousePanning ? " is-mouse-panning" : ""}`; const [headerWorkflowSlot, setHeaderWorkflowSlot] = useState(() => { if (typeof document === "undefined") return null; return document.getElementById("header-workflow-slot"); }); - const viewportMode = useViewportMode(); // Normalized search-active signal: trimmed and non-empty const isSearchActive = searchQuery.trim() !== ""; useEffect(() => { diff --git a/packages/dashboard/app/components/__tests__/Board.test.tsx b/packages/dashboard/app/components/__tests__/Board.test.tsx index 737fef33ba..94f231804f 100644 --- a/packages/dashboard/app/components/__tests__/Board.test.tsx +++ b/packages/dashboard/app/components/__tests__/Board.test.tsx @@ -2041,7 +2041,7 @@ describe("Board", () => { ]); }); - it("pans selected and All-workflows Board roots only while the pointer moves", async () => { + it("restores safe empty-column descendant panning in selected and All-workflows Boards", async () => { enableFlag( { "FN-1": "builtin:coding", "FN-2": "wf-custom" }, [DEFAULT_WORKFLOW, CUSTOM_WORKFLOW], @@ -2050,45 +2050,41 @@ describe("Board", () => { const selectedBoard = screen.getByRole("main") as HTMLElement; makeBoardHorizontallyScrollable(selectedBoard); - fireEvent.pointerDown(selectedBoard, { button: 0, clientX: 100, clientY: 50, pointerId: 1, pointerType: "mouse" }); - fireEvent.pointerMove(selectedBoard, { clientX: 40, clientY: 50, pointerId: 1, pointerType: "mouse" }); + const selectedEmptyText = within(selectedBoard).getAllByText("No tasks")[0]; + fireEvent.pointerDown(selectedEmptyText, { button: 0, clientX: 100, clientY: 50, pointerId: 1, pointerType: "mouse" }); + fireEvent.pointerMove(selectedEmptyText, { clientX: 40, clientY: 50, pointerId: 1, pointerType: "mouse" }); expect(selectedBoard.scrollLeft).toBe(160); expect(selectedBoard).toHaveClass("is-mouse-panning"); - fireEvent.pointerUp(selectedBoard, { pointerId: 1, pointerType: "mouse" }); + fireEvent.pointerUp(selectedEmptyText, { pointerId: 1, pointerType: "mouse" }); expect(selectedBoard).not.toHaveClass("is-mouse-panning"); expect(selectedBoard.scrollLeft).toBe(160); await selectWorkflow(ALL_WORKFLOWS_BOARD_VIEW_ID); const aggregateBoard = screen.getByRole("main") as HTMLElement; makeBoardHorizontallyScrollable(aggregateBoard); - fireEvent.pointerDown(aggregateBoard, { button: 0, clientX: 100, clientY: 50, pointerId: 2, pointerType: "mouse" }); - fireEvent.pointerMove(aggregateBoard, { clientX: 40, clientY: 50, pointerId: 2, pointerType: "mouse" }); - expect(aggregateBoard.scrollLeft).toBe(160); - fireEvent.pointerUp(aggregateBoard, { pointerId: 2, pointerType: "mouse" }); + const aggregateEmptyText = within(aggregateBoard).getAllByText("No tasks")[0]; + fireEvent.pointerDown(aggregateEmptyText, { button: 0, clientX: 100, clientY: 50, pointerId: 2, pointerType: "mouse" }); + fireEvent.pointerMove(aggregateEmptyText, { clientX: 40, clientY: 50, pointerId: 2, pointerType: "mouse" }); expect(aggregateBoard.scrollLeft).toBe(160); + fireEvent.pointerUp(aggregateEmptyText, { pointerId: 2, pointerType: "mouse" }); + expect(aggregateBoard).not.toHaveClass("is-mouse-panning"); }); - it("keeps empty text and non-overflow Board surfaces outside mouse panning", async () => { - enableFlag( - { "FN-1": "builtin:coding", "FN-2": "wf-custom" }, - [DEFAULT_WORKFLOW, CUSTOM_WORKFLOW], - ); - renderBoard({ tasks: [mkTask({ id: "FN-1" }), mkTask({ id: "FN-2", column: "intake" })] }); + it("keeps non-overflow Boards and stationary edgeward pointers outside continued panning", () => { + enableFlag({}); + renderBoard(); + const board = screen.getByRole("main") as HTMLElement; + makeBoardHorizontallyScrollable(board); - const selectedBoard = screen.getByRole("main") as HTMLElement; - makeBoardHorizontallyScrollable(selectedBoard); - dispatchMouseDrag(within(selectedBoard).getAllByText("No tasks")[0]); - expect(selectedBoard.scrollLeft).toBe(100); + fireEvent.pointerDown(within(board).getAllByText("No tasks")[0], { button: 0, clientX: 100, clientY: 50, pointerId: 1, pointerType: "mouse" }); + fireEvent.pointerMove(board, { clientX: 190, clientY: 50, pointerId: 1, pointerType: "mouse" }); + const scrollAfterPointerMove = board.scrollLeft; + fireEvent.pointerUp(board, { pointerId: 1, pointerType: "mouse" }); + expect(board.scrollLeft).toBe(scrollAfterPointerMove); - Object.defineProperty(selectedBoard, "scrollWidth", { configurable: true, value: 200 }); - dispatchMouseDrag(selectedBoard, 2); - expect(selectedBoard.scrollLeft).toBe(100); - - await selectWorkflow(ALL_WORKFLOWS_BOARD_VIEW_ID); - const aggregateBoard = screen.getByRole("main") as HTMLElement; - makeBoardHorizontallyScrollable(aggregateBoard); - dispatchMouseDrag(within(aggregateBoard).getAllByText("No tasks")[0], 3); - expect(aggregateBoard.scrollLeft).toBe(100); + Object.defineProperty(board, "scrollWidth", { configurable: true, value: 200 }); + dispatchMouseDrag(board, 2); + expect(board.scrollLeft).toBe(scrollAfterPointerMove); }); it("keeps touch and task-card interactions native", async () => { @@ -2123,6 +2119,29 @@ describe("Board", () => { expect(onQuickCreate).toHaveBeenCalledTimes(1); }); + it("disables desktop mouse panning at the mobile viewport without changing touch ownership", () => { + const harness = installMobileBoardStabilizationHarness(); + try { + enableFlag({}); + renderBoard(); + const board = screen.getByRole("main") as HTMLElement; + makeBoardHorizontallyScrollable(board); + const emptyText = within(board).getAllByText("No tasks")[0]; + + fireEvent.pointerDown(emptyText, { button: 0, clientX: 100, clientY: 50, pointerId: 1, pointerType: "mouse" }); + fireEvent.pointerMove(emptyText, { clientX: 40, clientY: 50, pointerId: 1, pointerType: "mouse" }); + fireEvent.pointerUp(emptyText, { pointerId: 1, pointerType: "mouse" }); + fireEvent.pointerDown(emptyText, { button: 0, clientX: 100, clientY: 50, pointerId: 2, pointerType: "touch" }); + fireEvent.pointerMove(emptyText, { clientX: 40, clientY: 50, pointerId: 2, pointerType: "touch" }); + fireEvent.pointerUp(emptyText, { pointerId: 2, pointerType: "touch" }); + + expect(board.scrollLeft).toBe(100); + expect(board).not.toHaveClass("is-mouse-panning"); + } finally { + harness.restore(); + } + }); + it("preserves all-workflows board scroll during mobile visualViewport refresh stabilization", async () => { const harness = installMobileBoardStabilizationHarness(); try { diff --git a/packages/dashboard/app/components/__tests__/board-mobile.test.tsx b/packages/dashboard/app/components/__tests__/board-mobile.test.tsx index 950cb97ce9..5ae4507963 100644 --- a/packages/dashboard/app/components/__tests__/board-mobile.test.tsx +++ b/packages/dashboard/app/components/__tests__/board-mobile.test.tsx @@ -98,13 +98,13 @@ describe("mobile board magnetic column snap wiring (FN-8235)", () => { remain are the selected and aggregate workflow-column renders, and the point of this guard is that BOTH share the one scroll-snap hook, which still holds. */ - it("shares mobile snap and bounded mouse-pan wiring across the selected and aggregate live boards", () => { + it("shares mobile snap and explicitly disabled mouse-pan wiring across the selected and aggregate live boards", () => { const boardSource = readAppFile("components/Board.tsx"); expect(boardSource).toContain('import { useColumnScrollSnap } from "../hooks/useColumnScrollSnap";'); expect(boardSource).toContain('import { useBoardMousePan } from "../hooks/useBoardMousePan";'); expect(boardSource).toContain("useColumnScrollSnap(boardElement, { mobileOnly: true });"); - expect(boardSource).toContain("useBoardMousePan(boardElement)"); + expect(boardSource).toContain('useBoardMousePan(boardElement, viewportMode !== "mobile")'); expect(boardSource.match(/ref=\{setBoardRef\}/g)).toHaveLength(2); expect(boardSource.match(/\{\.\.\.boardMousePanBindings\}/g)).toHaveLength(2); expect(boardSource.match(/className=\{boardClassName\}/g)).toHaveLength(2); diff --git a/packages/dashboard/app/hooks/__tests__/useBoardMousePan.test.tsx b/packages/dashboard/app/hooks/__tests__/useBoardMousePan.test.tsx index 2724d450a5..ff44d7f842 100644 --- a/packages/dashboard/app/hooks/__tests__/useBoardMousePan.test.tsx +++ b/packages/dashboard/app/hooks/__tests__/useBoardMousePan.test.tsx @@ -3,31 +3,29 @@ import { fireEvent, render } from "@testing-library/react"; import { describe, expect, it, vi } from "vitest"; import { useBoardMousePan } from "../useBoardMousePan"; -function PanHarness({ onClick = vi.fn() }: { onClick?: () => void }) { +function PanHarness({ enabled = true, onClick = vi.fn() }: { enabled?: boolean; onClick?: () => void }) { const [boardElement, setBoardElement] = useState(null); - const { isPanning, ...bindings } = useBoardMousePan(boardElement); + const { isPanning, ...bindings } = useBoardMousePan(boardElement, enabled); return ( -
+

No tasks

+
Safe surface
-
Card
+
Editable content
+
Draggable
+
Card
); } -function renderPanHarness(onClick = vi.fn()) { - const result = render(); +function renderPanHarness(enabled = true, onClick = vi.fn()) { + const result = render(); const board = result.getByTestId("board"); - Object.defineProperty(board, "clientWidth", { configurable: true, value: 200 }); - Object.defineProperty(board, "scrollWidth", { configurable: true, value: 600 }); + Object.defineProperties(board, { + clientWidth: { configurable: true, value: 200 }, + scrollWidth: { configurable: true, value: 600 }, + }); return { ...result, board }; } @@ -44,15 +42,15 @@ function pointerUp(target: HTMLElement, pointerId = 1, pointerType = "mouse") { } describe("useBoardMousePan", () => { - it("pans direct Board-root primary mouse drags by inverse horizontal delta in both directions", () => { - const { board } = renderPanHarness(); + it("pans safe descendants and the root by inverse horizontal delta in both directions", () => { + const { board, getByTestId } = renderPanHarness(); board.scrollLeft = 100; - pointerDown(board); - pointerMove(board, 140); + pointerDown(getByTestId("empty-text")); + pointerMove(getByTestId("empty-text"), 140); expect(board.scrollLeft).toBe(60); expect(board).toHaveAttribute("data-panning", "true"); - pointerUp(board); + pointerUp(getByTestId("empty-text")); board.scrollLeft = 100; pointerDown(board, 100, 50, 2); @@ -60,79 +58,75 @@ describe("useBoardMousePan", () => { expect(board.scrollLeft).toBe(130); }); - it("does not pan before horizontal intent, vertically, or without overflow", () => { - const { board } = renderPanHarness(); - board.scrollLeft = 100; - - pointerDown(board); - pointerMove(board, 103); - pointerMove(board, 104, 110); - pointerUp(board); - expect(board.scrollLeft).toBe(100); - - Object.defineProperty(board, "scrollWidth", { configurable: true, value: 200 }); - pointerDown(board, 100, 50, 2); - pointerMove(board, 140, 50, 2); - pointerUp(board, 2); - expect(board.scrollLeft).toBe(100); - }); - - it("never starts from text, cards, or controls", () => { + it("leaves excluded interactive, editable, native-draggable, and task surfaces native", () => { const { board, getByTestId } = renderPanHarness(); board.scrollLeft = 100; - for (const [index, target] of [ - getByTestId("empty-text"), - getByTestId("card"), - getByTestId("button"), - getByTestId("input"), - ].entries()) { - const pointerId = index + 1; - pointerDown(target, 100, 50, pointerId); - pointerMove(target, 40, 50, pointerId); - pointerUp(target, pointerId); + for (const [index, target] of ["button", "input", "contenteditable", "draggable", "card"].map(getByTestId).entries()) { + pointerDown(target, 100, 50, index + 1); + pointerMove(target, 40, 50, index + 1); + pointerUp(target, index + 1); } expect(board.scrollLeft).toBe(100); expect(board).toHaveAttribute("data-panning", "false"); }); - it("ignores touch, pen, and non-primary mouse input", () => { - const { board } = renderPanHarness(); - board.scrollLeft = 100; - - for (const [pointerType, pointerId] of [["touch", 1], ["pen", 2]] as const) { - pointerDown(board, 100, 50, pointerId, pointerType); - pointerMove(board, 40, 50, pointerId, pointerType); - pointerUp(board, pointerId, pointerType); - } - pointerDown(board, 100, 50, 3, "mouse", 2); - pointerMove(board, 40, 50, 3); - pointerUp(board, 3); - - expect(board.scrollLeft).toBe(100); - expect(board).toHaveAttribute("data-panning", "false"); - }); - - it("only changes scrollLeft during pointer moves and suppresses one compatibility click after panning", () => { + it("is inert when disabled for mobile and for touch, pen, or non-primary input", () => { const onClick = vi.fn(); - const { board } = renderPanHarness(onClick); + const { board, getByTestId, rerender } = renderPanHarness(false, onClick); board.scrollLeft = 100; + const surface = getByTestId("surface"); - pointerDown(board); - pointerMove(board, 190); - const scrollAfterMove = board.scrollLeft; - pointerUp(board); - expect(board.scrollLeft).toBe(scrollAfterMove); - fireEvent.click(board); - fireEvent.click(board); - + pointerDown(surface); + pointerMove(surface, 40); + pointerUp(surface); + fireEvent.click(surface); + expect(board.scrollLeft).toBe(100); + expect(board).toHaveAttribute("data-panning", "false"); expect(onClick).toHaveBeenCalledTimes(1); + + rerender(); + board.scrollLeft = 100; + for (const [pointerType, pointerId] of [["touch", 1], ["pen", 2]] as const) { + pointerDown(getByTestId("surface"), 100, 50, pointerId, pointerType); + pointerMove(getByTestId("surface"), 40, 50, pointerId, pointerType); + pointerUp(getByTestId("surface"), pointerId, pointerType); + } + pointerDown(getByTestId("surface"), 100, 50, 3, "mouse", 2); + pointerMove(getByTestId("surface"), 40, 50, 3); + expect(board.scrollLeft).toBe(100); }); - it("cleans active feedback and click guards on cancellation, lost capture, and unmount", () => { + it("does not pan before horizontal intent, without overflow, or after a stationary edgeward pointer", () => { + const { board, getByTestId } = renderPanHarness(); + const surface = getByTestId("surface"); + board.scrollLeft = 100; + + pointerDown(surface); + pointerMove(surface, 103); + pointerMove(surface, 104, 110); + pointerUp(surface); + expect(board.scrollLeft).toBe(100); + + Object.defineProperty(board, "scrollWidth", { configurable: true, value: 200 }); + pointerDown(surface, 100, 50, 2); + pointerMove(surface, 190, 50, 2); + pointerUp(surface, 2); + expect(board.scrollLeft).toBe(100); + + Object.defineProperty(board, "scrollWidth", { configurable: true, value: 600 }); + pointerDown(surface, 100, 50, 3); + pointerMove(surface, 190, 50, 3); + const scrollAfterMove = board.scrollLeft; + pointerUp(surface, 3); + expect(board.scrollLeft).toBe(scrollAfterMove); + }); + + it("suppresses one compatibility click after a pan and cleans up cancellation, lost capture, and unmount", () => { const onClick = vi.fn(); - const { board, unmount } = renderPanHarness(onClick); + const { board, getByTestId, unmount } = renderPanHarness(true, onClick); + const surface = getByTestId("surface"); const setPointerCapture = vi.fn(); const hasPointerCapture = vi.fn(() => true); const releasePointerCapture = vi.fn(); @@ -142,22 +136,29 @@ describe("useBoardMousePan", () => { releasePointerCapture: { configurable: true, value: releasePointerCapture }, }); - pointerDown(board); - pointerMove(board, 140); - fireEvent.pointerCancel(board, { pointerId: 1 }); - expect(board).toHaveAttribute("data-panning", "false"); - fireEvent.click(board); + pointerDown(surface); + pointerMove(surface, 140); + pointerUp(surface); + fireEvent.click(surface); + fireEvent.click(surface); + expect(onClick).toHaveBeenCalledTimes(1); - pointerDown(board, 100, 50, 2); - pointerMove(board, 140, 50, 2); - fireEvent.lostPointerCapture(board, { pointerId: 2 }); + pointerDown(surface, 100, 50, 2); + pointerMove(surface, 140, 50, 2); + fireEvent.pointerCancel(surface, { pointerId: 2 }); expect(board).toHaveAttribute("data-panning", "false"); - fireEvent.click(board); + fireEvent.click(surface); - pointerDown(board, 100, 50, 3); + pointerDown(surface, 100, 50, 3); + pointerMove(surface, 140, 50, 3); + fireEvent.lostPointerCapture(surface, { pointerId: 3 }); + expect(board).toHaveAttribute("data-panning", "false"); + fireEvent.click(surface); + + pointerDown(surface, 100, 50, 4); unmount(); - expect(setPointerCapture).toHaveBeenCalledWith(3); - expect(releasePointerCapture).toHaveBeenCalledWith(3); - expect(onClick).toHaveBeenCalledTimes(2); + expect(setPointerCapture).toHaveBeenCalledWith(4); + expect(releasePointerCapture).toHaveBeenCalledWith(4); + expect(onClick).toHaveBeenCalledTimes(3); }); }); diff --git a/packages/dashboard/app/hooks/useBoardMousePan.ts b/packages/dashboard/app/hooks/useBoardMousePan.ts index c2593b3159..e015b66907 100644 --- a/packages/dashboard/app/hooks/useBoardMousePan.ts +++ b/packages/dashboard/app/hooks/useBoardMousePan.ts @@ -29,12 +29,21 @@ export interface BoardMousePanBindings { } /* -FNXC:BoardNavigation 2026-08-20-02:44: -Desktop Board navigation intentionally pans only from a direct primary-mouse drag on the bare -overflowing Board root. Descendant text and controls remain native, edge proximity can never -continue scrolling after pointer movement stops, and mobile touch remains owned by its separate -column-snap hook. +FNXC:BoardNavigation 2026-08-20-04:47: +Desktop and tablet Board navigation restores primary-mouse click-drag panning from safe, +noninteractive descendants such as empty-column text. Interactive, editable, native-draggable, +and task-card descendants keep their native behavior. Mobile owns touch scrolling and column snap, +so this hook is disabled there; edge proximity and stationary pointers never continue scrolling. */ +function isExcludedBoardPanTarget(target: EventTarget | null): boolean { + if (!(target instanceof Element)) return true; + return Boolean( + target.closest( + "button, a, input, textarea, select, option, label, summary, [contenteditable='true'], [draggable='true'], [data-id], [role='button'], [role='link'], [role='textbox'], [role='menuitem'], [role='checkbox'], [role='combobox'], [role='radio'], [role='slider'], [role='switch']", + ), + ); +} + function releasePointerCapture(session: BoardMousePanSession): void { const { element, pointerId } = session; try { @@ -46,7 +55,7 @@ function releasePointerCapture(session: BoardMousePanSession): void { } } -export function useBoardMousePan(boardElement: HTMLElement | null): BoardMousePanBindings { +export function useBoardMousePan(boardElement: HTMLElement | null, enabled: boolean): BoardMousePanBindings { const sessionRef = useRef(null); const didPanRef = useRef(false); const [isPanning, setIsPanning] = useState(false); @@ -63,9 +72,10 @@ export function useBoardMousePan(boardElement: HTMLElement | null): BoardMousePa const onPointerDown = useCallback((event: ReactPointerEvent) => { const element = event.currentTarget; if ( - event.pointerType !== "mouse" + !enabled + || event.pointerType !== "mouse" || event.button !== 0 - || event.target !== element + || isExcludedBoardPanTarget(event.target) || element.scrollWidth <= element.clientWidth ) { return; @@ -81,9 +91,10 @@ export function useBoardMousePan(boardElement: HTMLElement | null): BoardMousePa isPanning: false, }; element.setPointerCapture?.(event.pointerId); - }, []); + }, [enabled]); const onPointerMove = useCallback((event: ReactPointerEvent) => { + if (!enabled) return; const session = sessionRef.current; if (!session || session.pointerId !== event.pointerId) return; @@ -100,7 +111,7 @@ export function useBoardMousePan(boardElement: HTMLElement | null): BoardMousePa event.preventDefault(); session.element.scrollLeft = session.startScrollLeft - deltaX; - }, []); + }, [enabled]); const onPointerUp = useCallback((event: ReactPointerEvent) => { endSession(event.pointerId, false); @@ -115,11 +126,11 @@ export function useBoardMousePan(boardElement: HTMLElement | null): BoardMousePa }, [endSession]); const onClickCapture = useCallback((event: ReactMouseEvent) => { - if (!didPanRef.current) return; + if (!enabled || !didPanRef.current) return; didPanRef.current = false; event.preventDefault(); event.stopPropagation(); - }, []); + }, [enabled]); useEffect(() => () => { const session = sessionRef.current; @@ -127,7 +138,7 @@ export function useBoardMousePan(boardElement: HTMLElement | null): BoardMousePa sessionRef.current = null; didPanRef.current = false; setIsPanning(false); - }, [boardElement]); + }, [boardElement, enabled]); return { isPanning,