Skip to content

Commit 164673b

Browse files
authored
Merge pull request #10526 from IgniteUI/dkamburov/pivot-keyboard-nav
feat(pivot): keyboard navigation for pivot grid
2 parents a9229b8 + 6e76efa commit 164673b

18 files changed

+444
-40
lines changed

projects/igniteui-angular/src/lib/grids/headers/grid-header-group.component.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -324,7 +324,7 @@ export class IgxGridHeaderGroupComponent implements DoCheck {
324324
this.column.applySelectableClass = false;
325325
}
326326

327-
private get activeNode() {
327+
protected get activeNode() {
328328
return {
329329
row: -1, column: this.column.visibleIndex, level: this.column.level,
330330
mchCache: { level: this.column.level, visibleIndex: this.column.visibleIndex },

projects/igniteui-angular/src/lib/grids/headers/grid-header.component.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,9 @@ export class IgxGridHeaderComponent implements DoCheck, OnDestroy {
213213
}
214214
}
215215
}
216-
this.grid.theadRow.nativeElement.focus();
216+
if (!this.grid.isPivot || !this.grid.navigation.isRowHeaderActive) {
217+
this.grid.theadRow.nativeElement.focus();
218+
}
217219
}
218220

219221
/**
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
import { TestBed, fakeAsync, ComponentFixture } from '@angular/core/testing';
2+
import { By } from '@angular/platform-browser';
3+
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
4+
import { IgxPivotGridComponent, IgxPivotGridModule } from 'igniteui-angular';
5+
import { configureTestSuite } from '../../test-utils/configure-suite';
6+
import { GridFunctions } from '../../test-utils/grid-functions.spec';
7+
import { IgxPivotGridMultipleRowComponent } from '../../test-utils/pivot-grid-samples.spec';
8+
import { UIInteractions, wait } from '../../test-utils/ui-interactions.spec';
9+
10+
const DEBOUNCE_TIME = 250;
11+
const PIVOT_TBODY_CSS_CLASS = '.igx-grid__tbody';
12+
const PIVOT_ROW_DIMENSION_CONTENT = 'igx-pivot-row-dimension-content';
13+
const PIVOT_HEADER_ROW = 'igx-pivot-header-row';
14+
const HEADER_CELL_CSS_CLASS = '.igx-grid-th';
15+
const ACTIVE_CELL_CSS_CLASS = '.igx-grid-th--active';
16+
17+
describe('IgxPivotGrid - Keyboard navigation #pivotGrid', () => {
18+
19+
let fixture: ComponentFixture<IgxPivotGridMultipleRowComponent>;
20+
let pivotGrid: IgxPivotGridComponent;
21+
configureTestSuite((() => {
22+
TestBed.configureTestingModule({
23+
declarations: [
24+
IgxPivotGridMultipleRowComponent
25+
],
26+
imports: [
27+
NoopAnimationsModule, IgxPivotGridModule]
28+
});
29+
}));
30+
31+
beforeEach(fakeAsync(() => {
32+
fixture = TestBed.createComponent(IgxPivotGridMultipleRowComponent);
33+
fixture.detectChanges();
34+
pivotGrid = fixture.componentInstance.pivotGrid;
35+
}));
36+
37+
it('should allow navigating between row headers', () => {
38+
const [firstCell, secondCell] = fixture.debugElement.queryAll(
39+
By.css(`${PIVOT_TBODY_CSS_CLASS} ${PIVOT_ROW_DIMENSION_CONTENT} ${HEADER_CELL_CSS_CLASS}`));
40+
UIInteractions.simulateClickAndSelectEvent(firstCell);
41+
fixture.detectChanges();
42+
43+
GridFunctions.verifyHeaderIsFocused(firstCell.parent);
44+
let activeCells = fixture.debugElement.queryAll(By.css(`${ACTIVE_CELL_CSS_CLASS}`));
45+
expect(activeCells.length).toBe(1);
46+
47+
UIInteractions.triggerKeyDownEvtUponElem('ArrowRight', firstCell.nativeElement);
48+
fixture.detectChanges();
49+
GridFunctions.verifyHeaderIsFocused(secondCell.parent);
50+
activeCells = fixture.debugElement.queryAll(By.css(`${ACTIVE_CELL_CSS_CLASS}`));
51+
expect(activeCells.length).toBe(1);
52+
});
53+
54+
it('should not go outside of the boundaries of the row dimensions content', () => {
55+
const [firstCell, _, thirdCell] = fixture.debugElement.queryAll(
56+
By.css(`${PIVOT_TBODY_CSS_CLASS} ${PIVOT_ROW_DIMENSION_CONTENT} ${HEADER_CELL_CSS_CLASS}`));
57+
UIInteractions.simulateClickAndSelectEvent(firstCell);
58+
fixture.detectChanges();
59+
60+
UIInteractions.triggerKeyDownEvtUponElem('ArrowLeft', firstCell.nativeElement);
61+
fixture.detectChanges();
62+
63+
GridFunctions.verifyHeaderIsFocused(firstCell.parent);
64+
let activeCells = fixture.debugElement.queryAll(By.css(`${ACTIVE_CELL_CSS_CLASS}`));
65+
expect(activeCells.length).toBe(1);
66+
67+
UIInteractions.simulateClickAndSelectEvent(thirdCell);
68+
fixture.detectChanges();
69+
70+
UIInteractions.triggerKeyDownEvtUponElem('ArrowRight', thirdCell.nativeElement);
71+
fixture.detectChanges();
72+
73+
GridFunctions.verifyHeaderIsFocused(thirdCell.parent);
74+
activeCells = fixture.debugElement.queryAll(By.css(`${ACTIVE_CELL_CSS_CLASS}`));
75+
expect(activeCells.length).toBe(1);
76+
});
77+
78+
it('should allow navigating from first to last row headers in a row(Home/End)', () => {
79+
const [firstCell, _, thirdCell] = fixture.debugElement.queryAll(
80+
By.css(`${PIVOT_TBODY_CSS_CLASS} ${PIVOT_ROW_DIMENSION_CONTENT} ${HEADER_CELL_CSS_CLASS}`));
81+
UIInteractions.simulateClickAndSelectEvent(firstCell);
82+
fixture.detectChanges();
83+
84+
UIInteractions.triggerKeyDownEvtUponElem('End', firstCell.nativeElement);
85+
fixture.detectChanges();
86+
GridFunctions.verifyHeaderIsFocused(thirdCell.parent);
87+
let activeCells = fixture.debugElement.queryAll(By.css(`${ACTIVE_CELL_CSS_CLASS}`));
88+
expect(activeCells.length).toBe(1);
89+
90+
UIInteractions.triggerKeyDownEvtUponElem('Home', thirdCell.nativeElement);
91+
fixture.detectChanges();
92+
GridFunctions.verifyHeaderIsFocused(firstCell.parent);
93+
activeCells = fixture.debugElement.queryAll(By.css(`${ACTIVE_CELL_CSS_CLASS}`));
94+
expect(activeCells.length).toBe(1);
95+
});
96+
97+
it('should allow navigating from first to last row headers(Ctrl + ArrowDown)', () => {
98+
const [_firstCell, _secondCell, thirdCell] = fixture.debugElement.queryAll(
99+
By.css(`${PIVOT_TBODY_CSS_CLASS} ${PIVOT_ROW_DIMENSION_CONTENT} ${HEADER_CELL_CSS_CLASS}`));
100+
UIInteractions.simulateClickAndSelectEvent(thirdCell);
101+
fixture.detectChanges();
102+
103+
UIInteractions.triggerKeyDownEvtUponElem('ArrowDown', thirdCell.nativeElement, true, false, false, true);
104+
fixture.detectChanges();
105+
106+
const allCells = fixture.debugElement.queryAll(
107+
By.css(`${PIVOT_TBODY_CSS_CLASS} ${PIVOT_ROW_DIMENSION_CONTENT} ${HEADER_CELL_CSS_CLASS}`));
108+
const lastCell = allCells[allCells.length - 1];
109+
GridFunctions.verifyHeaderIsFocused(lastCell.parent);
110+
const activeCells = fixture.debugElement.queryAll(By.css(`${ACTIVE_CELL_CSS_CLASS}`));
111+
expect(activeCells.length).toBe(1);
112+
});
113+
114+
it('should allow navigating between column headers', () => {
115+
const [firstHeader, secondHeader] = fixture.debugElement.queryAll(
116+
By.css(`${PIVOT_HEADER_ROW} ${HEADER_CELL_CSS_CLASS}`));
117+
UIInteractions.simulateClickAndSelectEvent(firstHeader);
118+
fixture.detectChanges();
119+
120+
GridFunctions.verifyHeaderIsFocused(firstHeader.parent);
121+
let activeCells = fixture.debugElement.queryAll(By.css(`${ACTIVE_CELL_CSS_CLASS}`));
122+
expect(activeCells.length).toBe(1);
123+
124+
UIInteractions.triggerKeyDownEvtUponElem('ArrowRight', firstHeader.nativeElement);
125+
fixture.detectChanges();
126+
GridFunctions.verifyHeaderIsFocused(secondHeader.parent);
127+
activeCells = fixture.debugElement.queryAll(By.css(`${ACTIVE_CELL_CSS_CLASS}`));
128+
expect(activeCells.length).toBe(1);
129+
});
130+
131+
it('should allow navigating from first to last column headers', async () => {
132+
const [firstHeader] = fixture.debugElement.queryAll(
133+
By.css(`${PIVOT_HEADER_ROW} ${HEADER_CELL_CSS_CLASS}`));
134+
UIInteractions.simulateClickAndSelectEvent(firstHeader);
135+
fixture.detectChanges();
136+
137+
GridFunctions.verifyHeaderIsFocused(firstHeader.parent);
138+
let activeCells = fixture.debugElement.queryAll(By.css(`${ACTIVE_CELL_CSS_CLASS}`));
139+
expect(activeCells.length).toBe(1);
140+
141+
UIInteractions.triggerKeyDownEvtUponElem('End', pivotGrid.theadRow.nativeElement);
142+
await wait(DEBOUNCE_TIME);
143+
fixture.detectChanges();
144+
145+
const allHeaders = fixture.debugElement.queryAll(
146+
By.css(`${PIVOT_HEADER_ROW} ${HEADER_CELL_CSS_CLASS}`));
147+
const lastHeader = allHeaders[allHeaders.length - 1];
148+
GridFunctions.verifyHeaderIsFocused(lastHeader.parent);
149+
activeCells = fixture.debugElement.queryAll(By.css(`${ACTIVE_CELL_CSS_CLASS}`));
150+
expect(activeCells.length).toBe(1);
151+
});
152+
153+
it('should allow navigating in column headers when switching focus from rows to columns', () => {
154+
const [firstCell] = fixture.debugElement.queryAll(
155+
By.css(`${PIVOT_TBODY_CSS_CLASS} ${PIVOT_ROW_DIMENSION_CONTENT} ${HEADER_CELL_CSS_CLASS}`));
156+
UIInteractions.simulateClickAndSelectEvent(firstCell);
157+
fixture.detectChanges();
158+
159+
const [firstHeader, secondHeader] = fixture.debugElement.queryAll(
160+
By.css(`${PIVOT_HEADER_ROW} ${HEADER_CELL_CSS_CLASS}`));
161+
UIInteractions.simulateClickAndSelectEvent(firstHeader);
162+
fixture.detectChanges();
163+
164+
GridFunctions.verifyHeaderIsFocused(firstHeader.parent);
165+
let activeCells = fixture.debugElement.queryAll(By.css(`${ACTIVE_CELL_CSS_CLASS}`));
166+
expect(activeCells.length).toBe(1);
167+
168+
UIInteractions.triggerKeyDownEvtUponElem('ArrowRight', firstHeader.nativeElement);
169+
fixture.detectChanges();
170+
GridFunctions.verifyHeaderIsFocused(secondHeader.parent);
171+
activeCells = fixture.debugElement.queryAll(By.css(`${ACTIVE_CELL_CSS_CLASS}`));
172+
expect(activeCells.length).toBe(1);
173+
});
174+
});
Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,53 @@
11
import { IgxGridNavigationService } from '../grid-navigation.service';
22
import { Injectable } from '@angular/core';
33
import { IgxPivotGridComponent } from './pivot-grid.component';
4+
import { HEADER_KEYS } from '../../core/utils';
45

56
@Injectable()
67
export class IgxPivotGridNavigationService extends IgxGridNavigationService {
78
public grid: IgxPivotGridComponent;
89

9-
public dispatchEvent(event: KeyboardEvent) {
10-
// TODO
10+
public isRowHeaderActive: boolean;
11+
12+
public get lastRowDimensionsIndex() {
13+
return this.grid.rowDimensions.length - 1;
14+
}
15+
16+
public focusOutRowHeader() {
17+
this.isRowHeaderActive = false;
18+
}
19+
20+
public handleNavigation(event: KeyboardEvent) {
21+
if (this.isRowHeaderActive) {
22+
const key = event.key.toLowerCase();
23+
const ctrl = event.ctrlKey;
24+
if (!HEADER_KEYS.has(key)) {
25+
return;
26+
}
27+
event.preventDefault();
28+
29+
const newActiveNode = {
30+
row: this.activeNode.row, column: this.activeNode.column, level: null,
31+
mchCache: null,
32+
layout: null
33+
}
34+
35+
if ((key.includes('left') || key === 'home') && this.activeNode.column > 0) {
36+
newActiveNode.column = ctrl || key === 'home' ? 0 : this.activeNode.column - 1;
37+
}
38+
if ((key.includes('right') || key === 'end') && this.activeNode.column < this.lastRowDimensionsIndex) {
39+
newActiveNode.column = ctrl || key === 'end' ? this.lastRowDimensionsIndex : this.activeNode.column + 1;
40+
}
41+
if ((key.includes('up')) && this.activeNode.row > 0) {
42+
newActiveNode.row = ctrl ? 0 : this.activeNode.row - 1;
43+
}
44+
if ((key.includes('down')) && this.activeNode.row < this.findLastDataRowIndex()) {
45+
newActiveNode.row = ctrl ? this.findLastDataRowIndex() : this.activeNode.row + 1;
46+
}
47+
this.setActiveNode(newActiveNode);
48+
this.grid.navigateTo(newActiveNode.row);
49+
} else {
50+
super.handleNavigation(event);
51+
}
1152
}
1253
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
</igx-pivot-header-row>
2020

2121
<div igxGridBody (keydown.control.c)="copyHandler($event)" (copy)="copyHandler($event)" class="igx-grid__tbody" role="rowgroup">
22-
<div class="igx-grid__tbody-content" tabindex="0" [attr.role]="dataView.length ? null : 'row'" (keydown)="navigation.handleNavigation($event)"
22+
<div class="igx-grid__tbody-content" tabindex="0" [attr.role]="dataView.length ? null : 'row'" (keydown)="navigation.handleNavigation($event)" (focus)="navigation.focusTbody($event)"
2323
(dragStop)="selectionService.dragMode = $event" (scroll)='preventContainerScroll($event)'
2424
(dragScroll)="dragScroll($event)" [igxGridDragSelect]="selectionService.dragMode"
2525
[style.height.px]='totalHeight' [style.width.px]='calcWidth || null' #tbody [attr.aria-activedescendant]="activeDescendant">

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

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -775,13 +775,6 @@ export class IgxPivotGridComponent extends IgxGridBaseDirective implements OnIni
775775
return;
776776
}
777777

778-
/**
779-
* @hidden @internal
780-
*/
781-
public getColumnByVisibleIndex(_index: number): IgxColumnComponent {
782-
return;
783-
}
784-
785778
/**
786779
* @hidden @internal
787780
*/

projects/igniteui-angular/src/lib/grids/pivot-grid/pivot-grid.module.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { IgxPivotRowPipe, IgxPivotColumnPipe, IgxPivotGridFilterPipe,
77
import { IgxGridComponent } from '../grid/grid.component';
88
import { IgxPivotHeaderRowComponent } from './pivot-header-row.component';
99
import { IgxPivotRowDimensionContentComponent } from './pivot-row-dimension-content.component';
10+
import { IgxPivotRowDimensionHeaderGroupComponent } from './pivot-row-dimension-header-group.component';
1011

1112
/**
1213
* @hidden
@@ -17,6 +18,7 @@ import { IgxPivotRowDimensionContentComponent } from './pivot-row-dimension-cont
1718
IgxPivotRowComponent,
1819
IgxPivotHeaderRowComponent,
1920
IgxPivotRowDimensionContentComponent,
21+
IgxPivotRowDimensionHeaderGroupComponent,
2022
IgxPivotRowPipe,
2123
IgxPivotRowExpansionPipe,
2224
IgxPivotColumnPipe,
@@ -30,6 +32,7 @@ import { IgxPivotRowDimensionContentComponent } from './pivot-row-dimension-cont
3032
IgxPivotRowComponent,
3133
IgxPivotHeaderRowComponent,
3234
IgxPivotRowDimensionContentComponent,
35+
IgxPivotRowDimensionHeaderGroupComponent,
3336
IgxPivotRowExpansionPipe,
3437
IgxPivotRowPipe,
3538
IgxPivotColumnPipe,

projects/igniteui-angular/src/lib/grids/pivot-grid/pivot-grid.pipes.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { IgxPivotNumericAggregate } from './pivot-grid-aggregate';
66
import { IPivotConfiguration } from './pivot-grid.interface';
77
import { IgxPivotColumnPipe, IgxPivotRowExpansionPipe, IgxPivotRowPipe } from './pivot-grid.pipes';
88

9-
describe('Pivot pipes', () => {
9+
describe('Pivot pipes #pivotGrid', () => {
1010
let rowPipe: IgxPivotRowPipe;
1111
let rowStatePipe: IgxPivotRowExpansionPipe;
1212
let columnPipe: IgxPivotColumnPipe;

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

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,8 @@
106106
<igx-grid-header-group [ngClass]="column.headerGroupClasses"
107107
[ngStyle]="column.headerGroupStyles | igxHeaderGroupStyle:column:grid.pipeTrigger" [column]="column"
108108
[style.min-width]="column.calcWidth | igxHeaderGroupWidth:grid.defaultHeaderGroupMinWidth:hasMRL"
109-
[style.flex-basis]="column.calcWidth | igxHeaderGroupWidth:grid.defaultHeaderGroupMinWidth:hasMRL">
109+
[style.flex-basis]="column.calcWidth | igxHeaderGroupWidth:grid.defaultHeaderGroupMinWidth:hasMRL"
110+
(pointerdown)="grid.navigation.focusOutRowHeader($event)">
110111
</igx-grid-header-group>
111112
</ng-container>
112113
</ng-container>
@@ -120,7 +121,8 @@
120121
<igx-grid-header-group [ngClass]="column.headerGroupClasses"
121122
[ngStyle]="column.headerGroupStyles |igxHeaderGroupStyle:column:grid.pipeTrigger" [column]="column"
122123
[style.min-width]="column.calcWidth | igxHeaderGroupWidth:grid.defaultHeaderGroupMinWidth:hasMRL"
123-
[style.flex-basis]="column.calcWidth | igxHeaderGroupWidth:grid.defaultHeaderGroupMinWidth:hasMRL">
124+
[style.flex-basis]="column.calcWidth | igxHeaderGroupWidth:grid.defaultHeaderGroupMinWidth:hasMRL"
125+
(pointerdown)="grid.navigation.focusOutRowHeader($event)">
124126
</igx-grid-header-group>
125127
</ng-template>
126128

@@ -131,7 +133,8 @@
131133
[ngStyle]="column.headerGroupStyles |igxHeaderGroupStyle:column:grid.pipeTrigger" [column]="column"
132134
[style.min-width]="column.calcWidth | igxHeaderGroupWidth:grid.defaultHeaderGroupMinWidth:hasMRL"
133135
[style.flex-basis]="column.calcWidth | igxHeaderGroupWidth:grid.defaultHeaderGroupMinWidth:hasMRL"
134-
[style.left]="column.rightPinnedOffset">
136+
[style.left]="column.rightPinnedOffset"
137+
(pointerdown)="grid.navigation.focusOutRowHeader($event)">
135138
</igx-grid-header-group>
136139
</ng-container>
137140
</ng-container>

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@
55
<!-- Unpinned columns collection -->
66
<ng-template ngFor #headerVirtualContainer let-column
77
[ngForOf]="unpinnedColumnCollection | igxTopLevel">
8-
<igx-grid-header-group [ngClass]="column.headerGroupClasses"
8+
<igx-pivot-row-dimension-header-group [ngClass]="column.headerGroupClasses"
99
[ngStyle]="column.headerGroupStyles |igxHeaderGroupStyle:column:grid.pipeTrigger" [column]="column"
1010
[style.min-width]="column.calcWidth | igxHeaderGroupWidth:grid.defaultHeaderGroupMinWidth:hasMRL"
11-
[style.flex-basis]="column.calcWidth | igxHeaderGroupWidth:grid.defaultHeaderGroupMinWidth:hasMRL">
12-
</igx-grid-header-group>
11+
[style.flex-basis]="column.calcWidth | igxHeaderGroupWidth:grid.defaultHeaderGroupMinWidth:hasMRL"
12+
[intRow]="intRow">
13+
</igx-pivot-row-dimension-header-group>
1314
</ng-template>
1415
</div>
1516
</div>

0 commit comments

Comments
 (0)