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

IgniteUI / igniteui-webcomponents / 28880951667

07 Jul 2026 04:10PM UTC coverage: 98.335% (+0.008%) from 98.327%
28880951667

Pull #2278

github

web-flow
Merge ca7adce28 into fcab913eb
Pull Request #2278: chore: more analyzer ignore tags

5941 of 6247 branches covered (95.1%)

Branch coverage included in aggregate %.

24 of 24 new or added lines in 15 files covered. (100.0%)

16 existing lines in 4 files now uncovered.

42028 of 42534 relevant lines covered (98.81%)

1581.86 hits per line

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

96.12
/src/components/tree/tree.navigation.ts
1
import {
9✔
2
  addKeybindings,
9✔
3
  arrowDown,
9✔
4
  arrowLeft,
9✔
5
  arrowRight,
9✔
6
  arrowUp,
9✔
7
  ctrlKey,
9✔
8
  endKey,
9✔
9
  enterKey,
9✔
10
  homeKey,
9✔
11
  shiftKey,
9✔
12
  spaceBar,
9✔
13
} from '../common/controllers/key-bindings.js';
9✔
14
import { isLTR, scrollIntoView } from '../common/util.js';
9✔
15
import type IgcTreeComponent from './tree.js';
9✔
16
import type { IgcTreeSelectionService } from './tree.selection.js';
9✔
17
import type IgcTreeItemComponent from './tree-item.js';
9✔
18

9✔
19
/**
9✔
20
 * Handles roving-tabindex keyboard navigation and active/focused item tracking for the tree.
9✔
21
 *
9✔
22
 * Unlike the previous implementation, "visible" (keyboard-navigable) items are **not** cached.
9✔
23
 * They are derived on demand, only when a navigation key is actually pressed, directly from each
9✔
24
 * item's live `expanded`/`disabled`/`parent` state. This avoids doing any work on every single
9✔
25
 * item mount/expand/disable — the dominant cost in trees with many items — since navigation only
9✔
26
 * needs to know the visible set at the moment of a keypress, not continuously.
9✔
27
 *
9✔
28
 * @hidden @internal
9✔
29
 */
9✔
30
export class IgcTreeNavigationService {
9✔
31
  private _focusedItem: IgcTreeItemComponent | null = null;
9✔
32
  private _activeItem: IgcTreeItemComponent | null = null;
9✔
33

9✔
34
  constructor(
9✔
35
    private readonly tree: IgcTreeComponent,
114✔
36
    private readonly selection: IgcTreeSelectionService
114✔
37
  ) {
114✔
38
    addKeybindings(tree, {
114✔
39
      bindingDefaults: { preventDefault: true, repeat: true },
114✔
40
    })
114✔
41
      .set(homeKey, () => this.setFocusedAndActiveItem(this._navigable[0]))
114✔
42
      .set(endKey, () => this.setFocusedAndActiveItem(this._lastVisible()))
114✔
43
      .set(arrowLeft, this._arrowLeft)
114✔
44
      .set(arrowRight, this._arrowRight)
114✔
45
      .set(arrowUp, () => this._arrowVertical(-1, true))
114✔
46
      .set(arrowDown, () => this._arrowVertical(1, true))
114✔
47
      .set([ctrlKey, arrowUp], () => this._arrowVertical(-1, false))
114✔
48
      .set([ctrlKey, arrowDown], () => this._arrowVertical(1, false))
114✔
49
      .set('*', this._asterisk)
114✔
50
      .set(spaceBar, () => this._space(false))
114✔
51
      .set([shiftKey, spaceBar], () => this._space(true))
114✔
52
      .set(enterKey, this._enter, { preventDefault: false });
114✔
53
  }
114✔
54

9✔
55
  public get focusedItem(): IgcTreeItemComponent | null {
9✔
56
    return this._focusedItem;
241✔
57
  }
241✔
58

9✔
59
  public get activeItem(): IgcTreeItemComponent | null {
9✔
60
    return this._activeItem;
127✔
61
  }
127✔
62

9✔
63
  public focusItem(
9✔
64
    value: IgcTreeItemComponent | null,
338✔
65
    shouldFocus = true
338✔
66
  ): void {
338✔
67
    if (this._focusedItem === value) {
338✔
68
      return;
5✔
69
    }
5✔
70

333✔
71
    this._focusedItem?.removeAttribute('tabindex');
338✔
72
    this._focusedItem = value;
338✔
73

338✔
74
    if (this._focusedItem && shouldFocus) {
338✔
75
      this._focusedItem.tabIndex = 0;
124✔
76
      this._focusedItem.focus({ preventScroll: true });
124✔
77
      this._scrollIntoView(this._focusedItem);
124✔
78
    }
124✔
79
  }
338✔
80

9✔
81
  public setActiveItem(
9✔
82
    value: IgcTreeItemComponent | null,
126✔
83
    shouldEmit = true
126✔
84
  ): void {
126✔
85
    if (this._activeItem === value) {
126✔
86
      return;
7✔
87
    }
7✔
88

119✔
89
    if (this._activeItem) {
126✔
90
      this._activeItem.active = false;
75✔
91
    }
75✔
92

119✔
93
    this._activeItem = value;
119✔
94

119✔
95
    if (this._activeItem) {
126✔
96
      this._activeItem.active = true;
75✔
97
      if (shouldEmit) {
75✔
98
        this.tree.emitEvent('igcActiveItem', { detail: this._activeItem });
29✔
99
      }
29✔
100
    }
75✔
101
  }
126✔
102

9✔
103
  /** Sets the item as focused (and optionally active). */
9✔
104
  public setFocusedAndActiveItem(
9✔
105
    item: IgcTreeItemComponent | undefined,
35✔
106
    isActive = true,
35✔
107
    shouldFocus = true
35✔
108
  ): void {
35✔
109
    if (!item) {
35!
UNCOV
110
      return;
×
UNCOV
111
    }
×
112
    if (isActive) {
35✔
113
      this.setActiveItem(item);
24✔
114
    }
24✔
115
    this.focusItem(item, shouldFocus);
35✔
116
  }
35✔
117

9✔
118
  /** Called by a tree item on `disconnectedCallback`. */
9✔
119
  public handleItemDisconnect(item: IgcTreeItemComponent): void {
9✔
120
    if (this._activeItem === item) {
1,372✔
121
      this.setActiveItem(null);
44✔
122
    }
44✔
123
    if (this._focusedItem === item) {
1,372✔
124
      this.focusItem(null, false);
95✔
125
      const next = this.tree.items.find((i) => !i.disabled);
95✔
126
      if (next) {
95✔
127
        next.tabIndex = 0;
95✔
128
        this.focusItem(next, false);
95✔
129
      }
95✔
130
    }
95✔
131
  }
1,372✔
132

9✔
133
  //#region Visible/navigable item resolution (computed on demand, not cached)
9✔
134

9✔
135
  private _isNavigable(item: IgcTreeItemComponent): boolean {
9✔
136
    if (item.disabled) {
616✔
137
      return false;
12✔
138
    }
12✔
139
    for (let ancestor = item.parent; ancestor; ancestor = ancestor.parent) {
604✔
140
      if (!ancestor.expanded) {
752✔
141
        return false;
184✔
142
      }
184✔
143
    }
752✔
144
    return true;
420✔
145
  }
616✔
146

9✔
147
  private get _navigable(): IgcTreeItemComponent[] {
9✔
148
    return this.tree.items.filter((item) => this._isNavigable(item));
28✔
149
  }
28✔
150

9✔
151
  private _lastVisible(): IgcTreeItemComponent {
9✔
152
    const items = this._navigable;
1✔
153
    return items[items.length - 1];
1✔
154
  }
1✔
155

9✔
156
  /** Next/previous navigable item relative to `item`, or `item` itself if there isn't one. */
9✔
157
  private _adjacent(
9✔
158
    item: IgcTreeItemComponent,
26✔
159
    dir: 1 | -1
26✔
160
  ): IgcTreeItemComponent {
26✔
161
    const items = this._navigable;
26✔
162
    const index = items.indexOf(item);
26✔
163
    return items[index + dir] ?? item;
26✔
164
  }
26✔
165

9✔
166
  private _scrollIntoView(item: IgcTreeItemComponent): void {
9✔
167
    scrollIntoView(item.wrapper);
124✔
168
  }
124✔
169

9✔
170
  //#endregion
9✔
171

9✔
172
  //#region Keyboard handlers
9✔
173

9✔
174
  private readonly _arrowLeft = (): void => {
9✔
175
    isLTR(this.tree)
9✔
176
      ? this._collapseOrGoToParent()
8✔
177
      : this._expandOrGoToFirstChild();
6✔
178
  };
9✔
179

9✔
180
  private readonly _arrowRight = (): void => {
9✔
181
    isLTR(this.tree)
12✔
182
      ? this._expandOrGoToFirstChild()
11✔
183
      : this._collapseOrGoToParent();
6✔
184
  };
12✔
185

9✔
186
  private _collapseOrGoToParent(): void {
9✔
187
    const item = this._focusedItem;
4✔
188
    if (!item) {
4!
UNCOV
189
      return;
×
UNCOV
190
    }
×
191
    if (item.expanded && item.getChildren().length) {
4✔
192
      this.setActiveItem(item);
2✔
193
      item.collapseWithEvent();
2✔
194
      return;
2✔
195
    }
2✔
196
    if (item.parent && !item.parent.disabled) {
4✔
197
      this.setFocusedAndActiveItem(item.parent);
1✔
198
    }
1✔
199
  }
4✔
200

9✔
201
  private _expandOrGoToFirstChild(): void {
9✔
202
    const item = this._focusedItem;
7✔
203
    if (!item || item.getChildren().length === 0) {
7✔
204
      return;
1✔
205
    }
1✔
206
    if (!item.expanded) {
7✔
207
      this.setActiveItem(item);
2✔
208
      item.expandWithEvent();
2✔
209
      return;
2✔
210
    }
2✔
211
    const firstChild = item.getChildren().find((child) => !child.disabled);
4✔
212
    if (firstChild) {
5✔
213
      this.setFocusedAndActiveItem(firstChild);
3✔
214
    }
3✔
215
  }
7✔
216

9✔
217
  private _arrowVertical(dir: 1 | -1, shouldActivate: boolean): void {
9✔
218
    const item = this._focusedItem;
26✔
219
    if (!item) {
26!
UNCOV
220
      return;
×
UNCOV
221
    }
×
222
    const next = this._adjacent(item, dir);
26✔
223
    if (next === item) {
26✔
224
      return;
2✔
225
    }
2✔
226
    this.setFocusedAndActiveItem(next, shouldActivate);
24✔
227
  }
26✔
228

9✔
229
  private readonly _asterisk = (): void => {
9✔
230
    const item = this._focusedItem;
7✔
231
    if (!item) {
7!
232
      return;
5✔
233
    }
5✔
234
    const siblings = item.parent
7!
235
      ? item.parent.getChildren()
5✔
236
      : this.tree.items.filter((i) => i.level === 0);
7✔
237

7✔
238
    for (const sibling of siblings) {
7✔
239
      if (!sibling.disabled && !sibling.expanded && sibling.hasChildren) {
13✔
240
        sibling.expandWithEvent();
9✔
241
      }
9✔
242
    }
13✔
243
  };
7✔
244

9✔
245
  private _space(shiftKey: boolean): void {
9✔
246
    const item = this._focusedItem;
6✔
247
    if (!item) {
6!
UNCOV
248
      return;
×
UNCOV
249
    }
×
250

6✔
251
    if (this.tree.selection === 'none') {
6✔
252
      this.setActiveItem(item);
1✔
253
      return;
1✔
254
    }
1✔
255

5✔
256
    this.setActiveItem(item);
5✔
257

5✔
258
    if (shiftKey) {
6✔
259
      this.selection.selectMultipleItems(item);
1✔
260
      return;
1✔
261
    }
1✔
262

4✔
263
    item.selected
4✔
264
      ? this.selection.deselectItem(item)
1✔
265
      : this.selection.selectItem(item);
3✔
266
  }
6✔
267

9✔
268
  private readonly _enter = (): void => {
9✔
269
    if (this._focusedItem) {
6✔
270
      this.setActiveItem(this._focusedItem);
6✔
271
    }
6✔
272
  };
6✔
273

9✔
274
  //#endregion
9✔
275
}
9✔
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