• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

IgniteUI / igniteui-angular / 30899782753

04 Aug 2026 10:14AM UTC coverage: 90.187% (-0.003%) from 90.19%
30899782753

Pull #17425

github

web-flow
Merge b8d071dbb into 0a0755cbb
Pull Request #17425: fix(grid): Recalculate horizontal size cache when scrolling horizontally with autosized columns

14972 of 17434 branches covered (85.88%)

Branch coverage included in aggregate %.

2 of 2 new or added lines in 1 file covered. (100.0%)

66 existing lines in 4 files now uncovered.

30100 of 32542 relevant lines covered (92.5%)

37585.15 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

88.76
/projects/igniteui-angular/core/src/core/touch.ts
1
import { NgZone } from '@angular/core';
2

3
/**
4
 * Normalized gesture event emitted by {@link IgxTouchManager}.
5
 *
6
 * It intentionally mirrors the small subset of the previous Hammer.js input shape
7
 * (`deltaX`, `deltaY`, `center`, `distance`, `pointerType`) so existing gesture
8
 * handlers can consume it without changes.
9
 *
10
 * @hidden
11
 * @internal
12
 */
13
export interface IgxGestureEvent {
14
    /** The type of the pointer that produced the gesture (e.g. `touch`, `pen`, `mouse`). */
15
    pointerType: string;
16
    /** Horizontal distance (in px) from the gesture origin to the current pointer position. */
17
    deltaX: number;
18
    /** Vertical distance (in px) from the gesture origin to the current pointer position. */
19
    deltaY: number;
20
    /** Euclidean distance (in px) from the gesture origin to the current pointer position. */
21
    distance: number;
22
    /** Gesture velocity in px/ms, measured from the gesture origin to the current pointer position. */
23
    velocity: number;
24
    /** Current pointer position. */
25
    center: { x: number; y: number };
26
    /** The original event target. */
27
    target: EventTarget;
28
    /** The underlying native pointer event. */
29
    originalEvent: PointerEvent;
30
    /** Prevents the default action of the underlying native pointer event. */
31
    preventDefault: () => void;
32
    /**
33
     * Resets the gesture origin to the current pointer position so that subsequent
34
     * `deltaX`/`deltaY`/`distance` values are measured relative to it.
35
     */
36
    resetOrigin: () => void;
37
}
38

39
/**
40
 * Callbacks invoked by {@link IgxTouchManager} for the recognized gestures.
41
 *
42
 * @hidden
43
 * @internal
44
 */
45
export interface IgxTouchManagerCallbacks {
46
    /**
47
     * Fired on pointer down once the pointer type passes the configured filter, before any movement.
48
     *
49
     * Return `false` to veto the gesture (e.g. when the touch does not start in an active zone).
50
     * The manager then stops tracking immediately and best-effort releases the pointer capture, so
51
     * normal page/component scrolling is not blocked by the `touchmove` listener.
52
     */
53
    pointerDown?: (event: IgxGestureEvent) => boolean | void;
54
    /** Fired on the first pointer move of a tracked gesture, once movement begins (mirrors Hammer's `panstart`). */
55
    panStart?: (event: IgxGestureEvent) => void;
56
    /** Fired on each pointer move while a gesture is tracked. */
57
    panMove?: (event: IgxGestureEvent) => void;
58
    /** Fired on pointer up at the end of a tracked gesture. */
59
    panEnd?: (event: IgxGestureEvent) => void;
60
    /** Fired on pointer cancel for a tracked gesture. */
61
    panCancel?: (event: IgxGestureEvent) => void;
62
    /** Fired on pointer up when the movement stays below `tapThreshold`. Suppresses `panEnd`. */
63
    tap?: (event: IgxGestureEvent) => void;
64
    /** Fired on pointer up for a fast, primarily horizontal gesture, before `panEnd`. */
65
    swipe?: (event: IgxGestureEvent) => void;
66
}
67

68
/**
69
 * Options controlling how {@link IgxTouchManager} recognizes gestures.
70
 *
71
 * @hidden
72
 * @internal
73
 */
74
export interface IgxTouchManagerOptions {
75
    /** Pointer types to handle. Defaults to `['touch', 'pen']` (mouse excluded). */
76
    pointerTypes?: string[];
77
    /** Whether to capture the pointer on the target during a gesture. Defaults to `true`. */
78
    setPointerCapture?: boolean;
79
    /** Maximum movement (in px) for a pointer up to be recognized as a tap. Defaults to `0` (disabled). */
80
    tapThreshold?: number;
81
    /** Minimum velocity (in px/ms) for a primarily horizontal gesture to be recognized as a swipe. Defaults to `0.3`. */
82
    swipeVelocityThreshold?: number;
83
    /**
84
     * Predicate evaluated on pointer down. When it returns `false` the gesture is not tracked at all,
85
     * so no pointer capture is taken and the native scrolling of the page is not prevented.
86
     * Defaults to always tracking.
87
     */
88
    canStart?: (event: PointerEvent) => boolean;
89
    /**
90
     * When provided, the listeners are attached outside of the Angular zone so that the high frequency
91
     * `panMove` callback does not trigger change detection. The remaining callbacks are still invoked
92
     * inside the Angular zone.
93
     */
94
    ngZone?: NgZone;
95
}
96

97
/**
98
 * Lightweight, zoneless pointer-based gesture manager.
99
 *
100
 * Consolidates the pan/swipe/tap recognition logic shared across components
101
 * (carousel, navigation drawer, list item, time picker) on top of native
102
 * Pointer Events. It does not require `NgZone`; consumers update their own
103
 * state inside the provided callbacks. An `NgZone` can optionally be supplied so
104
 * that the listeners are attached outside of Angular and only the discrete
105
 * callbacks re-enter it, keeping continuous dragging free of change detection.
106
 *
107
 * Outside of a browser environment (e.g. during server-side rendering) it is a
108
 * noop: no listeners are attached and no callbacks are invoked, so consumers can
109
 * construct it unconditionally without additional platform guards.
110
 *
111
 * @hidden
112
 * @internal
113
 *
114
 * @example
115
 * ```ts
116
 * this._gestures = new IgxTouchManager(this.element.nativeElement, {
117
 *     panMove: (e) => this.pan(e),
118
 *     panEnd: (e) => this.onPanEnd(e),
119
 *     tap: (e) => this.onTap(e)
120
 * }, { tapThreshold: 5 });
121
 * // ...
122
 * this._gestures.destroy();
123
 * ```
124
 */
125
export class IgxTouchManager {
126
    private _startX = 0;
2,406✔
127
    private _startY = 0;
2,406✔
128
    private _startTime = 0;
2,406✔
129
    private _tracking = false;
2,406✔
130
    private _panStarted = false;
2,406✔
131
    private _pointerId: number | null = null;
2,406✔
132
    private _startTarget: EventTarget | null = null;
2,406✔
133
    private readonly _supported: boolean;
134
    private readonly _pointerTypes: string[];
135
    private readonly _setPointerCapture: boolean;
136
    private readonly _tapThreshold: number;
137
    private readonly _swipeVelocityThreshold: number;
138
    private readonly _canStart: ((event: PointerEvent) => boolean) | null;
139
    private readonly _ngZone: NgZone | null;
140

141
    constructor(
142
        private target: EventTarget,
2,406✔
143
        private callbacks: IgxTouchManagerCallbacks,
2,406✔
144
        options: IgxTouchManagerOptions = {}
204✔
145
    ) {
146
        this._pointerTypes = options.pointerTypes ?? ['touch', 'pen'];
2,406✔
147
        this._setPointerCapture = options.setPointerCapture ?? true;
2,406✔
148
        this._tapThreshold = options.tapThreshold ?? 0;
2,406✔
149
        this._swipeVelocityThreshold = options.swipeVelocityThreshold ?? 0.3;
2,406✔
150
        this._canStart = options.canStart ?? null;
2,406✔
151
        this._ngZone = options.ngZone ?? null;
2,406✔
152

153
        // Behave as a noop outside of a browser (e.g. during server-side rendering),
154
        // mirroring the previous Hammer.js-based manager. Angular's platform-server
155
        // does not define a global `window`, so pointer events can never occur there
156
        // and attaching listeners would only add dead weight (or fail on partial DOMs).
157
        this._supported = typeof window !== 'undefined'
2,406✔
158
            && !!target
159
            && typeof (target as EventTarget).addEventListener === 'function';
160
        if (!this._supported) {
2,406!
UNCOV
161
            return;
×
162
        }
163

164
        this._runOutsideAngular(() => {
2,406✔
165
            this.target.addEventListener('pointerdown', this._onPointerDown);
2,406✔
166
            this.target.addEventListener('pointermove', this._onPointerMove);
2,406✔
167
            this.target.addEventListener('pointerup', this._onPointerUp);
2,406✔
168
            this.target.addEventListener('pointercancel', this._onPointerCancel);
2,406✔
169
            // prevents the default scrolling behavior on touch devices while a gesture is tracked
170
            // necessary on edge pans detected on the body as the browsers cancel the pointer events
171
            // and trigger a scroll / rubber band effect instead of the pan
172
            this.target.addEventListener('touchmove', this._onTouchMove, { passive: false });
2,406✔
173
        });
174
    }
175

176
    /** Detaches all listeners and stops tracking. */
177
    public destroy(): void {
178
        if (!this._supported) {
2,408!
UNCOV
179
            return;
×
180
        }
181
        this.target.removeEventListener('pointerdown', this._onPointerDown);
2,408✔
182
        this.target.removeEventListener('pointermove', this._onPointerMove);
2,408✔
183
        this.target.removeEventListener('pointerup', this._onPointerUp);
2,408✔
184
        this.target.removeEventListener('pointercancel', this._onPointerCancel);
2,408✔
185
        this.target.removeEventListener('touchmove', this._onTouchMove);
2,408✔
186
        this._tracking = false;
2,408✔
187
    }
188

189
    private _accepts(pointerType: string): boolean {
190
        return this._pointerTypes.includes(pointerType);
94✔
191
    }
192

193
    private _runOutsideAngular(fn: () => void): void {
194
        if (this._ngZone) {
2,406✔
195
            this._ngZone.runOutsideAngular(fn);
2,126✔
196
        } else {
197
            fn();
280✔
198
        }
199
    }
200

201
    /** Invokes a discrete (low frequency) callback back inside the Angular zone, when one is provided. */
202
    private _runInAngular(fn: () => void): void {
203
        if (this._ngZone) {
56✔
204
            this._ngZone.run(fn);
39✔
205
        } else {
206
            fn();
17✔
207
        }
208
    }
209

210
    private _createEvent(event: PointerEvent): IgxGestureEvent {
211
        const deltaX = event.clientX - this._startX;
66✔
212
        const deltaY = event.clientY - this._startY;
66✔
213
        const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
66✔
214
        const elapsed = Date.now() - this._startTime;
66✔
215
        const velocity = elapsed > 0 ? distance / elapsed : 0;
66✔
216

217
        return {
66✔
218
            pointerType: event.pointerType,
219
            deltaX,
220
            deltaY,
221
            distance,
222
            velocity,
223
            center: { x: event.clientX, y: event.clientY },
224
            // Report the element the gesture started on. Pointer capture retargets
225
            // subsequent pointer events to the captured host, so `event.target` would
226
            // otherwise no longer reflect the pressed element (e.g. for taps).
227
            target: this._startTarget ?? event.target,
66!
228
            originalEvent: event,
UNCOV
229
            preventDefault: () => event.preventDefault(),
×
230
            resetOrigin: () => {
231
                this._startX = event.clientX;
4✔
232
                this._startY = event.clientY;
4✔
233
                this._startTime = Date.now();
4✔
234
            }
235
        };
236
    }
237

238
    private _onPointerDown = (event: PointerEvent) => {
2,406✔
239
        if (this._tracking || !this._accepts(event.pointerType) || this._canStart?.(event) === false) {
37✔
240
            return;
5✔
241
        }
242
        this._startX = event.clientX;
32✔
243
        this._startY = event.clientY;
32✔
244
        this._startTime = Date.now();
32✔
245
        this._startTarget = event.target;
32✔
246
        this._tracking = true;
32✔
247
        this._panStarted = false;
32✔
248
        this._pointerId = event.pointerId;
32✔
249

250
        if (this._setPointerCapture && typeof (this.target as Element).setPointerCapture === 'function') {
32✔
251
            try {
25✔
252
                (this.target as Element).setPointerCapture(event.pointerId);
25✔
253
            } catch {
254
                // `setPointerCapture` can throw a `NotFoundError` when the pointer is no longer active
255
                // (e.g. for synthetic events). Capturing is a best-effort enhancement, so ignore it.
256
            }
257
        }
258

259
        // Let the consumer veto the gesture (e.g. the touch did not start in an active
260
        // zone). Returning `false` stops tracking immediately so the `touchmove` listener
261
        // does not block normal scrolling and other gestures are not interfered with.
262
        if (this.callbacks.pointerDown) {
32✔
263
            const gesture = this._createEvent(event);
7✔
264
            this._runInAngular(() => {
7✔
265
                if (this.callbacks.pointerDown?.(gesture) === false) {
7✔
266
                    this._stopTracking(event.pointerId);
1✔
267
                }
268
            });
269
        }
270
    };
271

272
    private _onPointerMove = (event: PointerEvent) => {
2,406✔
273
        if (!this._tracking || event.pointerId !== this._pointerId || !this._accepts(event.pointerType)) {
36✔
274
            return;
6✔
275
        }
276
        const gesture = this._createEvent(event);
30✔
277
        // Defer `panStart` until movement actually begins, mirroring Hammer's `panstart`.
278
        // A press with no movement (a tap) therefore never raises `panStart`.
279
        if (!this._panStarted) {
30✔
280
            this._panStarted = true;
30✔
281
            if (this.callbacks.panStart) {
30✔
282
                this._runInAngular(() => this.callbacks.panStart(gesture));
20✔
283
            }
284
        }
285
        // `panMove` is intentionally invoked outside of the Angular zone (when one is provided)
286
        // as it fires for every pointer move and running change detection for each of them
287
        // makes continuous dragging lag behind the pointer.
288
        this.callbacks.panMove?.(gesture);
30✔
289
    };
290

291
    private _onPointerUp = (event: PointerEvent) => {
2,406✔
292
        if (!this._tracking || event.pointerId !== this._pointerId || !this._accepts(event.pointerType)) {
30✔
293
            return;
3✔
294
        }
295
        this._tracking = false;
27✔
296
        this._pointerId = null;
27✔
297
        const gesture = this._createEvent(event);
27✔
298

299
        this._runInAngular(() => {
27✔
300
            if (this.callbacks.tap && gesture.distance < this._tapThreshold) {
27!
UNCOV
301
                this.callbacks.tap(gesture);
×
UNCOV
302
                return;
×
303
            }
304

305
            if (this.callbacks.swipe &&
27✔
306
                gesture.velocity > this._swipeVelocityThreshold &&
307
                Math.abs(gesture.deltaX) > Math.abs(gesture.deltaY)) {
308
                this.callbacks.swipe(gesture);
2✔
309
            }
310

311
            this.callbacks.panEnd?.(gesture);
27✔
312
        });
313
    };
314

315
    private _onPointerCancel = (event: PointerEvent) => {
2,406✔
316
        if (!this._tracking || event.pointerId !== this._pointerId) {
2!
UNCOV
317
            return;
×
318
        }
319
        this._tracking = false;
2✔
320
        this._pointerId = null;
2✔
321
        if (this.callbacks.panCancel) {
2✔
322
            const gesture = this._createEvent(event);
2✔
323
            this._runInAngular(() => this.callbacks.panCancel(gesture));
2✔
324
        }
325
    };
326

327
    private _onTouchMove = (event: TouchEvent) => {
2,406✔
328
        // Prevent scrolling only while a gesture is actively tracked.
329
        if (this._tracking && event.cancelable) {
1!
UNCOV
330
            event.preventDefault();
×
331
        }
332
    }
333

334
    /** Stops tracking the current gesture and best-effort releases the pointer capture. */
335
    private _stopTracking(pointerId: number): void {
336
        this._tracking = false;
1✔
337
        this._panStarted = false;
1✔
338
        this._pointerId = null;
1✔
339
        this._startTarget = null;
1✔
340

341
        if (this._setPointerCapture && typeof (this.target as Element).releasePointerCapture === 'function') {
1!
UNCOV
342
            try {
×
UNCOV
343
                (this.target as Element).releasePointerCapture(pointerId);
×
344
            } catch {
345
                // `releasePointerCapture` can throw when the pointer is no longer captured.
346
                // Releasing is a best-effort cleanup, so ignore it.
347
            }
348
        }
349
    }
350
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc