Skip to content

Fix stuck pan when middle-button release lands on toolbar overlay#94

Merged
mucow24 merged 1 commit into
mainfrom
claude/cranky-stonebraker-c673f4
May 3, 2026
Merged

Fix stuck pan when middle-button release lands on toolbar overlay#94
mucow24 merged 1 commit into
mainfrom
claude/cranky-stonebraker-c673f4

Conversation

@mucow24
Copy link
Copy Markdown
Owner

@mucow24 mucow24 commented May 3, 2026

Summary

Middle-drag pan was sticking to the cursor (with no buttons held) when the user released the middle mouse button while hovering over the floating top toolbar — pan only stopped on the next click anywhere.

Root cause

Two compounding issues:

  • Dual-path pan architecture: Pixi pointermove drove pan when the cursor was inside the canvas; a window mousemove listener drove it when outside. The toolbar overlays the canvas, so neither handler could see the release once the cursor was over a toolbar element.
  • No button validation: Neither move handler checked that any mouse button was actually held, so once panDragRef was set, any future move kept panning.

What changed

  • Collapsed pan to a single window-level pointer-event listener (pointermove / pointerup / pointercancel).
  • Switched from mouse events to pointer events because Chromium fires only pointer events for CDP-driven middle-button gestures (verified via in-browser console diagnostic).
  • Added a defensive event.buttons === 0 recovery so a missed pointerup self-heals on the next move.
  • Removed the dual-path Pixi pan handler, the isClientPointInsideViewport early-return, the isPanDragging state, and the handleScenePointerMove plumbing through stageHandlers and CanvasStage — all dead after the collapse.

Net diff: -36 lines across 7 files.

TDD

Added a red-first jsdom test: while pan is active, dispatch a window pointermove with buttons: 0 and assert the pan was cleared. Verified failing before the fix, passing after.

Existing pan tests adapted to dispatch PointerEvent with buttons set (a PointerEvent polyfill was added in src/test/setup.ts since jsdom doesn't ship one). The synthetic test API in canvasTestApi.ts now tracks pressedButton through middle-button-only startPanDrag paths so subsequent pointermove dispatches report buttons=4 instead of 0.

Test plan

  • npm run preflight (lint + typecheck + unit + e2e)
  • New jsdom regression test passes
  • Manual: middle-drag pan from canvas, slide cursor onto top toolbar, release middle button there, move cursor back over canvas — pan must not follow
  • Sanity: spacebar+left-drag pan still works; pan-tool (Hand) drag still works; normal middle-drag release on the canvas still ends the pan

No Playwright e2e for the regression itself: headless Chromium on Linux/macOS doesn't reproduce the Windows middle-button trigger that surfaces the bug, so a passing e2e would be misleading coverage.

🤖 Generated with Claude Code

Middle-drag pan continued tracking the cursor until the user clicked
again if the release happened over the floating top toolbar. The dual-
path pan architecture (Pixi pointermove inside the viewport, window
mousemove outside) meant neither handler could see the release once the
cursor was over an overlay element, and neither validated that any
button was still held.

Collapse pan to a single window-level pointer event listener. Switch
from mouse events to pointer events because Chromium fires only pointer
events for CDP-driven middle-button gestures (verified via browser
console diagnostic). Add a buttons === 0 recovery check so a missed
pointerup self-heals on the next move.

Net diff is -36 lines: removed handleStagePointerMove from the viewport
hook, the handleScenePointerMove plumbing through stageHandlers and
CanvasStage, the isClientPointInsideViewport early-return, and the
isPanDragging state. Added a PointerEvent polyfill for jsdom and fixed
the synthetic test API to track pressedButton through middle-button
pan starts.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@mucow24 mucow24 merged commit 5b2f024 into main May 3, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant