From e185e9db51609413d63a7e64b07e3aadf97194aa Mon Sep 17 00:00:00 2001 From: Ivan Minchev Date: Mon, 26 Jan 2026 17:58:40 +0200 Subject: [PATCH 1/6] test(grid): test for remerging child grid cells on focus out from parent grid --- .../grids/grid/src/cell-merge.spec.ts | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/projects/igniteui-angular/grids/grid/src/cell-merge.spec.ts b/projects/igniteui-angular/grids/grid/src/cell-merge.spec.ts index 918975267a2..e7b9b6acd5f 100644 --- a/projects/igniteui-angular/grids/grid/src/cell-merge.spec.ts +++ b/projects/igniteui-angular/grids/grid/src/cell-merge.spec.ts @@ -1112,6 +1112,70 @@ describe('IgxGrid - Cell merging #grid', () => { ]); }); + it('should remerge child grid cells when focus moves to parent grid.', async () => { + const ri = fix.componentInstance.rowIsland; + ri.cellMergeMode = 'always'; + ri.getColumnByName('ProductName').merge = true; + fix.detectChanges(); + + const firstRow = grid.gridAPI.get_row_by_index(0) as IgxHierarchicalRowComponent; + firstRow.toggle(); + fix.detectChanges(); + + const childGrid = grid.gridAPI.getChildGrids(false)[0] as IgxHierarchicalGridComponent; + expect(childGrid).toBeDefined(); + + const childCol = childGrid.getColumnByName('ProductName'); + GridFunctions.verifyColumnMergedState(childGrid, childCol, [ + { value: 'Product A', span: 2 }, + { value: 'Product B', span: 1 }, + { value: 'Product A', span: 1 } + ]); + + await wait(1); + fix.detectChanges(); + + const allGrids = fix.debugElement.queryAll(By.directive(IgxHierarchicalGridComponent)); + const childGridDE = allGrids.find(x => x.componentInstance === childGrid); + expect(childGridDE).toBeDefined(); + const childRows = childGridDE.queryAll(By.css(CSS_CLASS_GRID_ROW)); + childRows.shift(); + const childRowDE = childRows[0]; + const childCells = childRowDE.queryAll(By.css('.igx-grid__td')); + const childCellDE = childCells[1]; + UIInteractions.simulateClickAndSelectEvent(childCellDE.nativeElement); + await wait(1); + fix.detectChanges(); + + GridFunctions.verifyColumnMergedState(childGrid, childCol, [ + { value: 'Product A', span: 1 }, + { value: 'Product A', span: 1 }, + { value: 'Product B', span: 1 }, + { value: 'Product A', span: 1 } + ]); + + const rootGridDE = allGrids.find(x => x.componentInstance === grid); + expect(rootGridDE).toBeDefined(); + const parentRows = rootGridDE.queryAll(By.css(CSS_CLASS_GRID_ROW)); + parentRows.shift(); + const parentRowDE = parentRows[0]; + const parentCells = parentRowDE.queryAll(By.css('.igx-grid__td')); + const parentCellDE = parentCells[1]; + childCellDE.nativeElement.dispatchEvent(new FocusEvent('focusout', { + bubbles: true, + relatedTarget: parentCellDE.nativeElement + })); + UIInteractions.simulateClickAndSelectEvent(parentCellDE.nativeElement); + await wait(1); + fix.detectChanges(); + + GridFunctions.verifyColumnMergedState(childGrid, childCol, [ + { value: 'Product A', span: 2 }, + { value: 'Product B', span: 1 }, + { value: 'Product A', span: 1 } + ]); + }); + }); describe('TreeGrid', () => { From b2258a2e9e1ba62e707c55dd4a2a7bf2a743899b Mon Sep 17 00:00:00 2001 From: Ivan Minchev Date: Mon, 26 Jan 2026 17:58:52 +0200 Subject: [PATCH 2/6] fix(grid): improve focusout handling to clear active node correctly --- .../grids/grid/src/grid-base.directive.ts | 26 ++++++++++++++----- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/projects/igniteui-angular/grids/grid/src/grid-base.directive.ts b/projects/igniteui-angular/grids/grid/src/grid-base.directive.ts index 5f9569e92a3..27fe2002180 100644 --- a/projects/igniteui-angular/grids/grid/src/grid-base.directive.ts +++ b/projects/igniteui-angular/grids/grid/src/grid-base.directive.ts @@ -3688,14 +3688,26 @@ 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) => { + fromEvent(this.nativeElement, 'focusout').pipe(filter(() => !!this.navigation.activeNode), destructor).subscribe((event: FocusEvent) => { 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)) { + if (this.crudService.cell || !activeNode) { + return; + } + const nextFocused = event.relatedTarget as Node; + const activeElement = (nextFocused || this.document.activeElement) as Node; + const focusWithin = !!activeElement && this.nativeElement.contains(activeElement); + const allowClear = !(this.rowEditable && this.crudService.rowEditingBlocked && this.crudService.rowInEditMode); + + if (!focusWithin && allowClear) { + this.clearActiveNode(); + return; + } + + if (((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)) && + allowClear) { this.clearActiveNode(); } }); From d3188b2816781c100390eb3fb03d05bcc2999a81 Mon Sep 17 00:00:00 2001 From: Ivan Minchev Date: Tue, 27 Jan 2026 15:24:07 +0200 Subject: [PATCH 3/6] fix(grid): update focus handling to use focusin event for clearing active node --- .../grids/grid/src/cell-merge.spec.ts | 5 +-- .../grids/grid/src/grid-base.directive.ts | 38 ++++++++----------- 2 files changed, 16 insertions(+), 27 deletions(-) diff --git a/projects/igniteui-angular/grids/grid/src/cell-merge.spec.ts b/projects/igniteui-angular/grids/grid/src/cell-merge.spec.ts index e7b9b6acd5f..5161b17969e 100644 --- a/projects/igniteui-angular/grids/grid/src/cell-merge.spec.ts +++ b/projects/igniteui-angular/grids/grid/src/cell-merge.spec.ts @@ -1161,11 +1161,8 @@ describe('IgxGrid - Cell merging #grid', () => { const parentRowDE = parentRows[0]; const parentCells = parentRowDE.queryAll(By.css('.igx-grid__td')); const parentCellDE = parentCells[1]; - childCellDE.nativeElement.dispatchEvent(new FocusEvent('focusout', { - bubbles: true, - relatedTarget: parentCellDE.nativeElement - })); UIInteractions.simulateClickAndSelectEvent(parentCellDE.nativeElement); + parentCellDE.nativeElement.dispatchEvent(new FocusEvent('focusin', { bubbles: true })); await wait(1); fix.detectChanges(); diff --git a/projects/igniteui-angular/grids/grid/src/grid-base.directive.ts b/projects/igniteui-angular/grids/grid/src/grid-base.directive.ts index 27fe2002180..7f37cc6750a 100644 --- a/projects/igniteui-angular/grids/grid/src/grid-base.directive.ts +++ b/projects/igniteui-angular/grids/grid/src/grid-base.directive.ts @@ -3688,29 +3688,21 @@ 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: FocusEvent) => { - const activeNode = this.navigation.activeNode; - if (this.crudService.cell || !activeNode) { - return; - } - const nextFocused = event.relatedTarget as Node; - const activeElement = (nextFocused || this.document.activeElement) as Node; - const focusWithin = !!activeElement && this.nativeElement.contains(activeElement); - const allowClear = !(this.rowEditable && this.crudService.rowEditingBlocked && this.crudService.rowInEditMode); - - if (!focusWithin && allowClear) { - this.clearActiveNode(); - return; - } - - if (((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)) && - allowClear) { - this.clearActiveNode(); - } - }); + fromEvent(this.document, 'focusin') + .pipe(filter(() => !!this.navigation.activeNode), destructor) + .subscribe((event: FocusEvent) => { + if (this.crudService.cell || !this.navigation.activeNode) { + return; + } + if (this.rowEditable && this.crudService.rowEditingBlocked && this.crudService.rowInEditMode) { + return; + } + const target = event.target as Node; + if (target && this.nativeElement.contains(target)) { + return; + } + Promise.resolve().then(() => this.clearActiveNode()); + }); this.rowAddedNotifier.pipe(destructor).subscribe(args => this.refreshGridState(args)); this.rowDeletedNotifier.pipe(destructor).subscribe(args => { this.summaryService.deleteOperation = true; From b5b85e1fe0bcf411b6d7645dda786908d6fdfea5 Mon Sep 17 00:00:00 2001 From: Ivan Minchev Date: Tue, 3 Feb 2026 17:07:26 +0200 Subject: [PATCH 4/6] fix(grid): update focusout handling to clear active node and manage merge state --- .../grids/grid/src/grid-base.directive.ts | 34 +++++++++++-------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/projects/igniteui-angular/grids/grid/src/grid-base.directive.ts b/projects/igniteui-angular/grids/grid/src/grid-base.directive.ts index 7f37cc6750a..18b94ebd44d 100644 --- a/projects/igniteui-angular/grids/grid/src/grid-base.directive.ts +++ b/projects/igniteui-angular/grids/grid/src/grid-base.directive.ts @@ -3688,21 +3688,25 @@ export abstract class IgxGridBaseDirective implements GridType, */ public _setupListeners() { const destructor = takeUntil(this.destroy$); - fromEvent(this.document, 'focusin') - .pipe(filter(() => !!this.navigation.activeNode), destructor) - .subscribe((event: FocusEvent) => { - if (this.crudService.cell || !this.navigation.activeNode) { - return; - } - if (this.rowEditable && this.crudService.rowEditingBlocked && this.crudService.rowInEditMode) { - return; - } - const target = event.target as Node; - if (target && this.nativeElement.contains(target)) { - return; - } - Promise.resolve().then(() => this.clearActiveNode()); - }); + fromEvent(this.nativeElement, 'focusout').pipe(filter(() => !!this.navigation.activeNode), destructor).subscribe((event) => { + const activeNode = this.navigation.activeNode; + + // In hierarchical grids, activation can be cleared by child highlight logic, leaving an empty object. + // If merging is enabled, clear cached active indexes to allow merge state to restore. + if (!Object.keys(activeNode).length && this.hasCellsToMerge) { + this._activeRowIndexes = null; + this.notifyChanges(); + } + + 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.clearActiveNode(); + } + }); this.rowAddedNotifier.pipe(destructor).subscribe(args => this.refreshGridState(args)); this.rowDeletedNotifier.pipe(destructor).subscribe(args => { this.summaryService.deleteOperation = true; From bb7e46bf5bb0c0f9efa3349df2bdd80921de39e8 Mon Sep 17 00:00:00 2001 From: Ivan Minchev Date: Wed, 4 Feb 2026 14:12:54 +0200 Subject: [PATCH 5/6] fix(grid): clean up focusout handling and emit activeNodeChange event --- .../grids/grid/src/grid-base.directive.ts | 8 -------- .../src/hierarchical-grid-navigation.service.ts | 2 ++ 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/projects/igniteui-angular/grids/grid/src/grid-base.directive.ts b/projects/igniteui-angular/grids/grid/src/grid-base.directive.ts index 18b94ebd44d..5f9569e92a3 100644 --- a/projects/igniteui-angular/grids/grid/src/grid-base.directive.ts +++ b/projects/igniteui-angular/grids/grid/src/grid-base.directive.ts @@ -3690,14 +3690,6 @@ export abstract class IgxGridBaseDirective implements GridType, const destructor = takeUntil(this.destroy$); fromEvent(this.nativeElement, 'focusout').pipe(filter(() => !!this.navigation.activeNode), destructor).subscribe((event) => { const activeNode = this.navigation.activeNode; - - // In hierarchical grids, activation can be cleared by child highlight logic, leaving an empty object. - // If merging is enabled, clear cached active indexes to allow merge state to restore. - if (!Object.keys(activeNode).length && this.hasCellsToMerge) { - this._activeRowIndexes = null; - this.notifyChanges(); - } - if (!this.crudService.cell && !!activeNode && ((event.target === this.tbody.nativeElement && activeNode.row >= 0 && activeNode.row < this.dataView.length) diff --git a/projects/igniteui-angular/grids/hierarchical-grid/src/hierarchical-grid-navigation.service.ts b/projects/igniteui-angular/grids/hierarchical-grid/src/hierarchical-grid-navigation.service.ts index c2180c755be..9cc4e413db3 100644 --- a/projects/igniteui-angular/grids/hierarchical-grid/src/hierarchical-grid-navigation.service.ts +++ b/projects/igniteui-angular/grids/hierarchical-grid/src/hierarchical-grid-navigation.service.ts @@ -365,6 +365,8 @@ export class IgxHierarchicalGridNavigationService extends IgxGridNavigationServi if (this.activeNode && Object.keys(this.activeNode).length) { this.activeNode = Object.assign({} as IActiveNode); } + + this.grid.activeNodeChange.emit(); } private hasNextTarget(grid: GridType, index: number, isNext: boolean) { From d523c320d41a993db62215bf79cadfd46ed7b4e9 Mon Sep 17 00:00:00 2001 From: Ivan Minchev Date: Wed, 4 Feb 2026 14:31:57 +0200 Subject: [PATCH 6/6] test(hgrid): minor adjustments --- .../igniteui-angular/grids/grid/src/cell-merge.spec.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/projects/igniteui-angular/grids/grid/src/cell-merge.spec.ts b/projects/igniteui-angular/grids/grid/src/cell-merge.spec.ts index 5161b17969e..47b59df3f21 100644 --- a/projects/igniteui-angular/grids/grid/src/cell-merge.spec.ts +++ b/projects/igniteui-angular/grids/grid/src/cell-merge.spec.ts @@ -6,7 +6,7 @@ import { IgxPaginatorComponent } from 'igniteui-angular/paginator';; import { DataParent } from '../../../test-utils/sample-test-data.spec'; import { GridFunctions, GridSelectionFunctions } from '../../../test-utils/grid-functions.spec'; import { By } from '@angular/platform-browser'; -import { UIInteractions, wait } from '../../../test-utils/ui-interactions.spec'; +import { UIInteractions, wait, waitForActiveNodeChange } from '../../../test-utils/ui-interactions.spec'; import { hasClass, setElementSize } from '../../../test-utils/helper-utils.spec'; import { ColumnLayoutTestComponent } from './grid.multi-row-layout.spec'; import { IgxHierarchicalGridTestBaseComponent } from '../../hierarchical-grid/src/hierarchical-grid.spec'; @@ -1161,9 +1161,10 @@ describe('IgxGrid - Cell merging #grid', () => { const parentRowDE = parentRows[0]; const parentCells = parentRowDE.queryAll(By.css('.igx-grid__td')); const parentCellDE = parentCells[1]; + const activeChange = waitForActiveNodeChange(childGrid); UIInteractions.simulateClickAndSelectEvent(parentCellDE.nativeElement); - parentCellDE.nativeElement.dispatchEvent(new FocusEvent('focusin', { bubbles: true })); - await wait(1); + await activeChange; + await wait(20); fix.detectChanges(); GridFunctions.verifyColumnMergedState(childGrid, childCol, [