diff --git a/projects/igniteui-angular/grids/core/src/grid.common.ts b/projects/igniteui-angular/grids/core/src/grid.common.ts index 0d2b5b40313..9b6e890ee3c 100644 --- a/projects/igniteui-angular/grids/core/src/grid.common.ts +++ b/projects/igniteui-angular/grids/core/src/grid.common.ts @@ -63,6 +63,72 @@ export class RowEditPositionStrategy extends ConnectedPositioningStrategy { super.position(contentElement, { width: targetElement.clientWidth, height: targetElement.clientHeight }, document, initialCall, targetElement); + + // After positioning in the top layer, keep the overlay clipped to the visible grid body. + 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(); + + // Convert the clipped overflow on each side to CSS inset values. + 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)); + + // When the overlay is fully outside the clipping rect, hide it and block its action buttons. + const fullyClipped = top >= contentRect.height || bottom >= contentRect.height || + left >= contentRect.width || right >= contentRect.width; + + // Row-edit overlays are rendered in the top layer, so clip the content explicitly to the grid's visible area. + 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 = fullyClipped ? 'hidden' : ''; + } + + private getClippingRect(element: HTMLElement): Pick { + const document = element.ownerDocument; + const rect = element.getBoundingClientRect(); + // Start with the current grid body, then narrow it by every clipping parent. + const clippingRect = { top: rect.top, right: rect.right, bottom: rect.bottom, left: rect.left }; + + let parent = element.parentElement; + + // Intersect with clipping ancestors so nested grids respect their parent grid scroll bounds. + while (parent && parent !== document.body && parent !== document.documentElement) { + const style = document.defaultView?.getComputedStyle(parent) ?? parent.style; + const overflow = `${style.overflow}${style.overflowX}${style.overflowY}`; + + // Only ancestors that clip their children should reduce the visible area. + 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; + } + + // Keep the clipping area inside the viewport because popover content is viewport-positioned. + 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.spec.ts b/projects/igniteui-angular/grids/hierarchical-grid/src/hierarchical-grid.spec.ts index a2ac5d7c87f..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 @@ -783,6 +783,51 @@ 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); + 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; + + 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); + 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).', () => { const row = hierarchicalGrid.gridAPI.get_row_by_index(0) as IgxHierarchicalRowComponent; UIInteractions.simulateClickAndSelectEvent(row.expander);