Skip to content

Commit 59b4b5f

Browse files
authored
Merge pull request #14984 from IgniteUI/sstoychev/focus-changes-editing
fix(editing): re-trigger activation clear on endedit #14842
2 parents 396130c + be5c3e0 commit 59b4b5f

File tree

10 files changed

+186
-66
lines changed

10 files changed

+186
-66
lines changed

projects/igniteui-angular/src/lib/combo/combo.component.spec.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2066,7 +2066,7 @@ describe('igxCombo', () => {
20662066
spyOn(combo.dropdown, 'onToggleClosing').and.callThrough();
20672067
spyOn(combo.dropdown, 'onToggleClosed').and.callThrough();
20682068
combo.toggle();
2069-
await wait(30);
2069+
await wait();
20702070
fixture.detectChanges();
20712071
expect(combo.collapsed).toEqual(false);
20722072
expect(combo.dropdown.onToggleOpening).toHaveBeenCalledTimes(1);
@@ -2075,17 +2075,17 @@ describe('igxCombo', () => {
20752075
expect(virtDir.getScroll().scrollTop).toEqual(0);
20762076
expect(vContainerScrollHeight).toBeGreaterThan(combo.itemHeight);
20772077
virtDir.getScroll().scrollTop = Math.floor(vContainerScrollHeight / 2);
2078-
await wait(30);
2078+
await firstValueFrom(combo.virtualScrollContainer.chunkLoad);
20792079
fixture.detectChanges();
20802080
expect(virtDir.getScroll().scrollTop).toBeGreaterThan(0);
20812081
UIInteractions.simulateClickEvent(document.documentElement);
2082-
await wait(10);
2082+
await wait();
20832083
fixture.detectChanges();
20842084
expect(combo.collapsed).toEqual(true);
20852085
expect(combo.dropdown.onToggleClosing).toHaveBeenCalledTimes(1);
20862086
expect(combo.dropdown.onToggleClosed).toHaveBeenCalledTimes(1);
20872087
combo.toggle();
2088-
await wait(30);
2088+
await wait();
20892089
fixture.detectChanges();
20902090
expect(combo.collapsed).toEqual(false);
20912091
expect(combo.dropdown.onToggleOpening).toHaveBeenCalledTimes(2);

projects/igniteui-angular/src/lib/grids/common/crud.service.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -258,8 +258,10 @@ export class IgxCellCrudState {
258258
let activeElement;
259259
if (cellNode) {
260260
const document = cellNode.getRootNode() as Document | ShadowRoot;
261-
activeElement = document.activeElement as HTMLElement;
262-
activeElement.blur();
261+
if (cellNode.contains(document.activeElement)) {
262+
activeElement = document.activeElement as HTMLElement;
263+
this.grid.tbody.nativeElement.focus();
264+
}
263265
}
264266

265267
const formControl = this.grid.validation.getFormControl(this.cell.id.rowID, this.cell.column.field);

projects/igniteui-angular/src/lib/grids/grid-base.directive.ts

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3614,16 +3614,14 @@ export abstract class IgxGridBaseDirective implements GridType,
36143614
public _setupListeners() {
36153615
const destructor = takeUntil<any>(this.destroy$);
36163616
fromEvent(this.nativeElement, 'focusout').pipe(filter(() => !!this.navigation.activeNode), destructor).subscribe((event) => {
3617-
if (!this.crudService.cell &&
3618-
!!this.navigation.activeNode &&
3619-
((event.target === this.tbody.nativeElement && this.navigation.activeNode.row >= 0 &&
3620-
this.navigation.activeNode.row < this.dataView.length)
3621-
|| (event.target === this.theadRow.nativeElement && this.navigation.activeNode.row === -1)
3622-
|| (event.target === this.tfoot.nativeElement && this.navigation.activeNode.row === this.dataView.length)) &&
3617+
const activeNode = this.navigation.activeNode;
3618+
if (!this.crudService.cell && !!activeNode &&
3619+
((event.target === this.tbody.nativeElement && activeNode.row >= 0 &&
3620+
activeNode.row < this.dataView.length)
3621+
|| (event.target === this.theadRow.nativeElement && activeNode.row === -1)
3622+
|| (event.target === this.tfoot.nativeElement && activeNode.row === this.dataView.length)) &&
36233623
!(this.rowEditable && this.crudService.rowEditingBlocked && this.crudService.rowInEditMode)) {
3624-
this.navigation.lastActiveNode = this.navigation.activeNode;
3625-
this.navigation.activeNode = {} as IActiveNode;
3626-
this.notifyChanges();
3624+
this.clearActiveNode();
36273625
}
36283626
});
36293627
this.rowAddedNotifier.pipe(destructor).subscribe(args => this.refreshGridState(args));
@@ -6075,10 +6073,7 @@ export abstract class IgxGridBaseDirective implements GridType,
60756073
return true;
60766074
}
60776075

6078-
const activeCell = this.gridAPI.grid.navigation.activeNode;
6079-
if (activeCell && activeCell.row !== -1) {
6080-
this.tbody.nativeElement.focus();
6081-
}
6076+
this.navigation.restoreActiveNodeFocus();
60826077
}
60836078

60846079
/**
@@ -6265,7 +6260,20 @@ export abstract class IgxGridBaseDirective implements GridType,
62656260
// TODO: do not remove this, as it is used in rowEditTemplate, but mark is as internal and hidden
62666261
/* blazorCSSuppress */
62676262
public endEdit(commit = true, event?: Event): boolean {
6268-
return this.crudService.endEdit(commit, event);
6263+
const document = this.nativeElement?.getRootNode() as Document | ShadowRoot;
6264+
const focusWithin = this.nativeElement?.contains(document.activeElement);
6265+
6266+
const success = this.crudService.endEdit(commit, event);
6267+
6268+
if (focusWithin) {
6269+
// restore focus for navigation
6270+
this.navigation.restoreActiveNodeFocus();
6271+
} else if (this.navigation.activeNode) {
6272+
// grid already lost focus, clear active node
6273+
this.clearActiveNode();
6274+
}
6275+
6276+
return success;
62696277
}
62706278

62716279
/**
@@ -7861,4 +7869,13 @@ export abstract class IgxGridBaseDirective implements GridType,
78617869
if (!newData || !newData.length) return false;
78627870
return Object.keys(oldData[0]).join() !== Object.keys(newData[0]).join();
78637871
}
7872+
7873+
/**
7874+
* Clears the current navigation service active node
7875+
*/
7876+
private clearActiveNode() {
7877+
this.navigation.lastActiveNode = this.navigation.activeNode;
7878+
this.navigation.activeNode = {} as IActiveNode;
7879+
this.notifyChanges();
7880+
}
78647881
}

projects/igniteui-angular/src/lib/grids/grid-navigation.service.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,23 @@ export class IgxGridNavigationService {
339339
return isChanged;
340340
}
341341

342+
/** Focus the Grid section (header, body, footer) depending on the current activeNode */
343+
public restoreActiveNodeFocus() {
344+
if (!this.activeNode || !Object.keys(this.activeNode).length) {
345+
return;
346+
}
347+
348+
if (this.activeNode.row >= 0 && this.activeNode.row < this.grid.dataView.length) {
349+
this.grid.tbody.nativeElement.focus();
350+
}
351+
if (this.activeNode.row === -1) {
352+
this.grid.theadRow.nativeElement.focus();
353+
}
354+
if (this.activeNode.row === this.grid.dataView.length) {
355+
this.grid.tfoot.nativeElement.focus();
356+
}
357+
}
358+
342359
protected getNextPosition(rowIndex: number, colIndex: number, key: string, shift: boolean, ctrl: boolean, event: KeyboardEvent) {
343360
if (!this.isDataRow(rowIndex, true) && (key.indexOf('down') < 0 || key.indexOf('up') < 0) && ctrl) {
344361
return { rowIndex, colIndex };

projects/igniteui-angular/src/lib/grids/grid/grid-cell-editing.spec.ts

Lines changed: 43 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,7 @@ describe('IgxGrid - Cell Editing #grid', () => {
251251
expect(cell.value).toBeNull();
252252
});
253253

254-
it('Should not revert cell\' value when doubleClick while in editMode', fakeAsync(() => {
254+
it('Should not revert cell\' value when doubleClick while in editMode', fakeAsync(() => {
255255
const cellElem = fixture.debugElement.query(By.css(CELL_CSS_CLASS));
256256
const firstCell = grid.gridAPI.get_cell_by_index(0, 'fullName');
257257

@@ -618,7 +618,7 @@ describe('IgxGrid - Cell Editing #grid', () => {
618618
it(`Should properly emit 'cellEditEnter' event`, () => {
619619
spyOn(grid.cellEditEnter, 'emit').and.callThrough();
620620
const cell = grid.gridAPI.get_cell_by_index(0, 'fullName');
621-
let initialRowData = {...cell.row.data};
621+
let initialRowData = { ...cell.row.data };
622622
expect(cell.editMode).toBeFalsy();
623623

624624
UIInteractions.simulateDoubleClickAndSelectEvent(cell);
@@ -647,7 +647,7 @@ describe('IgxGrid - Cell Editing #grid', () => {
647647

648648
expect(cell.editMode).toBeFalsy();
649649
const cell2 = grid.getCellByColumn(0, 'age');
650-
initialRowData = {...cell2.row.data};
650+
initialRowData = { ...cell2.row.data };
651651
cellArgs = {
652652
cellID: cell2.id,
653653
rowID: cell2.row.key,
@@ -672,7 +672,7 @@ describe('IgxGrid - Cell Editing #grid', () => {
672672
e.cancel = true;
673673
});
674674
let cell = grid.gridAPI.get_cell_by_index(0, 'fullName');
675-
let initialRowData = {...cell.row.data};
675+
let initialRowData = { ...cell.row.data };
676676
expect(cell.editMode).toBeFalsy();
677677

678678
UIInteractions.simulateDoubleClickAndSelectEvent(cell);
@@ -682,7 +682,7 @@ describe('IgxGrid - Cell Editing #grid', () => {
682682
cellID: cell.cellID,
683683
rowKey: cell.row.key,
684684
rowID: cell.row.key,
685-
primaryKey: cell.row.key,
685+
primaryKey: cell.row.key,
686686
rowData: initialRowData,
687687
oldValue: 'John Brown',
688688
cancel: true,
@@ -697,7 +697,7 @@ describe('IgxGrid - Cell Editing #grid', () => {
697697

698698
// press enter on a cell
699699
cell = grid.gridAPI.get_cell_by_index(0, 'age');
700-
initialRowData = {...cell.row.data};
700+
initialRowData = { ...cell.row.data };
701701
UIInteractions.simulateClickAndSelectEvent(cell);
702702
fixture.detectChanges();
703703

@@ -725,7 +725,7 @@ describe('IgxGrid - Cell Editing #grid', () => {
725725
it(`Should properly emit 'cellEditExit' event`, () => {
726726
spyOn(grid.cellEditExit, 'emit').and.callThrough();
727727
let cell = grid.gridAPI.get_cell_by_index(0, 'fullName');
728-
let initialRowData = {...cell.row.data};
728+
let initialRowData = { ...cell.row.data };
729729
expect(cell.editMode).toBeFalsy();
730730

731731
UIInteractions.simulateDoubleClickAndSelectEvent(cell);
@@ -758,7 +758,7 @@ describe('IgxGrid - Cell Editing #grid', () => {
758758

759759
expect(cell.editMode).toBeFalsy();
760760
cell = grid.gridAPI.get_cell_by_index(0, 'age');
761-
initialRowData = {...cell.row.data};
761+
initialRowData = { ...cell.row.data };
762762
cellArgs = {
763763
cellID: cell.cellID,
764764
rowKey: cell.row.key,
@@ -846,7 +846,7 @@ describe('IgxGrid - Cell Editing #grid', () => {
846846
e.cancel = true;
847847
});
848848
const cell = grid.gridAPI.get_cell_by_index(0, 'fullName');
849-
const initialRowData = {...cell.row.data};
849+
const initialRowData = { ...cell.row.data };
850850

851851
UIInteractions.simulateDoubleClickAndSelectEvent(cell);
852852
fixture.detectChanges();
@@ -965,7 +965,7 @@ describe('IgxGrid - Cell Editing #grid', () => {
965965
grid.cellEdit.subscribe((e: IGridEditEventArgs) => {
966966
const rowIndex: number = e.cellID.rowIndex;
967967
const row = grid.gridAPI.get_row_by_index(rowIndex);
968-
grid.updateRow({[(row as any).columns[e.cellID.columnID].field]: e.newValue}, row.key);
968+
grid.updateRow({ [(row as any).columns[e.cellID.columnID].field]: e.newValue }, row.key);
969969
e.cancel = true;
970970
});
971971

@@ -1026,7 +1026,7 @@ describe('IgxGrid - Cell Editing #grid', () => {
10261026
it(`Should properly emit 'cellEditExit' event`, () => {
10271027
spyOn(grid.cellEditExit, 'emit').and.callThrough();
10281028
const cell = grid.gridAPI.get_cell_by_index(0, 'fullName');
1029-
const initialRowData = {...cell.row.data};
1029+
const initialRowData = { ...cell.row.data };
10301030

10311031
UIInteractions.simulateDoubleClickAndSelectEvent(cell);
10321032
fixture.detectChanges();
@@ -1276,6 +1276,38 @@ describe('IgxGrid - Cell Editing #grid', () => {
12761276
expect(cell.value).toBe('Rick Gilmore');
12771277
expect(grid.gridAPI.crudService.cell).toBeNull();
12781278
});
1279+
1280+
it('should clean active state when endEdit on focusout of the grid', async () => {
1281+
const handleFocusOut = ($event: FocusEvent) => {
1282+
if (!$event.relatedTarget || !grid.nativeElement.contains($event.relatedTarget as Node)) {
1283+
grid.endEdit(true);
1284+
grid.clearCellSelection();
1285+
}
1286+
};
1287+
grid.nativeElement.addEventListener('focusout', handleFocusOut);
1288+
const cell = grid.gridAPI.get_cell_by_index(0, 'fullName');
1289+
const cellDom = fixture.debugElement.queryAll(By.css(CELL_CSS_CLASS))[0];
1290+
1291+
UIInteractions.simulateDoubleClickAndSelectEvent(cell);
1292+
fixture.detectChanges();
1293+
await wait(16 /* igxFocus raf */);
1294+
expect(cell.editMode).toBe(true);
1295+
1296+
const editTemplate = cellDom.query(By.css('input'));
1297+
expect(document.activeElement).toBe(editTemplate.nativeElement);
1298+
1299+
UIInteractions.clickAndSendInputElementValue(editTemplate, 'Edit Cell');
1300+
fixture.detectChanges();
1301+
1302+
editTemplate.nativeElement.blur();
1303+
fixture.detectChanges();
1304+
1305+
expect(cell.editMode).toBe(false);
1306+
expect(cell.value).toBe('Edit Cell');
1307+
expect(Object.keys(grid.navigation.activeNode).length).toBe(0);
1308+
1309+
grid.nativeElement.removeEventListener('focusout', handleFocusOut);
1310+
});
12791311
});
12801312

12811313
it('Cell editing (when rowEditable=false) - default column editable value is false', fakeAsync(() => {

projects/igniteui-angular/src/lib/grids/grid/grid-row-editing.spec.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2244,7 +2244,7 @@ describe('IgxGrid - Row Editing #grid', () => {
22442244
const gridContent = GridFunctions.getGridContent(fix);
22452245

22462246
const grid = fix.componentInstance.grid;
2247-
let cellElem = grid.gridAPI.get_cell_by_index(0, 'ProductName');
2247+
const cellElem = grid.gridAPI.get_cell_by_index(0, 'ProductName');
22482248
spyOn(grid.gridAPI.crudService, 'endEdit').and.callThrough();
22492249
UIInteractions.simulateDoubleClickAndSelectEvent(cellElem);
22502250
fix.detectChanges();
@@ -2254,12 +2254,15 @@ describe('IgxGrid - Row Editing #grid', () => {
22542254
UIInteractions.triggerEventHandlerKeyDown('tab', gridContent);
22552255
fix.detectChanges();
22562256

2257-
cellElem = grid.gridAPI.get_cell_by_index(0, 'ReorderLevel');
22582257
expect(parseInt(GridFunctions.getRowEditingBannerText(fix), 10)).toEqual(1);
22592258

22602259
fix.componentInstance.buttons.last.element.nativeElement.click();
22612260
expect(grid.gridAPI.crudService.endEdit).toHaveBeenCalled();
22622261
expect(grid.gridAPI.crudService.endEdit).toHaveBeenCalledTimes(1);
2262+
2263+
fix.detectChanges();
2264+
expect(cellElem.active).toBeTruthy();
2265+
expect(grid.nativeElement.contains(document.activeElement)).toBeTrue();
22632266
});
22642267

22652268
it('Empty template', () => {

src/app/grid-cellEditing/grid-cellEditing.component.html

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ <h4 class="sample-title">Grid with primary key ProductID</h4>
33
<div class="density-chooser">
44
<igx-buttongroup [values]="sizes" (selected)="selectDensity($event)"></igx-buttongroup>
55
</div>
6-
<igx-grid (cellEdit)='cellEdit($event)' #grid1 [filterMode]="'excelStyleFilter'" [moving]="true" locale="fr-FR" [data]="data" [primaryKey]="'ProductID'" [rowEditable]="true" width="1500px" height="550px" [rowSelection]="selectionMode">
6+
<igx-grid (focusout)="handleFocusOut($event)" (cellEdit)='cellEdit($event)' #grid1 [filterMode]="'excelStyleFilter'" [moving]="true" locale="fr-FR" [data]="data" [primaryKey]="'ProductID'" [rowEditable]="true" width="1500px" height="550px" [rowSelection]="selectionMode">
77
<igx-column field="OrderDate" width="200px" [dataType]="'date'" [hidden]="orderDateHidden" [filterable]="true" [hasSummary]="true" [summaries]="earliest" [editable]="true" [resizable]="true">
88
<!-- <ng-template igxCell let-cell="cell" let-val>
99
{{val | date}}
@@ -24,24 +24,45 @@ <h4 class="sample-title">Grid with primary key ProductID</h4>
2424
</igx-column> -->
2525
<igx-column field="ReorderLevel" width="200px" [sortable]="true" [filterable]="true" [editable]="true" [dataType]="'number'" [hasSummary]="false">
2626
</igx-column>
27-
<igx-column field="ProductName" width="200px" [groupable]="kk" [header]="pname" [sortable]="true" [dataType]="'string'" [editable]="true" [resizable]="true">
27+
<igx-column field="ProductName" width="200px" [groupable]="groupable" [header]="pname" [sortable]="true" [dataType]="'string'" [editable]="true" [resizable]="true">
2828
</igx-column>
2929
<igx-column required field="UnitsInStock" header="UnitsInStock" width="200px" [dataType]="'number'" [editable]="true" [sortable]="true" [hasSummary]="false">
3030
</igx-column>
3131
<igx-column field="Discontinued" header="Discontinued" [dataType]="'boolean'" width="200px" [hasSummary]="true" [editable]="true" [resizable]="true">
3232
</igx-column>
3333
<igx-paginator></igx-paginator>
3434
</igx-grid>
35-
<input igxButton="contained" id="updBtn" type="button" (click)="updRecord()" value="Update cell/record">
36-
<button igxButton="contained" (click)="addRow()">Add Row</button>
37-
<button igxButton="contained" (click)="updateCell()">Update Cell</button>
38-
<button igxButton="contained" (click)="pin()">Pin/Unpin</button>
39-
<button igxButton="contained" (click)="hideColumn()">Hide/Show OrderDate</button>
40-
<button igxButton="contained" (click)="updateSpecificRow()">Update Row by ID</button>
41-
<button igxButton="contained" (click)="enDisSummaries()">Enable/DisableSummaries</button>
42-
<button igxButton="contained" (click)="kk = !kk">groupable</button>
43-
<button igxButton="contained" (click)="changeFormatOptions()">Change formatting</button>
44-
<input type="text" [(ngModel)]="pname">
35+
<div class="sample-actions">
36+
<input igxButton="contained" id="updBtn" type="button" (click)="updRecord()" value="Update cell/record">
37+
<button igxButton="contained" (click)="addRow()">Add Row</button>
38+
<button igxButton="contained" (click)="updateCell()">Update Cell</button>
39+
<button igxButton="contained" (click)="pin()">Pin/Unpin</button>
40+
<button igxButton="contained" (click)="hideColumn()">Hide/Show OrderDate</button>
41+
<button igxButton="contained" (click)="updateSpecificRow()">Update Row by ID</button>
42+
<button igxButton="contained" (click)="enDisSummaries()">Enable/DisableSummaries</button>
43+
<button igxButton="contained" (click)="groupable = !groupable">groupable</button>
44+
<button igxButton="contained" (click)="changeFormatOptions()">Change formatting</button>
45+
<input type="text" [(ngModel)]="pname">
46+
</div>
47+
<!-- TODO: Props panel -->
48+
<div class="sample-actions">
49+
<igx-switch [(ngModel)]="exitEditOnBlur">Exit Edit On Blur</igx-switch>
50+
51+
<igx-select [(ngModel)]="grid1.cellSelection" type="border">
52+
<label igxLabel>Cell Selection</label>
53+
@for (item of selectionModes; track item) {
54+
<igx-select-item [value]="item">{{ item }}</igx-select-item>
55+
}
56+
</igx-select>
57+
58+
<igx-select [(ngModel)]="grid1.rowSelection" type="border">
59+
<label igxLabel>Row Selection</label>
60+
@for (item of selectionModes; track item) {
61+
<igx-select-item [value]="item">{{ item }}</igx-select-item>
62+
}
63+
</igx-select>
64+
</div>
65+
4566
<h4 class="sample-title">Grid without PK</h4>
4667
<igx-grid (cellEdit)='cellEdit($event)' #grid [data]="dataWithoutPK" class="grid-size" width="800px" height="550px" [moving]="true" [rowSelection]="selectionMode">
4768
<igx-column [pinned]="true">

src/app/grid-cellEditing/grid-cellEditing.component.scss

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,8 @@
1-
button {
2-
margin: 0.2rem;
3-
}
4-
5-
[igxButton="contained"] {
6-
margin: 0.5rem 0.5rem 0.5rem 0;
7-
8-
&:nth-child(12) {
9-
margin-bottom: 3rem;
10-
}
11-
12-
&:last-child {
13-
margin-bottom: 3rem;
14-
}
1+
.sample-actions {
2+
display: flex;
3+
flex-wrap: wrap;
4+
margin: 1rem 0;
5+
gap: 0.5rem;
156
}
167

178
.density-chooser {

0 commit comments

Comments
 (0)