From 71bb96bf27ad14cbcc4d2ce94cde272a1716589c Mon Sep 17 00:00:00 2001 From: georgianastasov Date: Fri, 29 May 2026 14:24:07 +0300 Subject: [PATCH 1/3] fix(hierarchical-grid): hide child row editing overlay on scroll --- .../src/hierarchical-grid.component.ts | 37 +++++++++- .../src/hierarchical-grid.spec.ts | 67 +++++++++++++++++++ 2 files changed, 103 insertions(+), 1 deletion(-) diff --git a/projects/igniteui-angular/grids/hierarchical-grid/src/hierarchical-grid.component.ts b/projects/igniteui-angular/grids/hierarchical-grid/src/hierarchical-grid.component.ts index fb4c2f4aa85..f9b17f9a2e2 100644 --- a/projects/igniteui-angular/grids/hierarchical-grid/src/hierarchical-grid.component.ts +++ b/projects/igniteui-angular/grids/hierarchical-grid/src/hierarchical-grid.component.ts @@ -8,7 +8,7 @@ import { IgxColumnComponent, } from 'igniteui-angular/grids/core'; import { IgxHierarchicalGridNavigationService } from './hierarchical-grid-navigation.service'; import { IgxGridSummaryService } from 'igniteui-angular/grids/core'; import { IgxHierarchicalGridBaseDirective } from './hierarchical-grid-base.directive'; -import { takeUntil } from 'rxjs/operators'; +import { first, takeUntil } from 'rxjs/operators'; import { CellType, GridType, IGX_GRID_BASE, IGX_GRID_SERVICE_BASE, RowType } from 'igniteui-angular/grids/core'; import { IgxRowIslandAPIService } from './row-island-api.service'; import { IgxGridCRUDService } from 'igniteui-angular/grids/core'; @@ -1115,6 +1115,7 @@ export class IgxHierarchicalGridComponent extends IgxHierarchicalGridBaseDirecti /** @hidden @internal **/ public onContainerScroll() { this.hideOverlays(); + this.updateChildRowEditingOverlayStateOnScroll(); } /** @@ -1137,6 +1138,15 @@ export class IgxHierarchicalGridComponent extends IgxHierarchicalGridBaseDirecti return this.gridAPI.getChildGrids(inDeph); } + /** @hidden @internal **/ + protected override verticalScrollHandler(event) { + super.verticalScrollHandler(event); + + this.zone.onStable.pipe(first()).subscribe(() => { + this.updateChildRowEditingOverlayStateOnScroll(); + }); + } + protected override generateDataFields(data: any[]): string[] { return super.generateDataFields(data).filter((field) => { const layoutsList = this.parentIsland ? this.parentIsland.children : this.childLayoutList; @@ -1295,4 +1305,29 @@ export class IgxHierarchicalGridComponent extends IgxHierarchicalGridBaseDirecti childEntities: childEntities } } + + private updateChildRowEditingOverlayStateOnScroll() { + const visibleArea = this.tbodyContainer.nativeElement.getBoundingClientRect(); + const childGrids = this.gridAPI.getChildGrids(true) as IgxHierarchicalGridComponent[]; + + childGrids.forEach((grid) => { + const row = grid.crudService.rowInEditMode; + + if (!grid.rowEditable || !grid.rowEditingOverlay || grid.rowEditingOverlay.collapsed) { + return; + } + + if (!row || !this.isElementInVisibleArea(row.nativeElement, visibleArea)) { + grid.toggleRowEditingOverlay(false); + } else { + grid.toggleRowEditingOverlay(true); + grid.repositionRowEditingOverlay(row); + } + }); + } + + private isElementInVisibleArea(element: HTMLElement, visibleArea: DOMRect) { + const rect = element.getBoundingClientRect(); + return rect.bottom > visibleArea.top && rect.top < visibleArea.bottom; + } } diff --git a/projects/igniteui-angular/grids/hierarchical-grid/src/hierarchical-grid.spec.ts b/projects/igniteui-angular/grids/hierarchical-grid/src/hierarchical-grid.spec.ts index a2ac5d7c87f..a81ce7c141d 100644 --- a/projects/igniteui-angular/grids/hierarchical-grid/src/hierarchical-grid.spec.ts +++ b/projects/igniteui-angular/grids/hierarchical-grid/src/hierarchical-grid.spec.ts @@ -783,6 +783,73 @@ describe('Basic IgxHierarchicalGrid #hGrid', () => { expect(childGrids[1].height).toBe('200px'); }); + it('should hide child row editing overlay when parent scroll moves child row out of view', () => { + hierarchicalGrid.getRowByIndex(0).expanded = true; + fixture.detectChanges(); + + const childGrid = hierarchicalGrid.gridAPI.getChildGrids()[0] as IgxHierarchicalGridComponent; + childGrid.primaryKey = 'ID'; + childGrid.rowEditable = true; + fixture.detectChanges(); + + const row = childGrid.gridAPI.get_row_by_index(0); + spyOnProperty(childGrid.crudService, 'rowInEditMode', 'get').and.returnValue(row); + spyOnProperty(childGrid.rowEditingOverlay, 'collapsed', 'get').and.returnValue(false); + childGrid.rowEditingOverlay.element.style.display = 'block'; + + spyOn((hierarchicalGrid as any).tbodyContainer.nativeElement, 'getBoundingClientRect').and.returnValue({ + top: 0, + bottom: 200 + } as DOMRect); + let childRowRect = { + top: 40, + bottom: 80 + } as DOMRect; + spyOn(row.nativeElement, 'getBoundingClientRect').and.callFake(() => childRowRect); + const repositionOverlaySpy = spyOn(childGrid, 'repositionRowEditingOverlay'); + const toggleOverlaySpy = spyOn(childGrid, 'toggleRowEditingOverlay').and.callThrough(); + + const scroll = hierarchicalGrid.verticalScrollContainer.getScroll(); + scroll.scrollTop = 10; + (hierarchicalGrid as any).verticalScrollHandler({ target: scroll }); + (hierarchicalGrid as any).zone.onStable.emit(null); + fixture.detectChanges(); + + expect(repositionOverlaySpy).toHaveBeenCalledWith(row); + expect(toggleOverlaySpy).not.toHaveBeenCalledWith(false); + expect(childGrid.rowEditingOverlay.element.style.display).not.toBe('none'); + + repositionOverlaySpy.calls.reset(); + toggleOverlaySpy.calls.reset(); + childRowRect = { + top: -80, + bottom: -40 + } as DOMRect; + scroll.scrollTop = 1000; + (hierarchicalGrid as any).verticalScrollHandler({ target: scroll }); + (hierarchicalGrid as any).zone.onStable.emit(null); + fixture.detectChanges(); + + expect(repositionOverlaySpy).not.toHaveBeenCalled(); + expect(toggleOverlaySpy).toHaveBeenCalledWith(false); + expect(childGrid.rowEditingOverlay.element.style.display).toBe('none'); + + repositionOverlaySpy.calls.reset(); + toggleOverlaySpy.calls.reset(); + childRowRect = { + top: 40, + bottom: 80 + } as DOMRect; + scroll.scrollTop = 10; + (hierarchicalGrid as any).verticalScrollHandler({ target: scroll }); + (hierarchicalGrid as any).zone.onStable.emit(null); + fixture.detectChanges(); + + expect(toggleOverlaySpy).toHaveBeenCalledWith(true); + expect(repositionOverlaySpy).toHaveBeenCalledWith(row); + expect(childGrid.rowEditingOverlay.element.style.display).not.toBe('none'); + }); + it('Should apply runtime option changes to all related child grids (both existing and not yet initialized).', () => { const row = hierarchicalGrid.gridAPI.get_row_by_index(0) as IgxHierarchicalRowComponent; UIInteractions.simulateClickAndSelectEvent(row.expander); From 95904c854affbe9c9942affc15ad153c3fcf93cc Mon Sep 17 00:00:00 2001 From: georgianastasov Date: Fri, 29 May 2026 16:24:19 +0300 Subject: [PATCH 2/3] refactor(hierarchical-grid): defer edited row lookup after overlay guards --- .../hierarchical-grid/src/hierarchical-grid.component.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/projects/igniteui-angular/grids/hierarchical-grid/src/hierarchical-grid.component.ts b/projects/igniteui-angular/grids/hierarchical-grid/src/hierarchical-grid.component.ts index f9b17f9a2e2..ac56db7b381 100644 --- a/projects/igniteui-angular/grids/hierarchical-grid/src/hierarchical-grid.component.ts +++ b/projects/igniteui-angular/grids/hierarchical-grid/src/hierarchical-grid.component.ts @@ -1311,12 +1311,12 @@ export class IgxHierarchicalGridComponent extends IgxHierarchicalGridBaseDirecti const childGrids = this.gridAPI.getChildGrids(true) as IgxHierarchicalGridComponent[]; childGrids.forEach((grid) => { - const row = grid.crudService.rowInEditMode; - if (!grid.rowEditable || !grid.rowEditingOverlay || grid.rowEditingOverlay.collapsed) { return; } + const row = grid.crudService.rowInEditMode; + if (!row || !this.isElementInVisibleArea(row.nativeElement, visibleArea)) { grid.toggleRowEditingOverlay(false); } else { From 1ec9838a6b80d61083fcca115d7c35ca2389386f Mon Sep 17 00:00:00 2001 From: georgianastasov Date: Wed, 3 Jun 2026 16:27:50 +0300 Subject: [PATCH 3/3] fix(hierarchical-grid): keep child row edit overlay inside grid --- .../grids/core/src/grid.common.ts | 58 +++++++++++++ .../src/hierarchical-grid.component.ts | 37 +-------- .../src/hierarchical-grid.spec.ts | 82 +++++++------------ 3 files changed, 89 insertions(+), 88 deletions(-) diff --git a/projects/igniteui-angular/grids/core/src/grid.common.ts b/projects/igniteui-angular/grids/core/src/grid.common.ts index 0d2b5b40313..095d35d5466 100644 --- a/projects/igniteui-angular/grids/core/src/grid.common.ts +++ b/projects/igniteui-angular/grids/core/src/grid.common.ts @@ -63,6 +63,64 @@ export class RowEditPositionStrategy extends ConnectedPositioningStrategy { super.position(contentElement, { width: targetElement.clientWidth, height: targetElement.clientHeight }, document, initialCall, targetElement); + + this.updateContentClip(contentElement); + } + + private updateContentClip(contentElement: HTMLElement): void { + const container = this.settings.container; + + if (!container) { + return; + } + + const clippingRect = this.getClippingRect(container); + const contentRect = contentElement.getBoundingClientRect(); + + const top = Math.round(Math.max(clippingRect.top - contentRect.top, 0)); + const right = Math.round(Math.max(contentRect.right - clippingRect.right, 0)); + const bottom = Math.round(Math.max(contentRect.bottom - clippingRect.bottom, 0)); + const left = Math.round(Math.max(clippingRect.left - contentRect.left, 0)); + + const fullyClipped = top >= contentRect.height || bottom >= contentRect.height || + left >= contentRect.width || right >= contentRect.width; + + contentElement.style.clipPath = fullyClipped ? 'inset(100%)' : + (top || right || bottom || left ? `inset(${top}px ${right}px ${bottom}px ${left}px)` : ''); + + contentElement.style.pointerEvents = fullyClipped ? 'none' : ''; + contentElement.style.visibility = ''; + } + + private getClippingRect(element: HTMLElement): Pick { + const document = element.ownerDocument; + const rect = element.getBoundingClientRect(); + const clippingRect = { top: rect.top, right: rect.right, bottom: rect.bottom, left: rect.left }; + + let parent = element.parentElement; + + while (parent && parent !== document.body && parent !== document.documentElement) { + const style = getComputedStyle(parent); + const overflow = `${style.overflow}${style.overflowX}${style.overflowY}`; + + if (/(auto|scroll|hidden|clip)/.test(overflow)) { + const parentRect = parent.getBoundingClientRect(); + + clippingRect.top = Math.max(clippingRect.top, parentRect.top); + clippingRect.right = Math.min(clippingRect.right, parentRect.right); + clippingRect.bottom = Math.min(clippingRect.bottom, parentRect.bottom); + clippingRect.left = Math.max(clippingRect.left, parentRect.left); + } + + parent = parent.parentElement; + } + + return { + top: Math.max(clippingRect.top, 0), + right: Math.min(clippingRect.right, document.documentElement.clientWidth), + bottom: Math.min(clippingRect.bottom, document.documentElement.clientHeight), + left: Math.max(clippingRect.left, 0) + }; } /** diff --git a/projects/igniteui-angular/grids/hierarchical-grid/src/hierarchical-grid.component.ts b/projects/igniteui-angular/grids/hierarchical-grid/src/hierarchical-grid.component.ts index ac56db7b381..fb4c2f4aa85 100644 --- a/projects/igniteui-angular/grids/hierarchical-grid/src/hierarchical-grid.component.ts +++ b/projects/igniteui-angular/grids/hierarchical-grid/src/hierarchical-grid.component.ts @@ -8,7 +8,7 @@ import { IgxColumnComponent, } from 'igniteui-angular/grids/core'; import { IgxHierarchicalGridNavigationService } from './hierarchical-grid-navigation.service'; import { IgxGridSummaryService } from 'igniteui-angular/grids/core'; import { IgxHierarchicalGridBaseDirective } from './hierarchical-grid-base.directive'; -import { first, takeUntil } from 'rxjs/operators'; +import { takeUntil } from 'rxjs/operators'; import { CellType, GridType, IGX_GRID_BASE, IGX_GRID_SERVICE_BASE, RowType } from 'igniteui-angular/grids/core'; import { IgxRowIslandAPIService } from './row-island-api.service'; import { IgxGridCRUDService } from 'igniteui-angular/grids/core'; @@ -1115,7 +1115,6 @@ export class IgxHierarchicalGridComponent extends IgxHierarchicalGridBaseDirecti /** @hidden @internal **/ public onContainerScroll() { this.hideOverlays(); - this.updateChildRowEditingOverlayStateOnScroll(); } /** @@ -1138,15 +1137,6 @@ export class IgxHierarchicalGridComponent extends IgxHierarchicalGridBaseDirecti return this.gridAPI.getChildGrids(inDeph); } - /** @hidden @internal **/ - protected override verticalScrollHandler(event) { - super.verticalScrollHandler(event); - - this.zone.onStable.pipe(first()).subscribe(() => { - this.updateChildRowEditingOverlayStateOnScroll(); - }); - } - protected override generateDataFields(data: any[]): string[] { return super.generateDataFields(data).filter((field) => { const layoutsList = this.parentIsland ? this.parentIsland.children : this.childLayoutList; @@ -1305,29 +1295,4 @@ export class IgxHierarchicalGridComponent extends IgxHierarchicalGridBaseDirecti childEntities: childEntities } } - - private updateChildRowEditingOverlayStateOnScroll() { - const visibleArea = this.tbodyContainer.nativeElement.getBoundingClientRect(); - const childGrids = this.gridAPI.getChildGrids(true) as IgxHierarchicalGridComponent[]; - - childGrids.forEach((grid) => { - if (!grid.rowEditable || !grid.rowEditingOverlay || grid.rowEditingOverlay.collapsed) { - return; - } - - const row = grid.crudService.rowInEditMode; - - if (!row || !this.isElementInVisibleArea(row.nativeElement, visibleArea)) { - grid.toggleRowEditingOverlay(false); - } else { - grid.toggleRowEditingOverlay(true); - grid.repositionRowEditingOverlay(row); - } - }); - } - - private isElementInVisibleArea(element: HTMLElement, visibleArea: DOMRect) { - const rect = element.getBoundingClientRect(); - return rect.bottom > visibleArea.top && rect.top < visibleArea.bottom; - } } diff --git a/projects/igniteui-angular/grids/hierarchical-grid/src/hierarchical-grid.spec.ts b/projects/igniteui-angular/grids/hierarchical-grid/src/hierarchical-grid.spec.ts index a81ce7c141d..3b801ed21ba 100644 --- a/projects/igniteui-angular/grids/hierarchical-grid/src/hierarchical-grid.spec.ts +++ b/projects/igniteui-angular/grids/hierarchical-grid/src/hierarchical-grid.spec.ts @@ -794,60 +794,38 @@ describe('Basic IgxHierarchicalGrid #hGrid', () => { const row = childGrid.gridAPI.get_row_by_index(0); spyOnProperty(childGrid.crudService, 'rowInEditMode', 'get').and.returnValue(row); - spyOnProperty(childGrid.rowEditingOverlay, 'collapsed', 'get').and.returnValue(false); - childGrid.rowEditingOverlay.element.style.display = 'block'; + childGrid.openRowOverlay(row.key); + fixture.detectChanges(); + + expect(childGrid.rowEditingOverlay.collapsed).toBeFalse(); + + const parentTbody = hierarchicalGrid.tbody.nativeElement.parentElement; + const childTbody = childGrid.tbody.nativeElement.parentElement; + const overlayContent = childGrid.rowEditingOverlay.element.parentElement; - spyOn((hierarchicalGrid as any).tbodyContainer.nativeElement, 'getBoundingClientRect').and.returnValue({ - top: 0, - bottom: 200 + parentTbody.style.overflow = 'hidden'; + childTbody.style.overflow = 'hidden'; + spyOn(parentTbody, 'getBoundingClientRect').and.returnValue({ + top: 0, right: 500, bottom: 200, left: 0 + } as DOMRect); + spyOn(childTbody, 'getBoundingClientRect').and.returnValue({ + top: 0, right: 500, bottom: 200, left: 0 + } as DOMRect); + spyOn(childGrid.tbody.nativeElement, 'getBoundingClientRect').and.returnValue({ + top: -100, right: 500, bottom: 500, left: 0 } as DOMRect); - let childRowRect = { - top: 40, - bottom: 80 - } as DOMRect; - spyOn(row.nativeElement, 'getBoundingClientRect').and.callFake(() => childRowRect); - const repositionOverlaySpy = spyOn(childGrid, 'repositionRowEditingOverlay'); - const toggleOverlaySpy = spyOn(childGrid, 'toggleRowEditingOverlay').and.callThrough(); - - const scroll = hierarchicalGrid.verticalScrollContainer.getScroll(); - scroll.scrollTop = 10; - (hierarchicalGrid as any).verticalScrollHandler({ target: scroll }); - (hierarchicalGrid as any).zone.onStable.emit(null); - fixture.detectChanges(); - - expect(repositionOverlaySpy).toHaveBeenCalledWith(row); - expect(toggleOverlaySpy).not.toHaveBeenCalledWith(false); - expect(childGrid.rowEditingOverlay.element.style.display).not.toBe('none'); - - repositionOverlaySpy.calls.reset(); - toggleOverlaySpy.calls.reset(); - childRowRect = { - top: -80, - bottom: -40 - } as DOMRect; - scroll.scrollTop = 1000; - (hierarchicalGrid as any).verticalScrollHandler({ target: scroll }); - (hierarchicalGrid as any).zone.onStable.emit(null); - fixture.detectChanges(); - - expect(repositionOverlaySpy).not.toHaveBeenCalled(); - expect(toggleOverlaySpy).toHaveBeenCalledWith(false); - expect(childGrid.rowEditingOverlay.element.style.display).toBe('none'); - - repositionOverlaySpy.calls.reset(); - toggleOverlaySpy.calls.reset(); - childRowRect = { - top: 40, - bottom: 80 - } as DOMRect; - scroll.scrollTop = 10; - (hierarchicalGrid as any).verticalScrollHandler({ target: scroll }); - (hierarchicalGrid as any).zone.onStable.emit(null); - fixture.detectChanges(); - - expect(toggleOverlaySpy).toHaveBeenCalledWith(true); - expect(repositionOverlaySpy).toHaveBeenCalledWith(row); - expect(childGrid.rowEditingOverlay.element.style.display).not.toBe('none'); + spyOn(row.nativeElement, 'getBoundingClientRect').and.returnValue({ + top: -120, right: 500, bottom: -80, left: 0 + } as DOMRect); + spyOn(overlayContent, 'getBoundingClientRect').and.returnValue({ + top: -80, right: 500, bottom: -32, left: 0, width: 500, height: 48 + } as DOMRect); + + childGrid.rowEditingOverlay.reposition(); + fixture.detectChanges(); + + expect(overlayContent.style.clipPath).toBe('inset(100%)'); + expect(overlayContent.style.pointerEvents).toBe('none'); }); it('Should apply runtime option changes to all related child grids (both existing and not yet initialized).', () => {