Skip to content

fix(gestures): end drag/press from capture phase so child stopPropagation can't trap the gesture#3731

Open
mattgperry wants to merge 1 commit into
mainfrom
worktree-fix-issue-2794
Open

fix(gestures): end drag/press from capture phase so child stopPropagation can't trap the gesture#3731
mattgperry wants to merge 1 commit into
mainfrom
worktree-fix-issue-2794

Conversation

@mattgperry
Copy link
Copy Markdown
Collaborator

Summary

Fixes #2794.

When a child of a drag element calls event.stopPropagation() inside its own onPointerUp handler, the drag gesture never ended — the element kept following the cursor until the next pointer interaction.

Cause

PanSession attaches its pointermove / pointerup / pointercancel listeners to window in the bubble phase. A descendant calling stopPropagation() on pointerup stops the event before it reaches window, so handlePointerUp never runs, the session is never torn down, and pointermove keeps dragging the element.

Fix

  • PanSession: attach the gesture pointer listeners in the capture phase, so descendant stopPropagation() can no longer prevent the gesture from ending.
  • addDomEvent: pass the listener options to removeEventListener, otherwise capture-phase listeners are never removed (capture flag must match) and would leak.
  • press gesture: move its end (pointerup / pointercancel) listeners to the capture phase as well. This keeps the existing "a drag suppresses the tap" ordering intact (press still observes isDragActive() before the drag releases its lock) and makes the press/tap gesture robust to the same stopPropagation problem (the issue's repro literally stops propagation on pointerup).

The fix is internal only — no public API changes.

Test plan

  • Added a regression test in the drag suite: a drag element whose child calls stopPropagation() in onPointerUp — asserts onDragEnd fires. Verified it fails against the unfixed code and passes with the fix.
  • yarn build succeeds.
  • yarn test — full suite green (798 passed, 7 pre-existing skips).
  • Gesture suites (drag, pan, press, tap, hover) all pass, confirming no regression to the drag-suppresses-tap behavior.

🤖 Generated with Claude Code

A descendant calling event.stopPropagation() in its own pointerup
handler prevented the PanSession's window-level pointerup listener
from firing, so the drag gesture never ended and the element kept
following the cursor.

Attach the gesture-ending pointer listeners in the capture phase so
descendant stopPropagation() can no longer break gesture cleanup, and
pass listener options through to removeEventListener so capture-phase
listeners are removed correctly. The press gesture's end listeners are
moved to the capture phase too, which keeps the drag-suppresses-tap
ordering intact and makes press robust to the same issue.

Fixes #2794

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented May 15, 2026

Greptile Summary

This PR fixes a bug where a child element calling event.stopPropagation() in its onPointerUp handler could prevent PanSession and press gestures from ending. The fix moves the gesture-end listeners (pointerup, pointercancel) — and pointermove for PanSession — to the capture phase on window, so they fire before any descendant handlers. It also corrects a latent bug in addDomEvent where removeEventListener was called without the options object, causing capture-phase listeners to silently leak.

  • addDomEvent: passes options to removeEventListener so the capture flag matches — without this, every capture listener added after this PR would have leaked.
  • PanSession: registers pointermove, pointerup, and pointercancel with { passive: true, capture: true }, and adds a targeted regression test (#2794).
  • press/index.ts: builds endEventOptions by spreading eventOptions with capture: true, keeping the abort-signal cleanup path intact while matching the capture flag on both add and remove.

Confidence Score: 5/5

Safe to merge — the change is surgical and well-tested, touching only the event-listener registration phase without altering any gesture logic.

All three code changes are internally consistent: addDomEvent now correctly mirrors its addEventListener options in removeEventListener, PanSession registers in capture with matching cleanup, and press derives endEventOptions from the shared eventOptions so the abort-signal cleanup path remains intact. The regression test directly reproduces the reported issue and verifies the fix. No public API is touched.

No files require special attention.

Important Files Changed

Filename Overview
packages/motion-dom/src/events/add-dom-event.ts Critical one-line bug fix: passes options to removeEventListener so capture-phase listeners are actually removed; previously leaked silently.
packages/framer-motion/src/gestures/pan/PanSession.ts Adds { passive: true, capture: true } to the three gesture listeners so descendant stopPropagation() calls cannot suppress drag end.
packages/motion-dom/src/gestures/press/index.ts Derives endEventOptions from the existing eventOptions (preserving passive and abort signal) and adds capture: true for the pointerup/pointercancel end listeners.
packages/framer-motion/src/gestures/drag/tests/index.test.tsx Adds a regression test that fires pointerUp on a child with stopPropagation and asserts onDragEnd fires; correctly imports pointerUp from the test setup.

Sequence Diagram

sequenceDiagram
    participant Child
    participant Window (capture)
    participant Window (bubble)

    Note over Child,Window (bubble): Before fix — child stopPropagation breaks drag end
    Child->>Window (bubble): pointerup bubbles up
    Child--xWindow (bubble): stopPropagation() blocks window handler
    Note over Window (bubble): handlePointerUp never runs → drag leaks

    Note over Child,Window (bubble): After fix — capture phase fires before child handlers
    Window (capture)->>Window (capture): handlePointerUp (capture) fires first
    Note over Window (capture): Drag/press gesture ends correctly
    Child->>Child: onPointerUp fires → stopPropagation()
    Note over Child: Nothing left to block
Loading

Reviews (1): Last reviewed commit: "fix(gestures): end drag/press gestures f..." | Re-trigger Greptile

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.

[BUG]When using the drag gesture and calling stopPropagation inside onPointerUp, the scroll element starts following the cursor.

1 participant