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

IgniteUI / igniteui-angular / 26023601418

18 May 2026 08:57AM UTC coverage: 4.854% (-85.3%) from 90.174%
26023601418

Pull #17281

github

web-flow
Merge e7ce7a18e into 5a85df190
Pull Request #17281: feat: Added virtual scroll component and sample implementation

400 of 17347 branches covered (2.31%)

Branch coverage included in aggregate %.

63 of 222 new or added lines in 4 files covered. (28.38%)

27932 existing lines in 341 files now uncovered.

2022 of 32547 relevant lines covered (6.21%)

0.72 hits per line

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

0.54
/projects/igniteui-angular/tree/src/tree/tree-navigation.service.ts
1
import { Injectable, OnDestroy, inject } from '@angular/core';
2
import { IgxTree, IgxTreeNode, IgxTreeSelectionType } from './common';
3
import { NAVIGATION_KEYS } from 'igniteui-angular/core';
4
import { IgxTreeService } from './tree.service';
5
import { IgxTreeSelectionService } from './tree-selection.service';
6
import { Subject } from 'rxjs';
7

8
/** @hidden @internal */
9
@Injectable()
10
export class IgxTreeNavigationService implements OnDestroy {
3✔
UNCOV
11
    private treeService = inject(IgxTreeService);
×
UNCOV
12
    private selectionService = inject(IgxTreeSelectionService);
×
13

14
    private tree: IgxTree;
15

UNCOV
16
    private _focusedNode: IgxTreeNode<any> = null;
×
UNCOV
17
    private _lastFocusedNode: IgxTreeNode<any> = null;
×
UNCOV
18
    private _activeNode: IgxTreeNode<any> = null;
×
19

UNCOV
20
    private _visibleChildren: IgxTreeNode<any>[] = [];
×
UNCOV
21
    private _invisibleChildren: Set<IgxTreeNode<any>> = new Set();
×
UNCOV
22
    private _disabledChildren: Set<IgxTreeNode<any>> = new Set();
×
23

UNCOV
24
    private _cacheChange = new Subject<void>();
×
25

26
    constructor() {
UNCOV
27
        this._cacheChange.subscribe(() => {
×
UNCOV
28
            this._visibleChildren =
×
29
                this.tree?.nodes ?
×
UNCOV
30
                    this.tree.nodes.filter(e => !(this._invisibleChildren.has(e) || this._disabledChildren.has(e))) :
×
31
                    [];
32
        });
33
    }
34

35
    public register(tree: IgxTree) {
UNCOV
36
        this.tree = tree;
×
37
    }
38

39
    public get focusedNode() {
UNCOV
40
        return this._focusedNode;
×
41
    }
42

43
    public set focusedNode(value: IgxTreeNode<any>) {
UNCOV
44
        if (this._focusedNode === value) {
×
UNCOV
45
            return;
×
46
        }
UNCOV
47
        this._lastFocusedNode = this._focusedNode;
×
UNCOV
48
        if (this._lastFocusedNode) {
×
UNCOV
49
            this._lastFocusedNode.tabIndex = -1;
×
50
        }
UNCOV
51
        this._focusedNode = value;
×
UNCOV
52
        if (this._focusedNode !== null) {
×
UNCOV
53
            this._focusedNode.tabIndex = 0;
×
UNCOV
54
            this._focusedNode.header.nativeElement.focus();
×
55
        }
56
    }
57

58
    public get activeNode() {
UNCOV
59
        return this._activeNode;
×
60
    }
61

62
    public set activeNode(value: IgxTreeNode<any>) {
UNCOV
63
        if (this._activeNode === value) {
×
UNCOV
64
            return;
×
65
        }
UNCOV
66
        this._activeNode = value;
×
UNCOV
67
        this.tree.activeNodeChanged.emit(this._activeNode);
×
68
    }
69

70
    public get visibleChildren(): IgxTreeNode<any>[] {
UNCOV
71
        return this._visibleChildren;
×
72
    }
73

74
    public update_disabled_cache(node: IgxTreeNode<any>): void {
UNCOV
75
        if (node.disabled) {
×
UNCOV
76
            this._disabledChildren.add(node);
×
77
        } else {
UNCOV
78
            this._disabledChildren.delete(node);
×
79
        }
UNCOV
80
        this._cacheChange.next();
×
81
    }
82

83
    public init_invisible_cache() {
UNCOV
84
        this.tree.nodes.filter(e => e.level === 0).forEach(node => {
×
UNCOV
85
            this.update_visible_cache(node, node.expanded, false);
×
86
        });
UNCOV
87
        this._cacheChange.next();
×
88
    }
89

90
    public update_visible_cache(node: IgxTreeNode<any>, expanded: boolean, shouldEmit = true): void {
×
UNCOV
91
        if (expanded) {
×
UNCOV
92
            node._children.forEach(child => {
×
UNCOV
93
                this._invisibleChildren.delete(child);
×
UNCOV
94
                this.update_visible_cache(child, child.expanded, false);
×
95
            });
96
        } else {
UNCOV
97
            node.allChildren.forEach(c => this._invisibleChildren.add(c));
×
98
        }
99

UNCOV
100
        if (shouldEmit) {
×
UNCOV
101
            this._cacheChange.next();
×
102
        }
103
    }
104

105
    /**
106
     * Sets the node as focused (and active)
107
     *
108
     * @param node target node
109
     * @param isActive if true, sets the node as active
110
     */
111
    public setFocusedAndActiveNode(node: IgxTreeNode<any>, isActive = true): void {
×
UNCOV
112
        if (isActive) {
×
UNCOV
113
            this.activeNode = node;
×
114
        }
UNCOV
115
        this.focusedNode = node;
×
116
    }
117

118
    /** Handler for keydown events. Used in tree.component.ts */
119
    public handleKeydown(event: KeyboardEvent) {
UNCOV
120
        const key = event.key.toLowerCase();
×
UNCOV
121
        if (!this.focusedNode) {
×
UNCOV
122
            return;
×
123
        }
UNCOV
124
        if (!(NAVIGATION_KEYS.has(key) || key === '*')) {
×
UNCOV
125
            if (key === 'enter') {
×
UNCOV
126
                this.activeNode = this.focusedNode;
×
127
            }
UNCOV
128
            return;
×
129
        }
UNCOV
130
        event.preventDefault();
×
UNCOV
131
        if (event.repeat) {
×
UNCOV
132
            setTimeout(() => this.handleNavigation(event), 1);
×
133
        } else {
UNCOV
134
            this.handleNavigation(event);
×
135
        }
136
    }
137

138
    public ngOnDestroy() {
UNCOV
139
        this._cacheChange.next();
×
UNCOV
140
        this._cacheChange.complete();
×
141
    }
142

143
    private handleNavigation(event: KeyboardEvent) {
UNCOV
144
        switch (event.key.toLowerCase()) {
×
145
            case 'home':
UNCOV
146
                this.setFocusedAndActiveNode(this.visibleChildren[0]);
×
UNCOV
147
                break;
×
148
            case 'end':
UNCOV
149
                this.setFocusedAndActiveNode(this.visibleChildren[this.visibleChildren.length - 1]);
×
UNCOV
150
                break;
×
151
            case 'arrowleft':
152
            case 'left':
UNCOV
153
                this.handleArrowLeft();
×
UNCOV
154
                break;
×
155
            case 'arrowright':
156
            case 'right':
UNCOV
157
                this.handleArrowRight();
×
UNCOV
158
                break;
×
159
            case 'arrowup':
160
            case 'up':
UNCOV
161
                this.handleUpDownArrow(true, event);
×
UNCOV
162
                break;
×
163
            case 'arrowdown':
164
            case 'down':
UNCOV
165
                this.handleUpDownArrow(false, event);
×
UNCOV
166
                break;
×
167
            case '*':
UNCOV
168
                this.handleAsterisk();
×
UNCOV
169
                break;
×
170
            case ' ':
171
            case 'spacebar':
172
            case 'space':
UNCOV
173
                this.handleSpace(event.shiftKey);
×
UNCOV
174
                break;
×
175
            default:
176
                return;
×
177
        }
178
    }
179

180
    private handleArrowLeft(): void {
UNCOV
181
        if (this.focusedNode.expanded && !this.treeService.collapsingNodes.has(this.focusedNode) && this.focusedNode._children?.length) {
×
UNCOV
182
            this.activeNode = this.focusedNode;
×
UNCOV
183
            this.focusedNode.collapse();
×
184
        } else {
UNCOV
185
            const parentNode = this.focusedNode.parentNode;
×
UNCOV
186
            if (parentNode && !parentNode.disabled) {
×
UNCOV
187
                this.setFocusedAndActiveNode(parentNode);
×
188
            }
189
        }
190
    }
191

192
    private handleArrowRight(): void {
UNCOV
193
        if (this.focusedNode._children.length > 0) {
×
UNCOV
194
            if (!this.focusedNode.expanded) {
×
UNCOV
195
                this.activeNode = this.focusedNode;
×
UNCOV
196
                this.focusedNode.expand();
×
197
            } else {
UNCOV
198
                if (this.treeService.collapsingNodes.has(this.focusedNode)) {
×
199
                    this.focusedNode.expand();
×
200
                    return;
×
201
                }
UNCOV
202
                const firstChild = this.focusedNode._children.find(node => !node.disabled);
×
UNCOV
203
                if (firstChild) {
×
UNCOV
204
                    this.setFocusedAndActiveNode(firstChild);
×
205
                }
206
            }
207
        }
208
    }
209

210
    private handleUpDownArrow(isUp: boolean, event: KeyboardEvent): void {
UNCOV
211
        const next = this.getVisibleNode(this.focusedNode, isUp ? -1 : 1);
×
UNCOV
212
        if (next === this.focusedNode) {
×
UNCOV
213
            return;
×
214
        }
215

UNCOV
216
        if (event.ctrlKey) {
×
UNCOV
217
            this.setFocusedAndActiveNode(next, false);
×
218
        } else {
UNCOV
219
            this.setFocusedAndActiveNode(next);
×
220
        }
221
    }
222

223
    private handleAsterisk(): void {
UNCOV
224
        const nodes = this.focusedNode.parentNode ? this.focusedNode.parentNode._children : this.tree.rootNodes;
×
UNCOV
225
        nodes?.forEach(node => {
×
UNCOV
226
            if (!node.disabled && (!node.expanded || this.treeService.collapsingNodes.has(node))) {
×
UNCOV
227
                node.expand();
×
228
            }
229
        });
230
    }
231

232
    private handleSpace(shiftKey = false): void {
×
UNCOV
233
        if (this.tree.selection === IgxTreeSelectionType.None) {
×
UNCOV
234
            return;
×
235
        }
236

UNCOV
237
        this.activeNode = this.focusedNode;
×
UNCOV
238
        if (shiftKey) {
×
UNCOV
239
            this.selectionService.selectMultipleNodes(this.focusedNode);
×
UNCOV
240
            return;
×
241
        }
242

UNCOV
243
        if (this.focusedNode.selected) {
×
UNCOV
244
            this.selectionService.deselectNode(this.focusedNode);
×
245
        } else {
UNCOV
246
            this.selectionService.selectNode(this.focusedNode);
×
247
        }
248
    }
249

250
    /** Gets the next visible node in the given direction - 1 -> next, -1 -> previous */
251
    private getVisibleNode(node: IgxTreeNode<any>, dir: 1 | -1 = 1): IgxTreeNode<any> {
×
UNCOV
252
        const nodeIndex = this.visibleChildren.indexOf(node);
×
UNCOV
253
        return this.visibleChildren[nodeIndex + dir] || node;
×
254
    }
255
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc