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

IgniteUI / igniteui-angular / 13416627295

19 Feb 2025 03:46PM CUT coverage: 91.615% (+0.02%) from 91.595%
13416627295

Pull #15246

github

web-flow
Merge 2a114cdda into 10ddb05cf
Pull Request #15246: fix(excel-export): Get correct grid column collection from row island…

12987 of 15218 branches covered (85.34%)

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

380 existing lines in 31 files now uncovered.

26385 of 28800 relevant lines covered (91.61%)

34358.69 hits per line

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

82.3
/projects/igniteui-angular/src/lib/grids/hierarchical-grid/hierarchical-grid-navigation.service.ts
1
import { Injectable } from '@angular/core';
2
import { first } from 'rxjs/operators';
3
import { NAVIGATION_KEYS, SUPPORTED_KEYS } from '../../core/utils';
4
import { GridType, IPathSegment, RowType } from '../common/grid.interface';
5
import { IActiveNode, IgxGridNavigationService } from '../grid-navigation.service';
6

7
@Injectable()
8
export class IgxHierarchicalGridNavigationService extends IgxGridNavigationService {
2✔
9
    protected _pendingNavigation = false;
892✔
10

11

12
    public override dispatchEvent(event: KeyboardEvent) {
13
        const key = event.key.toLowerCase();
47✔
14
        const cellOrRowInEdit = this.grid.crudService.cell || this.grid.crudService.row;
47✔
15
        if (!this.activeNode || !(SUPPORTED_KEYS.has(key) || (key === 'tab' && cellOrRowInEdit))) {
47!
16
            return;
×
17
        }
18

19
        const targetGrid = this.getClosestElemByTag(event.target, 'igx-hierarchical-grid')
47!
20
            || this.getClosestElemByTag(event.target, 'igc-hierarchical-grid');
21
        if (targetGrid !== this.grid.nativeElement) {
47!
22
            return;
×
23
        }
24

25
        if (this._pendingNavigation && NAVIGATION_KEYS.has(key)) {
47!
26
            // In case focus needs to be moved from one grid to another, however there is a pending scroll operation
27
            // which is an async operation, any additional navigation keys should be ignored
28
            // untill operation complete.
29
            event.preventDefault();
×
30
            return;
×
31
        }
32
        super.dispatchEvent(event);
47✔
33
    }
34

35
    public override navigateInBody(rowIndex, visibleColIndex, cb: (arg: any) => void = null): void {
×
36
        const rec = this.grid.dataView[rowIndex];
49✔
37
        if (rec && this.grid.isChildGridRecord(rec)) {
49✔
38
             // target is child grid
39
            const virtState = this.grid.verticalScrollContainer.state;
15✔
40
             const inView = rowIndex >= virtState.startIndex && rowIndex <= virtState.startIndex + virtState.chunkSize;
15✔
41
             const isNext =  this.activeNode.row < rowIndex;
15✔
42
             const targetLayoutIndex = isNext ? null : this.grid.childLayoutKeys.length - 1;
15✔
43
             if (inView) {
15!
44
                this._moveToChild(rowIndex, visibleColIndex, isNext, targetLayoutIndex, cb);
15✔
45
            } else {
46
                let scrollAmount = this.grid.verticalScrollContainer.getScrollForIndex(rowIndex, !isNext);
×
47
                scrollAmount += isNext ? 1 : -1;
×
48
                this.grid.verticalScrollContainer.getScroll().scrollTop = scrollAmount;
×
49
                this._pendingNavigation = true;
×
50
                this.grid.verticalScrollContainer.chunkLoad.pipe(first()).subscribe(() => {
×
51
                    this._moveToChild(rowIndex, visibleColIndex, isNext, targetLayoutIndex, cb);
×
52
                    this._pendingNavigation = false;
×
53
                });
54
            }
55
            return;
15✔
56
        }
57

58
        const isLast = rowIndex === this.grid.dataView.length;
34✔
59
        if ((rowIndex === -1 || isLast) &&
34✔
60
            this.grid.parent !== null) {
61
            // reached end of child grid
62
            const nextSiblingIndex = this.nextSiblingIndex(isLast);
10✔
63
            if (nextSiblingIndex !== null) {
10✔
64
                this.grid.parent.navigation._moveToChild(this.grid.childRow.index, visibleColIndex, isLast, nextSiblingIndex, cb);
4✔
65
            } else {
66
                this._moveToParent(isLast, visibleColIndex, cb);
6✔
67
            }
68
            return;
10✔
69
        }
70

71
        if (this.grid.parent) {
24✔
72
            const isNext = this.activeNode && typeof this.activeNode.row === 'number' ? rowIndex > this.activeNode.row : false;
16✔
73
            const cbHandler = (args) => {
16✔
74
                this._handleScrollInChild(rowIndex, isNext);
15✔
75
                cb(args);
15✔
76
            };
77
            if (!this.activeNode) {
16!
78
                this.activeNode = { row: null, column: null };
×
79
            }
80
            super.navigateInBody(rowIndex, visibleColIndex, cbHandler);
16✔
81
            return;
16✔
82
        }
83

84
        if (!this.activeNode) {
8!
85
            this.activeNode = { row: null, column: null };
×
86
        }
87
        super.navigateInBody(rowIndex, visibleColIndex, cb);
8✔
88
    }
89

90
    public override shouldPerformVerticalScroll(index, visibleColumnIndex = -1, isNext?) {
×
91
        const targetRec = this.grid.dataView[index];
145✔
92
        if (this.grid.isChildGridRecord(targetRec)) {
145✔
93
            const scrollAmount = this.grid.verticalScrollContainer.getScrollForIndex(index, !isNext);
2✔
94
            const currScroll = this.grid.verticalScrollContainer.getScroll().scrollTop;
2✔
95
            const shouldScroll = !isNext ? scrollAmount > currScroll : currScroll < scrollAmount;
2!
96
            return shouldScroll;
2✔
97
        } else {
98
            return super.shouldPerformVerticalScroll(index, visibleColumnIndex);
143✔
99
        }
100
    }
101

102
    public override focusTbody(event) {
103
        if (!this.activeNode || this.activeNode.row === null) {
97!
104
            this.activeNode = {
×
105
                row: 0,
106
                column: 0
107
            };
108

109
            this.grid.navigateTo(0, 0, (obj) => {
×
110
                this.grid.clearCellSelection();
×
111
                obj.target.activate(event);
×
112
            });
113

114
        } else {
115
            super.focusTbody(event);
97✔
116
        }
117
    }
118

119
    protected nextSiblingIndex(isNext) {
120
        const layoutKey = this.grid.childRow.layout.key;
10✔
121
        const layoutIndex = this.grid.parent.childLayoutKeys.indexOf(layoutKey);
10✔
122
        const nextIndex = isNext ? layoutIndex + 1 : layoutIndex - 1;
10✔
123
        if (nextIndex <= this.grid.parent.childLayoutKeys.length - 1 && nextIndex > -1) {
10✔
124
            return nextIndex;
4✔
125
        } else {
126
            return null;
6✔
127
        }
128
    }
129

130
    /**
131
     * Handles scrolling in child grid and ensures target child row is in main grid view port.
132
     *
133
     * @param rowIndex The row index which should be in view.
134
     * @param isNext  Optional. Whether we are navigating to next. Used to determine scroll direction.
135
     * @param cb  Optional.Callback function called when operation is complete.
136
     */
137
    protected _handleScrollInChild(rowIndex: number, isNext?: boolean, cb?: () => void) {
138
        const shouldScroll = this.shouldPerformVerticalScroll(rowIndex, -1, isNext);
31✔
139
        if (shouldScroll) {
31✔
140
            this.grid.navigation.performVerticalScrollToCell(rowIndex, -1, () => {
9✔
141
                this.positionInParent(rowIndex, isNext, cb);
9✔
142
            });
143
        } else {
144
            this.positionInParent(rowIndex, isNext, cb);
22✔
145
        }
146
    }
147

148
    /**
149
     *
150
     * @param rowIndex Row index that should come in view.
151
     * @param isNext  Whether we are navigating to next. Used to determine scroll direction.
152
     * @param cb  Optional.Callback function called when operation is complete.
153
     */
154
    protected positionInParent(rowIndex, isNext, cb?: () => void) {
155
        const row = this.grid.gridAPI.get_row_by_index(rowIndex);
31✔
156
        if (!row) {
31!
157
            if (cb) {
×
158
                cb();
×
159
            }
160
            return;
×
161
        }
162
        const positionInfo = this.getPositionInfo(row, isNext);
31✔
163
        if (!positionInfo.inView) {
31✔
164
            // stop event from triggering multiple times before scrolling is complete.
165
            this._pendingNavigation = true;
8✔
166
            const scrollableGrid = isNext ? this.getNextScrollableDown(this.grid) : this.getNextScrollableUp(this.grid);
8✔
167
            scrollableGrid.grid.verticalScrollContainer.recalcUpdateSizes();
8✔
168
            scrollableGrid.grid.verticalScrollContainer.addScrollTop(positionInfo.offset);
8✔
169
            scrollableGrid.grid.verticalScrollContainer.chunkLoad.pipe(first()).subscribe(() => {
8✔
170
                this._pendingNavigation = false;
8✔
171
                if (cb) {
8✔
172
                    cb();
1✔
173
                }
174
            });
175
        } else {
176
            if (cb) {
23✔
177
                cb();
15✔
178
            }
179
        }
180
    }
181

182
    /**
183
     * Navigates to the specific child grid based on the array of paths leading to it
184
     *
185
     * @param pathToChildGrid Array of IPathSegments that describe the path to the child grid
186
     * each segment is described by the rowKey of the parent row and the rowIslandKey.
187
     */
188
    public navigateToChildGrid(pathToChildGrid: IPathSegment[], cb?: () => void) {
189
        if (pathToChildGrid.length == 0) {
5✔
190
            if (cb) {
2✔
191
                cb();
2✔
192
            }
193
            return;
2✔
194
        }
195
        const pathElem = pathToChildGrid.shift();
3✔
196
        const rowKey = pathElem.rowKey;
3✔
197
        const rowIndex = this.grid.gridAPI.get_row_index_in_data(rowKey);
3✔
198
        if (rowIndex === -1) {
3!
UNCOV
199
            if (cb) {
×
UNCOV
200
                cb();
×
201
            }
UNCOV
202
            return;
×
203
        }
204
        // scroll to row, since it can be out of view
205
        this.performVerticalScrollToCell(rowIndex, -1, () => {
3✔
206
            this.grid.cdr.detectChanges();
3✔
207
            // next, expand row, if it is collapsed
208
            const row = this.grid.getRowByIndex(rowIndex);
3✔
209
            if (!row.expanded) {
3✔
210
                row.expanded = true;
3✔
211
                // update sizes after expand
212
                this.grid.verticalScrollContainer.recalcUpdateSizes();
3✔
213
                this.grid.cdr.detectChanges();
3✔
214
            }
215

216
            const childGrid =  this.grid.gridAPI.getChildGrid([pathElem]);
3✔
217
            if (!childGrid) {
3!
UNCOV
218
                if (cb) {
×
UNCOV
219
                    cb();
×
220
                }
UNCOV
221
                return;
×
222
            }
223
            const positionInfo = this.getElementPosition(childGrid.nativeElement, false);
3✔
224
            this.grid.verticalScrollContainer.addScrollTop(positionInfo.offset);
3✔
225
            this.grid.verticalScrollContainer.chunkLoad.pipe(first()).subscribe(() => {
3✔
226
                childGrid.navigation.navigateToChildGrid(pathToChildGrid, cb);
3✔
227
            });
228
        });
229
    }
230

231
    /**
232
     * Moves navigation to child grid.
233
     *
234
     * @param parentRowIndex The parent row index, at which the child grid is rendered.
235
     * @param childLayoutIndex Optional. The index of the child row island to which the child grid belongs to. Uses first if not set.
236
     */
237
    protected _moveToChild(parentRowIndex: number, visibleColIndex: number, isNext: boolean, childLayoutIndex?: number,
238
                            cb?: (arg: any) => void) {
239
        const ri = typeof childLayoutIndex !== 'number' ?
20✔
240
         this.grid.childLayoutList.first : this.grid.childLayoutList.toArray()[childLayoutIndex];
241
        const rowId = this.grid.dataView[parentRowIndex].rowID;
20✔
242
        const pathSegment: IPathSegment = {
20✔
243
            rowID: rowId,
244
            rowKey: rowId,
245
            rowIslandKey: ri.key
246
        };
247
        const childGrid =  this.grid.gridAPI.getChildGrid([pathSegment]);
20✔
248
        const targetIndex = isNext ? 0 : childGrid.dataView.length - 1;
20✔
249
        const targetRec =  childGrid.dataView[targetIndex];
20✔
250
        if (!targetRec) {
20✔
251
            // if no target rec, then move on in next sibling or parent
252
            childGrid.navigation.navigateInBody(targetIndex, visibleColIndex, cb);
4✔
253
            return;
4✔
254
        }
255
        if (childGrid.isChildGridRecord(targetRec)) {
16✔
256
            // if target is a child grid record should move into it.
257
            this.grid.navigation.activeNode.row = null;
1✔
258
            childGrid.navigation.activeNode = { row: targetIndex, column: this.activeNode.column};
1✔
259
            childGrid.navigation._handleScrollInChild(targetIndex, isNext, () => {
1✔
260
                const targetLayoutIndex = isNext ? 0 : childGrid.childLayoutList.toArray().length - 1;
1!
261
                childGrid.navigation._moveToChild(targetIndex, visibleColIndex, isNext, targetLayoutIndex, cb);
1✔
262
            });
263
            return;
1✔
264
        }
265

266
        const childGridNav =  childGrid.navigation;
15✔
267
        this.clearActivation();
15✔
268
        const lastVisibleIndex = childGridNav.lastColumnIndex;
15✔
269
        const columnIndex = visibleColIndex <= lastVisibleIndex ? visibleColIndex : lastVisibleIndex;
15✔
270
        childGridNav.activeNode = { row: targetIndex, column: columnIndex};
15✔
271
        childGrid.tbody.nativeElement.focus({preventScroll: true});
15✔
272
        this._pendingNavigation = false;
15✔
273
        childGrid.navigation._handleScrollInChild(targetIndex, isNext, () => {
15✔
274
            childGrid.navigateTo(targetIndex, columnIndex, cb);
15✔
275
        });
276
    }
277

278
    /**
279
     * Moves navigation back to parent grid.
280
     *
281
     * @param rowIndex
282
     */
283
    protected _moveToParent(isNext: boolean, columnIndex, cb?) {
284
        const indexInParent = this.grid.childRow.index;
6✔
285
        const hasNextTarget = this.hasNextTarget(this.grid.parent, indexInParent, isNext);
6✔
286
        if (!hasNextTarget) {
6!
UNCOV
287
            return;
×
288
        }
289
        this.clearActivation();
6✔
290
        const targetRowIndex =  isNext ? indexInParent + 1 : indexInParent - 1;
6✔
291
        const lastVisibleIndex = this.grid.parent.navigation.lastColumnIndex;
6✔
292
        const nextColumnIndex = columnIndex <= lastVisibleIndex ? columnIndex : lastVisibleIndex;
6!
293
        this._pendingNavigation = true;
6✔
294
        const cbFunc = (args) => {
6✔
295
            this._pendingNavigation = false;
6✔
296
            cb(args);
6✔
297
            args.target.grid.tbody.nativeElement.focus();
6✔
298
        };
299
        this.grid.parent.navigation.navigateInBody(targetRowIndex, nextColumnIndex, cbFunc);
6✔
300
    }
301

302
    /**
303
     * Gets information on the row position relative to the root grid view port.
304
     * Returns whether the row is in view and its offset.
305
     *
306
     * @param rowObj
307
     * @param isNext
308
     */
309
    protected getPositionInfo(row: RowType, isNext: boolean) {
310
        // XXX: Fix type
311
        let rowElem = row.nativeElement;
31✔
312
        if ((row as any).layout) {
31✔
313
            const childLayoutKeys = this.grid.childLayoutKeys;
1✔
314
            const riKey = isNext ? childLayoutKeys[0] : childLayoutKeys[childLayoutKeys.length - 1];
1!
315
            const pathSegment: IPathSegment = {
1✔
316
                rowID: row.data.rowID, rowKey: row.data.rowID,
317
                rowIslandKey: riKey
318
            };
319
            const childGrid =  this.grid.gridAPI.getChildGrid([pathSegment]);
1✔
320
            rowElem = childGrid.tfoot.nativeElement;
1✔
321
        }
322

323
        return this.getElementPosition(rowElem, isNext);
31✔
324
    }
325

326
    protected getElementPosition(element: HTMLElement, isNext: boolean) {
327
        // Special handling for scenarios where there is css transformations applied that affects scale.
328
        // getBoundingClientRect().height returns size after transformations
329
        // element.offsetHeight returns size without any transformations
330
        // get the ratio to figure out if anything has applied transformations
331
        const scaling = element.getBoundingClientRect().height / element.offsetHeight;
34✔
332

333
        const gridBottom = this._getMinBottom(this.grid);
34✔
334
        const diffBottom =
335
        element.getBoundingClientRect().bottom - gridBottom;
34✔
336
        const gridTop = this._getMaxTop(this.grid);
34✔
337
        const diffTop = element.getBoundingClientRect().bottom -
34✔
338
        element.getBoundingClientRect().height - gridTop;
339
        // Adding Math.Round because Chrome has some inconsistencies when the page is zoomed
340
        const isInView = isNext ? Math.round(diffBottom) <= 0 : Math.round(diffTop) >= 0;
34✔
341
        const calcOffset =  isNext ? diffBottom : diffTop;
34✔
342

343
        return { inView: isInView, offset: calcOffset / scaling};
34✔
344
    }
345

346
    /**
347
     * Gets closest element by its tag name.
348
     *
349
     * @param sourceElem The element from which to start the search.
350
     * @param targetTag The target element tag name, for which to search.
351
     */
352
    protected getClosestElemByTag(sourceElem, targetTag) {
353
        let result = sourceElem;
47✔
354
        while (result !== null && result.nodeType === 1) {
47✔
355
            if (result.tagName.toLowerCase() === targetTag.toLowerCase()) {
181✔
356
                return result;
47✔
357
            }
358
            result = result.parentNode;
134✔
359
        }
UNCOV
360
        return null;
×
361
    }
362

363
    private clearActivation() {
364
        // clear if previous activation exists.
365
        if (this.activeNode && Object.keys(this.activeNode).length) {
521✔
366
            this.activeNode = Object.assign({} as IActiveNode);
46✔
367
        }
368
    }
369

370
    private hasNextTarget(grid: GridType, index: number, isNext: boolean) {
371
        const targetRowIndex =  isNext ? index + 1 : index - 1;
6✔
372
        const hasTargetRecord = !!grid.dataView[targetRowIndex];
6✔
373
        if (hasTargetRecord) {
6!
374
            return true;
6✔
375
        } else {
376
            let hasTargetRecordInParent = false;
×
377
            if (grid.parent) {
×
UNCOV
378
                const indexInParent = grid.childRow.index;
×
UNCOV
379
                hasTargetRecordInParent = this.hasNextTarget(grid.parent, indexInParent, isNext);
×
380
            }
UNCOV
381
            return hasTargetRecordInParent;
×
382
        }
383
    }
384

385
    /**
386
     * Gets the max top view in the current grid hierarchy.
387
     *
388
     * @param grid
389
     */
390
    private _getMaxTop(grid) {
391
        let currGrid = grid;
34✔
392
        let top = currGrid.tbody.nativeElement.getBoundingClientRect().top;
34✔
393
        while (currGrid.parent) {
34✔
394
            currGrid = currGrid.parent;
35✔
395
            const pinnedRowsHeight = currGrid.hasPinnedRecords && currGrid.isRowPinningToTop ? currGrid.pinnedRowHeight : 0;
35!
396
            top = Math.max(top, currGrid.tbody.nativeElement.getBoundingClientRect().top + pinnedRowsHeight);
35✔
397
        }
398
        return top;
34✔
399
    }
400

401
    /**
402
     * Gets the min bottom view in the current grid hierarchy.
403
     *
404
     * @param grid
405
     */
406
    private _getMinBottom(grid) {
407
        let currGrid = grid;
34✔
408
        let bottom = currGrid.tbody.nativeElement.getBoundingClientRect().bottom;
34✔
409
        while (currGrid.parent) {
34✔
410
            currGrid = currGrid.parent;
35✔
411
            const pinnedRowsHeight = currGrid.hasPinnedRecords && !currGrid.isRowPinningToTop ? currGrid.pinnedRowHeight : 0;
35!
412
            bottom = Math.min(bottom, currGrid.tbody.nativeElement.getBoundingClientRect().bottom - pinnedRowsHeight);
35✔
413
        }
414
        return bottom;
34✔
415
    }
416

417
    /**
418
     * Finds the next grid that allows scrolling down.
419
     *
420
     * @param grid The grid from which to begin the search.
421
     */
422
    private getNextScrollableDown(grid) {
423
        let currGrid = grid.parent;
5✔
424
        if (!currGrid) {
5!
UNCOV
425
            return { grid, prev: null };
×
426
        }
427
        let scrollTop = currGrid.verticalScrollContainer.scrollPosition;
5✔
428
        let scrollHeight = currGrid.verticalScrollContainer.getScroll().scrollHeight;
5✔
429
        let nonScrollable = scrollHeight === 0 ||
5✔
430
            Math.round(scrollTop + currGrid.verticalScrollContainer.igxForContainerSize) === scrollHeight;
431
        let prev = grid;
5✔
432
        while (nonScrollable && currGrid.parent !== null) {
5!
UNCOV
433
            prev = currGrid;
×
UNCOV
434
            currGrid = currGrid.parent;
×
UNCOV
435
            scrollTop = currGrid.verticalScrollContainer.scrollPosition;
×
UNCOV
436
            scrollHeight = currGrid.verticalScrollContainer.getScroll().scrollHeight;
×
UNCOV
437
            nonScrollable = scrollHeight === 0 ||
×
438
                Math.round(scrollTop + currGrid.verticalScrollContainer.igxForContainerSize) === scrollHeight;
439
        }
440
        return { grid: currGrid, prev };
5✔
441
    }
442

443
    /**
444
     * Finds the next grid that allows scrolling up.
445
     *
446
     * @param grid The grid from which to begin the search.
447
     */
448
    private getNextScrollableUp(grid) {
449
        let currGrid = grid.parent;
3✔
450
        if (!currGrid) {
3!
UNCOV
451
            return { grid, prev: null };
×
452
        }
453
        let nonScrollable = currGrid.verticalScrollContainer.scrollPosition === 0;
3✔
454
        let prev = grid;
3✔
455
        while (nonScrollable && currGrid.parent !== null) {
3✔
456
            prev = currGrid;
1✔
457
            currGrid = currGrid.parent;
1✔
458
            nonScrollable = currGrid.verticalScrollContainer.scrollPosition === 0;
1✔
459
        }
460
        return { grid: currGrid, prev };
3✔
461
    }
462
}
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

© 2025 Coveralls, Inc