Skip to content

Single discrete mouse wheel tick never advances a slide: wheel-gestures willEndTimeout (400ms) exceeds Embla DragTracker logInterval (170ms), zeroing velocity #234

Description

@domaine-stijn-slats

Description

A single MX Mouse (ratchet mode) wheel tick produces exactly one WheelEvent. The plugin correctly starts a gesture and dispatches mousedown + mousemove (+120px), but the slide never advances — it snaps back to the current position.

Root cause: timing mismatch between wheel-gestures and Embla's DragTracker

The failure chain:

  1. Single wheel event arrives → wheel-gestures starts a gesture, publishes it, then schedules end() via willEnd() with willEndTimeout = WILL_END_TIMEOUT_DEFAULT = 400ms (because updateWillEndTimeout is only called after WHEELEVENTS_TO_MERGE = 2 events are merged — a single-event gesture never updates the timeout).

  2. The plugin dispatches mousedown + mousemove (+120px) synchronously during the wheel event handler. Both events have nearly identical timeStamp values.

  3. 400ms later (no further wheel events), end() fires → isEnding: true → plugin dispatches mouseup.

  4. Embla's DragTracker.pointerUp:

const expired = readTime(evt) - readTime(lastEvent) > logInterval; // logInterval = 170ms
const isFlick = diffTime && !expired && mathAbs(force) > 0.1;
return isFlick ? force : 0;

readTime(mouseup) - readTime(lastMousemove) ≈ 400ms > 170msexpired = true → returns 0.

  1. rawForce = 0 × forceBoost(300) = 0. allowedForce(0) falls into the position-only branch:
if (dragFree || mathAbs(force) < goToNextThreshold) return baseForce(force);

baseForce(0) finds the closest snap to the current drag position (120px on a 600px slide = 20%). Closest snap = current slide → snaps back.

The 50% position threshold is a red herring

The slide would advance if velocity (isFlick = true) were available — allowedForce would go to scrollTarget.byIndex(next, 0).distance directly. The position being < 50% only matters because velocity is already 0.

If willEndTimeout were < 170ms (e.g. 50ms), the mouseup would arrive before expired triggers, DragTracker would compute force = 120px / 50ms = 2.4px/ms, rawForce = 720, and allowedForce(720) > goToNextThreshold → slide advances. The timing mismatch is the sole cause.

Affected scenario

  • Discrete mouse wheel (ratchet mode): one click = one WheelEvent, no follow-up events
  • WILL_END_TIMEOUT_DEFAULT = 400ms is necessary for trackpad gesture detection, but fatal for single-event gestures
  • Any Embla carousel where the user expects one tick → one slide advance

Potential fixes

In embla-carousel-wheel-gestures: On isEnding && !isMomentum, if only a single wheel event was observed (no merged scroll points), call embla.scrollNext() / embla.scrollPrev() directly instead of dispatching mouseup through the drag pipeline — the drag physics path is irreparably broken for this case.

In wheel-gestures: Detect single-event gestures and use a short willEndTimeout (< 170ms) to fire end() before Embla's logInterval expires, preserving the velocity reading.

Versions

  • embla-carousel-wheel-gestures@9.0.0-rc01
  • embla-carousel@9.0.0-rc02
  • wheel-gestures@2.2.48

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions