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

IgniteUI / igniteui-angular / 13331632524

14 Feb 2025 02:51PM CUT coverage: 22.015% (-69.6%) from 91.622%
13331632524

Pull #15372

github

web-flow
Merge d52d57714 into bcb78ae0a
Pull Request #15372: chore(*): test ci passing

1990 of 15592 branches covered (12.76%)

431 of 964 new or added lines in 18 files covered. (44.71%)

19956 existing lines in 307 files now uncovered.

6452 of 29307 relevant lines covered (22.02%)

249.17 hits per line

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

1.67
/projects/igniteui-angular/src/lib/grids/grid-navigation.service.ts
1
import { Injectable } from '@angular/core';
2
import { first } from 'rxjs/operators';
3
import { IgxForOfDirective } from '../directives/for-of/for_of.directive';
4
import { GridType } from './common/grid.interface';
5
import {
6
    NAVIGATION_KEYS,
7
    ROW_COLLAPSE_KEYS,
8
    ROW_EXPAND_KEYS,
9
    SUPPORTED_KEYS,
10
    HORIZONTAL_NAV_KEYS,
11
    HEADER_KEYS,
12
    ROW_ADD_KEYS,
13
    PlatformUtil
14
} from '../core/utils';
15
import { GridKeydownTargetType, GridSelectionMode, FilterMode } from './common/enums';
16
import { IActiveNodeChangeEventArgs } from './common/events';
17
import { IgxGridGroupByRowComponent } from './grid/groupby-row.component';
18
import { IMultiRowLayoutNode } from './common/types';
19
import { SortingDirection } from '../data-operations/sorting-strategy';
20
export interface ColumnGroupsCache {
21
    level: number;
22
    visibleIndex: number;
23
}
24
export interface IActiveNode {
25
    gridID?: string;
26
    row: number;
27
    column?: number;
28
    level?: number;
29
    mchCache?: ColumnGroupsCache;
30
    layout?: IMultiRowLayoutNode;
31
}
32

33
/** @hidden */
34
@Injectable()
35
export class IgxGridNavigationService {
2✔
36
    public grid: GridType;
37
    public _activeNode: IActiveNode = {} as IActiveNode;
37✔
38
    public lastActiveNode: IActiveNode = {} as IActiveNode;
37✔
39
    protected pendingNavigation = false;
37✔
40

41
    public get activeNode() {
42
        return this._activeNode;
15,924✔
43
    }
44

45
    public set activeNode(value: IActiveNode) {
46
        this._activeNode = value;
22✔
47
    }
48

49
    constructor(protected platform: PlatformUtil) { }
37✔
50

51
    public handleNavigation(event: KeyboardEvent) {
UNCOV
52
        const key = event.key.toLowerCase();
×
UNCOV
53
        if (NAVIGATION_KEYS.has(key)) {
×
UNCOV
54
            event.stopPropagation();
×
55
        }
UNCOV
56
        if (this.grid.crudService.cell && NAVIGATION_KEYS.has(key)) {
×
UNCOV
57
            return;
×
58
        }
UNCOV
59
        if (event.repeat && SUPPORTED_KEYS.has(key) || (key === 'tab' && this.grid.crudService.cell)) {
×
UNCOV
60
            event.preventDefault();
×
61
        }
UNCOV
62
        if (event.repeat) {
×
63
            setTimeout(() => this.dispatchEvent(event), 1);
×
64
        } else {
UNCOV
65
            this.dispatchEvent(event);
×
66
        }
67
    }
68

69
    public dispatchEvent(event: KeyboardEvent) {
UNCOV
70
        const key = event.key.toLowerCase();
×
UNCOV
71
        const cellOrRowInEdit = this.grid.crudService.cell || this.grid.crudService.row;
×
UNCOV
72
        if (!this.activeNode || !(SUPPORTED_KEYS.has(key) || (key === 'tab' && cellOrRowInEdit))) {
×
UNCOV
73
            return;
×
74
        }
UNCOV
75
        const shift = event.shiftKey;
×
UNCOV
76
        const ctrl = event.ctrlKey;
×
UNCOV
77
        if (NAVIGATION_KEYS.has(key) && this.pendingNavigation) {
×
78
            event.preventDefault();
×
79
            return;
×
80
        }
81

UNCOV
82
        const type = this.isDataRow(this.activeNode.row) ? 'dataCell' :
×
83
            this.isDataRow(this.activeNode.row, true) ? 'summaryCell' : 'groupRow';
×
UNCOV
84
        if (this.emitKeyDown(type, this.activeNode.row, event)) {
×
85
            return;
×
86
        }
UNCOV
87
        if (event.altKey) {
×
UNCOV
88
            this.handleAlt(key, event);
×
UNCOV
89
            return;
×
90
        }
UNCOV
91
        if ([' ', 'spacebar', 'space'].indexOf(key) === -1) {
×
UNCOV
92
            this.grid.selectionService.keyboardStateOnKeydown(this.activeNode, shift, shift && key === 'tab');
×
93
        }
UNCOV
94
        const position = this.getNextPosition(this.activeNode.row, this.activeNode.column, key, shift, ctrl, event);
×
UNCOV
95
        if (NAVIGATION_KEYS.has(key)) {
×
UNCOV
96
            event.preventDefault();
×
UNCOV
97
            this.navigateInBody(position.rowIndex, position.colIndex, (obj) => {
×
UNCOV
98
                obj.target.activate(event);
×
UNCOV
99
                this.grid.cdr.detectChanges();
×
100
            });
101
        }
UNCOV
102
        this.grid.cdr.detectChanges();
×
103
    }
104

105
    public summaryNav(event: KeyboardEvent) {
UNCOV
106
        if (this.grid.hasSummarizedColumns) {
×
UNCOV
107
            this.horizontalNav(event, event.key.toLowerCase(), this.grid.dataView.length, 'summaryCell');
×
108
        }
109
    }
110

111
    public headerNavigation(event: KeyboardEvent) {
UNCOV
112
        const key = event.key.toLowerCase();
×
UNCOV
113
        if (!HEADER_KEYS.has(key) || this.activeNode?.row !== -1) {
×
UNCOV
114
            return;
×
115
        }
UNCOV
116
        event.preventDefault();
×
117

UNCOV
118
        const ctrl = event.ctrlKey;
×
UNCOV
119
        const shift = event.shiftKey;
×
UNCOV
120
        const alt = event.altKey;
×
121

UNCOV
122
        this.performHeaderKeyCombination(this.currentActiveColumn, key, shift, ctrl, alt, event);
×
UNCOV
123
        if (shift || alt || (ctrl && (key.includes('down') || key.includes('down')))) {
×
UNCOV
124
            return;
×
125
        }
UNCOV
126
        if (this.grid.hasColumnGroups) {
×
UNCOV
127
            this.handleMCHeaderNav(key, ctrl);
×
128
        } else {
UNCOV
129
            this.horizontalNav(event, key, -1, 'headerCell');
×
130
        }
131
    }
132

133
    public focusTbody(event) {
UNCOV
134
        const gridRows = this.grid.verticalScrollContainer.totalItemCount ?? this.grid.dataView.length;
×
UNCOV
135
        if (gridRows < 1) {
×
UNCOV
136
            this.activeNode = null;
×
UNCOV
137
            return;
×
138
        }
UNCOV
139
        if (!this.activeNode || !Object.keys(this.activeNode).length || this.activeNode.row < 0 || this.activeNode.row > gridRows - 1) {
×
UNCOV
140
            const hasLastActiveNode = Object.keys(this.lastActiveNode).length;
×
UNCOV
141
            const shouldClearSelection = hasLastActiveNode && (this.lastActiveNode.row < 0 || this.lastActiveNode.row > gridRows - 1);
×
UNCOV
142
            this.setActiveNode(this.lastActiveNode.row >= 0 && this.lastActiveNode.row < gridRows ?
×
143
                this.firstVisibleNode(this.lastActiveNode.row) : this.firstVisibleNode());
UNCOV
144
            if (shouldClearSelection || (this.grid.cellSelection !== GridSelectionMode.multiple)) {
×
145
                this.grid.clearCellSelection();
×
146
                this.grid.navigateTo(this.activeNode.row, this.activeNode.column, (obj) => {
×
147
                    obj.target?.activate(event);
×
148
                    this.grid.cdr.detectChanges();
×
149
                });
150
            } else {
UNCOV
151
                if (hasLastActiveNode && !this.grid.selectionService.selected(this.lastActiveNode)) {
×
UNCOV
152
                    return;
×
153
                }
UNCOV
154
                const range = {
×
155
                    rowStart: this.activeNode.row, rowEnd: this.activeNode.row,
156
                    columnStart: this.activeNode.column, columnEnd: this.activeNode.column
157
                };
UNCOV
158
                this.grid.selectRange(range);
×
UNCOV
159
                this.grid.notifyChanges();
×
160
            }
161
        }
162
    }
163

164
    public focusFirstCell(header = true) {
×
UNCOV
165
        if ((header || this.grid.dataView.length) && this.activeNode &&
×
166
            (this.activeNode.row === -1 || this.activeNode.row === this.grid.dataView.length ||
167
                (!header && !this.grid.hasSummarizedColumns))) {
UNCOV
168
            return;
×
169
        }
UNCOV
170
        const shouldScrollIntoView = this.lastActiveNode && (header && this.lastActiveNode.row !== -1) ||
×
171
            (!header && this.lastActiveNode.row !== this.grid.dataView.length);
UNCOV
172
        this.setActiveNode(this.firstVisibleNode(header ? -1 : this.grid.dataView.length));
×
UNCOV
173
        if (shouldScrollIntoView) {
×
UNCOV
174
            this.performHorizontalScrollToCell(this.activeNode.column);
×
175
        }
176
    }
177

178
    public isColumnFullyVisible(columnIndex: number) {
UNCOV
179
        if (columnIndex < 0 || this.isColumnPinned(columnIndex, this.forOfDir())) {
×
UNCOV
180
            return true;
×
181
        }
UNCOV
182
        const index = this.getColumnUnpinnedIndex(columnIndex);
×
UNCOV
183
        const width = this.forOfDir().getColumnScrollLeft(index + 1) - this.forOfDir().getColumnScrollLeft(index);
×
UNCOV
184
        if (this.displayContainerWidth < width && this.displayContainerScrollLeft === this.forOfDir().getColumnScrollLeft(index)) {
×
185
            return true;
×
186
        }
UNCOV
187
        return this.displayContainerWidth >= this.forOfDir().getColumnScrollLeft(index + 1) - this.displayContainerScrollLeft &&
×
188
            this.displayContainerScrollLeft <= this.forOfDir().getColumnScrollLeft(index);
189
    }
190

191
    public shouldPerformHorizontalScroll(visibleColIndex: number, rowIndex = -1) {
×
UNCOV
192
        if (visibleColIndex < 0 || visibleColIndex > this.grid.visibleColumns.length - 1) {
×
UNCOV
193
            return false;
×
194
        }
UNCOV
195
        if (rowIndex < 0 || rowIndex > this.grid.dataView.length - 1) {
×
UNCOV
196
            return !this.isColumnFullyVisible(visibleColIndex);
×
197
        }
UNCOV
198
        const row = this.grid.dataView[rowIndex];
×
UNCOV
199
        return row.expression || row.detailsData ? false : !this.isColumnFullyVisible(visibleColIndex);
×
200
    }
201

202
    public shouldPerformVerticalScroll(targetRowIndex: number, _visibleColIndex: number): boolean {
UNCOV
203
        if (this.grid.isRecordPinnedByViewIndex(targetRowIndex)) {
×
UNCOV
204
            return false;
×
205
        }
UNCOV
206
        const scrollRowIndex = this.grid.hasPinnedRecords && this.grid.isRowPinningToTop ?
×
207
            targetRowIndex - this.grid.pinnedDataView.length : targetRowIndex;
UNCOV
208
        const targetRow = this.getRowElementByIndex(targetRowIndex);
×
UNCOV
209
        const rowHeight = this.grid.verticalScrollContainer.getSizeAt(scrollRowIndex);
×
UNCOV
210
        const containerHeight = this.grid.calcHeight ? Math.ceil(this.grid.calcHeight) : 0;
×
UNCOV
211
        const endTopOffset = targetRow ? targetRow.offsetTop + rowHeight + this.containerTopOffset : containerHeight + rowHeight;
×
212
        // this is workaround: endTopOffset - containerHeight > 5 and should be replaced with: containerHeight < endTopOffset
213
        // when the page is zoomed the grid does not scroll the row completely in the view
UNCOV
214
        return !targetRow || targetRow.offsetTop < Math.abs(this.containerTopOffset)
×
215
            || containerHeight && endTopOffset - containerHeight > 5;
216
    }
217

218
    public performVerticalScrollToCell(rowIndex: number, visibleColIndex = -1, cb?: () => void) {
×
UNCOV
219
        if (!this.shouldPerformVerticalScroll(rowIndex, visibleColIndex)) {
×
220
            if (cb) {
×
221
                cb();
×
222
            }
223
            return;
×
224
        }
UNCOV
225
        this.pendingNavigation = true;
×
226
        // Only for top pinning we need to subtract pinned count because virtualization indexing doesn't count pinned rows.
UNCOV
227
        const scrollRowIndex = this.grid.hasPinnedRecords && this.grid.isRowPinningToTop ?
×
228
            rowIndex - this.grid.pinnedDataView.length : rowIndex;
UNCOV
229
        this.grid.verticalScrollContainer.scrollTo(scrollRowIndex);
×
UNCOV
230
        this.grid.verticalScrollContainer.chunkLoad
×
231
            .pipe(first()).subscribe(() => {
UNCOV
232
                this.pendingNavigation = false;
×
UNCOV
233
                if (cb) {
×
UNCOV
234
                    cb();
×
235
                }
236
            });
237
    }
238

239
    public performHorizontalScrollToCell(visibleColumnIndex: number, cb?: () => void) {
UNCOV
240
        if (this.grid.rowList < 1 && this.grid.summariesRowList.length < 1 && this.grid.hasColumnGroups) {
×
UNCOV
241
            let column = this.grid.getColumnByVisibleIndex(visibleColumnIndex);
×
UNCOV
242
            while (column.parent) {
×
UNCOV
243
                column = column.parent;
×
244
            }
UNCOV
245
            visibleColumnIndex = this.forOfDir().igxForOf.indexOf(column);
×
246
        }
UNCOV
247
        if (!this.shouldPerformHorizontalScroll(visibleColumnIndex)) {
×
UNCOV
248
            return;
×
249
        }
UNCOV
250
        this.pendingNavigation = true;
×
UNCOV
251
        this.grid.parentVirtDir.chunkLoad
×
252
            .pipe(first())
253
            .subscribe(() => {
UNCOV
254
                this.pendingNavigation = false;
×
UNCOV
255
                if (cb) {
×
UNCOV
256
                    cb();
×
257
                }
258
            });
UNCOV
259
        this.forOfDir().scrollTo(this.getColumnUnpinnedIndex(visibleColumnIndex));
×
260
    }
261

262
    public isDataRow(rowIndex: number, includeSummary = false) {
×
263
        let curRow: any;
264

UNCOV
265
        if (rowIndex < 0 || rowIndex > this.grid.dataView.length - 1) {
×
UNCOV
266
            curRow = this.grid.dataView[rowIndex - this.grid.virtualizationState.startIndex];
×
UNCOV
267
            if (!curRow) {
×
268
                // if data is remote, record might not be in the view yet.
UNCOV
269
                return this.grid.verticalScrollContainer.isRemote && rowIndex >= 0 && rowIndex <= (this.grid as any).totalItemCount - 1;
×
270
            }
271
        } else {
UNCOV
272
            curRow = this.grid.dataView[rowIndex];
×
273
        }
UNCOV
274
        return curRow && !this.grid.isGroupByRecord(curRow) && !this.grid.isDetailRecord(curRow)
×
275
            && !curRow.childGridsData && (includeSummary || !curRow.summaries);
276
    }
277

278
    public isGroupRow(rowIndex: number): boolean {
UNCOV
279
        if (rowIndex < 0 || rowIndex > this.grid.dataView.length - 1) {
×
280
            return false;
×
281
        }
UNCOV
282
        const curRow = this.grid.dataView[rowIndex];
×
UNCOV
283
        return curRow && this.grid.isGroupByRecord(curRow);
×
284
    }
285

286
    public setActiveNode(activeNode: IActiveNode) {
UNCOV
287
        if (!this.isActiveNodeChanged(activeNode)) {
×
UNCOV
288
            return;
×
289
        }
290

UNCOV
291
        if (!this.activeNode) {
×
UNCOV
292
            this.activeNode = activeNode;
×
293
        }
294

UNCOV
295
        Object.assign(this.activeNode, activeNode);
×
296

UNCOV
297
        const currRow = this.grid.dataView[activeNode.row];
×
UNCOV
298
        const type: GridKeydownTargetType = activeNode.row < 0 ? 'headerCell' :
×
299
            this.isDataRow(activeNode.row) ? 'dataCell' :
×
300
                currRow && this.grid.isGroupByRecord(currRow) ? 'groupRow' :
×
301
                    currRow && this.grid.isDetailRecord(currRow) ? 'masterDetailRow' : 'summaryCell';
×
302

UNCOV
303
        const args: IActiveNodeChangeEventArgs = {
×
304
            row: this.activeNode.row,
305
            column: this.activeNode.column,
306
            level: this.activeNode.level,
307
            tag: type
308
        };
309

UNCOV
310
        this.grid.activeNodeChange.emit(args);
×
311
    }
312

313
    public isActiveNodeChanged(activeNode: IActiveNode) {
UNCOV
314
        let isChanged = false;
×
UNCOV
315
        const checkInnerProp = (aciveNode: ColumnGroupsCache | IMultiRowLayoutNode, prop) => {
×
UNCOV
316
            if (!aciveNode) {
×
UNCOV
317
                isChanged = true;
×
UNCOV
318
                return;
×
319
            }
320

UNCOV
321
            props = Object.getOwnPropertyNames(aciveNode);
×
UNCOV
322
            for (const propName of props) {
×
UNCOV
323
                if (this.activeNode[prop][propName] !== aciveNode[propName]) {
×
UNCOV
324
                    isChanged = true;
×
325
                }
326
            }
327
        };
328

UNCOV
329
        if (!this.activeNode) {
×
UNCOV
330
            return isChanged = true;
×
331
        }
332

UNCOV
333
        let props = Object.getOwnPropertyNames(activeNode);
×
UNCOV
334
        for (const propName of props) {
×
UNCOV
335
            if (!!this.activeNode[propName] && typeof this.activeNode[propName] === 'object') {
×
UNCOV
336
                checkInnerProp(activeNode[propName], propName);
×
UNCOV
337
            } else if (this.activeNode[propName] !== activeNode[propName]) {
×
UNCOV
338
                isChanged = true;
×
339
            }
340
        }
341

UNCOV
342
        return isChanged;
×
343
    }
344

345
    /** Focus the Grid section (header, body, footer) depending on the current activeNode */
346
    public restoreActiveNodeFocus() {
UNCOV
347
        if (!this.activeNode || !Object.keys(this.activeNode).length) {
×
UNCOV
348
            return;
×
349
        }
350

UNCOV
351
        if (this.activeNode.row >= 0 && this.activeNode.row < this.grid.dataView.length) {
×
UNCOV
352
            this.grid.tbody.nativeElement.focus();
×
353
        }
UNCOV
354
        if (this.activeNode.row === -1) {
×
355
            this.grid.theadRow.nativeElement.focus();
×
356
        }
UNCOV
357
        if (this.activeNode.row === this.grid.dataView.length) {
×
358
            this.grid.tfoot.nativeElement.focus();
×
359
        }
360
    }
361

362
    protected getNextPosition(rowIndex: number, colIndex: number, key: string, shift: boolean, ctrl: boolean, event: KeyboardEvent) {
UNCOV
363
        if (!this.isDataRow(rowIndex, true) && (key.indexOf('down') < 0 || key.indexOf('up') < 0) && ctrl) {
×
UNCOV
364
            return { rowIndex, colIndex };
×
365
        }
UNCOV
366
        switch (key) {
×
367
            case 'pagedown':
368
            case 'pageup':
UNCOV
369
                event.preventDefault();
×
UNCOV
370
                if (key === 'pagedown') {
×
UNCOV
371
                    this.grid.verticalScrollContainer.scrollNextPage();
×
372
                } else {
UNCOV
373
                    this.grid.verticalScrollContainer.scrollPrevPage();
×
374
                }
UNCOV
375
                const editCell = this.grid.crudService.cell;
×
UNCOV
376
                this.grid.verticalScrollContainer.chunkLoad
×
377
                    .pipe(first()).subscribe(() => {
UNCOV
378
                        if (editCell && this.grid.rowList.map(r => r.index).indexOf(editCell.rowIndex) < 0) {
×
379
                            this.grid.tbody.nativeElement.focus({ preventScroll: true });
×
380
                        }
381
                    });
UNCOV
382
                break;
×
383
            case 'tab':
UNCOV
384
                this.handleEditing(shift, event);
×
UNCOV
385
                break;
×
386
            case 'end':
UNCOV
387
                rowIndex = ctrl ? this.findLastDataRowIndex() : this.activeNode.row;
×
UNCOV
388
                colIndex = this.lastColumnIndex;
×
UNCOV
389
                break;
×
390
            case 'home':
UNCOV
391
                rowIndex = ctrl ? this.findFirstDataRowIndex() : this.activeNode.row;
×
UNCOV
392
                colIndex = 0;
×
UNCOV
393
                break;
×
394
            case 'arrowleft':
395
            case 'left':
UNCOV
396
                colIndex = ctrl ? 0 : this.activeNode.column - 1;
×
UNCOV
397
                break;
×
398
            case 'arrowright':
399
            case 'right':
UNCOV
400
                colIndex = ctrl ? this.lastColumnIndex : this.activeNode.column + 1;
×
UNCOV
401
                break;
×
402
            case 'arrowup':
403
            case 'up':
UNCOV
404
                if (ctrl && !this.isDataRow(rowIndex) || (this.grid.rowEditable && this.grid.crudService.rowEditingBlocked)) {
×
UNCOV
405
                    break;
×
406
                }
UNCOV
407
                colIndex = this.activeNode.column !== undefined ? this.activeNode.column : 0;
×
UNCOV
408
                rowIndex = ctrl ? this.findFirstDataRowIndex() : this.activeNode.row - 1;
×
UNCOV
409
                break;
×
410
            case 'arrowdown':
411
            case 'down':
UNCOV
412
                if ((ctrl && !this.isDataRow(rowIndex)) || (this.grid.rowEditable && this.grid.crudService.rowEditingBlocked)) {
×
UNCOV
413
                    break;
×
414
                }
UNCOV
415
                colIndex = this.activeNode.column !== undefined ? this.activeNode.column : 0;
×
UNCOV
416
                rowIndex = ctrl ? this.findLastDataRowIndex() : this.activeNode.row + 1;
×
UNCOV
417
                break;
×
418
            case 'enter':
419
            case 'f2':
UNCOV
420
                const cell = this.grid.gridAPI.get_cell_by_visible_index(this.activeNode.row, this.activeNode.column);
×
UNCOV
421
                if (!this.isDataRow(rowIndex) || !cell.editable) {
×
UNCOV
422
                    break;
×
423
                }
UNCOV
424
                this.grid.crudService.enterEditMode(cell, event);
×
UNCOV
425
                break;
×
426
            case 'escape':
427
            case 'esc':
UNCOV
428
                if (!this.isDataRow(rowIndex)) {
×
UNCOV
429
                    break;
×
430
                }
431

UNCOV
432
                if (this.grid.crudService.isInCompositionMode) {
×
433
                    return;
×
434
                }
435

UNCOV
436
                if (this.grid.crudService.cellInEditMode || this.grid.crudService.rowInEditMode) {
×
UNCOV
437
                    this.grid.crudService.endEdit(false, event);
×
UNCOV
438
                    if (this.platform.isEdge) {
×
439
                        this.grid.cdr.detectChanges();
×
440
                    }
UNCOV
441
                    this.grid.tbody.nativeElement.focus();
×
442
                }
UNCOV
443
                break;
×
444
            case ' ':
445
            case 'spacebar':
446
            case 'space':
UNCOV
447
                const rowObj = this.grid.gridAPI.get_row_by_index(this.activeNode.row);
×
UNCOV
448
                if (this.grid.isRowSelectable && rowObj) {
×
UNCOV
449
                    if (this.isDataRow(rowIndex)) {
×
UNCOV
450
                        if (rowObj.selected) {
×
UNCOV
451
                            this.grid.selectionService.deselectRow(rowObj.key, event);
×
452
                        } else {
UNCOV
453
                            this.grid.selectionService.selectRowById(rowObj.key, false, event);
×
454
                        }
455
                    }
UNCOV
456
                    if (this.isGroupRow(rowIndex)) {
×
UNCOV
457
                        ((rowObj as any) as IgxGridGroupByRowComponent).onGroupSelectorClick(event);
×
458
                    }
459
                }
UNCOV
460
                break;
×
461
            default:
462
                return;
×
463
        }
UNCOV
464
        return { rowIndex, colIndex };
×
465
    }
466

467
    protected horizontalNav(event: KeyboardEvent, key: string, rowIndex: number, tag: GridKeydownTargetType) {
UNCOV
468
        const ctrl = event.ctrlKey;
×
UNCOV
469
        if (!HORIZONTAL_NAV_KEYS.has(event.key.toLowerCase())) {
×
UNCOV
470
            return;
×
471
        }
UNCOV
472
        event.preventDefault();
×
UNCOV
473
        this.activeNode.row = rowIndex;
×
UNCOV
474
        if (rowIndex > 0) {
×
UNCOV
475
            if (this.emitKeyDown('summaryCell', this.activeNode.row, event)) {
×
476
                return;
×
477
            }
478
        }
479

UNCOV
480
        const newActiveNode = {
×
481
            column: this.activeNode.column,
482
            mchCache: {
483
                level: this.activeNode.level,
484
                visibleIndex: this.activeNode.column
485
            }
486
        };
487

UNCOV
488
        if ((key.includes('left') || key === 'home') && this.activeNode.column > 0) {
×
UNCOV
489
            newActiveNode.column = ctrl || key === 'home' ? 0 : this.activeNode.column - 1;
×
490
        }
UNCOV
491
        if ((key.includes('right') || key === 'end') && this.activeNode.column < this.lastColumnIndex) {
×
UNCOV
492
            newActiveNode.column = ctrl || key === 'end' ? this.lastColumnIndex : this.activeNode.column + 1;
×
493
        }
494

UNCOV
495
        if (tag === 'headerCell') {
×
UNCOV
496
            const column = this.grid.getColumnByVisibleIndex(newActiveNode.column);
×
UNCOV
497
            newActiveNode.mchCache.level = column.level;
×
UNCOV
498
            newActiveNode.mchCache.visibleIndex = column.visibleIndex;
×
499
        }
500

UNCOV
501
        this.setActiveNode({ row: this.activeNode.row, column: newActiveNode.column, mchCache: newActiveNode.mchCache });
×
UNCOV
502
        this.performHorizontalScrollToCell(this.activeNode.column);
×
503
    }
504

505
    public get lastColumnIndex() {
UNCOV
506
        return Math.max(...this.grid.visibleColumns.map(col => col.visibleIndex));
×
507
    }
508
    public get displayContainerWidth() {
UNCOV
509
        return Math.round(this.grid.parentVirtDir.dc.instance._viewContainer.element.nativeElement.offsetWidth);
×
510
    }
511
    public get displayContainerScrollLeft() {
UNCOV
512
        return Math.ceil(this.grid.headerContainer.scrollPosition);
×
513
    }
514
    public get containerTopOffset() {
UNCOV
515
        return parseInt(this.grid.verticalScrollContainer.dc.instance._viewContainer.element.nativeElement.style.top, 10);
×
516
    }
517

518
    protected getColumnUnpinnedIndex(visibleColumnIndex: number) {
UNCOV
519
        const column = this.grid.unpinnedColumns.find((col) => !col.columnGroup && col.visibleIndex === visibleColumnIndex);
×
UNCOV
520
        return this.grid.pinnedColumns.length ? this.grid.unpinnedColumns.filter((c) => !c.columnGroup).indexOf(column) :
×
521
            visibleColumnIndex;
522
    }
523

524
    protected forOfDir(): IgxForOfDirective<any> {
UNCOV
525
        const forOfDir = this.grid.dataRowList.length > 0 ? this.grid.dataRowList.first.virtDirRow : this.grid.summariesRowList.length ?
×
526
            this.grid.summariesRowList.first.virtDirRow : this.grid.headerContainer;
UNCOV
527
        return forOfDir as IgxForOfDirective<any>;
×
528
    }
529

530
    protected handleAlt(key: string, event: KeyboardEvent) {
UNCOV
531
        event.preventDefault();
×
532
        // todo TODO ROW
UNCOV
533
        const row = this.grid.gridAPI.get_row_by_index(this.activeNode.row);
×
534

UNCOV
535
        if (!(this.isToggleKey(key) || this.isAddKey(key)) || !row) {
×
536
            return;
×
537
        }
UNCOV
538
        if (this.isAddKey(key)) {
×
UNCOV
539
            if (!this.grid.rowEditable) {
×
540
                console.warn('The grid must be in row edit mode to perform row adding!');
×
541
                return;
×
542
            }
543

UNCOV
544
            if (event.shiftKey && row.treeRow !== undefined) {
×
545
                this.grid.crudService.enterAddRowMode(row, true, event);
×
UNCOV
546
            } else if (!event.shiftKey) {
×
UNCOV
547
                this.grid.crudService.enterAddRowMode(row, false, event);
×
548
            }
UNCOV
549
        } else if (!row.expanded && ROW_EXPAND_KEYS.has(key)) {
×
UNCOV
550
            if (row.key === undefined) {
×
551
                // TODO use expanded row.expanded = !row.expanded;
UNCOV
552
                (row as any).toggle();
×
553
            } else {
UNCOV
554
                this.grid.gridAPI.set_row_expansion_state(row.key, true, event);
×
555
            }
UNCOV
556
        } else if (row.expanded && ROW_COLLAPSE_KEYS.has(key)) {
×
UNCOV
557
            if (row.key === undefined) {
×
558
                // TODO use expanded row.expanded = !row.expanded;
UNCOV
559
                (row as any).toggle();
×
560
            } else {
UNCOV
561
                this.grid.gridAPI.set_row_expansion_state(row.key, false, event);
×
562
            }
563
        }
UNCOV
564
        this.grid.notifyChanges();
×
565
    }
566

567
    protected handleEditing(shift: boolean, event: KeyboardEvent) {
UNCOV
568
        const next = shift ? this.grid.getPreviousCell(this.activeNode.row, this.activeNode.column, col => col.editable) :
×
UNCOV
569
            this.grid.getNextCell(this.activeNode.row, this.activeNode.column, col => col.editable);
×
UNCOV
570
        if (!this.grid.crudService.rowInEditMode && this.isActiveNode(next.rowIndex, next.visibleColumnIndex)) {
×
571
            this.grid.crudService.endEdit(true, event);
×
572
            this.grid.tbody.nativeElement.focus();
×
573
            return;
×
574
        }
UNCOV
575
        event.preventDefault();
×
UNCOV
576
        if ((this.grid.crudService.rowInEditMode && this.grid.rowEditTabs.length) &&
×
577
            (this.activeNode.row !== next.rowIndex || this.isActiveNode(next.rowIndex, next.visibleColumnIndex))) {
UNCOV
578
            const args = this.grid.crudService.updateCell(true, event);
×
UNCOV
579
            if (args.cancel) {
×
580
                return;
×
UNCOV
581
            } else if (shift) {
×
UNCOV
582
                this.grid.rowEditTabs.last.element.nativeElement.focus();
×
583
            } else {
UNCOV
584
                this.grid.rowEditTabs.first.element.nativeElement.focus();
×
585
            }
UNCOV
586
            return;
×
587
        }
588

UNCOV
589
        if (this.grid.crudService.rowInEditMode && !this.grid.rowEditTabs.length) {
×
UNCOV
590
            if (shift && next.rowIndex === this.activeNode.row && next.visibleColumnIndex === this.activeNode.column) {
×
UNCOV
591
                next.visibleColumnIndex = this.grid.lastEditableColumnIndex;
×
UNCOV
592
            } else if (!shift && next.rowIndex === this.activeNode.row && next.visibleColumnIndex === this.activeNode.column) {
×
593
                next.visibleColumnIndex = this.grid.firstEditableColumnIndex;
×
594
            } else {
UNCOV
595
                next.rowIndex = this.activeNode.row;
×
596
            }
597
        }
598

UNCOV
599
        this.navigateInBody(next.rowIndex, next.visibleColumnIndex, (obj) => {
×
UNCOV
600
            obj.target.activate(event);
×
UNCOV
601
            this.grid.cdr.detectChanges();
×
602
        });
603
    }
604

605
    protected navigateInBody(rowIndex, visibleColIndex, cb: (arg: any) => void = null): void {
×
UNCOV
606
        if (!this.isValidPosition(rowIndex, visibleColIndex) || this.isActiveNode(rowIndex, visibleColIndex)) {
×
UNCOV
607
            return;
×
608
        }
UNCOV
609
        this.grid.navigateTo(rowIndex, visibleColIndex, cb);
×
610
    }
611

612

613
    protected emitKeyDown(type: GridKeydownTargetType, rowIndex, event) {
UNCOV
614
        const row = this.grid.summariesRowList.toArray().concat(this.grid.rowList.toArray()).find(r => r.index === rowIndex);
×
UNCOV
615
        if (!row) {
×
UNCOV
616
            return;
×
617
        }
618

UNCOV
619
        const target = type === 'groupRow' ? row :
×
UNCOV
620
            type === 'dataCell' ? row.cells?.find(c => c.visibleColumnIndex === this.activeNode.column) :
×
UNCOV
621
                row.summaryCells?.find(c => c.visibleColumnIndex === this.activeNode.column);
×
UNCOV
622
        const keydownArgs = { targetType: type, event, cancel: false, target };
×
UNCOV
623
        this.grid.gridKeydown.emit(keydownArgs);
×
UNCOV
624
        if (keydownArgs.cancel && type === 'dataCell') {
×
625
            this.grid.selectionService.clear();
×
626
            this.grid.selectionService.keyboardState.active = true;
×
627
            return keydownArgs.cancel;
×
628
        }
629
    }
630

631
    protected isColumnPinned(columnIndex: number, forOfDir: IgxForOfDirective<any>): boolean {
UNCOV
632
        const horizontalScroll = forOfDir.getScroll();
×
UNCOV
633
        return (!horizontalScroll.clientWidth || this.grid.getColumnByVisibleIndex(columnIndex)?.pinned);
×
634
    }
635

636
    protected findFirstDataRowIndex(): number {
UNCOV
637
        return this.grid.dataView.findIndex(rec => !this.grid.isGroupByRecord(rec) && !this.grid.isDetailRecord(rec) && !rec.summaries);
×
638
    }
639

640
    protected findLastDataRowIndex(): number {
UNCOV
641
        if ((this.grid as any).totalItemCount) {
×
642
            return (this.grid as any).totalItemCount - 1;
×
643
        }
UNCOV
644
        let i = this.grid.dataView.length;
×
UNCOV
645
        while (i--) {
×
UNCOV
646
            if (this.isDataRow(i)) {
×
UNCOV
647
                return i;
×
648
            }
649
        }
650
    }
651

652
    protected getRowElementByIndex(index) {
UNCOV
653
        if (this.grid.hasDetails) {
×
UNCOV
654
            const detail = this.grid.nativeElement.querySelector(`[detail="true"][data-rowindex="${index}"]`);
×
UNCOV
655
            if (detail) {
×
UNCOV
656
                return detail;
×
657
            }
658
        }
UNCOV
659
        return this.grid.rowList.toArray().concat(this.grid.summariesRowList.toArray()).find(r => r.index === index)?.nativeElement;
×
660
    }
661

662
    protected isValidPosition(rowIndex: number, colIndex: number): boolean {
UNCOV
663
        const length = (this.grid as any).totalItemCount ?? this.grid.dataView.length;
×
UNCOV
664
        if (rowIndex < 0 || colIndex < 0 || length - 1 < rowIndex || this.lastColumnIndex < colIndex) {
×
UNCOV
665
            return false;
×
666
        }
UNCOV
667
        return this.activeNode.column !== colIndex && !this.isDataRow(rowIndex, true) ? false : true;
×
668
    }
669
    protected performHeaderKeyCombination(column, key, shift, ctrl, alt, event) {
UNCOV
670
        let direction = this.grid.sortingExpressions.find(expr => expr.fieldName === column.field)?.dir;
×
UNCOV
671
        if (ctrl && key.includes('up') && column.sortable && !column.columnGroup) {
×
UNCOV
672
            direction = direction === SortingDirection.Asc ? SortingDirection.None : SortingDirection.Asc;
×
UNCOV
673
            this.grid.sort({ fieldName: column.field, dir: direction, ignoreCase: false });
×
UNCOV
674
            return;
×
675
        }
UNCOV
676
        if (ctrl && key.includes('down') && column.sortable && !column.columnGroup) {
×
UNCOV
677
            direction = direction === SortingDirection.Desc ? SortingDirection.None : SortingDirection.Desc;
×
UNCOV
678
            this.grid.sort({ fieldName: column.field, dir: direction, ignoreCase: false });
×
UNCOV
679
            return;
×
680
        }
UNCOV
681
        if (shift && alt && this.isToggleKey(key) && !column.columnGroup && column.groupable) {
×
UNCOV
682
            direction = direction || SortingDirection.Asc;
×
UNCOV
683
            if (key.includes('right')) {
×
UNCOV
684
                (this.grid as any).groupBy({
×
685
                    fieldName: column.field,
686
                    dir: direction,
687
                    ignoreCase: column.sortingIgnoreCase,
688
                    strategy: column.sortStrategy,
689
                    groupingComparer: column.groupingComparer,
690
                });
691
            } else {
UNCOV
692
                (this.grid as any).clearGrouping(column.field);
×
693
            }
UNCOV
694
            this.activeNode.column = key.includes('right') && (this.grid as any).hideGroupedColumns &&
×
695
                column.visibleIndex === this.lastColumnIndex ? this.lastColumnIndex - 1 : this.activeNode.column;
UNCOV
696
            return;
×
697
        }
UNCOV
698
        if (alt && (ROW_EXPAND_KEYS.has(key) || ROW_COLLAPSE_KEYS.has(key))) {
×
UNCOV
699
            this.handleMCHExpandCollapse(key, column);
×
UNCOV
700
            return;
×
701
        }
UNCOV
702
        if ([' ', 'spacebar', 'space'].indexOf(key) !== -1) {
×
UNCOV
703
            this.handleColumnSelection(column, event);
×
704
        }
UNCOV
705
        if (alt && (key === 'l' || key === '¬') && this.grid.allowAdvancedFiltering) {
×
UNCOV
706
            this.grid.openAdvancedFilteringDialog();
×
707
        }
UNCOV
708
        if (ctrl && shift && key === 'l' && this.grid.allowFiltering && !column.columnGroup && column.filterable) {
×
UNCOV
709
            if (this.grid.filterMode === FilterMode.excelStyleFilter) {
×
UNCOV
710
                const headerEl = this.grid.headerGroups.find(g => g.active).nativeElement;
×
UNCOV
711
                this.grid.filteringService.toggleFilterDropdown(headerEl, column);
×
712
            } else {
UNCOV
713
                this.performHorizontalScrollToCell(column.visibleIndex);
×
UNCOV
714
                this.grid.filteringService.filteredColumn = column;
×
UNCOV
715
                this.grid.filteringService.isFilterRowVisible = true;
×
716
            }
717
        }
718
    }
719

720
    private firstVisibleNode(rowIndex?) {
UNCOV
721
        const colIndex = this.lastActiveNode.column !== undefined ? this.lastActiveNode.column :
×
UNCOV
722
            this.grid.visibleColumns.sort((c1, c2) => c1.visibleIndex - c2.visibleIndex)
×
UNCOV
723
                .find(c => this.isColumnFullyVisible(c.visibleIndex))?.visibleIndex;
×
UNCOV
724
        const column = this.grid.visibleColumns.find((col) => !col.columnLayout && col.visibleIndex === colIndex);
×
UNCOV
725
        const rowInd = rowIndex ? rowIndex : this.grid.rowList.find(r => !this.shouldPerformVerticalScroll(r.index, colIndex))?.index;
×
UNCOV
726
        const node = {
×
727
            row: rowInd ?? 0,
×
728
            column: column?.visibleIndex ?? 0, level: column?.level ?? 0,
×
729
            mchCache: column ? { level: column.level, visibleIndex: column.visibleIndex } : {} as ColumnGroupsCache,
×
730
            layout: column && column.columnLayoutChild ? {
×
731
                rowStart: column.rowStart, colStart: column.colStart,
732
                rowEnd: column.rowEnd, colEnd: column.colEnd, columnVisibleIndex: column.visibleIndex
733
            } : null
734
        };
UNCOV
735
        return node;
×
736
    }
737

738
    private handleMCHeaderNav(key: string, ctrl: boolean) {
UNCOV
739
        const newHeaderNode: ColumnGroupsCache = {
×
740
            visibleIndex: this.activeNode.mchCache.visibleIndex,
741
            level: this.activeNode.mchCache.level
742
        };
UNCOV
743
        const activeCol = this.currentActiveColumn;
×
UNCOV
744
        const lastGroupIndex = Math.max(... this.grid.visibleColumns.
×
UNCOV
745
            filter(c => c.level <= this.activeNode.level).map(col => col.visibleIndex));
×
UNCOV
746
        let nextCol = activeCol;
×
UNCOV
747
        if ((key.includes('left') || key === 'home') && this.activeNode.column > 0) {
×
UNCOV
748
            const index = ctrl || key === 'home' ? 0 : this.activeNode.column - 1;
×
UNCOV
749
            nextCol = this.getNextColumnMCH(index);
×
UNCOV
750
            newHeaderNode.visibleIndex = nextCol.visibleIndex;
×
751
        }
UNCOV
752
        if ((key.includes('right') || key === 'end') && activeCol.visibleIndex < lastGroupIndex) {
×
UNCOV
753
            const nextVIndex = activeCol.children ? Math.max(...activeCol.allChildren.map(c => c.visibleIndex)) + 1 :
×
754
                activeCol.visibleIndex + 1;
UNCOV
755
            nextCol = ctrl || key === 'end' ? this.getNextColumnMCH(this.lastColumnIndex) : this.getNextColumnMCH(nextVIndex);
×
UNCOV
756
            newHeaderNode.visibleIndex = nextCol.visibleIndex;
×
757
        }
UNCOV
758
        if (!ctrl && key.includes('up') && this.activeNode.level > 0) {
×
UNCOV
759
            nextCol = activeCol.parent;
×
UNCOV
760
            newHeaderNode.level = nextCol.level;
×
761
        }
UNCOV
762
        if (!ctrl && key.includes('down') && activeCol.children) {
×
UNCOV
763
            nextCol = activeCol.children.find(c => c.visibleIndex === newHeaderNode.visibleIndex) ||
×
UNCOV
764
                activeCol.children.toArray().sort((a, b) => b.visibleIndex - a.visibleIndex)
×
UNCOV
765
                    .filter(col => col.visibleIndex < newHeaderNode.visibleIndex)[0];
×
UNCOV
766
            newHeaderNode.level = nextCol.level;
×
767
        }
768

UNCOV
769
        this.setActiveNode({
×
770
            row: this.activeNode.row,
771
            column: nextCol.visibleIndex,
772
            level: nextCol.level,
773
            mchCache: newHeaderNode
774
        });
UNCOV
775
        this.performHorizontalScrollToCell(nextCol.visibleIndex);
×
776
    }
777

778
    private handleMCHExpandCollapse(key, column) {
UNCOV
779
        if (!column.children || !column.collapsible) {
×
UNCOV
780
            return;
×
781
        }
UNCOV
782
        if (!column.expanded && ROW_EXPAND_KEYS.has(key)) {
×
UNCOV
783
            column.expanded = true;
×
UNCOV
784
        } else if (column.expanded && ROW_COLLAPSE_KEYS.has(key)) {
×
UNCOV
785
            column.expanded = false;
×
786
        }
787
    }
788

789
    private handleColumnSelection(column, event) {
UNCOV
790
        if (!column.selectable || this.grid.columnSelection === GridSelectionMode.none) {
×
UNCOV
791
            return;
×
792
        }
UNCOV
793
        const clearSelection = this.grid.columnSelection === GridSelectionMode.single;
×
UNCOV
794
        const columnsToSelect = !column.children ? [column.field] :
×
UNCOV
795
            column.allChildren.filter(c => !c.hidden && c.selectable && !c.columnGroup).map(c => c.field);
×
UNCOV
796
        if (column.selected) {
×
UNCOV
797
            this.grid.selectionService.deselectColumns(columnsToSelect, event);
×
798
        } else {
UNCOV
799
            this.grid.selectionService.selectColumns(columnsToSelect, clearSelection, false, event);
×
800
        }
801
    }
802

803
    private getNextColumnMCH(visibleIndex) {
UNCOV
804
        let col = this.grid.getColumnByVisibleIndex(visibleIndex);
×
UNCOV
805
        let parent = col.parent;
×
UNCOV
806
        while (parent && col.level > this.activeNode.mchCache.level) {
×
UNCOV
807
            col = col.parent;
×
UNCOV
808
            parent = col.parent;
×
809
        }
UNCOV
810
        return col;
×
811
    }
812

813
    private get currentActiveColumn() {
UNCOV
814
        return this.grid.visibleColumns.find(c => c.visibleIndex === this.activeNode.column && c.level === this.activeNode.level);
×
815
    }
816

817
    private isActiveNode(rIndex: number, cIndex: number): boolean {
UNCOV
818
        return this.activeNode ? this.activeNode.row === rIndex && this.activeNode.column === cIndex : false;
×
819
    }
820

821
    private isToggleKey(key: string): boolean {
UNCOV
822
        return ROW_COLLAPSE_KEYS.has(key) || ROW_EXPAND_KEYS.has(key);
×
823
    }
824

825
    private isAddKey(key: string): boolean {
UNCOV
826
        return ROW_ADD_KEYS.has(key);
×
827
    }
828
}
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