diff --git a/projects/igniteui-angular/src/lib/grids/common/crud.service.ts b/projects/igniteui-angular/src/lib/grids/common/crud.service.ts index 7fc017f8046..4f4f45381e6 100644 --- a/projects/igniteui-angular/src/lib/grids/common/crud.service.ts +++ b/projects/igniteui-angular/src/lib/grids/common/crud.service.ts @@ -258,8 +258,10 @@ export class IgxCellCrudState { let activeElement; if (cellNode) { const document = cellNode.getRootNode() as Document | ShadowRoot; - activeElement = document.activeElement as HTMLElement; - activeElement.blur(); + if (cellNode.contains(document.activeElement)) { + activeElement = document.activeElement as HTMLElement; + this.grid.tbody.nativeElement.focus(); + } } const formControl = this.grid.validation.getFormControl(this.cell.id.rowID, this.cell.column.field); diff --git a/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts b/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts index 4f359e7d620..108878cdb8a 100644 --- a/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts +++ b/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts @@ -3606,16 +3606,14 @@ export abstract class IgxGridBaseDirective implements GridType, public _setupListeners() { const destructor = takeUntil(this.destroy$); fromEvent(this.nativeElement, 'focusout').pipe(filter(() => !!this.navigation.activeNode), destructor).subscribe((event) => { - if (!this.crudService.cell && - !!this.navigation.activeNode && - ((event.target === this.tbody.nativeElement && this.navigation.activeNode.row >= 0 && - this.navigation.activeNode.row < this.dataView.length) - || (event.target === this.theadRow.nativeElement && this.navigation.activeNode.row === -1) - || (event.target === this.tfoot.nativeElement && this.navigation.activeNode.row === this.dataView.length)) && + const activeNode = this.navigation.activeNode; + if (!this.crudService.cell && !!activeNode && + ((event.target === this.tbody.nativeElement && activeNode.row >= 0 && + activeNode.row < this.dataView.length) + || (event.target === this.theadRow.nativeElement && activeNode.row === -1) + || (event.target === this.tfoot.nativeElement && activeNode.row === this.dataView.length)) && !(this.rowEditable && this.crudService.rowEditingBlocked && this.crudService.rowInEditMode)) { - this.navigation.lastActiveNode = this.navigation.activeNode; - this.navigation.activeNode = {} as IActiveNode; - this.notifyChanges(); + this.clearActiveNode(); } }); this.rowAddedNotifier.pipe(destructor).subscribe(args => this.refreshGridState(args)); @@ -6067,10 +6065,7 @@ export abstract class IgxGridBaseDirective implements GridType, return true; } - const activeCell = this.gridAPI.grid.navigation.activeNode; - if (activeCell && activeCell.row !== -1) { - this.tbody.nativeElement.focus(); - } + this.navigation.restoreActiveNodeFocus(); } /** @@ -6257,7 +6252,20 @@ export abstract class IgxGridBaseDirective implements GridType, // TODO: do not remove this, as it is used in rowEditTemplate, but mark is as internal and hidden /* blazorCSSuppress */ public endEdit(commit = true, event?: Event): boolean { - return this.crudService.endEdit(commit, event); + const document = this.nativeElement?.getRootNode() as Document | ShadowRoot; + const focusWithin = this.nativeElement?.contains(document.activeElement); + + const success = this.crudService.endEdit(commit, event); + + if (focusWithin) { + // restore focus for navigation + this.navigation.restoreActiveNodeFocus(); + } else if (this.navigation.activeNode) { + // grid already lost focus, clear active node + this.clearActiveNode(); + } + + return success; } /** @@ -7842,4 +7850,13 @@ export abstract class IgxGridBaseDirective implements GridType, if (!newData || !newData.length) return false; return Object.keys(oldData[0]).join() !== Object.keys(newData[0]).join(); } + + /** + * Clears the current navigation service active node + */ + private clearActiveNode() { + this.navigation.lastActiveNode = this.navigation.activeNode; + this.navigation.activeNode = {} as IActiveNode; + this.notifyChanges(); + } } diff --git a/projects/igniteui-angular/src/lib/grids/grid-navigation.service.ts b/projects/igniteui-angular/src/lib/grids/grid-navigation.service.ts index 7ca7e559bb4..8da82099c47 100644 --- a/projects/igniteui-angular/src/lib/grids/grid-navigation.service.ts +++ b/projects/igniteui-angular/src/lib/grids/grid-navigation.service.ts @@ -339,6 +339,23 @@ export class IgxGridNavigationService { return isChanged; } + /** Focus the Grid section (header, body, footer) depending on the current activeNode */ + public restoreActiveNodeFocus() { + if (!this.activeNode || !Object.keys(this.activeNode).length) { + return; + } + + if (this.activeNode.row >= 0 && this.activeNode.row < this.grid.dataView.length) { + this.grid.tbody.nativeElement.focus(); + } + if (this.activeNode.row === -1) { + this.grid.theadRow.nativeElement.focus(); + } + if (this.activeNode.row === this.grid.dataView.length) { + this.grid.tfoot.nativeElement.focus(); + } + } + protected getNextPosition(rowIndex: number, colIndex: number, key: string, shift: boolean, ctrl: boolean, event: KeyboardEvent) { if (!this.isDataRow(rowIndex, true) && (key.indexOf('down') < 0 || key.indexOf('up') < 0) && ctrl) { return { rowIndex, colIndex }; diff --git a/projects/igniteui-angular/src/lib/grids/grid/grid-cell-editing.spec.ts b/projects/igniteui-angular/src/lib/grids/grid/grid-cell-editing.spec.ts index a22158c7ec9..cae4b6c5753 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/grid-cell-editing.spec.ts +++ b/projects/igniteui-angular/src/lib/grids/grid/grid-cell-editing.spec.ts @@ -251,7 +251,7 @@ describe('IgxGrid - Cell Editing #grid', () => { expect(cell.value).toBeNull(); }); - it('Should not revert cell\' value when doubleClick while in editMode', fakeAsync(() => { + it('Should not revert cell\' value when doubleClick while in editMode', fakeAsync(() => { const cellElem = fixture.debugElement.query(By.css(CELL_CSS_CLASS)); const firstCell = grid.gridAPI.get_cell_by_index(0, 'fullName'); @@ -618,7 +618,7 @@ describe('IgxGrid - Cell Editing #grid', () => { it(`Should properly emit 'cellEditEnter' event`, () => { spyOn(grid.cellEditEnter, 'emit').and.callThrough(); const cell = grid.gridAPI.get_cell_by_index(0, 'fullName'); - let initialRowData = {...cell.row.data}; + let initialRowData = { ...cell.row.data }; expect(cell.editMode).toBeFalsy(); UIInteractions.simulateDoubleClickAndSelectEvent(cell); @@ -647,7 +647,7 @@ describe('IgxGrid - Cell Editing #grid', () => { expect(cell.editMode).toBeFalsy(); const cell2 = grid.getCellByColumn(0, 'age'); - initialRowData = {...cell2.row.data}; + initialRowData = { ...cell2.row.data }; cellArgs = { cellID: cell2.id, rowID: cell2.row.key, @@ -672,7 +672,7 @@ describe('IgxGrid - Cell Editing #grid', () => { e.cancel = true; }); let cell = grid.gridAPI.get_cell_by_index(0, 'fullName'); - let initialRowData = {...cell.row.data}; + let initialRowData = { ...cell.row.data }; expect(cell.editMode).toBeFalsy(); UIInteractions.simulateDoubleClickAndSelectEvent(cell); @@ -682,7 +682,7 @@ describe('IgxGrid - Cell Editing #grid', () => { cellID: cell.cellID, rowKey: cell.row.key, rowID: cell.row.key, - primaryKey: cell.row.key, + primaryKey: cell.row.key, rowData: initialRowData, oldValue: 'John Brown', cancel: true, @@ -697,7 +697,7 @@ describe('IgxGrid - Cell Editing #grid', () => { // press enter on a cell cell = grid.gridAPI.get_cell_by_index(0, 'age'); - initialRowData = {...cell.row.data}; + initialRowData = { ...cell.row.data }; UIInteractions.simulateClickAndSelectEvent(cell); fixture.detectChanges(); @@ -725,7 +725,7 @@ describe('IgxGrid - Cell Editing #grid', () => { it(`Should properly emit 'cellEditExit' event`, () => { spyOn(grid.cellEditExit, 'emit').and.callThrough(); let cell = grid.gridAPI.get_cell_by_index(0, 'fullName'); - let initialRowData = {...cell.row.data}; + let initialRowData = { ...cell.row.data }; expect(cell.editMode).toBeFalsy(); UIInteractions.simulateDoubleClickAndSelectEvent(cell); @@ -758,7 +758,7 @@ describe('IgxGrid - Cell Editing #grid', () => { expect(cell.editMode).toBeFalsy(); cell = grid.gridAPI.get_cell_by_index(0, 'age'); - initialRowData = {...cell.row.data}; + initialRowData = { ...cell.row.data }; cellArgs = { cellID: cell.cellID, rowKey: cell.row.key, @@ -846,7 +846,7 @@ describe('IgxGrid - Cell Editing #grid', () => { e.cancel = true; }); const cell = grid.gridAPI.get_cell_by_index(0, 'fullName'); - const initialRowData = {...cell.row.data}; + const initialRowData = { ...cell.row.data }; UIInteractions.simulateDoubleClickAndSelectEvent(cell); fixture.detectChanges(); @@ -965,7 +965,7 @@ describe('IgxGrid - Cell Editing #grid', () => { grid.cellEdit.subscribe((e: IGridEditEventArgs) => { const rowIndex: number = e.cellID.rowIndex; const row = grid.gridAPI.get_row_by_index(rowIndex); - grid.updateRow({[(row as any).columns[e.cellID.columnID].field]: e.newValue}, row.key); + grid.updateRow({ [(row as any).columns[e.cellID.columnID].field]: e.newValue }, row.key); e.cancel = true; }); @@ -1026,7 +1026,7 @@ describe('IgxGrid - Cell Editing #grid', () => { it(`Should properly emit 'cellEditExit' event`, () => { spyOn(grid.cellEditExit, 'emit').and.callThrough(); const cell = grid.gridAPI.get_cell_by_index(0, 'fullName'); - const initialRowData = {...cell.row.data}; + const initialRowData = { ...cell.row.data }; UIInteractions.simulateDoubleClickAndSelectEvent(cell); fixture.detectChanges(); @@ -1276,6 +1276,38 @@ describe('IgxGrid - Cell Editing #grid', () => { expect(cell.value).toBe('Rick Gilmore'); expect(grid.gridAPI.crudService.cell).toBeNull(); }); + + it('should clean active state when endEdit on focusout of the grid', async () => { + const handleFocusOut = ($event: FocusEvent) => { + if (!$event.relatedTarget || !grid.nativeElement.contains($event.relatedTarget as Node)) { + grid.endEdit(true); + grid.clearCellSelection(); + } + }; + grid.nativeElement.addEventListener('focusout', handleFocusOut); + const cell = grid.gridAPI.get_cell_by_index(0, 'fullName'); + const cellDom = fixture.debugElement.queryAll(By.css(CELL_CSS_CLASS))[0]; + + UIInteractions.simulateDoubleClickAndSelectEvent(cell); + fixture.detectChanges(); + await wait(16 /* igxFocus raf */); + expect(cell.editMode).toBe(true); + + const editTemplate = cellDom.query(By.css('input')); + expect(document.activeElement).toBe(editTemplate.nativeElement); + + UIInteractions.clickAndSendInputElementValue(editTemplate, 'Edit Cell'); + fixture.detectChanges(); + + editTemplate.nativeElement.blur(); + fixture.detectChanges(); + + expect(cell.editMode).toBe(false); + expect(cell.value).toBe('Edit Cell'); + expect(Object.keys(grid.navigation.activeNode).length).toBe(0); + + grid.nativeElement.removeEventListener('focusout', handleFocusOut); + }); }); it('Cell editing (when rowEditable=false) - default column editable value is false', fakeAsync(() => { diff --git a/projects/igniteui-angular/src/lib/grids/grid/grid-row-editing.spec.ts b/projects/igniteui-angular/src/lib/grids/grid/grid-row-editing.spec.ts index a52d4e9ad3a..bc7a2bd02cd 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/grid-row-editing.spec.ts +++ b/projects/igniteui-angular/src/lib/grids/grid/grid-row-editing.spec.ts @@ -2244,7 +2244,7 @@ describe('IgxGrid - Row Editing #grid', () => { const gridContent = GridFunctions.getGridContent(fix); const grid = fix.componentInstance.grid; - let cellElem = grid.gridAPI.get_cell_by_index(0, 'ProductName'); + const cellElem = grid.gridAPI.get_cell_by_index(0, 'ProductName'); spyOn(grid.gridAPI.crudService, 'endEdit').and.callThrough(); UIInteractions.simulateDoubleClickAndSelectEvent(cellElem); fix.detectChanges(); @@ -2254,12 +2254,15 @@ describe('IgxGrid - Row Editing #grid', () => { UIInteractions.triggerEventHandlerKeyDown('tab', gridContent); fix.detectChanges(); - cellElem = grid.gridAPI.get_cell_by_index(0, 'ReorderLevel'); expect(parseInt(GridFunctions.getRowEditingBannerText(fix), 10)).toEqual(1); fix.componentInstance.buttons.last.element.nativeElement.click(); expect(grid.gridAPI.crudService.endEdit).toHaveBeenCalled(); expect(grid.gridAPI.crudService.endEdit).toHaveBeenCalledTimes(1); + + fix.detectChanges(); + expect(cellElem.active).toBeTruthy(); + expect(grid.nativeElement.contains(document.activeElement)).toBeTrue(); }); it('Empty template', () => { diff --git a/src/app/grid-cellEditing/grid-cellEditing.component.html b/src/app/grid-cellEditing/grid-cellEditing.component.html index 4890c9fa443..423669fdc39 100644 --- a/src/app/grid-cellEditing/grid-cellEditing.component.html +++ b/src/app/grid-cellEditing/grid-cellEditing.component.html @@ -3,7 +3,7 @@

Grid with primary key ProductID

- + - + @@ -32,16 +32,37 @@

Grid with primary key ProductID

- - - - - - - - - - +
+ + + + + + + + + + +
+ +
+ Exit Edit On Blur + + + + @for (item of selectionModes; track item) { + {{ item }} + } + + + + + @for (item of selectionModes; track item) { + {{ item }} + } + +
+

Grid without PK

diff --git a/src/app/grid-cellEditing/grid-cellEditing.component.scss b/src/app/grid-cellEditing/grid-cellEditing.component.scss index 7078b80f067..3d4037836d0 100644 --- a/src/app/grid-cellEditing/grid-cellEditing.component.scss +++ b/src/app/grid-cellEditing/grid-cellEditing.component.scss @@ -1,17 +1,8 @@ -button { - margin: 0.2rem; -} - -[igxButton="contained"] { - margin: 0.5rem 0.5rem 0.5rem 0; - - &:nth-child(12) { - margin-bottom: 3rem; - } - - &:last-child { - margin-bottom: 3rem; - } +.sample-actions { + display: flex; + flex-wrap: wrap; + margin: 1rem 0; + gap: 0.5rem; } .density-chooser { diff --git a/src/app/grid-cellEditing/grid-cellEditing.component.ts b/src/app/grid-cellEditing/grid-cellEditing.component.ts index 5a511cab86e..d32d69f27fc 100644 --- a/src/app/grid-cellEditing/grid-cellEditing.component.ts +++ b/src/app/grid-cellEditing/grid-cellEditing.component.ts @@ -1,6 +1,20 @@ import { Component, HostBinding, ViewChild } from '@angular/core'; import { FormsModule } from '@angular/forms'; -import { GridSelectionMode, IgxButtonGroupComponent, IgxCellEditorTemplateDirective, IgxCellTemplateDirective, IgxColumnComponent, IgxColumnRequiredValidatorDirective, IgxDateSummaryOperand, IgxGridComponent, IgxPaginatorComponent, IgxSummaryResult } from 'igniteui-angular'; +import { + GridSelectionMode, + IGX_SELECT_DIRECTIVES, + IgxButtonDirective, + IgxButtonGroupComponent, + IgxCellEditorTemplateDirective, + IgxCellTemplateDirective, + IgxColumnComponent, + IgxColumnRequiredValidatorDirective, + IgxDateSummaryOperand, + IgxGridComponent, + IgxPaginatorComponent, + IgxSummaryResult, + IgxSwitchComponent, +} from 'igniteui-angular'; import { data, dataWithoutPK } from '../shared/data'; @@ -9,7 +23,19 @@ import { data, dataWithoutPK } from '../shared/data'; templateUrl: 'grid-cellEditing.component.html', styleUrl: 'grid-cellEditing.component.scss', standalone: true, - imports: [IgxButtonGroupComponent, IgxGridComponent, IgxColumnComponent, IgxColumnRequiredValidatorDirective, IgxCellTemplateDirective, IgxPaginatorComponent, FormsModule, IgxCellEditorTemplateDirective] + imports: [ + FormsModule, + IGX_SELECT_DIRECTIVES, + IgxButtonDirective, + IgxButtonGroupComponent, + IgxCellEditorTemplateDirective, + IgxCellTemplateDirective, + IgxColumnComponent, + IgxColumnRequiredValidatorDirective, + IgxGridComponent, + IgxPaginatorComponent, + IgxSwitchComponent, + ] }) export class GridCellEditingComponent { @ViewChild('grid1', { read: IgxGridComponent, static: true }) @@ -26,6 +52,7 @@ export class GridCellEditingComponent { public data: any; public dataWithoutPK: any; public size : "large" | "medium" | "small" = "small"; + public selectionModes = ['none', 'single', 'multiple']; public sizes; public options = { timezone: '+0430', @@ -39,7 +66,8 @@ export class GridCellEditingComponent { }; public formatOptions = this.options; - public kk = false; + public groupable = false; + public exitEditOnBlur = false; public pname = 'ProductName'; public selectionMode; public earliest = EarliestSummary; @@ -222,6 +250,15 @@ export class GridCellEditingComponent { this.gridWithPK.navigateTo(target.rowIndex + 1, target.visibleColumnIndex, (obj) => obj.target.nativeElement.focus()); } } + + public handleFocusOut = (event: FocusEvent) => { + if (!this.exitEditOnBlur) return; + + if (!event.relatedTarget || !this.gridWithPK.nativeElement.contains(event.relatedTarget as Node)) { + this.gridWithPK.endEdit(true); + this.gridWithPK.clearCellSelection(); + } + } } class EarliestSummary extends IgxDateSummaryOperand { diff --git a/src/app/grid-row-edit/grid-row-edit-sample.component.html b/src/app/grid-row-edit/grid-row-edit-sample.component.html index 587d69019e6..af06ad274e5 100644 --- a/src/app/grid-row-edit/grid-row-edit-sample.component.html +++ b/src/app/grid-row-edit/grid-row-edit-sample.component.html @@ -114,8 +114,8 @@

Cancel Grid Edit Events

- -
+ +
@@ -124,10 +124,10 @@

Cancel Grid Edit Events

- + - +