Skip to content
68 changes: 68 additions & 0 deletions projects/igniteui-angular/grids/core/src/grid.common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export class IgxGridBodyDirective { }
*/
export interface RowEditPositionSettings extends PositionSettings {
container?: HTMLElement;
clipToVisibleArea?: boolean;
}

/**
Expand Down Expand Up @@ -63,6 +64,73 @@ export class RowEditPositionStrategy extends ConnectedPositioningStrategy {

super.position(contentElement, { width: targetElement.clientWidth, height: targetElement.clientHeight },
document, initialCall, targetElement);

if (this.settings.clipToVisibleArea) {
// 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<DOMRect, 'top' | 'right' | 'bottom' | 'left'> {
const document = element.ownerDocument;
const gridBody = element.closest('[igxgridbody]') as HTMLElement || element;
const rect = gridBody.getBoundingClientRect();
// Start with the current grid body, then narrow it by parent grid bodies.
const clippingRect = { top: rect.top, right: rect.right, bottom: rect.bottom, left: rect.left };

let parent = gridBody.parentElement?.closest('[igxgridbody]') as HTMLElement;

// Intersect with parent grid bodies so nested grids respect their parent scroll bounds.
while (parent) {
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);

if (clippingRect.top >= clippingRect.bottom || clippingRect.left >= clippingRect.right) {
break;
}

parent = parent.parentElement?.closest('[igxgridbody]') as HTMLElement;
}

// 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)
};
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8143,6 +8143,8 @@ export abstract class IgxGridBaseDirective implements GridType,
settings = overlay.settings;
}
this.rowEditPositioningStrategy.settings.container = this.tbody.nativeElement;
this.rowEditPositioningStrategy.settings.clipToVisibleArea =
this.type === 'hierarchical' && (this as GridType).rootGrid !== this;
const pinned = this._pinnedRecordIDs.indexOf(rowID) !== -1;
const targetRow = !pinned ?
this.gridAPI.get_row_by_key(rowID) as IgxRowDirective
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Comment thread
georgianastasov marked this conversation as resolved.
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);
Expand Down
Loading