1- import { ESchedulerPriority } from "../../lib" ;
2- import { debounce } from "../utils/schedule" ;
3-
41// Time in milliseconds to keep trackpad detection state
52const TRACKPAD_DETECTION_STATE_TIMEOUT = 60_000 ; // 1 minute
63
@@ -11,11 +8,11 @@ const TRACKPAD_DETECTION_STATE_TIMEOUT = 60_000; // 1 minute
118 * the input device type. The detection is based on several behavioral patterns:
129 *
1310 * - **Pinch-to-zoom gestures**: Trackpads generate wheel events with modifier keys (Ctrl/Meta)
14- * and continuous (non-integer ) delta values
11+ * and continuous (usually fractional ) delta values.
1512 * - **Horizontal scrolling**: Trackpads naturally produce horizontal scroll events (deltaX),
16- * while mice typically only scroll vertically
17- * - **Continuous scrolling**: Trackpad scroll deltas are usually fractional values, while mouse
18- * wheels produce discrete integer values
13+ * while mice typically only scroll vertically (unless Shift is pressed).
14+ * - **Continuous scrolling**: Trackpad scroll deltas are usually fractional or very small values,
15+ * while mouse wheels produce discrete larger integer values (typically 100 or 120).
1916 *
2017 * The detector maintains state across events to provide consistent results during a scroll session.
2118 * Once a trackpad is detected, the state persists for 60 seconds before resetting.
@@ -37,73 +34,95 @@ const TRACKPAD_DETECTION_STATE_TIMEOUT = 60_000; // 1 minute
3734 */
3835function isTrackpadDetector ( ) {
3936 let isTrackpadDetected = false ;
37+ let lastDetectionTime : number | null = null ;
38+ let lastDpr = 0 ;
4039
4140 /**
42- * Debounced function to reset trackpad detection state.
43- * Uses scheduler-based timing instead of setTimeout for consistency with the rest of the library.
44- */
45- const resetState = debounce (
46- ( ) => {
47- isTrackpadDetected = false ;
48- } ,
49- {
50- priority : ESchedulerPriority . LOWEST ,
51- frameTimeout : TRACKPAD_DETECTION_STATE_TIMEOUT ,
52- }
53- ) ;
54-
55- /**
56- * Marks the current input device as trackpad and schedules state reset.
41+ * Marks the current input device as trackpad and records the detection time.
5742 * This ensures consistent detection during continuous scroll operations.
5843 */
59- const markAsTrackpad = ( ) : void => {
44+ const markAsTrackpad = ( dpr : number ) : void => {
6045 isTrackpadDetected = true ;
61- resetState ( ) ;
46+ lastDetectionTime = performance . now ( ) ;
47+ lastDpr = dpr ;
6248 } ;
6349
6450 /**
6551 * Analyzes a wheel event to determine if it originated from a trackpad.
6652 *
6753 * @param e - The WheelEvent to analyze
6854 * @param dpr - Device pixel ratio for normalizing delta values. Defaults to window.devicePixelRatio.
69- * This normalization accounts for browser zoom levels to improve detection accuracy .
55+ * This normalization accounts for browser zoom levels.
7056 * @returns `true` if the event is from a trackpad, `false` if from a mouse wheel
7157 */
7258 return ( e : WheelEvent , dpr : number = globalThis . devicePixelRatio || 1 ) => {
59+ const now = performance . now ( ) ;
60+
61+ // Fast path: if we have valid cached detection state and DPR hasn't changed,
62+ // we already know it's a trackpad. We refresh the detection if we see more evidence below.
63+ if ( isTrackpadDetected && lastDetectionTime !== null ) {
64+ if ( now - lastDetectionTime < TRACKPAD_DETECTION_STATE_TIMEOUT && lastDpr === dpr ) {
65+ // We'll continue to check for new evidence to refresh the timeout
66+ } else {
67+ // State expired or DPR changed - need to re-evaluate
68+ isTrackpadDetected = false ;
69+ lastDetectionTime = null ;
70+ }
71+ }
72+
7373 const normalizedDeltaY = e . deltaY * dpr ;
7474 const normalizedDeltaX = e . deltaX * dpr ;
75- const hasFractionalDelta = normalizedDeltaY && ! Number . isInteger ( normalizedDeltaY ) ;
7675
77- // Detection 1: Pinch-to-zoom gesture
76+ // Detection 1: Small or fractional deltas
77+ // Trackpads produce fractional values or very small integers.
78+ // Mouse produce large integers
79+ const hasFractionalDelta =
80+ ! Number . isInteger ( e . deltaY ) ||
81+ ! Number . isInteger ( e . deltaX ) ||
82+ ! Number . isInteger ( normalizedDeltaY ) ||
83+ ! Number . isInteger ( normalizedDeltaX ) ;
84+
85+ const isSmallDelta = Math . abs ( e . deltaY ) < 50 && Math . abs ( e . deltaX ) < 50 ;
86+
87+ // Detection 2: Pinch-to-zoom gesture
7888 // Trackpad pinch-to-zoom generates wheel events with ctrlKey or metaKey.
79- // Combined with non-integer deltaY , this is a strong indicator of trackpad.
80- const isPinchToZoomGesture = ( e . ctrlKey || e . metaKey ) && hasFractionalDelta ;
81- if ( isPinchToZoomGesture ) {
82- markAsTrackpad ( ) ;
89+ // Combined with small or fractional deltas , this is a very strong indicator of trackpad.
90+ // Note: Mouse wheel with Ctrl pressed usually has large delta (e.g., 100).
91+ if ( ( e . ctrlKey || e . metaKey ) && ( hasFractionalDelta || isSmallDelta ) ) {
92+ markAsTrackpad ( dpr ) ;
8393 return true ;
8494 }
8595
86- // Detection 2 : Horizontal scroll (deltaX)
96+ // Detection 3 : Horizontal scroll (deltaX)
8797 // Trackpad naturally produces horizontal scroll events.
88- // Note: When Shift is pressed, browser swaps deltaX and deltaY for mouse wheel,
89- // so we skip this check to avoid false positives.
90- const hasHorizontalScroll = normalizedDeltaX !== 0 ;
91- const isShiftPressed = e . shiftKey ;
92- if ( hasHorizontalScroll && ! isShiftPressed ) {
93- markAsTrackpad ( ) ;
98+ // Note: When Shift is pressed, browsers swap deltaX and deltaY for mouse wheel,
99+ // so we skip this check to avoid false positives from mouse.
100+ if ( normalizedDeltaX !== 0 && ! e . shiftKey ) {
101+ markAsTrackpad ( dpr ) ;
94102 return true ;
95103 }
96104
97- // Detection 3: Fractional deltaY (mouse produces integer values)
98- // If we have non-integer deltaY without ctrl/meta keys, it's likely NOT trackpad
99- // (could be browser zoom or other factors), so we explicitly return false.
105+ // Detection 4: Smooth scrolling
106+ // If we have non-integer values, it's almost certainly a trackpad or high-precision scroll.
100107 if ( hasFractionalDelta ) {
101- return false ;
108+ markAsTrackpad ( dpr ) ;
109+ return true ;
110+ }
111+
112+ // Fallback: If we already detected a trackpad recently, stay in trackpad mode
113+ // unless we see obvious mouse-like behavior (large integer deltas).
114+ if ( isTrackpadDetected && lastDetectionTime !== null ) {
115+ // If it's still small delta, refresh the timestamp to keep trackpad state alive
116+ if ( isSmallDelta ) {
117+ lastDetectionTime = now ;
118+ }
119+ return true ;
102120 }
103121
104- // Fallback: Return previously detected state
105- // This helps maintain consistency across rapid scroll events.
106- return isTrackpadDetected ;
122+ // No trackpad detected
123+ isTrackpadDetected = false ;
124+ lastDetectionTime = null ;
125+ return false ;
107126 } ;
108127}
109128
0 commit comments