Skip to content

Commit 9cf92ca

Browse files
Merge pull request #17303 from IgniteUI/ganastasov/fix-17275-21.2.x
fix(hierarchical-grid): hide child row editing overlay on scroll - 21.2.x
2 parents bde6e04 + 4707cff commit 9cf92ca

3 files changed

Lines changed: 115 additions & 0 deletions

File tree

projects/igniteui-angular/grids/core/src/grid.common.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export class IgxGridBodyDirective { }
1818
*/
1919
export interface RowEditPositionSettings extends PositionSettings {
2020
container?: HTMLElement;
21+
clipToVisibleArea?: boolean;
2122
}
2223

2324
/**
@@ -63,6 +64,73 @@ export class RowEditPositionStrategy extends ConnectedPositioningStrategy {
6364

6465
super.position(contentElement, { width: targetElement.clientWidth, height: targetElement.clientHeight },
6566
document, initialCall, targetElement);
67+
68+
if (this.settings.clipToVisibleArea) {
69+
// After positioning in the top layer, keep the overlay clipped to the visible grid body.
70+
this.updateContentClip(contentElement);
71+
}
72+
}
73+
74+
private updateContentClip(contentElement: HTMLElement): void {
75+
const container = this.settings.container;
76+
77+
if (!container) {
78+
return;
79+
}
80+
81+
const clippingRect = this.getClippingRect(container);
82+
const contentRect = contentElement.getBoundingClientRect();
83+
84+
// Convert the clipped overflow on each side to CSS inset values.
85+
const top = Math.round(Math.max(clippingRect.top - contentRect.top, 0));
86+
const right = Math.round(Math.max(contentRect.right - clippingRect.right, 0));
87+
const bottom = Math.round(Math.max(contentRect.bottom - clippingRect.bottom, 0));
88+
const left = Math.round(Math.max(clippingRect.left - contentRect.left, 0));
89+
90+
// When the overlay is fully outside the clipping rect, hide it and block its action buttons.
91+
const fullyClipped = top >= contentRect.height || bottom >= contentRect.height ||
92+
left >= contentRect.width || right >= contentRect.width;
93+
94+
// Row-edit overlays are rendered in the top layer, so clip the content explicitly to the grid's visible area.
95+
contentElement.style.clipPath = fullyClipped ? 'inset(100%)' :
96+
(top || right || bottom || left ? `inset(${top}px ${right}px ${bottom}px ${left}px)` : '');
97+
98+
contentElement.style.pointerEvents = fullyClipped ? 'none' : '';
99+
contentElement.style.visibility = fullyClipped ? 'hidden' : '';
100+
}
101+
102+
private getClippingRect(element: HTMLElement): Pick<DOMRect, 'top' | 'right' | 'bottom' | 'left'> {
103+
const document = element.ownerDocument;
104+
const gridBody = element.closest('[igxgridbody]') as HTMLElement || element;
105+
const rect = gridBody.getBoundingClientRect();
106+
// Start with the current grid body, then narrow it by parent grid bodies.
107+
const clippingRect = { top: rect.top, right: rect.right, bottom: rect.bottom, left: rect.left };
108+
109+
let parent = gridBody.parentElement?.closest('[igxgridbody]') as HTMLElement;
110+
111+
// Intersect with parent grid bodies so nested grids respect their parent scroll bounds.
112+
while (parent) {
113+
const parentRect = parent.getBoundingClientRect();
114+
115+
clippingRect.top = Math.max(clippingRect.top, parentRect.top);
116+
clippingRect.right = Math.min(clippingRect.right, parentRect.right);
117+
clippingRect.bottom = Math.min(clippingRect.bottom, parentRect.bottom);
118+
clippingRect.left = Math.max(clippingRect.left, parentRect.left);
119+
120+
if (clippingRect.top >= clippingRect.bottom || clippingRect.left >= clippingRect.right) {
121+
break;
122+
}
123+
124+
parent = parent.parentElement?.closest('[igxgridbody]') as HTMLElement;
125+
}
126+
127+
// Keep the clipping area inside the viewport because popover content is viewport-positioned.
128+
return {
129+
top: Math.max(clippingRect.top, 0),
130+
right: Math.min(clippingRect.right, document.documentElement.clientWidth),
131+
bottom: Math.min(clippingRect.bottom, document.documentElement.clientHeight),
132+
left: Math.max(clippingRect.left, 0)
133+
};
66134
}
67135

68136
/**

projects/igniteui-angular/grids/grid/src/grid-base.directive.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8143,6 +8143,8 @@ export abstract class IgxGridBaseDirective implements GridType,
81438143
settings = overlay.settings;
81448144
}
81458145
this.rowEditPositioningStrategy.settings.container = this.tbody.nativeElement;
8146+
this.rowEditPositioningStrategy.settings.clipToVisibleArea =
8147+
this.type === 'hierarchical' && (this as GridType).rootGrid !== this;
81468148
const pinned = this._pinnedRecordIDs.indexOf(rowID) !== -1;
81478149
const targetRow = !pinned ?
81488150
this.gridAPI.get_row_by_key(rowID) as IgxRowDirective

projects/igniteui-angular/grids/hierarchical-grid/src/hierarchical-grid.spec.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -783,6 +783,51 @@ describe('Basic IgxHierarchicalGrid #hGrid', () => {
783783
expect(childGrids[1].height).toBe('200px');
784784
});
785785

786+
it('should hide child row editing overlay when parent scroll moves child row out of view', () => {
787+
hierarchicalGrid.getRowByIndex(0).expanded = true;
788+
fixture.detectChanges();
789+
790+
const childGrid = hierarchicalGrid.gridAPI.getChildGrids()[0] as IgxHierarchicalGridComponent;
791+
childGrid.primaryKey = 'ID';
792+
childGrid.rowEditable = true;
793+
fixture.detectChanges();
794+
795+
const row = childGrid.gridAPI.get_row_by_index(0);
796+
spyOnProperty(childGrid.crudService, 'rowInEditMode', 'get').and.returnValue(row);
797+
childGrid.openRowOverlay(row.key);
798+
fixture.detectChanges();
799+
800+
expect(childGrid.rowEditingOverlay.collapsed).toBeFalse();
801+
802+
const parentTbody = hierarchicalGrid.tbody.nativeElement.parentElement;
803+
const childTbody = childGrid.tbody.nativeElement.parentElement;
804+
const overlayContent = childGrid.rowEditingOverlay.element.parentElement;
805+
806+
parentTbody.style.overflow = 'hidden';
807+
childTbody.style.overflow = 'hidden';
808+
spyOn(parentTbody, 'getBoundingClientRect').and.returnValue({
809+
top: 0, right: 500, bottom: 200, left: 0
810+
} as DOMRect);
811+
spyOn(childTbody, 'getBoundingClientRect').and.returnValue({
812+
top: 0, right: 500, bottom: 200, left: 0
813+
} as DOMRect);
814+
spyOn(childGrid.tbody.nativeElement, 'getBoundingClientRect').and.returnValue({
815+
top: -100, right: 500, bottom: 500, left: 0
816+
} as DOMRect);
817+
spyOn(row.nativeElement, 'getBoundingClientRect').and.returnValue({
818+
top: -120, right: 500, bottom: -80, left: 0
819+
} as DOMRect);
820+
spyOn(overlayContent, 'getBoundingClientRect').and.returnValue({
821+
top: -80, right: 500, bottom: -32, left: 0, width: 500, height: 48
822+
} as DOMRect);
823+
824+
childGrid.rowEditingOverlay.reposition();
825+
fixture.detectChanges();
826+
827+
expect(overlayContent.style.clipPath).toBe('inset(100%)');
828+
expect(overlayContent.style.pointerEvents).toBe('none');
829+
});
830+
786831
it('Should apply runtime option changes to all related child grids (both existing and not yet initialized).', () => {
787832
const row = hierarchicalGrid.gridAPI.get_row_by_index(0) as IgxHierarchicalRowComponent;
788833
UIInteractions.simulateClickAndSelectEvent(row.expander);

0 commit comments

Comments
 (0)