@clr/angular 18.2.1 → 18.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/fesm2022/clr-angular-button.mjs +11 -5
- package/fesm2022/clr-angular-button.mjs.map +1 -1
- package/fesm2022/clr-angular-icon.mjs +16 -2
- package/fesm2022/clr-angular-icon.mjs.map +1 -1
- package/fesm2022/clr-angular-popover-common.mjs +328 -39
- package/fesm2022/clr-angular-popover-common.mjs.map +1 -1
- package/package.json +2 -2
- package/types/clr-angular-icon.d.ts +4 -2
- package/types/clr-angular-popover-common.d.ts +62 -0
|
@@ -108,6 +108,156 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
108
108
|
type: Injectable
|
|
109
109
|
}] });
|
|
110
110
|
|
|
111
|
+
/*
|
|
112
|
+
* Copyright (c) 2016-2026 Broadcom. All Rights Reserved.
|
|
113
|
+
* The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries.
|
|
114
|
+
* This software is released under MIT license.
|
|
115
|
+
* The full license information can be found in LICENSE in the root directory of this project.
|
|
116
|
+
*/
|
|
117
|
+
// Realm-independent shape checks for getScrollableParents(): `instanceof` fails for
|
|
118
|
+
// nodes created by a different window's constructors (e.g. an iframe's own Element,
|
|
119
|
+
// ShadowRoot, or HTMLHtmlElement classes), so these duck-type via nodeType/tagName
|
|
120
|
+
// instead, which works the same regardless of which window created the node.
|
|
121
|
+
function isElementOrShadowRoot(node) {
|
|
122
|
+
return node.nodeType === Node.ELEMENT_NODE || isShadowRoot(node);
|
|
123
|
+
}
|
|
124
|
+
function isShadowRoot(node) {
|
|
125
|
+
return node.nodeType === Node.DOCUMENT_FRAGMENT_NODE && 'host' in node;
|
|
126
|
+
}
|
|
127
|
+
function isHtmlElement(node) {
|
|
128
|
+
return node.nodeType === Node.ELEMENT_NODE && node.tagName === 'HTML';
|
|
129
|
+
}
|
|
130
|
+
// Same-origin-safe: returns null (rather than throwing) if `frameElement` can't be
|
|
131
|
+
// read, which happens when `win` is embedded in a cross-origin parent window.
|
|
132
|
+
function getFrameElement(win) {
|
|
133
|
+
try {
|
|
134
|
+
return win.frameElement;
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/*
|
|
142
|
+
* Copyright (c) 2016-2026 Broadcom. All Rights Reserved.
|
|
143
|
+
* The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries.
|
|
144
|
+
* This software is released under MIT license.
|
|
145
|
+
* The full license information can be found in LICENSE in the root directory of this project.
|
|
146
|
+
*/
|
|
147
|
+
/**
|
|
148
|
+
* Resolves an origin to its element and owning window, but only when that window differs
|
|
149
|
+
* from this one - i.e. the origin lives inside an iframe. Returns null for same-window
|
|
150
|
+
* origins and for non-element (point) origins, so a single check lets callers skip all
|
|
151
|
+
* cross-window handling for the overwhelming majority of popovers.
|
|
152
|
+
*
|
|
153
|
+
* Cheap: a few property reads, no layout access.
|
|
154
|
+
*/
|
|
155
|
+
function getCrossWindowOriginContext(origin) {
|
|
156
|
+
const element = getOriginElement(origin);
|
|
157
|
+
const elementWindow = element?.ownerDocument?.defaultView;
|
|
158
|
+
return element && elementWindow && elementWindow !== window ? { element, elementWindow } : null;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* CDK's FlexibleConnectedPositionStrategy anchors the overlay by reading
|
|
162
|
+
* origin.getBoundingClientRect() (for an Element/ElementRef origin) directly off the
|
|
163
|
+
* origin, relative to the origin's own window. That only matches the overlay panel's
|
|
164
|
+
* window (always the one this code runs in) when the origin lives in the same document.
|
|
165
|
+
* When the origin sits inside an iframe (e.g. a micro-frontend mounting its trigger in a
|
|
166
|
+
* nested document, possibly combined with a ShadowRoot), the raw rect is relative to the
|
|
167
|
+
* wrong viewport and the overlay is mispositioned.
|
|
168
|
+
*
|
|
169
|
+
* If the origin is same-window (or not an Element/ElementRef at all), this returns it
|
|
170
|
+
* unchanged. Otherwise it returns a Point-based origin (the other shape
|
|
171
|
+
* FlexibleConnectedPositionStrategy natively supports) whose x/y/width/height are live
|
|
172
|
+
* getters that recompute the element's rect plus the cumulative offset of every ancestor
|
|
173
|
+
* iframe between the origin's window and this one, so the overlay still anchors
|
|
174
|
+
* correctly across repositions (scroll, resize). This has no effect on anything besides
|
|
175
|
+
* overlay positioning math - it never replaces the origin ElementRef consumers rely on
|
|
176
|
+
* elsewhere (e.g. scroll listeners), since it only wraps a local copy used for
|
|
177
|
+
* positioning.
|
|
178
|
+
*
|
|
179
|
+
* If the ancestor iframe chain can't be safely resolved (e.g. a cross-origin frame
|
|
180
|
+
* boundary this code can't introspect), the getters fall back to the element's own
|
|
181
|
+
* un-translated rect - the exact numbers CDK would compute if given the raw
|
|
182
|
+
* Element/ElementRef directly, so behavior for that case is unchanged.
|
|
183
|
+
*/
|
|
184
|
+
function resolveCrossWindowOrigin(origin) {
|
|
185
|
+
const crossWindowOrigin = getCrossWindowOriginContext(origin);
|
|
186
|
+
// Same-window origins (and point origins) are handed back untouched - no wrapping, no
|
|
187
|
+
// translation, no behavior change from a popover that never crosses a window boundary.
|
|
188
|
+
if (!crossWindowOrigin) {
|
|
189
|
+
return origin;
|
|
190
|
+
}
|
|
191
|
+
const { element, elementWindow } = crossWindowOrigin;
|
|
192
|
+
let cached = null;
|
|
193
|
+
// CDK's position strategy reads x/y/width/height several times per apply() (it builds
|
|
194
|
+
// a rect from a Point origin as {top: y, bottom: y + height, left: x, right: x + width}).
|
|
195
|
+
// Memoizing per microtask collapses that into a single rect/offset computation instead
|
|
196
|
+
// of one forced layout read per property access, while still recomputing fresh on the
|
|
197
|
+
// next reposition (scroll, resize), since the cache clears before the next microtask.
|
|
198
|
+
const read = () => {
|
|
199
|
+
if (!cached) {
|
|
200
|
+
const rect = element.getBoundingClientRect();
|
|
201
|
+
const offset = getCumulativeFrameOffset(elementWindow) ?? { x: 0, y: 0 };
|
|
202
|
+
cached = { x: rect.left + offset.x, y: rect.top + offset.y, width: rect.width, height: rect.height };
|
|
203
|
+
queueMicrotask(() => (cached = null));
|
|
204
|
+
}
|
|
205
|
+
return cached;
|
|
206
|
+
};
|
|
207
|
+
return {
|
|
208
|
+
get x() {
|
|
209
|
+
return read().x;
|
|
210
|
+
},
|
|
211
|
+
get y() {
|
|
212
|
+
return read().y;
|
|
213
|
+
},
|
|
214
|
+
get width() {
|
|
215
|
+
return read().width;
|
|
216
|
+
},
|
|
217
|
+
get height() {
|
|
218
|
+
return read().height;
|
|
219
|
+
},
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
function getOriginElement(origin) {
|
|
223
|
+
if (origin instanceof ElementRef) {
|
|
224
|
+
return origin.nativeElement;
|
|
225
|
+
}
|
|
226
|
+
// `instanceof Element` is realm-sensitive and misses raw elements belonging to
|
|
227
|
+
// another window - exactly the population this util targets - so duck-type instead.
|
|
228
|
+
if (origin?.nodeType === Node.ELEMENT_NODE) {
|
|
229
|
+
return origin;
|
|
230
|
+
}
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Walks the chain of window.frameElement references from `originWindow` up to this
|
|
235
|
+
* code's own window, summing each ancestor iframe's position. Returns null if the
|
|
236
|
+
* chain can't be fully resolved (cross-origin boundary, or `originWindow` isn't
|
|
237
|
+
* actually embedded within this window).
|
|
238
|
+
*/
|
|
239
|
+
function getCumulativeFrameOffset(originWindow) {
|
|
240
|
+
let offsetX = 0;
|
|
241
|
+
let offsetY = 0;
|
|
242
|
+
let currentWindow = originWindow;
|
|
243
|
+
while (currentWindow && currentWindow !== window) {
|
|
244
|
+
const frameElement = getFrameElement(currentWindow);
|
|
245
|
+
if (!frameElement) {
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
const frameRect = frameElement.getBoundingClientRect();
|
|
249
|
+
// clientLeft/clientTop only cover the frame's border, not any CSS padding, which
|
|
250
|
+
// would otherwise skew the anchor - read computed style through the frame's own
|
|
251
|
+
// window for realm-safety.
|
|
252
|
+
const frameWindow = frameElement.ownerDocument?.defaultView ?? window;
|
|
253
|
+
const { borderLeftWidth, borderTopWidth, paddingLeft, paddingTop } = frameWindow.getComputedStyle(frameElement);
|
|
254
|
+
offsetX += frameRect.left + (parseFloat(borderLeftWidth) || 0) + (parseFloat(paddingLeft) || 0);
|
|
255
|
+
offsetY += frameRect.top + (parseFloat(borderTopWidth) || 0) + (parseFloat(paddingTop) || 0);
|
|
256
|
+
currentWindow = frameElement.ownerDocument?.defaultView ?? null;
|
|
257
|
+
}
|
|
258
|
+
return currentWindow === window ? { x: offsetX, y: offsetY } : null;
|
|
259
|
+
}
|
|
260
|
+
|
|
111
261
|
/*
|
|
112
262
|
* Copyright (c) 2016-2026 Broadcom. All Rights Reserved.
|
|
113
263
|
* The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries.
|
|
@@ -499,7 +649,10 @@ class ClrPopoverContent {
|
|
|
499
649
|
this._scrollToClose = !!scrollToClose;
|
|
500
650
|
}
|
|
501
651
|
set contentOrigin(origin) {
|
|
502
|
-
|
|
652
|
+
// `instanceof Element` is realm-sensitive and misses a raw element belonging to
|
|
653
|
+
// another window, so a consumer passing one here would bypass the ElementRef wrap
|
|
654
|
+
// resolveCrossWindowOrigin() relies on to detect a cross-window origin.
|
|
655
|
+
if (origin?.nodeType === Node.ELEMENT_NODE) {
|
|
503
656
|
this.popoverService.origin = new ElementRef(origin);
|
|
504
657
|
}
|
|
505
658
|
else {
|
|
@@ -507,10 +660,11 @@ class ClrPopoverContent {
|
|
|
507
660
|
}
|
|
508
661
|
}
|
|
509
662
|
get positionStrategy() {
|
|
663
|
+
const origin = resolveCrossWindowOrigin(this.popoverService.origin);
|
|
510
664
|
return this.overlay
|
|
511
665
|
.position()
|
|
512
|
-
.flexibleConnectedTo(
|
|
513
|
-
.setOrigin(
|
|
666
|
+
.flexibleConnectedTo(origin)
|
|
667
|
+
.setOrigin(origin)
|
|
514
668
|
.withPush(true)
|
|
515
669
|
.withPositions([this.preferredPosition, ...this._availablePositions])
|
|
516
670
|
.withFlexibleDimensions(true);
|
|
@@ -552,12 +706,7 @@ class ClrPopoverContent {
|
|
|
552
706
|
this.resetPosition();
|
|
553
707
|
}), this.popoverService.updatePositionChange().subscribe(() => {
|
|
554
708
|
this.overlayRef?.updatePosition();
|
|
555
|
-
}), this.
|
|
556
|
-
if (event && event.key && event.key === Keys.Escape && !hasModifierKey(event)) {
|
|
557
|
-
event.preventDefault();
|
|
558
|
-
this.closePopover();
|
|
559
|
-
}
|
|
560
|
-
}), this.popoverService.originPoint
|
|
709
|
+
}), this.createEscapeSubscription(), this.popoverService.originPoint
|
|
561
710
|
? this.createPointBasedOutsideClickSubscription()
|
|
562
711
|
: this.createElementBasedOutsideClickSubscription());
|
|
563
712
|
}
|
|
@@ -566,37 +715,146 @@ class ClrPopoverContent {
|
|
|
566
715
|
* mouseup from the same right-click that opened the popover.
|
|
567
716
|
*/
|
|
568
717
|
createPointBasedOutsideClickSubscription() {
|
|
569
|
-
|
|
718
|
+
const subscription = timer(500)
|
|
570
719
|
.pipe(switchMap(() => this.overlayRef.outsidePointerEvents()))
|
|
571
|
-
.subscribe(event =>
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
720
|
+
.subscribe(event => this.handlePointOutsideClick(event));
|
|
721
|
+
const crossWindowSubscription = this.createCrossWindowPointOutsideClickSubscription();
|
|
722
|
+
if (crossWindowSubscription) {
|
|
723
|
+
subscription.add(crossWindowSubscription);
|
|
724
|
+
}
|
|
725
|
+
return subscription;
|
|
726
|
+
}
|
|
727
|
+
handlePointOutsideClick(event) {
|
|
728
|
+
if (this.elementRef?.nativeElement?.contains(event.target)) {
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
if (this._outsideClickClose) {
|
|
732
|
+
this.closePopover();
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
/**
|
|
736
|
+
* Mirrors createCrossWindowOutsideClickSubscription for point-based origins (context
|
|
737
|
+
* menus): pointTargetElement - the element the point was derived from, e.g. the
|
|
738
|
+
* right-clicked element - can itself live inside a foreign iframe document, and CDK's
|
|
739
|
+
* outsidePointerEvents() only listens on this window's document, so a click elsewhere
|
|
740
|
+
* in that foreign document would otherwise never dismiss the menu. Delayed by the same
|
|
741
|
+
* 500ms as the same-window stream, for the same reason: avoid the click that opened the
|
|
742
|
+
* menu immediately closing it again.
|
|
743
|
+
*/
|
|
744
|
+
createCrossWindowPointOutsideClickSubscription() {
|
|
745
|
+
const pointTargetElement = this.popoverService.pointTargetElement;
|
|
746
|
+
const crossWindowOrigin = pointTargetElement ? getCrossWindowOriginContext(pointTargetElement) : null;
|
|
747
|
+
if (!crossWindowOrigin) {
|
|
748
|
+
return null;
|
|
749
|
+
}
|
|
750
|
+
const originWindow = crossWindowOrigin.elementWindow;
|
|
751
|
+
return timer(500)
|
|
752
|
+
.pipe(switchMap(() => this.zone.runOutsideAngular(() => merge(fromEvent(originWindow.document, 'click', { capture: true }), fromEvent(originWindow.document, 'auxclick', { capture: true })))))
|
|
753
|
+
.subscribe(event => this.zone.run(() => this.handlePointOutsideClick(event)));
|
|
579
754
|
}
|
|
580
755
|
/**
|
|
581
756
|
* Element-based origins close on outside clicks and suppress toggle-button
|
|
582
757
|
* re-clicks so the popover doesn't immediately reopen.
|
|
583
758
|
*/
|
|
584
759
|
createElementBasedOutsideClickSubscription() {
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
760
|
+
const subscription = this.overlayRef.outsidePointerEvents().subscribe(event => this.handleOutsideClick(event));
|
|
761
|
+
const crossWindowSubscription = this.createCrossWindowOutsideClickSubscription();
|
|
762
|
+
if (crossWindowSubscription) {
|
|
763
|
+
subscription.add(crossWindowSubscription);
|
|
764
|
+
}
|
|
765
|
+
return subscription;
|
|
766
|
+
}
|
|
767
|
+
handleOutsideClick(event) {
|
|
768
|
+
// The very click that opened this popover must never be treated as an outside click.
|
|
769
|
+
// CDK's OverlayOutsideClickDispatcher listens in the capture phase on this window's
|
|
770
|
+
// document.body, so it normally runs *before* the trigger's own click handler has
|
|
771
|
+
// attached the overlay, and the opening click is therefore never delivered here.
|
|
772
|
+
// That ordering does not hold everywhere: when the trigger lives in a ShadowRoot
|
|
773
|
+
// owned by a separately bootstrapped Angular application (an ESM micro-frontend
|
|
774
|
+
// plugin with its own CDK instance), the overlay can already be attached by the time
|
|
775
|
+
// the dispatcher sees the click, which delivers the opening click straight back as an
|
|
776
|
+
// "outside" click and tears the popover down microseconds after it opened.
|
|
777
|
+
// Comparing against the stored open event is exact - CDK re-emits the same Event
|
|
778
|
+
// object - and it leaves toggle-to-close untouched, since a later click on the
|
|
779
|
+
// trigger is always a different Event instance.
|
|
780
|
+
if (event === this.popoverService.openEvent) {
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
// web components (cds-icon) register as outside pointer events, so if the event target is inside the content panel return early
|
|
784
|
+
if (this.elementRef?.nativeElement?.contains(event.target)) {
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
// Check if the same element that opened the popover is the same element triggering the outside pointer events (toggle button)
|
|
788
|
+
// openEvent.target can itself be null (e.g. the original toggle button was removed
|
|
789
|
+
// from the DOM before this handler runs), so every access below is optional - an
|
|
790
|
+
// unguarded `.contains()` call here throws and aborts the whole handler, which is
|
|
791
|
+
// exactly what stops closePopover() from ever running.
|
|
792
|
+
const openEventTarget = this.popoverService.openEvent?.target;
|
|
793
|
+
const isToggleButton = !!openEventTarget &&
|
|
794
|
+
(openEventTarget.contains(event.target) ||
|
|
795
|
+
openEventTarget.parentElement?.contains(event.target) ||
|
|
796
|
+
openEventTarget === event.target);
|
|
797
|
+
if (isToggleButton) {
|
|
798
|
+
event.stopPropagation();
|
|
799
|
+
}
|
|
800
|
+
if (this._outsideClickClose || isToggleButton) {
|
|
801
|
+
this.closePopover();
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
/**
|
|
805
|
+
* CDK's outsidePointerEvents() only listens on the document that owns the overlay
|
|
806
|
+
* (this window's), so clicks inside a cross-window origin's own document (e.g. a
|
|
807
|
+
* ShadowRoot hosted in an iframe) are invisible to it - see resolveCrossWindowOrigin
|
|
808
|
+
* for the same cross-window scenario affecting positioning instead. Any pointer event
|
|
809
|
+
* dispatched in that foreign document is guaranteed to be outside the overlay panel,
|
|
810
|
+
* since the panel always renders in this window's document, so this only needs the
|
|
811
|
+
* toggle-button exclusion handleOutsideClick already does, not the content-panel
|
|
812
|
+
* containment check (which can never be true across documents).
|
|
813
|
+
*
|
|
814
|
+
* Listens on capture-phase click/auxclick, mirroring CDK's own OverlayOutsideClickDispatcher.
|
|
815
|
+
* Capture phase matters: for a toggle re-click, this listener must run and
|
|
816
|
+
* stopPropagation() BEFORE the trigger's own bubble-phase click handler
|
|
817
|
+
* (ClrDropdownTrigger's toggleWithEvent) gets a chance to fire, otherwise the popover
|
|
818
|
+
* closes here and immediately reopens there. A bubble-phase or pointerdown-based
|
|
819
|
+
* listener can't suppress that handler at all, since pointerdown and click are separate
|
|
820
|
+
* events and stopping one doesn't affect the other.
|
|
821
|
+
*
|
|
822
|
+
* The foreign document's own addEventListener isn't zone-patched (zone.js only patches
|
|
823
|
+
* the window it's loaded into), so the listener is registered directly and the handler
|
|
824
|
+
* is explicitly run back inside NgZone.
|
|
825
|
+
*/
|
|
826
|
+
createCrossWindowOutsideClickSubscription() {
|
|
827
|
+
// Same-window origins bail out here, so a popover that never crosses a window
|
|
828
|
+
// boundary registers no extra listeners at all.
|
|
829
|
+
const crossWindowOrigin = getCrossWindowOriginContext(this.popoverService.origin);
|
|
830
|
+
if (!crossWindowOrigin) {
|
|
831
|
+
return null;
|
|
832
|
+
}
|
|
833
|
+
const originWindow = crossWindowOrigin.elementWindow;
|
|
834
|
+
return this.zone.runOutsideAngular(() => merge(fromEvent(originWindow.document, 'click', { capture: true }), fromEvent(originWindow.document, 'auxclick', { capture: true })).subscribe(event => this.zone.run(() => this.handleOutsideClick(event))));
|
|
835
|
+
}
|
|
836
|
+
/**
|
|
837
|
+
* CDK's OverlayKeyboardDispatcher (the source behind `overlayRef.keydownEvents()`)
|
|
838
|
+
* attaches a single keydown listener on this window's document.body once, at app
|
|
839
|
+
* bootstrap - see its `add()`/`_keydownListener`. A keydown fired while focus sits
|
|
840
|
+
* inside a cross-window origin's own document (e.g. a ShadowRoot hosted in an iframe)
|
|
841
|
+
* never bubbles there, since events don't cross document boundaries, so Escape silently
|
|
842
|
+
* does nothing once the trigger (and focus) has moved into that foreign document. This
|
|
843
|
+
* mirrors createCrossWindowOutsideClickSubscription's rationale for outside clicks -
|
|
844
|
+
* merged into a single stream/handler here since both ultimately just detect Escape and
|
|
845
|
+
* close.
|
|
846
|
+
*/
|
|
847
|
+
createEscapeSubscription() {
|
|
848
|
+
const crossWindowOrigin = getCrossWindowOriginContext(this.popoverService.origin);
|
|
849
|
+
const keydownEvents = crossWindowOrigin
|
|
850
|
+
? merge(this.overlayRef.keydownEvents(), this.zone.runOutsideAngular(() => fromEvent(crossWindowOrigin.elementWindow.document, 'keydown')))
|
|
851
|
+
: this.overlayRef.keydownEvents();
|
|
852
|
+
return keydownEvents.subscribe(event => {
|
|
853
|
+
if (isEscapeKey(event)) {
|
|
854
|
+
this.zone.run(() => {
|
|
855
|
+
event.preventDefault();
|
|
856
|
+
this.closePopover();
|
|
857
|
+
});
|
|
600
858
|
}
|
|
601
859
|
});
|
|
602
860
|
}
|
|
@@ -676,19 +934,47 @@ class ClrPopoverContent {
|
|
|
676
934
|
this.intersectionObserver = null;
|
|
677
935
|
this.popoverService.popoverVisibleEmit(false);
|
|
678
936
|
}
|
|
937
|
+
/**
|
|
938
|
+
* Walks ancestors looking for scrollable containers, following the node up through
|
|
939
|
+
* ShadowRoots and, when the node's own ancestry crosses into an iframe (a different
|
|
940
|
+
* window realm - e.g. a ShadowRoot hosted in a micro-frontend iframe), continuing the
|
|
941
|
+
* walk into the parent window via window.frameElement, all the way up to the true
|
|
942
|
+
* top-level document. Every embedded document passed through this way is added as its
|
|
943
|
+
* own scrollable parent, so page-level scrolling inside an iframe is tracked too.
|
|
944
|
+
*
|
|
945
|
+
* `instanceof` checks are realm-sensitive (they fail for nodes created by a different
|
|
946
|
+
* window's constructors), so this deliberately uses realm-independent shape checks
|
|
947
|
+
* (nodeType, tagName, duck-typing ShadowRoot via `host`) instead, and always resolves
|
|
948
|
+
* computed style through the node's own window rather than this one. This is what lets
|
|
949
|
+
* the walk safely continue across a same-origin realm boundary instead of just
|
|
950
|
+
* stopping at it.
|
|
951
|
+
*/
|
|
679
952
|
getScrollableParents(node) {
|
|
680
|
-
let parent = node;
|
|
681
953
|
const overflowScrollKeys = ['auto', 'scroll', 'clip'];
|
|
682
954
|
const scrollableParents = [window.document];
|
|
683
|
-
|
|
684
|
-
|
|
955
|
+
let parent = node;
|
|
956
|
+
while (parent && isElementOrShadowRoot(parent)) {
|
|
957
|
+
if (isShadowRoot(parent)) {
|
|
685
958
|
parent = parent.host;
|
|
959
|
+
continue;
|
|
686
960
|
}
|
|
687
|
-
const
|
|
961
|
+
const element = parent;
|
|
962
|
+
const elementWindow = element.ownerDocument?.defaultView;
|
|
963
|
+
if (isHtmlElement(element)) {
|
|
964
|
+
const frameElement = elementWindow && elementWindow !== window ? getFrameElement(elementWindow) : null;
|
|
965
|
+
if (!frameElement) {
|
|
966
|
+
break;
|
|
967
|
+
}
|
|
968
|
+
// Leaving this document for its parent window - track its own page-level scroll too.
|
|
969
|
+
scrollableParents.push(element.ownerDocument);
|
|
970
|
+
parent = frameElement;
|
|
971
|
+
continue;
|
|
972
|
+
}
|
|
973
|
+
const { overflowY, overflowX } = (elementWindow ?? window).getComputedStyle(element);
|
|
688
974
|
if (overflowScrollKeys.includes(overflowY) || overflowScrollKeys.includes(overflowX)) {
|
|
689
|
-
scrollableParents.push(
|
|
975
|
+
scrollableParents.push(element);
|
|
690
976
|
}
|
|
691
|
-
parent =
|
|
977
|
+
parent = element.parentNode;
|
|
692
978
|
}
|
|
693
979
|
return scrollableParents;
|
|
694
980
|
}
|
|
@@ -780,6 +1066,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
780
1066
|
type: Input,
|
|
781
1067
|
args: ['clrPopoverContentOrigin']
|
|
782
1068
|
}] } });
|
|
1069
|
+
function isEscapeKey(event) {
|
|
1070
|
+
return !!event && event.key === Keys.Escape && !hasModifierKey(event);
|
|
1071
|
+
}
|
|
783
1072
|
|
|
784
1073
|
/*
|
|
785
1074
|
* Copyright (c) 2016-2026 Broadcom. All Rights Reserved.
|