• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

IgniteUI / igniteui-angular / 27264622044

10 Jun 2026 08:48AM UTC coverage: 90.153% (-0.001%) from 90.154%
27264622044

Pull #17302

github

web-flow
Merge fd43929f5 into c90f4009d
Pull Request #17302: fix(hierarchical-grid): hide child row editing overlay on scroll - master

14877 of 17329 branches covered (85.85%)

Branch coverage included in aggregate %.

29 of 31 new or added lines in 2 files covered. (93.55%)

29948 of 32392 relevant lines covered (92.45%)

34640.77 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

90.91
/projects/igniteui-angular/grids/core/src/grid.common.ts
1
import { Directive } from '@angular/core';
2
import { ConnectedPositioningStrategy } from 'igniteui-angular/core';
3
import { VerticalAlignment, PositionSettings, Point, Util } from 'igniteui-angular/core';
4
import { IgxForOfSyncService } from 'igniteui-angular/directives';
5
import { scaleInVerBottom, scaleInVerTop } from 'igniteui-angular/animations';
6

7

8
@Directive({
9
    selector: '[igxGridBody]',
10
    providers: [IgxForOfSyncService],
11
    standalone: true
12
})
13
export class IgxGridBodyDirective { }
3✔
14

15

16
/**
17
 * @hidden
18
 */
19
export interface RowEditPositionSettings extends PositionSettings {
20
    container?: HTMLElement;
21
    clipToVisibleArea?: boolean;
22
}
23

24
/**
25
 * @hidden
26
 */
27
export class RowEditPositionStrategy extends ConnectedPositioningStrategy {
28
    public isTop = false;
4,421✔
29
    public isTopInitialPosition = null;
4,421✔
30
    public override settings: RowEditPositionSettings;
31
    private io: IntersectionObserver | null = null;
4,421✔
32

33
    public override position(contentElement: HTMLElement, _size: { width: number; height: number }, document?: Document, initialCall?: boolean,
34
        target?: Point | HTMLElement): void {
35
        this.internalPosition(contentElement, _size, document, initialCall, target);
274✔
36
        // Use the IntersectionObserverHelper to manage position updates when the target moves
37
        this.io?.disconnect();
274✔
38
        const targetElement: HTMLElement = target as HTMLElement; // current grid.row
274✔
39
        this.io = Util.setupIntersectionObserver(
274✔
40
            targetElement,
41
            document,
42
            () => this.internalPosition(contentElement, { width: targetElement.clientWidth, height: targetElement.clientHeight }, document, false, targetElement)
26✔
43
        );
44
    }
45

46
    private internalPosition(contentElement: HTMLElement, _size: { width: number; height: number }, document?: Document, initialCall?: boolean,
47
        target?: Point | HTMLElement): void {
48
        const container = this.settings.container; // grid.tbody
300✔
49
        const targetElement: HTMLElement = target as HTMLElement; // current grid.row
300✔
50

51
        // Position of the overlay depends on the available space in the grid.
52
        // If the bottom space is not enough then the the row overlay will show at the top of the row.
53
        // Once shown, either top or bottom, then this position stays until the overlay is closed (isTopInitialPosition property),
54
        // which means that when scrolling then overlay may hide, while the row is still visible (UX requirement).
55
        this.isTop = this.isTopInitialPosition !== null ?
300!
56
            this.isTopInitialPosition :
57
            container.getBoundingClientRect().bottom <
58
            targetElement.getBoundingClientRect().bottom + contentElement.getBoundingClientRect().height;
59

60
        // Set width of the row editing overlay to equal row width, otherwise it fits 100% of the grid.
61
        contentElement.style.width = targetElement.clientWidth + 'px';
300✔
62
        this.settings.verticalStartPoint = this.settings.verticalDirection = this.isTop ? VerticalAlignment.Top : VerticalAlignment.Bottom;
300✔
63
        this.settings.openAnimation = this.isTop ? scaleInVerBottom : scaleInVerTop;
300✔
64

65
        super.position(contentElement, { width: targetElement.clientWidth, height: targetElement.clientHeight },
300✔
66
            document, initialCall, targetElement);
67

68
        if (this.settings.clipToVisibleArea) {
300✔
69
            // After positioning in the top layer, keep the overlay clipped to the visible grid body.
70
            this.updateContentClip(contentElement);
8✔
71
        }
72
    }
73

74
    private updateContentClip(contentElement: HTMLElement): void {
75
        const container = this.settings.container;
8✔
76

77
        if (!container) {
8!
NEW
78
            return;
×
79
        }
80

81
        const clippingRect = this.getClippingRect(container);
8✔
82
        const contentRect = contentElement.getBoundingClientRect();
8✔
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));
8✔
86
        const right = Math.round(Math.max(contentRect.right - clippingRect.right, 0));
8✔
87
        const bottom = Math.round(Math.max(contentRect.bottom - clippingRect.bottom, 0));
8✔
88
        const left = Math.round(Math.max(clippingRect.left - contentRect.left, 0));
8✔
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 ||
8✔
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%)' :
8✔
96
            (top || right || bottom || left ? `inset(${top}px ${right}px ${bottom}px ${left}px)` : '');
10!
97

98
        contentElement.style.pointerEvents = fullyClipped ? 'none' : '';
8✔
99
        contentElement.style.visibility = fullyClipped ? 'hidden' : '';
8✔
100
    }
101

102
    private getClippingRect(element: HTMLElement): Pick<DOMRect, 'top' | 'right' | 'bottom' | 'left'> {
103
        const document = element.ownerDocument;
8✔
104
        const gridBody = element.closest('[igxgridbody]') as HTMLElement || element;
8!
105
        const rect = gridBody.getBoundingClientRect();
8✔
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 };
8✔
108

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

111
        // Intersect with parent grid bodies so nested grids respect their parent scroll bounds.
112
        while (parent) {
8✔
113
            const parentRect = parent.getBoundingClientRect();
12✔
114

115
            clippingRect.top = Math.max(clippingRect.top, parentRect.top);
12✔
116
            clippingRect.right = Math.min(clippingRect.right, parentRect.right);
12✔
117
            clippingRect.bottom = Math.min(clippingRect.bottom, parentRect.bottom);
12✔
118
            clippingRect.left = Math.max(clippingRect.left, parentRect.left);
12✔
119

120
            if (clippingRect.top >= clippingRect.bottom || clippingRect.left >= clippingRect.right) {
12!
NEW
121
                break;
×
122
            }
123

124
            parent = parent.parentElement?.closest('[igxgridbody]') as HTMLElement;
12✔
125
        }
126

127
        // Keep the clipping area inside the viewport because popover content is viewport-positioned.
128
        return {
8✔
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
        };
134
    }
135

136
    /**
137
     * Cleans up the IntersectionObserver and stored references
138
     */
139
    public dispose(): void {
140
        this.io?.disconnect();
210✔
141
        this.io = null;
210✔
142
    }
143
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc