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

IgniteUI / igniteui-angular / 30354093041

28 Jul 2026 11:15AM UTC coverage: 90.184% (+0.04%) from 90.141%
30354093041

push

github

web-flow
Remove Hammer.js dependency, replace with native Pointer/Touch Events (#17359)

* feat(*): remove Hammer.js dependency, replace with native pointer events.
* refactor(*): add zoneless IgxTouchManager, drop NgZone from gestures
* fix(nav-drawer): applying pan-related fixes
* test(time-picker): add pan-to-scroll gesture spec
* fix(nav-drawer): adding correct gesture detach logic
* fix(list): ensuring no panStart list item event on touchmanager panstart
* fix(*): making touch ssr no-op, removing double as ios doesn't need it anymore
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Stamen Stoychev <SStoychev@infragistics.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

14953 of 17412 branches covered (85.88%)

Branch coverage included in aggregate %.

120 of 141 new or added lines in 5 files covered. (85.11%)

9 existing lines in 4 files now uncovered.

30067 of 32508 relevant lines covered (92.49%)

37545.53 hits per line

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

95.35
/projects/igniteui-angular/tree/src/tree/tree.component.ts
1
import { Component, QueryList, Input, Output, EventEmitter, ContentChild, Directive, TemplateRef, OnInit, AfterViewInit, ContentChildren, OnDestroy, HostBinding, ElementRef, booleanAttribute, inject, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
2

3
import { Subject } from 'rxjs';
4
import { takeUntil, throttleTime } from 'rxjs/operators';
5

6
import {
7
    IGX_TREE_COMPONENT, IgxTreeSelectionType, IgxTree, ITreeNodeToggledEventArgs,
8
    ITreeNodeTogglingEventArgs, ITreeNodeSelectionEvent, IgxTreeNode, IgxTreeSearchResolver
9
} from './common';
10
import { IgxTreeNavigationService } from './tree-navigation.service';
11
import { IgxTreeNodeComponent } from './tree-node/tree-node.component';
12
import { IgxTreeSelectionService } from './tree-selection.service';
13
import { IgxTreeService } from './tree.service';
14
import { growVerIn, growVerOut } from 'igniteui-angular/animations';
15
import { PlatformUtil, resizeObservable } from 'igniteui-angular/core';
16
import { ToggleAnimationSettings } from 'igniteui-angular/expansion-panel';
17

18
/**
19
 * @hidden @internal
20
 * Used for templating the select marker of the tree
21
 */
22
@Directive({
23
    selector: '[igxTreeSelectMarker]',
24
    standalone: true
25
})
26
export class IgxTreeSelectMarkerDirective {
3✔
27
}
28

29
/**
30
 * @hidden @internal
31
 * Used for templating the expand indicator of the tree
32
 */
33
@Directive({
34
    selector: '[igxTreeExpandIndicator]',
35
    standalone: true
36
})
37
export class IgxTreeExpandIndicatorDirective {
3✔
38
}
39

40
/**
41
 * Tree allows a developer to show a set of nodes in a hierarchical fashion.
42
 *
43
 * @igxModule IgxTreeModule
44
 * @igxKeywords tree
45
 * @igxTheme igx-tree-theme
46
 * @igxGroup Grids & Lists
47
 *
48
 * @remark
49
 * The Angular Tree Component allows users to represent hierarchical data in a tree-view structure,
50
 * maintaining parent-child relationships, as well as to define static tree-view structure without a corresponding data model.
51
 * Its primary purpose is to allow end-users to visualize and navigate within hierarchical data structures.
52
 * The Ignite UI for Angular Tree Component also provides load on demand capabilities, item activation,
53
 * bi-state and cascading selection of items through built-in checkboxes, built-in keyboard navigation and more.
54
 *
55
 * @example
56
 * ```html
57
 * <igx-tree>
58
 *   <igx-tree-node>
59
 *      I am a parent node 1
60
 *      <igx-tree-node>
61
 *          I am a child node 1
62
 *      </igx-tree-node>
63
 *      ...
64
 *   </igx-tree-node>
65
 *         ...
66
 * </igx-tree>
67
 * ```
68
 */
69
@Component({
70
    selector: 'igx-tree',
71
    templateUrl: 'tree.component.html',
72
    providers: [
73
        IgxTreeService,
74
        IgxTreeSelectionService,
75
        IgxTreeNavigationService,
76
        { provide: IGX_TREE_COMPONENT, useExisting: IgxTreeComponent },
77
    ],
78
    changeDetection: ChangeDetectionStrategy.Eager,
79
    standalone: true
80
})
81
export class IgxTreeComponent implements IgxTree, OnInit, AfterViewInit, OnDestroy {
3✔
82
    private navService = inject(IgxTreeNavigationService);
103✔
83
    private selectionService = inject(IgxTreeSelectionService);
103✔
84
    private treeService = inject(IgxTreeService);
103✔
85
    private element = inject<ElementRef<HTMLElement>>(ElementRef);
103✔
86
    private platform = inject(PlatformUtil);
103✔
87
    private cdr = inject(ChangeDetectorRef, { optional: true });
103✔
88

89

90
    @HostBinding('class.igx-tree')
91
    public cssClass = 'igx-tree';
103✔
92

93
    /**
94
     * Gets/Sets tree selection mode
95
     *
96
     * @remarks
97
     * By default the tree selection mode is 'None'
98
     * @param selectionMode: IgxTreeSelectionType
99
     */
100
    @Input()
101
    public get selection() {
102
        return this._selection;
42,760✔
103
    }
104

105
    public set selection(selectionMode: IgxTreeSelectionType) {
106
        this._selection = selectionMode;
82✔
107
        this.selectionService.clearNodesSelection();
82✔
108
    }
109

110
    /** Get/Set how the tree should handle branch expansion.
111
     * If set to `true`, only a single branch can be expanded at a time, collapsing all others
112
     *
113
     * ```html
114
     * <igx-tree [singleBranchExpand]="true">
115
     * ...
116
     * </igx-tree>
117
     * ```
118
     *
119
     * ```typescript
120
     * const tree: IgxTree = this.tree;
121
     * this.tree.singleBranchExpand = false;
122
     * ```
123
     */
124
    @Input({ transform: booleanAttribute })
125
    public singleBranchExpand = false;
103✔
126

127
    /** Get/Set if nodes should be expanded/collapsed when clicking over them.
128
     *
129
     * ```html
130
     * <igx-tree [toggleNodeOnClick]="true">
131
     * ...
132
     * </igx-tree>
133
     * ```
134
     *
135
     * ```typescript
136
     * const tree: IgxTree = this.tree;
137
     * this.tree.toggleNodeOnClick = false;
138
     * ```
139
     */
140
    @Input({ transform: booleanAttribute })
141
    public toggleNodeOnClick = false;
103✔
142

143

144
    /** Get/Set the animation settings that branches should use when expanding/collpasing.
145
     *
146
     * ```html
147
     * <igx-tree [animationSettings]="customAnimationSettings">
148
     * </igx-tree>
149
     * ```
150
     *
151
     * ```typescript
152
     * const animationSettings: ToggleAnimationSettings = {
153
     *      openAnimation: growVerIn,
154
     *      closeAnimation: growVerOut
155
     * };
156
     *
157
     * this.tree.animationSettings = animationSettings;
158
     * ```
159
     */
160
    @Input()
161
    public animationSettings: ToggleAnimationSettings = {
103✔
162
        openAnimation: growVerIn,
163
        closeAnimation: growVerOut
164
    };
165

166
    /** Emitted when the node selection is changed through interaction
167
     *
168
     * ```html
169
     * <igx-tree (nodeSelection)="handleNodeSelection($event)">
170
     * </igx-tree>
171
     * ```
172
     *
173
     *```typescript
174
     * public handleNodeSelection(event: ITreeNodeSelectionEvent) {
175
     *  const newSelection: IgxTreeNode<any>[] = event.newSelection;
176
     *  const added: IgxTreeNode<any>[] = event.added;
177
     *  console.log("New selection will be: ", newSelection);
178
     *  console.log("Added nodes: ", event.added);
179
     * }
180
     *```
181
     */
182
    @Output()
183
    public nodeSelection = new EventEmitter<ITreeNodeSelectionEvent>();
103✔
184

185
    /** Emitted when a node is expanding, before it finishes
186
     *
187
     * ```html
188
     * <igx-tree (nodeExpanding)="handleNodeExpanding($event)">
189
     * </igx-tree>
190
     * ```
191
     *
192
     *```typescript
193
     * public handleNodeExpanding(event: ITreeNodeTogglingEventArgs) {
194
     *  const expandedNode: IgxTreeNode<any> = event.node;
195
     *  if (expandedNode.disabled) {
196
     *      event.cancel = true;
197
     *  }
198
     * }
199
     *```
200
     */
201
    @Output()
202
    public nodeExpanding = new EventEmitter<ITreeNodeTogglingEventArgs>();
103✔
203

204
    /** Emitted when a node is expanded, after it finishes
205
     *
206
     * ```html
207
     * <igx-tree (nodeExpanded)="handleNodeExpanded($event)">
208
     * </igx-tree>
209
     * ```
210
     *
211
     *```typescript
212
     * public handleNodeExpanded(event: ITreeNodeToggledEventArgs) {
213
     *  const expandedNode: IgxTreeNode<any> = event.node;
214
     *  console.log("Node is expanded: ", expandedNode.data);
215
     * }
216
     *```
217
     */
218
    @Output()
219
    public nodeExpanded = new EventEmitter<ITreeNodeToggledEventArgs>();
103✔
220

221
    /** Emitted when a node is collapsing, before it finishes
222
     *
223
     * ```html
224
     * <igx-tree (nodeCollapsing)="handleNodeCollapsing($event)">
225
     * </igx-tree>
226
     * ```
227
     *
228
     *```typescript
229
     * public handleNodeCollapsing(event: ITreeNodeTogglingEventArgs) {
230
     *  const collapsedNode: IgxTreeNode<any> = event.node;
231
     *  if (collapsedNode.alwaysOpen) {
232
     *      event.cancel = true;
233
     *  }
234
     * }
235
     *```
236
     */
237
    @Output()
238
    public nodeCollapsing = new EventEmitter<ITreeNodeTogglingEventArgs>();
103✔
239

240
    /** Emitted when a node is collapsed, after it finishes
241
     *
242
     * @example
243
     * ```html
244
     * <igx-tree (nodeCollapsed)="handleNodeCollapsed($event)">
245
     * </igx-tree>
246
     * ```
247
     * ```typescript
248
     * public handleNodeCollapsed(event: ITreeNodeToggledEventArgs) {
249
     *  const collapsedNode: IgxTreeNode<any> = event.node;
250
     *  console.log("Node is collapsed: ", collapsedNode.data);
251
     * }
252
     * ```
253
     */
254
    @Output()
255
    public nodeCollapsed = new EventEmitter<ITreeNodeToggledEventArgs>();
103✔
256

257
    /**
258
     * Emitted when the active node is changed.
259
     *
260
     * @example
261
     * ```
262
     * <igx-tree (activeNodeChanged)="activeNodeChanged($event)"></igx-tree>
263
     * ```
264
     */
265
    @Output()
266
    public activeNodeChanged = new EventEmitter<IgxTreeNode<any>>();
103✔
267

268
    /**
269
     * A custom template to be used for the expand indicator of nodes
270
     * ```html
271
     * <igx-tree>
272
     *  <ng-template igxTreeExpandIndicator let-expanded>
273
     *      <igx-icon>{{ expanded ? "close_fullscreen": "open_in_full"}}</igx-icon>
274
     *  </ng-template>
275
     * </igx-tree>
276
     * ```
277
     */
278
    @ContentChild(IgxTreeExpandIndicatorDirective, { read: TemplateRef })
279
    public expandIndicator: TemplateRef<any>;
280

281
    /** @hidden @internal */
282
    @ContentChildren(IgxTreeNodeComponent, { descendants: true })
283
    public nodes: QueryList<IgxTreeNodeComponent<any>>;
284

285
    /** @hidden @internal */
286
    public disabledChange = new EventEmitter<IgxTreeNode<any>>();
103✔
287

288
    /**
289
     * Returns all **root level** nodes
290
     *
291
     * ```typescript
292
     * const tree: IgxTree = this.tree;
293
     * const rootNodes: IgxTreeNodeComponent<any>[] = tree.rootNodes;
294
     * ```
295
     */
296
    public get rootNodes(): IgxTreeNodeComponent<any>[] {
297
        return this.nodes?.filter(node => node.level === 0);
62✔
298
    }
299

300
    /**
301
     * Emitted when the active node is set through API
302
     *
303
     * @hidden @internal
304
     */
305
    public activeNodeBindingChange = new EventEmitter<IgxTreeNode<any>>();
103✔
306

307
    /** @hidden @internal */
308
    public forceSelect = [];
103✔
309

310
    /** @hidden @internal */
311
    public resizeNotify = new Subject<void>();
103✔
312

313
    private _selection: IgxTreeSelectionType = IgxTreeSelectionType.None;
103✔
314
    private destroy$ = new Subject<void>();
103✔
315
    private unsubChildren$ = new Subject<void>();
103✔
316

317
    constructor() {
318
        this.selectionService.register(this);
103✔
319
        this.treeService.register(this);
103✔
320
        this.navService.register(this);
103✔
321
    }
322

323
    /** @hidden @internal */
324
    public get nativeElement() {
325
        return this.element.nativeElement;
764✔
326
    }
327

328
    /**
329
     * Expands all of the passed nodes.
330
     * If no nodes are passed, expands ALL nodes
331
     *
332
     * @param nodes nodes to be expanded
333
     *
334
     * ```typescript
335
     * const targetNodes: IgxTreeNode<any> = this.tree.findNodes(true, (_data: any, node: IgxTreeNode<any>) => node.data.expandable);
336
     * tree.expandAll(nodes);
337
     * ```
338
     */
339
    public expandAll(nodes?: IgxTreeNode<any>[]) {
340
        nodes = nodes || this.nodes.toArray();
2✔
341
        nodes.forEach(e => e.expanded = true);
6✔
342
    }
343

344
    /**
345
     * Collapses all of the passed nodes.
346
     * If no nodes are passed, collapses ALL nodes
347
     *
348
     * @param nodes nodes to be collapsed
349
     *
350
     * ```typescript
351
     * const targetNodes: IgxTreeNode<any> = this.tree.findNodes(true, (_data: any, node: IgxTreeNode<any>) => node.data.collapsible);
352
     * tree.collapseAll(nodes);
353
     * ```
354
     */
355
    public collapseAll(nodes?: IgxTreeNode<any>[]) {
356
        nodes = nodes || this.nodes.toArray();
25✔
357
        nodes.forEach(e => e.expanded = false);
45✔
358
    }
359

360
    /**
361
     * Deselect all nodes if the nodes collection is empty. Otherwise, deselect the nodes in the nodes collection.
362
     *
363
     * @example
364
     * ```typescript
365
     *  const arr = [
366
     *      this.tree.nodes.toArray()[0],
367
     *      this.tree.nodes.toArray()[1]
368
     *  ];
369
     *  this.tree.deselectAll(arr);
370
     * ```
371
     * @param nodes: IgxTreeNodeComponent<any>[]
372
     */
373
    public deselectAll(nodes?: IgxTreeNodeComponent<any>[]) {
374
        this.selectionService.deselectNodesWithNoEvent(nodes);
4✔
375
    }
376

377
    /**
378
     * Returns all of the nodes that match the passed searchTerm.
379
     * Accepts a custom comparer function for evaluating the search term against the nodes.
380
     *
381
     * @remarks
382
     * Default search compares the passed `searchTerm` against the node's `data` Input.
383
     * When using `findNodes` w/o a `comparer`, make sure all nodes have `data` passed.
384
     *
385
     * @param searchTerm The data of the searched node
386
     * @param comparer A custom comparer function that evaluates the passed `searchTerm` against all nodes.
387
     * @returns Array of nodes that match the search. `null` if no nodes are found.
388
     *
389
     * ```html
390
     * <igx-tree>
391
     *     <igx-tree-node *ngFor="let node of data" [data]="node">
392
     *          {{ node.label }}
393
     *     </igx-tree-node>
394
     * </igx-tree>
395
     * ```
396
     *
397
     * ```typescript
398
     * public data: DataEntry[] = FETCHED_DATA;
399
     * ...
400
     * const matchedNodes: IgxTreeNode<DataEntry>[] = this.tree.findNodes<DataEntry>(searchTerm: data[5]);
401
     * ```
402
     *
403
     * Using a custom comparer
404
     * ```typescript
405
     * public data: DataEntry[] = FETCHED_DATA;
406
     * ...
407
     * const comparer: IgxTreeSearchResolver = (data: any, node: IgxTreeNode<DataEntry>) {
408
     *      return node.data.index % 2 === 0;
409
     * }
410
     * const evenIndexNodes: IgxTreeNode<DataEntry>[] = this.tree.findNodes<DataEntry>(null, comparer);
411
     * ```
412
     */
413
    public findNodes(searchTerm: any, comparer?: IgxTreeSearchResolver): IgxTreeNodeComponent<any>[] | null {
414
        const compareFunc = comparer || this._comparer;
3✔
415
        const results = this.nodes.filter(node => compareFunc(searchTerm, node));
15✔
416
        return results?.length === 0 ? null : results;
3✔
417
    }
418

419
    /** @hidden @internal */
420
    public handleKeydown(event: KeyboardEvent) {
421
        this.navService.handleKeydown(event);
38✔
422
    }
423

424
    /** @hidden @internal */
425
    public ngOnInit() {
426
        this.disabledChange.pipe(takeUntil(this.destroy$)).subscribe((e) => {
87✔
427
            this.navService.update_disabled_cache(e);
86✔
428
        });
429
        this.activeNodeBindingChange.pipe(takeUntil(this.destroy$)).subscribe((node) => {
87✔
430
            this.expandToNode(this.navService.activeNode);
175✔
431
            this.scrollNodeIntoView(node?.header?.nativeElement);
175✔
432
        });
433
        this.subToCollapsing();
87✔
434
        this.resizeNotify.pipe(
87✔
435
            throttleTime(40, null, { trailing: true }),
436
            takeUntil(this.destroy$)
437
        )
438
        .subscribe(() => {
439
            requestAnimationFrame(() => {
11✔
440
                this.scrollNodeIntoView(this.navService.activeNode?.header.nativeElement);
11✔
441
            });
442
        });
443
    }
444

445
    /** @hidden @internal */
446
    public ngAfterViewInit() {
447
        this.nodes.changes.pipe(takeUntil(this.destroy$)).subscribe(() => {
87✔
448
            this.subToChanges();
38✔
449
        });
450
        this.scrollNodeIntoView(this.navService.activeNode?.header?.nativeElement);
87✔
451
        this.subToChanges();
87✔
452
        resizeObservable(this.nativeElement).pipe(takeUntil(this.destroy$)).subscribe(() => this.resizeNotify.next());
87✔
453
    }
454

455
    /** @hidden @internal */
456
    public ngOnDestroy() {
457
        this.unsubChildren$.next();
121✔
458
        this.unsubChildren$.complete();
121✔
459
        this.destroy$.next();
121✔
460
        this.destroy$.complete();
121✔
461
    }
462

463
    private expandToNode(node: IgxTreeNode<any>) {
464
        if (node && node.parentNode) {
175✔
465
            node.path.forEach(n => {
121✔
466
                if (n !== node && !n.expanded) {
322!
UNCOV
467
                    n.expanded = true;
×
468
                }
469
            });
470
        }
471
    }
472

473
    private subToCollapsing() {
474
        this.nodeCollapsing.pipe(takeUntil(this.destroy$)).subscribe(event => {
87✔
475
            if (event.cancel) {
5!
UNCOV
476
                return;
×
477
            }
478
            this.navService.update_visible_cache(event.node, false);
5✔
479
        });
480
        this.nodeExpanding.pipe(takeUntil(this.destroy$)).subscribe(event => {
87✔
481
            if (event.cancel) {
26!
UNCOV
482
                return;
×
483
            }
484
            this.navService.update_visible_cache(event.node, true);
26✔
485
        });
486
    }
487

488
    private subToChanges() {
489
        this.unsubChildren$.next();
125✔
490
        const toBeSelected = [...this.forceSelect];
125✔
491
        if (this.platform.isBrowser) {
125✔
492
            requestAnimationFrame(() => {
125✔
493
                this.selectionService.selectNodesWithNoEvent(toBeSelected);
125✔
494
                this.cdr?.markForCheck();
125✔
495
            });
496
        }
497
        this.forceSelect = [];
125✔
498
        this.nodes.forEach(node => {
125✔
499
            node.expandedChange.pipe(takeUntil(this.unsubChildren$)).subscribe(nodeState => {
3,724✔
500
                this.navService.update_visible_cache(node, nodeState);
60✔
501
            });
502
            node.closeAnimationDone.pipe(takeUntil(this.unsubChildren$)).subscribe(() => {
3,724✔
503
                const targetElement = this.navService.focusedNode?.header.nativeElement;
2✔
504
                this.scrollNodeIntoView(targetElement);
2✔
505
            });
506
            node.openAnimationDone.pipe(takeUntil(this.unsubChildren$)).subscribe(() => {
3,724✔
507
                const targetElement = this.navService.focusedNode?.header.nativeElement;
11✔
508
                this.scrollNodeIntoView(targetElement);
11✔
509
            });
510
        });
511
        this.navService.init_invisible_cache();
125✔
512
    }
513

514
    private scrollNodeIntoView(el: HTMLElement) {
515
        if (!el) {
286✔
516
            return;
94✔
517
        }
518
        const nodeRect = el.getBoundingClientRect();
192✔
519
        const treeRect = this.nativeElement.getBoundingClientRect();
192✔
520
        const topOffset = treeRect.top > nodeRect.top ? nodeRect.top - treeRect.top : 0;
192✔
521
        const bottomOffset = treeRect.bottom < nodeRect.bottom ? nodeRect.bottom - treeRect.bottom : 0;
192✔
522
        const shouldScroll = !!topOffset || !!bottomOffset;
192✔
523
        if (shouldScroll && this.nativeElement.scrollHeight > this.nativeElement.clientHeight) {
192✔
524
            // this.nativeElement.scrollTop = nodeRect.y - treeRect.y - nodeRect.height;
525
            this.nativeElement.scrollTop =
121✔
526
                this.nativeElement.scrollTop + bottomOffset + topOffset + (topOffset ? -1 : +1) * nodeRect.height;
121✔
527
        }
528
    }
529

530
    private _comparer = <T>(data: T, node: IgxTreeNodeComponent<T>) => node.data === data;
103✔
531

532
}
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