Skip to content

Mouse Wheel Zoom Jumps to Maximum Instead of Step-by-Step Zoom #975

@kornelko2

Description

@kornelko2

Description

When using Ctrl + Mouse Wheel to zoom in/out on the timeline, the zoom jumps directly to maximum zoom instead of zooming step-by-step. This issue does NOT occur with trackpad scrolling, only with physical mouse wheels.

Environment

  • Package: [email protected]
  • Browser: Chrome/Firefox/Edge (all affected)
  • Input Device: Physical mouse with scroll wheel
  • OS: Windows (likely affects all platforms)

Expected Behavior

When scrolling with Ctrl + Mouse Wheel:

  • Each scroll tick should zoom in/out by a small, consistent amount
  • Multiple scroll ticks should progressively zoom in or out
  • Behavior should be smooth and controllable, similar to trackpad zoom

Actual Behavior

When scrolling with Ctrl + Mouse Wheel:

  • The zoom jumps dramatically (approximately 3x per scroll tick)
  • Often reaches maximum or minimum zoom in just 1-2 scroll ticks
  • Makes it impossible to find the desired zoom level
  • Trackpad works correctly with smooth, gradual zoom

Root Cause

The issue is in the handleWheel method in src/lib/scroll/ScrollElement.tsx (lines 73-95) and handleWheelZoom in src/lib/Timeline.tsx (lines 512-516).

Current Implementation:

// In ScrollElement.tsx
handleWheel = (e: WheelEvent) => {
  if (e.ctrlKey || e.metaKey || e.altKey) {
    e.preventDefault()
    const parentPosition = getParentPosition(e.currentTarget as HTMLElement)
    const xPosition = e.clientX - parentPosition.x
    
    const speed = e.ctrlKey ? 10 : e.metaKey ? 3 : 1
    
    this.props.onWheelZoom(speed, xPosition, e.deltaY)
  }
}

// In Timeline.tsx
handleWheelZoom = (speed: number, xPosition: number, deltaY: number) => {
  this.changeZoom(1.0 + (speed * deltaY) / 500, xPosition / this.state.width)
}

The Problem:

The deltaY value varies significantly between input devices:

  • Mouse Wheel: Sends deltaY values of ±100 or more per tick
  • Trackpad: Sends deltaY values of ±1 to ±20 per scroll event

With Ctrl pressed (speed = 10), the zoom calculation becomes:

  • Mouse Wheel: 1.0 + (10 * 100) / 500 = 3.03x zoom per tick
  • Trackpad: 1.0 + (10 * 5) / 500 = 1.11.1x zoom per tick

This causes mouse wheel users to experience extreme zoom jumps while trackpad users get smooth zoom.

Proposed Fix

File to Modify: src/lib/scroll/ScrollElement.tsx

Location: Lines 73-95 (in the handleWheel method)

Current Code:

handleWheel = (e: WheelEvent) => {
  //const { traditionalZoom } = this.props

  // zoom in the time dimension
  if (e.ctrlKey || e.metaKey || e.altKey) {
    e.preventDefault()
    const parentPosition = getParentPosition(e.currentTarget as HTMLElement)
    const xPosition = e.clientX - parentPosition.x

    const speed = e.ctrlKey ? 10 : e.metaKey ? 3 : 1

    // convert vertical zoom to horiziontal
    this.props.onWheelZoom(speed, xPosition, e.deltaY)
  } else if (e.shiftKey) {
    e.preventDefault()
    // shift+scroll event from a touchpad has deltaY property populated; shift+scroll event from a mouse has deltaX
    this.props.onScroll(this.scrollComponentRef.current!.scrollLeft + (e.deltaY || e.deltaX))
    // no modifier pressed? we prevented the default event, so scroll or zoom as needed
  }
}

Fixed Code (Solution 1 - Simple Clamping - RECOMMENDED):

handleWheel = (e: WheelEvent) => {
  //const { traditionalZoom } = this.props

  // zoom in the time dimension
  if (e.ctrlKey || e.metaKey || e.altKey) {
    e.preventDefault()
    const parentPosition = getParentPosition(e.currentTarget as HTMLElement)
    const xPosition = e.clientX - parentPosition.x

    const speed = e.ctrlKey ? 10 : e.metaKey ? 3 : 1

    // Normalize deltaY to prevent jumps with mouse wheels
    // Mouse wheels send large values (100+), trackpads send small values (1-20)
    const MAX_DELTA = 20
    const normalizedDeltaY = Math.sign(e.deltaY) * Math.min(Math.abs(e.deltaY), MAX_DELTA)

    // convert vertical zoom to horiziontal
    this.props.onWheelZoom(speed, xPosition, normalizedDeltaY)
  } else if (e.shiftKey) {
    e.preventDefault()
    // shift+scroll event from a touchpad has deltaY property populated; shift+scroll event from a mouse has deltaX
    this.props.onScroll(this.scrollComponentRef.current!.scrollLeft + (e.deltaY || e.deltaX))
    // no modifier pressed? we prevented the default event, so scroll or zoom as needed
  }
}

What Changed:

Added 3 lines (between const speed = ... and this.props.onWheelZoom):

const MAX_DELTA = 20
const normalizedDeltaY = Math.sign(e.deltaY) * Math.min(Math.abs(e.deltaY), MAX_DELTA)

Changed 1 line:

// OLD:
this.props.onWheelZoom(speed, xPosition, e.deltaY)

// NEW:
this.props.onWheelZoom(speed, xPosition, normalizedDeltaY)

Alternative Solutions

Solution 2: Use deltaMode Detection

handleWheel = (e: WheelEvent) => {
  if (e.ctrlKey || e.metaKey || e.altKey) {
    e.preventDefault()
    const parentPosition = getParentPosition(e.currentTarget as HTMLElement)
    const xPosition = e.clientX - parentPosition.x
    
    const speed = e.ctrlKey ? 10 : e.metaKey ? 3 : 1
    
    // deltaMode: 0 = pixels (trackpad), 1 = lines (mouse wheel), 2 = pages
    let normalizedDeltaY = e.deltaY
    if (e.deltaMode === 1) {
      // Mouse wheel in line mode - scale down
      normalizedDeltaY = e.deltaY * 0.2
    }
    
    this.props.onWheelZoom(speed, xPosition, normalizedDeltaY)
  }
  // ... rest of code
}

Solution 3: Adjust Speed Multiplier (Less Effective)

// In handleWheel method, change this line:
const speed = e.ctrlKey ? 2 : e.metaKey ? 1.5 : 1  // Reduced from 10, 3, 1

Note: This reduces zoom speed for everyone, including trackpad users.


Testing the Fix

Before Fix:

  1. Hold Ctrl and scroll mouse wheel once
  2. Result: Zoom jumps 3x (from year view to day view in one tick)

After Fix:

  1. Hold Ctrl and scroll mouse wheel once
  2. Result: Zoom increases by ~10-20% per tick
  3. Multiple scrolls needed to reach desired zoom level

Test Both Devices:

  • Mouse Wheel: Should zoom smoothly, step-by-step ✅
  • Trackpad: Should still zoom smoothly (unchanged behavior) ✅

Complete Diff for Pull Request

diff --git a/src/lib/scroll/ScrollElement.tsx b/src/lib/scroll/ScrollElement.tsx
index 1234567..abcdefg 100644
--- a/src/lib/scroll/ScrollElement.tsx
+++ b/src/lib/scroll/ScrollElement.tsx
@@ -78,8 +78,13 @@ class ScrollElement extends Component<Props, State> {
       const xPosition = e.clientX - parentPosition.x
 
       const speed = e.ctrlKey ? 10 : e.metaKey ? 3 : 1
 
+      // Normalize deltaY to prevent jumps with mouse wheels
+      // Mouse wheels send large values (100+), trackpads send small values (1-20)
+      const MAX_DELTA = 20
+      const normalizedDeltaY = Math.sign(e.deltaY) * Math.min(Math.abs(e.deltaY), MAX_DELTA)
+
       // convert vertical zoom to horiziontal
-      this.props.onWheelZoom(speed, xPosition, e.deltaY)
+      this.props.onWheelZoom(speed, xPosition, normalizedDeltaY)
     } else if (e.shiftKey) {
       e.preventDefault()
       // shift+scroll event from a touchpad has deltaY property populated; shift+scroll event from a mouse has deltaX

Implementation Steps for Maintainers

  1. Clone the repository

    git clone https://github.com/namespace-ee/react-calendar-timeline.git
    cd react-calendar-timeline
  2. Create a feature branch

    git checkout -b fix/normalize-mouse-wheel-delta
  3. Edit the file: src/lib/scroll/ScrollElement.tsx

    • Find line ~83: const speed = e.ctrlKey ? 10 : e.metaKey ? 3 : 1
    • Add the normalization code after it
    • Update the onWheelZoom call
  4. Test the changes

    npm install
    npm run start
    # Open demo in browser
    # Test with both mouse wheel AND trackpad
  5. Run tests

    npm test
  6. Commit and push

    git add src/lib/scroll/ScrollElement.tsx
    git commit -m "fix: normalize wheel deltaY to prevent mouse wheel zoom jumps
    
    - Clamp deltaY values to MAX_DELTA (20) to handle mouse wheels
    - Mouse wheels send deltaY ~100+, causing 3x zoom per tick
    - Trackpads send deltaY 1-20, already work correctly
    - Fixes zoom jumping to max in 1-2 scroll ticks"
    
    git push origin fix/normalize-mouse-wheel-delta
  7. Create Pull Request

    • Go to GitHub repository
    • Create PR from your branch to main
    • Link to this issue in the description

Why MAX_DELTA = 20?

Testing shows:

  • Trackpads: Typical deltaY range is ±1 to ±20
  • Mouse wheels: Typical deltaY range is ±100 to ±120

Setting MAX_DELTA = 20:

  • ✅ Preserves trackpad behavior (no change)
  • ✅ Normalizes mouse wheel to trackpad range
  • ✅ Results in smooth, controllable zoom for both

With the current formula:

zoom = 1.0 + (speed * deltaY) / 500

// With MAX_DELTA = 20:
// Ctrl + mouse wheel: 1.0 + (10 * 20) / 500 = 1.4 → 1.4x per tick ✅
// Ctrl + trackpad:    1.0 + (10 * 5) / 500  = 1.1 → 1.1x per tick ✅

Workaround for Users

Users can work around this issue by intercepting the wheel event before it reaches the library:

useEffect(() => {
  const scrollElement = document.querySelector('.rct-scroll');
  if (!scrollElement) return;

  const MAX_DELTA_Y = 10;
  
  const handleWheel = (e) => {
    if (e.ctrlKey || e.metaKey || e.altKey) {
      const normalizedDeltaY = Math.sign(e.deltaY) * Math.min(Math.abs(e.deltaY), MAX_DELTA_Y);
      
      if (Math.abs(e.deltaY) > MAX_DELTA_Y) {
        e.stopImmediatePropagation();
        e.preventDefault();
        
        const normalizedEvent = new WheelEvent('wheel', {
          ...e,
          deltaY: normalizedDeltaY,
          bubbles: true,
          cancelable: true,
        });
        
        e.currentTarget.dispatchEvent(normalizedEvent);
      }
    }
  };

  scrollElement.addEventListener('wheel', handleWheel, { capture: true, passive: false });

  return () => {
    scrollElement.removeEventListener('wheel', handleWheel, { capture: true });
  };
}, []);

Steps to Reproduce

  1. Create a timeline with the traditionalZoom={true} prop
  2. Use a physical mouse with scroll wheel
  3. Hold Ctrl key and scroll with the mouse wheel
  4. Observe that zoom jumps to maximum in 1-2 scroll ticks

Additional Context

  • The issue has been present across multiple versions
  • Affects all browsers (Chrome, Firefox, Edge, Safari)
  • Trackpad users are not affected due to smaller deltaY values
  • The documentation mentions "ctrl + mousewheel = zoom in/out 10× faster" which compounds the issue

Suggested Priority

High - This affects usability for all mouse wheel users, making the zoom feature nearly unusable without a trackpad.

Related Issues

Links

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions