Skip to content

Reset accumulated wheel delta in web WheelEventManager - #4484

Merged
m-bert merged 1 commit into
software-mansion:mainfrom
giaBaoJS:web-wheel-delta-reset
Sep 8, 2026
Merged

Reset accumulated wheel delta in web WheelEventManager#4484
m-bert merged 1 commit into
software-mansion:mainfrom
giaBaoJS:web-wheel-delta-reset

Conversation

@giaBaoJS

@giaBaoJS giaBaoJS commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Description

WheelEventManager has no pointer to follow, so it synthesizes coordinates by accumulating deltaX/deltaY on top of each wheel event's client coordinates. Its resetManager override calls only super.resetManager() and never clears wheelDelta, unlike PointerEventManager, which clears its own bookkeeping there.

resetManager runs on every handler reset (GestureHandler.reset -> delegate.reset -> manager.resetManager), which the orchestrator triggers once a gesture reaches END. So when a trackpad pan ends, the whole scroll distance of that gesture stays in wheelDelta.

To reproduce, put a Pan gesture with enableTrackpadTwoFingerGesture on a view, then do two two-finger trackpad pans in a row without moving the cursor in between. The second gesture reports absoluteX/absoluteY offset by the first gesture's total scroll. A two-finger scroll does not move the cursor, so the pointermove listener that clears the delta does not necessarily fire, and the offset keeps growing with each gesture.

Fix is to clear wheelDelta in resetManager, matching what PointerEventManager already does.

Test plan

Added src/web/tools/__tests__/WheelEventManager.test.ts with two cases:

  • deltas still accumulate across wheel events within one gesture (y is 130 after deltas of 100 and 30), so the accumulation behaviour is not lost.
  • after resetManager, the next wheel event reports only its own delta (y is 30, not 130).

The second test fails on main with Expected: 30, Received: 130 and passes with the fix. yarn jest src/web, yarn ts-check and eslint/prettier on the touched files all pass.

Copilot AI lite review requested due to automatic review settings September 6, 2026 04:09
@coderabbitai

coderabbitai Bot commented Sep 6, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Team

Run ID: 91f14035-8fe0-4773-a736-cc878ae296c2

📥 Commits

Reviewing files that changed from the base of the PR and between 41e0dd4 and 13d3fe0.

📒 Files selected for processing (2)
  • packages/react-native-gesture-handler/src/web/handlers/PanGestureHandler.ts
  • packages/react-native-gesture-handler/src/web/handlers/__tests__/PanGestureHandler.test.ts

Included review availability: Your plan provides up to 8 included reviews per hour; 7 remain after this review.


📝 Summary

Summary by CodeRabbit

  • Bug Fixes
    • Improved trackpad and mouse-wheel gesture handling so active gestures end correctly after wheel activity stops.
    • Ensured subsequent wheel gestures begin with fresh movement values instead of carrying over movement from the previous gesture.
    • Reset accumulated wheel movement when a gesture manager is reset, providing more consistent gesture tracking across interactions.

Walkthrough

Changes

Wheel gesture reset

Layer / File(s) Summary
Wheel end and delta reset
packages/react-native-gesture-handler/src/web/tools/WheelEventManager.ts, packages/react-native-gesture-handler/src/web/handlers/PanGestureHandler.ts
Wheel timeouts now reset active gestures. Mouse and touchpad paths use the updated scheduler. Reset operations clear accumulated wheel deltas.
Wheel reset validation
packages/react-native-gesture-handler/src/web/handlers/__tests__/PanGestureHandler.test.ts
Tests simulate wheel input, forward manager resets, and verify trackpad gestures end after 30 ms of inactivity. The next gesture starts with reset wheel coordinates.

Suggested reviewers: m-bert

Merge Risk: ⚪ Minimal · up to 13d3f

Web wheel gestures now clear prior gesture state and accumulated scroll offsets before the next gesture, preventing stale coordinates from carrying over. The covered reset behavior is ready to merge.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the primary change: resetting accumulated wheel delta in the web WheelEventManager.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The fix is small, targeted, and validated by focused tests that reproduce the reported failure mode.

Pull request overview

This PR fixes a web-specific coordinate drift issue in WheelEventManager by clearing its accumulated wheel delta when the manager is reset, aligning its bookkeeping behavior with other event managers and preventing stale deltas from affecting subsequent gestures.

Changes:

  • Reset wheelDelta inside WheelEventManager.resetManager() so accumulated deltas don’t leak across gestures.
  • Add Jest coverage to verify deltas still accumulate within a gesture, but are cleared after resetManager().
File summaries
File Description
packages/react-native-gesture-handler/src/web/tools/WheelEventManager.ts Clears stored wheelDelta on manager reset to prevent cross-gesture coordinate offset.
packages/react-native-gesture-handler/src/web/tools/__tests__/WheelEventManager.test.ts Adds regression tests covering accumulation behavior and reset behavior.
Review details
  • Files reviewed: 2/2 changed files
  • Comments generated: 0
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@m-bert m-bert left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @giaBaoJS! Thank you for submitting this PR! Unfortunately seems that it is not enough. The good news is, replacing

      if (this.state === State.ACTIVE) {
        this.end();
        this.tracker.removeFromTracker(event.pointerId);
        this.state = State.UNDETERMINED;
      }

with

      if (this.state === State.ACTIVE) {
        this.end();
		this.reset();
      }

in PanGestureHandler does the job.

The test passes as it calls manager.resetManager manually. Given that - I don't think we need this test file.

`WheelEventManager` synthesizes pointer coordinates by accumulating
`deltaX`/`deltaY` on top of the wheel event's client coordinates, since a
wheel does not move the cursor. Its `resetManager` override only called
`super.resetManager()` and left `wheelDelta` untouched, unlike
`PointerEventManager`, which clears its own bookkeeping there.

`PanGestureHandler.scheduleWheelEnd` ended the gesture by removing the
pointer from the tracker and assigning `State.UNDETERMINED` directly.
Because the handler never reached a finished state, the orchestrator's
`cleanupFinishedHandlers` microtask skipped it and `reset()` never ran,
so `GestureHandlerWebDelegate.reset` never reached the event managers.
Calling `reset()` covers both: `resetTracker` is a superset of the
single `removeFromTracker`, and it ends with the same state assignment.

The next wheel gesture on the same view then started from the previous
gesture's accumulated scroll distance and reported `absoluteX`/
`absoluteY` offset by it, growing with every gesture until a
`pointermove` happened to clear the delta.
@giaBaoJS
giaBaoJS force-pushed the web-wheel-delta-reset branch from 41e0dd4 to 13d3fe0 Compare September 8, 2026 02:46
@giaBaoJS

giaBaoJS commented Sep 8, 2026

Copy link
Copy Markdown
Contributor Author

Done, scheduleWheelEnd now calls reset(). Its event parameter became unused, so I dropped it.

The WheelEventManager.resetManager change is still needed alongside it. reset() reaches the wheel delta only through the delegate, and that path ends at resetManager():

// src/web/tools/GestureHandlerWebDelegate.ts:184
reset(): void {
  this.eventManagers.forEach((manager: EventManager<unknown>) =>
    manager.resetManager()
  );
}

With only the PanGestureHandler change, WheelEventManager.resetManager() is still just super.resetManager() and wheelDelta survives. I measured each half separately against the new test: reverting either one leaves the next gesture's first event at y: 130 instead of 30.

On why reset() never ran before: the old block assigned State.UNDETERMINED synchronously, so by the time the orchestrator's cleanupFinishedHandlers microtask looked at the handler, isFinished(handler.state) was false and cleanHandler (which calls handler.reset()) skipped it.

WheelEventManager.test.ts is gone. The case now lives in the existing web/handlers/__tests__/PanGestureHandler.test.ts: it dispatches real wheel events at a real WheelEventManager attached via handler.attachEventManager, advances the 30ms end timer, then checks the coordinates the next gesture reports. Nothing in the test calls resetManager; the delegate stub forwards reset() to the attached managers the way GestureHandlerWebDelegate does.

One note on the swap itself: reset() does more than the two lines it replaces. resetTracker() clears every tracked pointer and the velocity tracker rather than one pointer, and onReset() clears a pending activateAfterLongPress timeout. Both look correct for a gesture that has just ended, and the wheel path only tracks the synthetic -1 pointer anyway.

@m-bert m-bert left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The WheelEventManager.resetManager change is still needed alongside it.

I'm aware, that's why I said "it is not enough", not that it is wrong 😄

Thank you once again for this PR

@m-bert
m-bert merged commit 3ffb544 into software-mansion:main Sep 8, 2026
10 of 11 checks 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.

3 participants