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

IgniteUI / igniteui-angular / 11794105037

12 Nov 2024 09:11AM CUT coverage: 91.769% (-0.01%) from 91.783%
11794105037

push

github

web-flow
Merge pull request #15024 from IgniteUI/ganastasov/fix-15019-17.2.x

fix(slider): ensure upper thumb reaches maxValue with decimal steps - 17.2.x

12538 of 14639 branches covered (85.65%)

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

4 existing lines in 2 files now uncovered.

25643 of 27943 relevant lines covered (91.77%)

33087.15 hits per line

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

82.65
/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;
856✔
10

11

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

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

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

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

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

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

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

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

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

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

113
        } else {
114
            super.focusTbody(event);
93✔
115
        }
116
    }
117

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

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

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

181
    /**
182
     * Moves navigation to child grid.
183
     *
184
     * @param parentRowIndex The parent row index, at which the child grid is rendered.
185
     * @param childLayoutIndex Optional. The index of the child row island to which the child grid belongs to. Uses first if not set.
186
     */
187
    protected _moveToChild(parentRowIndex: number, visibleColIndex: number, isNext: boolean, childLayoutIndex?: number,
188
                            cb?: (arg: any) => void) {
189
        const ri = typeof childLayoutIndex !== 'number' ?
20✔
190
         this.grid.childLayoutList.first : this.grid.childLayoutList.toArray()[childLayoutIndex];
191
        const rowId = this.grid.dataView[parentRowIndex].rowID;
20✔
192
        const pathSegment: IPathSegment = {
20✔
193
            rowID: rowId,
194
            rowKey: rowId,
195
            rowIslandKey: ri.key
196
        };
197
        const childGrid =  this.grid.gridAPI.getChildGrid([pathSegment]);
20✔
198
        const targetIndex = isNext ? 0 : childGrid.dataView.length - 1;
20✔
199
        const targetRec =  childGrid.dataView[targetIndex];
20✔
200
        if (!targetRec) {
20✔
201
            // if no target rec, then move on in next sibling or parent
202
            childGrid.navigation.navigateInBody(targetIndex, visibleColIndex, cb);
4✔
203
            return;
4✔
204
        }
205
        if (childGrid.isChildGridRecord(targetRec)) {
16✔
206
            // if target is a child grid record should move into it.
207
            this.grid.navigation.activeNode.row = null;
1✔
208
            childGrid.navigation.activeNode = { row: targetIndex, column: this.activeNode.column};
1✔
209
            childGrid.navigation._handleScrollInChild(targetIndex, isNext, () => {
1✔
210
                const targetLayoutIndex = isNext ? 0 : childGrid.childLayoutList.toArray().length - 1;
1!
211
                childGrid.navigation._moveToChild(targetIndex, visibleColIndex, isNext, targetLayoutIndex, cb);
1✔
212
            });
213
            return;
1✔
214
        }
215

216
        const childGridNav =  childGrid.navigation;
15✔
217
        this.clearActivation();
15✔
218
        const lastVisibleIndex = childGridNav.lastColumnIndex;
15✔
219
        const columnIndex = visibleColIndex <= lastVisibleIndex ? visibleColIndex : lastVisibleIndex;
15✔
220
        childGridNav.activeNode = { row: targetIndex, column: columnIndex};
15✔
221
        childGrid.tbody.nativeElement.focus({preventScroll: true});
15✔
222
        this._pendingNavigation = false;
15✔
223
        childGrid.navigation._handleScrollInChild(targetIndex, isNext, () => {
15✔
224
            childGrid.navigateTo(targetIndex, columnIndex, cb);
15✔
225
        });
226
    }
227

228
    /**
229
     * Moves navigation back to parent grid.
230
     *
231
     * @param rowIndex
232
     */
233
    protected _moveToParent(isNext: boolean, columnIndex, cb?) {
234
        const indexInParent = this.grid.childRow.index;
6✔
235
        const hasNextTarget = this.hasNextTarget(this.grid.parent, indexInParent, isNext);
6✔
236
        if (!hasNextTarget) {
6!
237
            return;
×
238
        }
239
        this.clearActivation();
6✔
240
        const targetRowIndex =  isNext ? indexInParent + 1 : indexInParent - 1;
6✔
241
        const lastVisibleIndex = this.grid.parent.navigation.lastColumnIndex;
6✔
242
        const nextColumnIndex = columnIndex <= lastVisibleIndex ? columnIndex : lastVisibleIndex;
6!
243
        this._pendingNavigation = true;
6✔
244
        const cbFunc = (args) => {
6✔
245
            this._pendingNavigation = false;
6✔
246
            cb(args);
6✔
247
            args.target.grid.tbody.nativeElement.focus();
6✔
248
        };
249
        this.grid.parent.navigation.navigateInBody(targetRowIndex, nextColumnIndex, cbFunc);
6✔
250
    }
251

252
    /**
253
     * Gets information on the row position relative to the root grid view port.
254
     * Returns whether the row is in view and its offset.
255
     *
256
     * @param rowObj
257
     * @param isNext
258
     */
259
    protected getPositionInfo(row: RowType, isNext: boolean) {
260
        // XXX: Fix type
261
        let rowElem = row.nativeElement;
31✔
262
        if ((row as any).layout) {
31✔
263
            const childLayoutKeys = this.grid.childLayoutKeys;
1✔
264
            const riKey = isNext ? childLayoutKeys[0] : childLayoutKeys[childLayoutKeys.length - 1];
1!
265
            const pathSegment: IPathSegment = {
1✔
266
                rowID: row.data.rowID, rowKey: row.data.rowID,
267
                rowIslandKey: riKey
268
            };
269
            const childGrid =  this.grid.gridAPI.getChildGrid([pathSegment]);
1✔
270
            rowElem = childGrid.tfoot.nativeElement;
1✔
271
        }
272
        const gridBottom = this._getMinBottom(this.grid);
31✔
273
        const diffBottom =
274
        rowElem.getBoundingClientRect().bottom - gridBottom;
31✔
275
        const gridTop = this._getMaxTop(this.grid);
31✔
276
        const diffTop = rowElem.getBoundingClientRect().bottom -
31✔
277
        rowElem.offsetHeight - gridTop;
278
        // Adding Math.Round because Chrome has some inconsistencies when the page is zoomed
279
        const isInView = isNext ? Math.round(diffBottom) <= 0 : Math.round(diffTop) >= 0;
31✔
280
        const calcOffset =  isNext ? diffBottom : diffTop;
31✔
281

282
        return { inView: isInView, offset: calcOffset };
31✔
283
    }
284

285
    /**
286
     * Gets closest element by its tag name.
287
     *
288
     * @param sourceElem The element from which to start the search.
289
     * @param targetTag The target element tag name, for which to search.
290
     */
291
    protected getClosestElemByTag(sourceElem, targetTag) {
292
        let result = sourceElem;
47✔
293
        while (result !== null && result.nodeType === 1) {
47✔
294
            if (result.tagName.toLowerCase() === targetTag.toLowerCase()) {
181✔
295
                return result;
47✔
296
            }
297
            result = result.parentNode;
134✔
298
        }
299
        return null;
×
300
    }
301

302
    private clearActivation() {
303
        // clear if previous activation exists.
304
        if (this.activeNode && Object.keys(this.activeNode).length) {
521✔
305
            this.activeNode = Object.assign({} as IActiveNode);
46✔
306
        }
307
    }
308

309
    private hasNextTarget(grid: GridType, index: number, isNext: boolean) {
310
        const targetRowIndex =  isNext ? index + 1 : index - 1;
6✔
311
        const hasTargetRecord = !!grid.dataView[targetRowIndex];
6✔
312
        if (hasTargetRecord) {
6!
313
            return true;
6✔
314
        } else {
315
            let hasTargetRecordInParent = false;
×
316
            if (grid.parent) {
×
317
                const indexInParent = grid.childRow.index;
×
318
                hasTargetRecordInParent = this.hasNextTarget(grid.parent, indexInParent, isNext);
×
319
            }
320
            return hasTargetRecordInParent;
×
321
        }
322
    }
323

324
    /**
325
     * Gets the max top view in the current grid hierarchy.
326
     *
327
     * @param grid
328
     */
329
    private _getMaxTop(grid) {
330
        let currGrid = grid;
31✔
331
        let top = currGrid.tbody.nativeElement.getBoundingClientRect().top;
31✔
332
        while (currGrid.parent) {
31✔
333
            currGrid = currGrid.parent;
34✔
334
            const pinnedRowsHeight = currGrid.hasPinnedRecords && currGrid.isRowPinningToTop ? currGrid.pinnedRowHeight : 0;
34!
335
            top = Math.max(top, currGrid.tbody.nativeElement.getBoundingClientRect().top + pinnedRowsHeight);
34✔
336
        }
337
        return top;
31✔
338
    }
339

340
    /**
341
     * Gets the min bottom view in the current grid hierarchy.
342
     *
343
     * @param grid
344
     */
345
    private _getMinBottom(grid) {
346
        let currGrid = grid;
31✔
347
        let bottom = currGrid.tbody.nativeElement.getBoundingClientRect().bottom;
31✔
348
        while (currGrid.parent) {
31✔
349
            currGrid = currGrid.parent;
34✔
350
            const pinnedRowsHeight = currGrid.hasPinnedRecords && !currGrid.isRowPinningToTop ? currGrid.pinnedRowHeight : 0;
34!
351
            bottom = Math.min(bottom, currGrid.tbody.nativeElement.getBoundingClientRect().bottom - pinnedRowsHeight);
34✔
352
        }
353
        return bottom;
31✔
354
    }
355

356
    /**
357
     * Finds the next grid that allows scrolling down.
358
     *
359
     * @param grid The grid from which to begin the search.
360
     */
361
    private getNextScrollableDown(grid) {
362
        let currGrid = grid.parent;
5✔
363
        if (!currGrid) {
5!
364
            return { grid, prev: null };
×
365
        }
366
        let scrollTop = currGrid.verticalScrollContainer.scrollPosition;
5✔
367
        let scrollHeight = currGrid.verticalScrollContainer.getScroll().scrollHeight;
5✔
368
        let nonScrollable = scrollHeight === 0 ||
5✔
369
            Math.round(scrollTop + currGrid.verticalScrollContainer.igxForContainerSize) === scrollHeight;
370
        let prev = grid;
5✔
371
        while (nonScrollable && currGrid.parent !== null) {
5!
372
            prev = currGrid;
×
373
            currGrid = currGrid.parent;
×
374
            scrollTop = currGrid.verticalScrollContainer.scrollPosition;
×
375
            scrollHeight = currGrid.verticalScrollContainer.getScroll().scrollHeight;
×
376
            nonScrollable = scrollHeight === 0 ||
×
377
                Math.round(scrollTop + currGrid.verticalScrollContainer.igxForContainerSize) === scrollHeight;
378
        }
379
        return { grid: currGrid, prev };
5✔
380
    }
381

382
    /**
383
     * Finds the next grid that allows scrolling up.
384
     *
385
     * @param grid The grid from which to begin the search.
386
     */
387
    private getNextScrollableUp(grid) {
388
        let currGrid = grid.parent;
3✔
389
        if (!currGrid) {
3!
390
            return { grid, prev: null };
×
391
        }
392
        let nonScrollable = currGrid.verticalScrollContainer.scrollPosition === 0;
3✔
393
        let prev = grid;
3✔
394
        while (nonScrollable && currGrid.parent !== null) {
3✔
395
            prev = currGrid;
1✔
396
            currGrid = currGrid.parent;
1✔
397
            nonScrollable = currGrid.verticalScrollContainer.scrollPosition === 0;
1✔
398
        }
399
        return { grid: currGrid, prev };
3✔
400
    }
401
}
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