-
Notifications
You must be signed in to change notification settings - Fork 678
Description
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
deltaYvalues of ±100 or more per tick - Trackpad: Sends
deltaYvalues of ±1 to ±20 per scroll event
With Ctrl pressed (speed = 10), the zoom calculation becomes:
- Mouse Wheel:
1.0 + (10 * 100) / 500 = 3.0→ 3x zoom per tick ❌ - Trackpad:
1.0 + (10 * 5) / 500 = 1.1→ 1.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, 1Note: This reduces zoom speed for everyone, including trackpad users.
Testing the Fix
Before Fix:
- Hold
Ctrland scroll mouse wheel once - Result: Zoom jumps 3x (from year view to day view in one tick)
After Fix:
- Hold
Ctrland scroll mouse wheel once - Result: Zoom increases by ~10-20% per tick
- 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 deltaXImplementation Steps for Maintainers
-
Clone the repository
git clone https://github.com/namespace-ee/react-calendar-timeline.git cd react-calendar-timeline -
Create a feature branch
git checkout -b fix/normalize-mouse-wheel-delta
-
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
onWheelZoomcall
- Find line ~83:
-
Test the changes
npm install npm run start # Open demo in browser # Test with both mouse wheel AND trackpad
-
Run tests
npm test -
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
-
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
- Create a timeline with the
traditionalZoom={true}prop - Use a physical mouse with scroll wheel
- Hold
Ctrlkey and scroll with the mouse wheel - 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
- Chrome 73 Treats Document Level Wheel/Mousewheel Event Listeners as Passive #541 - Wheel/Mousewheel Event errors on chrome 73 (mentioned in CHANGELOG v0.25.1)
- Shift+Scroll doesn't scroll the timeline canvas horizontally on Mac #281 - Shift + Scroll via mouse wheel scrolls canvas horizontally (CHANGELOG v0.15.12)
Links
- Repository: https://github.com/namespace-ee/react-calendar-timeline
- NPM: https://www.npmjs.com/package/react-calendar-timeline
- Source files affected:
src/lib/scroll/ScrollElement.tsx(handleWheel method)src/lib/Timeline.tsx(handleWheelZoom method)