Skip to content

Commit 477cf70

Browse files
committed
feat(pivotGrid): Add navigation for row headers and wrap around to column headers and back.
1 parent 8171830 commit 477cf70

12 files changed

+473
-88
lines changed
Lines changed: 234 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,37 @@
1-
import { IgxGridNavigationService } from '../grid-navigation.service';
1+
import { IActiveNode, IgxGridNavigationService } from '../grid-navigation.service';
22
import { Injectable } from '@angular/core';
33
import { IgxPivotGridComponent } from './pivot-grid.component';
44
import { HEADER_KEYS } from '../../core/utils';
5+
import { IgxPivotRowDimensionMrlRowComponent } from './pivot-row-dimension-mrl-row.component';
6+
import { IMultiRowLayoutNode } from '../public_api';
57

68
@Injectable()
79
export class IgxPivotGridNavigationService extends IgxGridNavigationService {
810
public override grid: IgxPivotGridComponent;
11+
protected _isRowHeaderActive = false;
12+
protected _isRowDimensionHeaderActive = false;
913

10-
public isRowHeaderActive: boolean;
14+
public set isRowHeaderActive(value: boolean) {
15+
this._isRowHeaderActive = value;
16+
}
17+
public get isRowHeaderActive() {
18+
return this._isRowHeaderActive;
19+
}
20+
21+
public set isRowDimensionHeaderActive(value: boolean) {
22+
this._isRowDimensionHeaderActive = value;
23+
}
24+
public get isRowDimensionHeaderActive() {
25+
return this._isRowDimensionHeaderActive;
26+
}
1127

1228
public get lastRowDimensionsIndex() {
13-
return this.grid.rowDimensions.length - 1;
29+
return this.grid.visibleRowDimensions.length - 1;
1430
}
1531

1632
public focusOutRowHeader() {
1733
this.isRowHeaderActive = false;
34+
this.isRowDimensionHeaderActive = false;
1835
}
1936

2037
public override handleNavigation(event: KeyboardEvent) {
@@ -26,9 +43,101 @@ export class IgxPivotGridNavigationService extends IgxGridNavigationService {
2643
}
2744
event.preventDefault();
2845

29-
const newActiveNode = {
30-
row: this.activeNode.row, column: this.activeNode.column, level: null,
46+
const newActiveNode: IActiveNode = {
47+
row: this.activeNode.row,
48+
column: this.activeNode.column,
49+
level: null,
3150
mchCache: null,
51+
layout: this.activeNode.layout
52+
}
53+
54+
let verticalContainer;
55+
if (this.grid.horizontalRowDimensions) {
56+
let newPosition = {
57+
row: this.activeNode.row,
58+
column: this.activeNode.column,
59+
layout: this.activeNode.layout
60+
};
61+
verticalContainer = this.grid.verticalRowDimScrollContainers.first;
62+
if (key.includes('left')) {
63+
newPosition = this.getNextHorizontalPosition(true, ctrl);
64+
}
65+
if (key.includes('right')) {
66+
newPosition = this.getNextHorizontalPosition(false, ctrl);
67+
}
68+
if (key.includes('up') || key === 'home') {
69+
newPosition = this.getNextVerticalPosition(true, ctrl || key === 'home', key === 'home');
70+
}
71+
72+
if (key.includes('down') || key === 'end') {
73+
newPosition = this.getNextVerticalPosition(false, ctrl || key === 'end', key === 'end');
74+
}
75+
76+
newActiveNode.row = newPosition.row;
77+
newActiveNode.column = newPosition.column;
78+
newActiveNode.layout = newPosition.layout;
79+
} else {
80+
if ((key.includes('left') || key === 'home') && this.activeNode.column > 0) {
81+
newActiveNode.column = ctrl || key === 'home' ? 0 : this.activeNode.column - 1;
82+
}
83+
if ((key.includes('right') || key === 'end') && this.activeNode.column < this.lastRowDimensionsIndex) {
84+
newActiveNode.column = ctrl || key === 'end' ? this.lastRowDimensionsIndex : this.activeNode.column + 1;
85+
}
86+
87+
verticalContainer = this.grid.verticalRowDimScrollContainers.toArray()[newActiveNode.column];
88+
if (key.includes('up') && this.activeNode.row > 0) {
89+
newActiveNode.row = ctrl ? 0 : this.activeNode.row - 1;
90+
} else if (key.includes('up')) {
91+
newActiveNode.row = 0;
92+
newActiveNode.column = newActiveNode.layout.colStart - 1;
93+
newActiveNode.layout = null;
94+
this.isRowDimensionHeaderActive = true;
95+
this.isRowHeaderActive = false;
96+
this.grid.theadRow.nativeElement.focus();
97+
}
98+
99+
if (key.includes('down') && this.activeNode.row < this.findLastDataRowIndex()) {
100+
newActiveNode.row = ctrl ? verticalContainer.igxForOf.length - 1 : Math.min(this.activeNode.row + 1, verticalContainer.igxForOf.length - 1);
101+
}
102+
103+
if (key.includes('left') || key.includes('right')) {
104+
const prevRIndex = this.activeNode.row;
105+
const prevScrContainer = this.grid.verticalRowDimScrollContainers.toArray()[this.activeNode.column];
106+
const src = prevScrContainer.getScrollForIndex(prevRIndex);
107+
newActiveNode.row = this.activeNode.mchCache && this.activeNode.mchCache.level === newActiveNode.column ?
108+
this.activeNode.mchCache.visibleIndex :
109+
verticalContainer.getIndexAtScroll(src);
110+
newActiveNode.mchCache = {
111+
visibleIndex: this.activeNode.row,
112+
level: this.activeNode.column
113+
};
114+
}
115+
}
116+
117+
this.setActiveNode(newActiveNode);
118+
if (verticalContainer.isIndexOutsideView(newActiveNode.row)) {
119+
verticalContainer.scrollTo(newActiveNode.row);
120+
}
121+
} else {
122+
super.handleNavigation(event);
123+
}
124+
}
125+
126+
public override headerNavigation(event: KeyboardEvent) {
127+
const key = event.key.toLowerCase();
128+
const ctrl = event.ctrlKey;
129+
if (!HEADER_KEYS.has(key)) {
130+
return;
131+
}
132+
133+
if (this.isRowDimensionHeaderActive) {
134+
event.preventDefault();
135+
136+
const newActiveNode: IActiveNode = {
137+
row: this.activeNode.row,
138+
column: this.activeNode.column,
139+
level: null,
140+
mchCache: this.activeNode.mchCache,
32141
layout: null
33142
}
34143

@@ -37,33 +146,55 @@ export class IgxPivotGridNavigationService extends IgxGridNavigationService {
37146
}
38147
if ((key.includes('right') || key === 'end') && this.activeNode.column < this.lastRowDimensionsIndex) {
39148
newActiveNode.column = ctrl || key === 'end' ? this.lastRowDimensionsIndex : this.activeNode.column + 1;
40-
}
41-
const verticalContainer = this.grid.verticalRowDimScrollContainers.toArray()[newActiveNode.column];
42-
if ((key.includes('up')) && this.activeNode.row > 0) {
43-
newActiveNode.row = ctrl ? 0 : this.activeNode.row - 1;
44-
}
45-
if ((key.includes('down')) && this.activeNode.row < this.findLastDataRowIndex()) {
46-
newActiveNode.row = ctrl ? verticalContainer.igxForOf.length - 1 : Math.min(this.activeNode.row + 1, verticalContainer.igxForOf.length - 1);
149+
} else if (key.includes('right')) {
150+
this.isRowDimensionHeaderActive = false;
151+
newActiveNode.column = 0;
152+
newActiveNode.level = this.activeNode.mchCache?.level || 0;
153+
newActiveNode.mchCache = this.activeNode.mchCache || {
154+
level: 0,
155+
visibleIndex: 0
156+
};
47157
}
48158

49-
if (key.includes('left') || key.includes('right')) {
50-
const prevRIndex = this.activeNode.row;
51-
const prevScrContainer = this.grid.verticalRowDimScrollContainers.toArray()[this.activeNode.column];
52-
const src = prevScrContainer.getScrollForIndex(prevRIndex);
53-
newActiveNode.row = this.activeNode.mchCache && this.activeNode.mchCache.level === newActiveNode.column ?
54-
this.activeNode.mchCache.visibleIndex :
55-
verticalContainer.getIndexAtScroll(src);
56-
newActiveNode.mchCache = {
57-
visibleIndex: this.activeNode.row,
58-
level: this.activeNode.column
59-
};
159+
if (key.includes('down')) {
160+
if (this.grid.horizontalRowDimensions) {
161+
this.activeNode.row = 0;
162+
this.activeNode.layout = {
163+
rowStart: 1,
164+
rowEnd: 2,
165+
colStart: newActiveNode.column + 1,
166+
colEnd: newActiveNode.column + 2,
167+
columnVisibleIndex: newActiveNode.column
168+
};
169+
170+
const newPosition = this.getNextVerticalPosition(true, ctrl || key === 'home', key === 'home');
171+
newActiveNode.row = 0;
172+
newActiveNode.column = newPosition.column;
173+
newActiveNode.layout = newPosition.layout;
174+
} else {
175+
const verticalContainer = this.grid.verticalRowDimScrollContainers.toArray()[newActiveNode.column];
176+
newActiveNode.row = ctrl ? verticalContainer.igxForOf.length - 1 : 0;
177+
}
178+
179+
this.isRowDimensionHeaderActive = false;
180+
this.isRowHeaderActive = true;
181+
this.grid.rowDimensionContainer.toArray()[this.grid.horizontalRowDimensions ? 0 : newActiveNode.column].nativeElement.focus();
60182
}
183+
61184
this.setActiveNode(newActiveNode);
62-
if (verticalContainer.isIndexOutsideView(newActiveNode.row)) {
63-
verticalContainer.scrollTo(newActiveNode.row);
185+
} else if (key.includes('left') && this.activeNode.column === 0) {
186+
this.isRowDimensionHeaderActive = true;
187+
const newActiveNode: IActiveNode = {
188+
row: this.activeNode.row,
189+
column: this.lastRowDimensionsIndex,
190+
level: null,
191+
mchCache: this.activeNode.mchCache,
192+
layout: null
64193
}
194+
195+
this.setActiveNode(newActiveNode);
65196
} else {
66-
super.handleNavigation(event);
197+
super.headerNavigation(event);
67198
}
68199
}
69200

@@ -74,4 +205,81 @@ export class IgxPivotGridNavigationService extends IgxGridNavigationService {
74205
super.focusTbody(event);
75206
}
76207
}
208+
209+
public getNextVerticalPosition(previous, ctrl, homeEnd) {
210+
const parentRow = this.grid.rowDimensionMrlRowsCollection.toArray()[this.activeNode.row];
211+
const maxRowEnd = parentRow.rowGroup.length + 1;
212+
const curCellLayout = this.getNextVerticalColumnIndex(parentRow, this.activeNode.layout.rowStart, this.activeNode.layout.colStart);
213+
const nextBlock = (previous && curCellLayout.rowStart === 1) || (!previous && curCellLayout.rowEnd === maxRowEnd);
214+
if (nextBlock &&
215+
((previous && this.activeNode.row === 0) ||
216+
(!previous && this.activeNode.row === this.grid.rowDimensionMrlRowsCollection.length - 1))) {
217+
if (previous && this.grid.pivotUI.showRowHeaders) {
218+
this.isRowDimensionHeaderActive = true;
219+
this.isRowHeaderActive = false;
220+
this.grid.theadRow.nativeElement.focus();
221+
return { row: -1, column: this.activeNode.layout.colStart - 1, layout: this.activeNode.layout };
222+
}
223+
return { row: this.activeNode.row, column: this.activeNode.column, layout: this.activeNode.layout };
224+
}
225+
226+
const nextRowIndex = previous ?
227+
(ctrl ? 0 : this.activeNode.row - 1) :
228+
(ctrl ? this.grid.rowDimensionMrlRowsCollection.length - 1 : this.activeNode.row + 1) ;
229+
const nextRow = nextBlock || ctrl ? this.grid.rowDimensionMrlRowsCollection.toArray()[nextRowIndex] : parentRow;
230+
const nextRowStart = nextBlock ? (previous ? nextRow.rowGroup.length : 1) : curCellLayout.rowStart + (previous ? -1 : 1);
231+
const maxColEnd = Math.max(...nextRow.contentCells.map(cell => cell.layout.colEnd));
232+
const nextColumnLayout = this.getNextVerticalColumnIndex(
233+
nextRow,
234+
ctrl ? (previous ? 1 : nextRow.rowGroup.length) : nextRowStart,
235+
homeEnd ? (previous ? 1 : maxColEnd - 1) : this.activeNode.layout.colStart
236+
);
237+
return {
238+
row: nextBlock || ctrl ? nextRowIndex : this.activeNode.row,
239+
column: nextColumnLayout.columnVisibleIndex,
240+
layout: {
241+
rowStart: nextColumnLayout.rowStart,
242+
rowEnd: nextColumnLayout.rowEnd,
243+
colStart: homeEnd ? nextColumnLayout.colStart : this.activeNode.layout.colStart,
244+
colEnd: nextColumnLayout.colEnd,
245+
columnVisibleIndex: nextColumnLayout.columnVisibleIndex
246+
} as IMultiRowLayoutNode
247+
};
248+
}
249+
250+
public getNextHorizontalPosition(previous, ctrl) {
251+
const parentRow = this.grid.rowDimensionMrlRowsCollection.toArray()[this.activeNode.row];
252+
const maxColEnd = Math.max(...parentRow.contentCells.map(cell => cell.layout.colEnd));
253+
const curCellLayout = this.getNextVerticalColumnIndex(parentRow, this.activeNode.layout.rowStart, this.activeNode.layout.colStart);
254+
255+
if ((previous && curCellLayout.colStart === 1) || (!previous && curCellLayout.colEnd === maxColEnd)) {
256+
return { row: this.activeNode.row, column: this.activeNode.column, layout: this.activeNode.layout };
257+
}
258+
259+
const nextColumnLayout = this.getNextVerticalColumnIndex(
260+
parentRow,
261+
this.activeNode.layout.rowStart,
262+
ctrl ? (previous ? 1 : maxColEnd - 1) : curCellLayout.colStart + (previous ? -1 : 1)
263+
);
264+
return {
265+
row: this.activeNode.row,
266+
column: nextColumnLayout.columnVisibleIndex,
267+
layout: {
268+
rowStart: this.activeNode.layout.rowStart,
269+
rowEnd: nextColumnLayout.rowEnd,
270+
colStart: nextColumnLayout.colStart,
271+
colEnd: nextColumnLayout.colEnd,
272+
columnVisibleIndex: nextColumnLayout.columnVisibleIndex
273+
} as IMultiRowLayoutNode
274+
};
275+
}
276+
277+
278+
private getNextVerticalColumnIndex(nextRow: IgxPivotRowDimensionMrlRowComponent, newRowStart, newColStart) {
279+
const nextCell = nextRow.contentCells.find(cell => {
280+
return cell.layout.rowStart <= newRowStart && newRowStart < cell.layout.rowEnd &&
281+
cell.layout.colStart <= newColStart && newColStart < cell.layout.colEnd;
282+
});
283+
return nextCell.layout;
284+
}
77285
}

projects/igniteui-angular/src/lib/grids/pivot-grid/pivot-grid.component.html

Lines changed: 2 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@
147147
</ng-template>
148148

149149
<ng-template #horizontalRowDimensionsTemplate>
150-
<div tabindex="0" [style.height.px]="totalHeight" (focus)="navigation.focusTbody($event)" (keydown)="navigation.handleNavigation($event)">
150+
<div tabindex="0" #rowDimensionContainer [style.height.px]="totalHeight" (focus)="navigation.focusTbody($event)" (keydown)="navigation.handleNavigation($event)">
151151
<ng-container *ngIf="dataView | pivotGridHorizontalRowGrouping:pivotConfiguration:pipeTrigger as groupedData">
152152
<ng-template #verticalRowDimScrollContainer role="rowgroup" igxGridFor let-rowGroup let-rowIndex="index"
153153
[igxGridForOf]="groupedData"
@@ -157,26 +157,7 @@
157157
[igxForItemSize]="renderedRowHeight"
158158
[igxForSizePropName]="'height'"
159159
>
160-
<div class="igx-grid__tbody-pivot-dimension igx-grid__mrl-block" [ngStyle]="{
161-
'grid-template-rows': getRowMRLTemplate(true, rowGroup),
162-
'grid-template-columns': getRowMRLTemplate(false, rowGroup)
163-
}">
164-
<ng-template ngFor let-cell let-cellIndex="index" [ngForOf]="rowGroup
165-
| pivotGridHorizontalRowCellMerging:pivotConfiguration:pipeTrigger">
166-
<igx-pivot-row-dimension-content role="row" class="igx-grid-thead"
167-
[grid]="this"
168-
[dimension]="cell.rootDimension"
169-
[rootDimension]="cell.rootDimension"
170-
[rowIndex]="calcRowIndex(groupedData, rowIndex, cell)"
171-
[rowData]="cell.records[0]"
172-
[width]="rowDimensionWidthCombined(cell.dimensions)"
173-
[style.grid-row-start]="cell.rowStart"
174-
[style.grid-row-end]="cell.rowStart + cell.rowSpan"
175-
[style.grid-column-start]="cell.colStart"
176-
[style.grid-column-end]="cell.colStart + cell.colSpan">
177-
</igx-pivot-row-dimension-content>
178-
</ng-template>
179-
</div>
160+
<igx-pivot-row-dimension-mrl-row [rowIndex]="rowIndex" [rowGroup]="rowGroup" [groupedData]="groupedData"></igx-pivot-row-dimension-mrl-row>
180161
</ng-template>
181162
</ng-container>
182163
</div>

0 commit comments

Comments
 (0)