Skip to content

Commit 33a554c

Browse files
fix(editing): re-trigger activation clear on endedit #14842 (#14894)
* fix(editing): re-trigger activation clear on endedit #14842 * fix(grids): don't blur on moving/finalizing cell edit The goal is to blur the internal cell editor to cause a `change` and process the value, but should not use `blur()` directly as it throws focus on doc body, rather move the focus elsewhere in the grid and only if it already was there. * refactor(grid,navigation): shared logic to restore focus to correct active node --------- Co-authored-by: Damyan Petev <[email protected]>
1 parent 3f2e4d8 commit 33a554c

File tree

9 files changed

+182
-62
lines changed

9 files changed

+182
-62
lines changed

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
@@ -3606,16 +3606,14 @@ export abstract class IgxGridBaseDirective implements GridType,
36063606
public _setupListeners() {
36073607
const destructor = takeUntil<any>(this.destroy$);
36083608
fromEvent(this.nativeElement, 'focusout').pipe(filter(() => !!this.navigation.activeNode), destructor).subscribe((event) => {
3609-
if (!this.crudService.cell &&
3610-
!!this.navigation.activeNode &&
3611-
((event.target === this.tbody.nativeElement && this.navigation.activeNode.row >= 0 &&
3612-
this.navigation.activeNode.row < this.dataView.length)
3613-
|| (event.target === this.theadRow.nativeElement && this.navigation.activeNode.row === -1)
3614-
|| (event.target === this.tfoot.nativeElement && this.navigation.activeNode.row === this.dataView.length)) &&
3609+
const activeNode = this.navigation.activeNode;
3610+
if (!this.crudService.cell && !!activeNode &&
3611+
((event.target === this.tbody.nativeElement && activeNode.row >= 0 &&
3612+
activeNode.row < this.dataView.length)
3613+
|| (event.target === this.theadRow.nativeElement && activeNode.row === -1)
3614+
|| (event.target === this.tfoot.nativeElement && activeNode.row === this.dataView.length)) &&
36153615
!(this.rowEditable && this.crudService.rowEditingBlocked && this.crudService.rowInEditMode)) {
3616-
this.navigation.lastActiveNode = this.navigation.activeNode;
3617-
this.navigation.activeNode = {} as IActiveNode;
3618-
this.notifyChanges();
3616+
this.clearActiveNode();
36193617
}
36203618
});
36213619
this.rowAddedNotifier.pipe(destructor).subscribe(args => this.refreshGridState(args));
@@ -6067,10 +6065,7 @@ export abstract class IgxGridBaseDirective implements GridType,
60676065
return true;
60686066
}
60696067

6070-
const activeCell = this.gridAPI.grid.navigation.activeNode;
6071-
if (activeCell && activeCell.row !== -1) {
6072-
this.tbody.nativeElement.focus();
6073-
}
6068+
this.navigation.restoreActiveNodeFocus();
60746069
}
60756070

60766071
/**
@@ -6257,7 +6252,20 @@ export abstract class IgxGridBaseDirective implements GridType,
62576252
// TODO: do not remove this, as it is used in rowEditTemplate, but mark is as internal and hidden
62586253
/* blazorCSSuppress */
62596254
public endEdit(commit = true, event?: Event): boolean {
6260-
return this.crudService.endEdit(commit, event);
6255+
const document = this.nativeElement?.getRootNode() as Document | ShadowRoot;
6256+
const focusWithin = this.nativeElement?.contains(document.activeElement);
6257+
6258+
const success = this.crudService.endEdit(commit, event);
6259+
6260+
if (focusWithin) {
6261+
// restore focus for navigation
6262+
this.navigation.restoreActiveNodeFocus();
6263+
} else if (this.navigation.activeNode) {
6264+
// grid already lost focus, clear active node
6265+
this.clearActiveNode();
6266+
}
6267+
6268+
return success;
62616269
}
62626270

62636271
/**
@@ -7842,4 +7850,13 @@ export abstract class IgxGridBaseDirective implements GridType,
78427850
if (!newData || !newData.length) return false;
78437851
return Object.keys(oldData[0]).join() !== Object.keys(newData[0]).join();
78447852
}
7853+
7854+
/**
7855+
* Clears the current navigation service active node
7856+
*/
7857+
private clearActiveNode() {
7858+
this.navigation.lastActiveNode = this.navigation.activeNode;
7859+
this.navigation.activeNode = {} as IActiveNode;
7860+
this.notifyChanges();
7861+
}
78457862
}

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)