ng-primitives 0.112.3 → 0.114.0
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/dialog/index.d.ts +85 -28
- package/fesm2022/ng-primitives-dialog.mjs +164 -78
- package/fesm2022/ng-primitives-dialog.mjs.map +1 -1
- package/fesm2022/ng-primitives-portal.mjs +340 -62
- package/fesm2022/ng-primitives-portal.mjs.map +1 -1
- package/package.json +1 -1
- package/portal/index.d.ts +159 -4
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
import { coerceNumberProperty, coerceCssPixelValue } from '@angular/cdk/coercion';
|
|
2
2
|
import { isNil, isObject, isFunction, injectDisposables, uniqueId, safeTakeUntilDestroyed } from 'ng-primitives/utils';
|
|
3
3
|
import { FocusMonitor } from '@angular/cdk/a11y';
|
|
4
|
-
import { ViewportRuler } from '@angular/cdk/
|
|
4
|
+
import { ViewportRuler } from '@angular/cdk/scrolling';
|
|
5
5
|
import { DOCUMENT } from '@angular/common';
|
|
6
6
|
import * as i0 from '@angular/core';
|
|
7
|
-
import { Injectable,
|
|
7
|
+
import { Injectable, inject, NgZone, InjectionToken, ApplicationRef, TemplateRef, Injector, DestroyRef, signal, computed, runInInjectionContext } from '@angular/core';
|
|
8
8
|
import { ControlContainer } from '@angular/forms';
|
|
9
9
|
import { getOverflowAncestors, autoUpdate, offset, flip, shift, size, arrow, computePosition } from '@floating-ui/dom';
|
|
10
10
|
import { setupExitAnimation, explicitEffect, fromResizeEvent, injectElementRef } from 'ng-primitives/internal';
|
|
11
|
-
import { Subject
|
|
12
|
-
import { debounceTime } from 'rxjs/operators';
|
|
11
|
+
import { Subject } from 'rxjs';
|
|
13
12
|
import { VERSION } from '@angular/cdk';
|
|
14
13
|
import { ComponentPortal, DomPortalOutlet, TemplatePortal } from '@angular/cdk/portal';
|
|
15
14
|
import { createPrimitive, controlled, onDestroy, styleBinding, dataBinding } from 'ng-primitives/state';
|
|
@@ -141,6 +140,316 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImpor
|
|
|
141
140
|
args: [{ providedIn: 'root' }]
|
|
142
141
|
}] });
|
|
143
142
|
|
|
143
|
+
/**
|
|
144
|
+
* Singleton registry that tracks all visible overlays in open order and
|
|
145
|
+
* provides centralized dismiss routing (outside-click and escape-key).
|
|
146
|
+
*
|
|
147
|
+
* Document listeners are attached when the first overlay registers and
|
|
148
|
+
* detached when the last deregisters, avoiding permanent global listeners.
|
|
149
|
+
* @internal
|
|
150
|
+
*/
|
|
151
|
+
class NgpOverlayRegistry {
|
|
152
|
+
constructor() {
|
|
153
|
+
this.document = inject(DOCUMENT);
|
|
154
|
+
this.ngZone = inject(NgZone);
|
|
155
|
+
/** Ordered list of visible overlay entries (oldest first, newest/topmost last) */
|
|
156
|
+
this.entries = [];
|
|
157
|
+
/** Set of overlay IDs currently evaluating an async dismiss guard */
|
|
158
|
+
this.pendingGuardIds = new Set();
|
|
159
|
+
/** Tracks the pointerdown target for CDK-compatible outside pointer event dispatch */
|
|
160
|
+
this.pointerDownTarget = null;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Register an overlay as visible. The entry is appended to the end of the list
|
|
164
|
+
* (making it the topmost overlay). Attaches document listeners if this is the
|
|
165
|
+
* first entry.
|
|
166
|
+
*/
|
|
167
|
+
register(entry) {
|
|
168
|
+
// Avoid double-registration
|
|
169
|
+
if (this.entries.some(e => e.id === entry.id)) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
this.entries.push(entry);
|
|
173
|
+
// Attach listeners when the first overlay registers
|
|
174
|
+
if (this.entries.length === 1) {
|
|
175
|
+
this.attachListeners();
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Remove an overlay from the registry.
|
|
180
|
+
* Detaches document listeners if no entries remain.
|
|
181
|
+
*/
|
|
182
|
+
deregister(id) {
|
|
183
|
+
const index = this.entries.findIndex(e => e.id === id);
|
|
184
|
+
if (index !== -1) {
|
|
185
|
+
this.entries.splice(index, 1);
|
|
186
|
+
this.pendingGuardIds.delete(id);
|
|
187
|
+
}
|
|
188
|
+
// Detach listeners when the last overlay deregisters
|
|
189
|
+
if (this.entries.length === 0) {
|
|
190
|
+
this.detachListeners();
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Close all descendants of the given overlay.
|
|
195
|
+
* Called by NgpOverlay when it hides to cascade the close to children.
|
|
196
|
+
*/
|
|
197
|
+
closeDescendants(id) {
|
|
198
|
+
const descendants = this.getDescendants(id);
|
|
199
|
+
// Close deepest first to avoid re-entrant issues
|
|
200
|
+
for (let i = descendants.length - 1; i >= 0; i--) {
|
|
201
|
+
descendants[i].overlay.hideImmediate();
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Get all registered entries.
|
|
206
|
+
*/
|
|
207
|
+
getEntries() {
|
|
208
|
+
return this.entries;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Get the topmost (most recently opened) overlay entry, or null if none.
|
|
212
|
+
*/
|
|
213
|
+
getTopmost() {
|
|
214
|
+
return this.entries.length > 0 ? this.entries[this.entries.length - 1] : null;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Find the overlay whose elements contain the given DOM element.
|
|
218
|
+
* Returns the ID of the most recently opened (topmost) containing overlay, or null.
|
|
219
|
+
*/
|
|
220
|
+
findContainingOverlay(element) {
|
|
221
|
+
// Walk from topmost to oldest so we find the nearest containing overlay
|
|
222
|
+
for (let i = this.entries.length - 1; i >= 0; i--) {
|
|
223
|
+
const entry = this.entries[i];
|
|
224
|
+
const elements = entry.getElements();
|
|
225
|
+
if (elements.some(el => el.contains(element))) {
|
|
226
|
+
return entry.id;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Check whether overlay `ancestorId` is an ancestor of overlay `descendantId`
|
|
233
|
+
* by walking the parentId chain.
|
|
234
|
+
*/
|
|
235
|
+
isAncestorOf(ancestorId, descendantId) {
|
|
236
|
+
let currentId = descendantId;
|
|
237
|
+
while (currentId !== null) {
|
|
238
|
+
const entry = this.entries.find(e => e.id === currentId);
|
|
239
|
+
if (!entry) {
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
if (entry.parentId === ancestorId) {
|
|
243
|
+
return true;
|
|
244
|
+
}
|
|
245
|
+
currentId = entry.parentId;
|
|
246
|
+
}
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Get all descendants of the given overlay (children, grandchildren, etc.)
|
|
251
|
+
* by walking parentId chains of all entries.
|
|
252
|
+
*/
|
|
253
|
+
getDescendants(id) {
|
|
254
|
+
const descendants = [];
|
|
255
|
+
const ancestorIds = new Set([id]);
|
|
256
|
+
// Walk the list and collect entries whose parentId is in the ancestor set.
|
|
257
|
+
// Because entries are ordered by open time, a child always appears after its parent.
|
|
258
|
+
for (const entry of this.entries) {
|
|
259
|
+
if (entry.parentId !== null && ancestorIds.has(entry.parentId)) {
|
|
260
|
+
descendants.push(entry);
|
|
261
|
+
ancestorIds.add(entry.id);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return descendants;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Attach centralized document listeners for outside-click and escape-key.
|
|
268
|
+
* Runs outside Angular zone to avoid unnecessary change detection on every
|
|
269
|
+
* mouse/keyboard event.
|
|
270
|
+
*/
|
|
271
|
+
attachListeners() {
|
|
272
|
+
this.ngZone.runOutsideAngular(() => {
|
|
273
|
+
const onMouseUp = (event) => this.handleOutsideClick(event);
|
|
274
|
+
const onKeyDown = (event) => this.handleEscapeKey(event);
|
|
275
|
+
this.document.addEventListener('mouseup', onMouseUp, true);
|
|
276
|
+
this.document.addEventListener('keydown', onKeyDown, true);
|
|
277
|
+
this.removeOutsideClickListener = () => this.document.removeEventListener('mouseup', onMouseUp, true);
|
|
278
|
+
this.removeEscapeKeyListener = () => this.document.removeEventListener('keydown', onKeyDown, true);
|
|
279
|
+
// CDK-compatible outside pointer event dispatch:
|
|
280
|
+
// Track pointerdown origin, then emit click/auxclick/contextmenu events
|
|
281
|
+
// to entries that have an outsidePointerEvents$ subject.
|
|
282
|
+
const onPointerDown = (event) => {
|
|
283
|
+
this.pointerDownTarget = this.getComposedTarget(event);
|
|
284
|
+
};
|
|
285
|
+
const onPointerEvent = (event) => this.handleOutsidePointerEvent(event);
|
|
286
|
+
this.document.addEventListener('pointerdown', onPointerDown, true);
|
|
287
|
+
this.document.addEventListener('click', onPointerEvent, true);
|
|
288
|
+
this.document.addEventListener('auxclick', onPointerEvent, true);
|
|
289
|
+
this.document.addEventListener('contextmenu', onPointerEvent, true);
|
|
290
|
+
this.removeOutsidePointerListeners = () => {
|
|
291
|
+
this.document.removeEventListener('pointerdown', onPointerDown, true);
|
|
292
|
+
this.document.removeEventListener('click', onPointerEvent, true);
|
|
293
|
+
this.document.removeEventListener('auxclick', onPointerEvent, true);
|
|
294
|
+
this.document.removeEventListener('contextmenu', onPointerEvent, true);
|
|
295
|
+
};
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Detach centralized document listeners.
|
|
300
|
+
*/
|
|
301
|
+
detachListeners() {
|
|
302
|
+
this.removeOutsideClickListener?.();
|
|
303
|
+
this.removeOutsideClickListener = undefined;
|
|
304
|
+
this.removeEscapeKeyListener?.();
|
|
305
|
+
this.removeEscapeKeyListener = undefined;
|
|
306
|
+
this.removeOutsidePointerListeners?.();
|
|
307
|
+
this.removeOutsidePointerListeners = undefined;
|
|
308
|
+
this.pointerDownTarget = null;
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Outside-click dismiss: only the topmost overlay responds.
|
|
312
|
+
*/
|
|
313
|
+
handleOutsideClick(event) {
|
|
314
|
+
const topmost = this.getTopmost();
|
|
315
|
+
if (!topmost || this.pendingGuardIds.has(topmost.id)) {
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
// Ignore scrollbar clicks — the click lands outside the viewport's client area.
|
|
319
|
+
const { clientWidth, clientHeight } = this.document.documentElement;
|
|
320
|
+
if (clientWidth > 0 &&
|
|
321
|
+
clientHeight > 0 &&
|
|
322
|
+
(event.clientX >= clientWidth || event.clientY >= clientHeight)) {
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
// Check if the click is inside the topmost overlay
|
|
326
|
+
const path = event.composedPath();
|
|
327
|
+
const isInsideElements = topmost.getElements().some(el => path.includes(el));
|
|
328
|
+
const isInsideTrigger = path.includes(topmost.triggerElement);
|
|
329
|
+
const isInsideAnchor = topmost.anchorElement ? path.includes(topmost.anchorElement) : false;
|
|
330
|
+
if (isInsideElements || isInsideTrigger || isInsideAnchor) {
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
// Derive a proper Element from the composed path
|
|
334
|
+
const target = path.find((node) => node instanceof Element) ??
|
|
335
|
+
this.document.documentElement;
|
|
336
|
+
this.evaluateGuardAndDismiss(topmost.id, topmost.dismissPolicy.outsidePress, target, () => this.ngZone.run(() => topmost.overlay.hide()));
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Escape-key dismiss: only the topmost overlay responds.
|
|
340
|
+
*/
|
|
341
|
+
handleEscapeKey(event) {
|
|
342
|
+
if (event.key !== 'Escape' || event.isComposing) {
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
const topmost = this.getTopmost();
|
|
346
|
+
if (!topmost || this.pendingGuardIds.has(topmost.id)) {
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
this.evaluateGuardAndDismiss(topmost.id, topmost.dismissPolicy.escapeKey, event, () => this.ngZone.run(() => topmost.overlay.hide({ origin: 'keyboard', immediate: true })));
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* CDK-compatible outside pointer event dispatch.
|
|
353
|
+
* Iterates overlays from topmost to oldest (like CDK's OverlayOutsideClickDispatcher).
|
|
354
|
+
* For each overlay with an outsidePointerEvents$ subject, emits the event if the
|
|
355
|
+
* click was outside its elements. Stops iterating when an overlay contains the click.
|
|
356
|
+
*/
|
|
357
|
+
handleOutsidePointerEvent(event) {
|
|
358
|
+
const target = this.getComposedTarget(event);
|
|
359
|
+
// For click events, use the pointerdown origin to handle drag scenarios
|
|
360
|
+
const origin = event.type === 'click' && this.pointerDownTarget ? this.pointerDownTarget : target;
|
|
361
|
+
this.pointerDownTarget = null;
|
|
362
|
+
// Iterate from topmost to oldest (like CDK)
|
|
363
|
+
const overlays = this.entries.slice();
|
|
364
|
+
for (let i = overlays.length - 1; i >= 0; i--) {
|
|
365
|
+
const entry = overlays[i];
|
|
366
|
+
if (!entry.outsidePointerEvents$?.observers.length) {
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
const elements = entry.getElements();
|
|
370
|
+
if (this.containsElement(elements, target) || this.containsElement(elements, origin)) {
|
|
371
|
+
break;
|
|
372
|
+
}
|
|
373
|
+
this.ngZone.run(() => entry.outsidePointerEvents$.next(event));
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Get the composed event target, piercing shadow DOM boundaries.
|
|
378
|
+
*/
|
|
379
|
+
getComposedTarget(event) {
|
|
380
|
+
return event.composedPath?.()?.[0] ?? event.target;
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Check whether any of the given elements contain the target, piercing shadow DOM.
|
|
384
|
+
*/
|
|
385
|
+
containsElement(elements, target) {
|
|
386
|
+
if (!target || !(target instanceof Node)) {
|
|
387
|
+
return false;
|
|
388
|
+
}
|
|
389
|
+
return elements.some(el => {
|
|
390
|
+
let current = target;
|
|
391
|
+
while (current) {
|
|
392
|
+
if (current === el)
|
|
393
|
+
return true;
|
|
394
|
+
current =
|
|
395
|
+
typeof ShadowRoot !== 'undefined' && current instanceof ShadowRoot
|
|
396
|
+
? current.host
|
|
397
|
+
: current.parentNode;
|
|
398
|
+
}
|
|
399
|
+
return false;
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Evaluate a dismiss guard and call the dismiss function if allowed.
|
|
404
|
+
* Supports boolean values and sync/async guard functions.
|
|
405
|
+
*/
|
|
406
|
+
evaluateGuardAndDismiss(overlayId, guard, target, dismiss) {
|
|
407
|
+
if (guard === true || guard === undefined) {
|
|
408
|
+
dismiss();
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
if (guard === false) {
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
// Function guard → evaluate
|
|
415
|
+
let result;
|
|
416
|
+
try {
|
|
417
|
+
result = guard(target);
|
|
418
|
+
}
|
|
419
|
+
catch (error) {
|
|
420
|
+
console.error('NgpOverlayRegistry: dismiss guard threw', error);
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
if (typeof result === 'boolean') {
|
|
424
|
+
if (result) {
|
|
425
|
+
dismiss();
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
else {
|
|
429
|
+
// Promise — track as pending to prevent re-triggering
|
|
430
|
+
this.pendingGuardIds.add(overlayId);
|
|
431
|
+
result
|
|
432
|
+
.then(shouldDismiss => {
|
|
433
|
+
if (shouldDismiss) {
|
|
434
|
+
dismiss();
|
|
435
|
+
}
|
|
436
|
+
})
|
|
437
|
+
.catch(error => {
|
|
438
|
+
console.error('NgpOverlayRegistry: dismiss guard rejected', error);
|
|
439
|
+
})
|
|
440
|
+
.finally(() => {
|
|
441
|
+
this.pendingGuardIds.delete(overlayId);
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: NgpOverlayRegistry, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
446
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: NgpOverlayRegistry, providedIn: 'root' }); }
|
|
447
|
+
}
|
|
448
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: NgpOverlayRegistry, decorators: [{
|
|
449
|
+
type: Injectable,
|
|
450
|
+
args: [{ providedIn: 'root' }]
|
|
451
|
+
}] });
|
|
452
|
+
|
|
144
453
|
const NgpOverlayContextToken = new InjectionToken('NgpOverlayContextToken');
|
|
145
454
|
/**
|
|
146
455
|
* Injects the context for the overlay.
|
|
@@ -354,8 +663,8 @@ function createPortal(componentOrTemplate, viewContainerRef, injector, context)
|
|
|
354
663
|
}
|
|
355
664
|
|
|
356
665
|
/**
|
|
357
|
-
*
|
|
358
|
-
*
|
|
666
|
+
* Originally based on Angular CDK's scroll strategy.
|
|
667
|
+
* Modified to be fully standalone with no CDK overlay dependency.
|
|
359
668
|
*/
|
|
360
669
|
/** Cached result of the check that indicates whether the browser supports scroll behaviors. */
|
|
361
670
|
let scrollBehaviorSupported;
|
|
@@ -447,22 +756,21 @@ class BlockScrollStrategy {
|
|
|
447
756
|
// Note that we don't mutate the property if the browser doesn't support `scroll-behavior`,
|
|
448
757
|
// because it can throw off feature detections in `supportsScrollBehavior` which
|
|
449
758
|
// checks for `'scrollBehavior' in documentElement.style`.
|
|
450
|
-
if (
|
|
759
|
+
if (supportsScrollBehavior()) {
|
|
451
760
|
htmlStyle.scrollBehavior = bodyStyle.scrollBehavior = 'auto';
|
|
452
761
|
}
|
|
453
762
|
window.scroll(this.previousScrollPosition.left, this.previousScrollPosition.top);
|
|
454
|
-
if (
|
|
763
|
+
if (supportsScrollBehavior()) {
|
|
455
764
|
htmlStyle.scrollBehavior = previousHtmlScrollBehavior;
|
|
456
765
|
bodyStyle.scrollBehavior = previousBodyScrollBehavior;
|
|
457
766
|
}
|
|
458
767
|
}
|
|
459
768
|
}
|
|
460
769
|
canBeEnabled() {
|
|
461
|
-
// Since the scroll strategies can't be singletons, we have to use a global CSS class
|
|
462
|
-
// (`cdk-global-scrollblock`) to make sure that we don't try to disable global
|
|
463
|
-
// scrolling multiple times.
|
|
464
770
|
const html = this.document.documentElement;
|
|
465
|
-
if (html.classList.contains('cdk-global-scrollblock') ||
|
|
771
|
+
if (html.classList.contains('cdk-global-scrollblock') ||
|
|
772
|
+
html.hasAttribute('data-scrollblock') ||
|
|
773
|
+
this.isEnabled) {
|
|
466
774
|
return false;
|
|
467
775
|
}
|
|
468
776
|
const viewport = this.viewportRuler.getViewportSize();
|
|
@@ -593,10 +901,11 @@ class NgpOverlay {
|
|
|
593
901
|
this.config = config;
|
|
594
902
|
this.disposables = injectDisposables();
|
|
595
903
|
this.document = inject(DOCUMENT);
|
|
596
|
-
this.destroyRef = inject(DestroyRef);
|
|
597
904
|
this.viewportRuler = inject(ViewportRuler);
|
|
905
|
+
this.destroyRef = inject(DestroyRef);
|
|
598
906
|
this.focusMonitor = inject(FocusMonitor);
|
|
599
907
|
this.cooldownManager = inject(NgpOverlayCooldownManager);
|
|
908
|
+
this.registry = inject(NgpOverlayRegistry);
|
|
600
909
|
/** Access any parent overlays */
|
|
601
910
|
this.parentOverlay = inject(NgpOverlay, { optional: true });
|
|
602
911
|
/** Track child overlays for outside click detection */
|
|
@@ -680,56 +989,8 @@ class NgpOverlay {
|
|
|
680
989
|
this.hideImmediate();
|
|
681
990
|
}
|
|
682
991
|
});
|
|
683
|
-
// Register with parent overlay for outside click detection
|
|
684
|
-
if (this.parentOverlay) {
|
|
685
|
-
this.parentOverlay.registerChildOverlay(this);
|
|
686
|
-
}
|
|
687
|
-
// if there is a parent overlay and it is closed, close this overlay
|
|
688
|
-
this.parentOverlay?.closing
|
|
689
|
-
// we add a debounce here to ensure any dom events like clicks are processed first
|
|
690
|
-
.pipe(debounceTime(0), safeTakeUntilDestroyed(this.destroyRef))
|
|
691
|
-
.subscribe(() => {
|
|
692
|
-
if (this.isOpen()) {
|
|
693
|
-
this.hideImmediate();
|
|
694
|
-
}
|
|
695
|
-
});
|
|
696
|
-
// If closeOnOutsideClick is enabled, set up a click listener
|
|
697
|
-
fromEvent(this.document, 'mouseup', { capture: true })
|
|
698
|
-
.pipe(safeTakeUntilDestroyed(this.destroyRef))
|
|
699
|
-
.subscribe(event => {
|
|
700
|
-
if (!this.config.closeOnOutsideClick) {
|
|
701
|
-
return;
|
|
702
|
-
}
|
|
703
|
-
const overlay = this.portal();
|
|
704
|
-
if (!overlay || !this.isOpen()) {
|
|
705
|
-
return;
|
|
706
|
-
}
|
|
707
|
-
const path = event.composedPath();
|
|
708
|
-
const isInsideOverlay = overlay.getElements().some(el => path.includes(el));
|
|
709
|
-
const isInsideTrigger = path.includes(this.config.triggerElement);
|
|
710
|
-
const isInsideAnchor = this.config.anchorElement
|
|
711
|
-
? path.includes(this.config.anchorElement)
|
|
712
|
-
: false;
|
|
713
|
-
if (!isInsideOverlay &&
|
|
714
|
-
!isInsideTrigger &&
|
|
715
|
-
!isInsideAnchor &&
|
|
716
|
-
!this.isInsideChildOverlay(path)) {
|
|
717
|
-
this.hide();
|
|
718
|
-
}
|
|
719
|
-
});
|
|
720
|
-
// If closeOnEscape is enabled, set up a keydown listener
|
|
721
|
-
fromEvent(this.document, 'keydown', { capture: true })
|
|
722
|
-
.pipe(safeTakeUntilDestroyed(this.destroyRef))
|
|
723
|
-
.subscribe(event => {
|
|
724
|
-
if (!this.config.closeOnEscape)
|
|
725
|
-
return;
|
|
726
|
-
if (event.key === 'Escape' && this.isOpen()) {
|
|
727
|
-
this.hide({ origin: 'keyboard', immediate: true });
|
|
728
|
-
}
|
|
729
|
-
});
|
|
730
992
|
// Ensure cleanup on destroy
|
|
731
993
|
this.destroyRef.onDestroy(() => {
|
|
732
|
-
this.parentOverlay?.unregisterChildOverlay(this);
|
|
733
994
|
this.destroy();
|
|
734
995
|
});
|
|
735
996
|
}
|
|
@@ -1077,6 +1338,19 @@ class NgpOverlay {
|
|
|
1077
1338
|
this.setupPositioning(outletElement);
|
|
1078
1339
|
// Mark as open
|
|
1079
1340
|
this.isOpen.set(true);
|
|
1341
|
+
// Register with the overlay registry for centralized dismiss routing
|
|
1342
|
+
this.registry.register({
|
|
1343
|
+
id: this.id(),
|
|
1344
|
+
parentId: this.parentOverlay?.id() ?? null,
|
|
1345
|
+
overlay: this,
|
|
1346
|
+
getElements: () => this.getElements(),
|
|
1347
|
+
triggerElement: this.config.triggerElement,
|
|
1348
|
+
anchorElement: this.config.anchorElement,
|
|
1349
|
+
dismissPolicy: {
|
|
1350
|
+
outsidePress: this.config.closeOnOutsideClick ?? false,
|
|
1351
|
+
escapeKey: this.config.closeOnEscape ?? false,
|
|
1352
|
+
},
|
|
1353
|
+
});
|
|
1080
1354
|
// Register as active overlay for this type (skip when cooldown is bypassed)
|
|
1081
1355
|
if (this.config.overlayType && !skipCooldown) {
|
|
1082
1356
|
this.cooldownManager.registerActive(this.config.overlayType, this, this.config.cooldown ?? 0);
|
|
@@ -1197,6 +1471,10 @@ class NgpOverlay {
|
|
|
1197
1471
|
if (!portal) {
|
|
1198
1472
|
return;
|
|
1199
1473
|
}
|
|
1474
|
+
// Close any descendant overlays before destroying this one
|
|
1475
|
+
this.registry.closeDescendants(this.id());
|
|
1476
|
+
// Deregister from the overlay registry
|
|
1477
|
+
this.registry.deregister(this.id());
|
|
1200
1478
|
// Unregister from active overlays
|
|
1201
1479
|
if (this.config.overlayType) {
|
|
1202
1480
|
this.cooldownManager.unregisterActive(this.config.overlayType, this);
|
|
@@ -1374,5 +1652,5 @@ function coerceShift(value) {
|
|
|
1374
1652
|
* Generated bundle index. Do not edit.
|
|
1375
1653
|
*/
|
|
1376
1654
|
|
|
1377
|
-
export { BlockScrollStrategy, CloseScrollStrategy, NgpComponentPortal, NgpOverlay, NgpOverlayArrowStateToken, NgpOverlayCooldownManager, NgpPortal, NgpTemplatePortal, NoopScrollStrategy, coerceFlip, coerceOffset, coerceShift, createOverlay, createPortal, injectOverlay, injectOverlayArrowState, injectOverlayContext, ngpOverlayArrow, provideOverlayArrowState, provideOverlayContext };
|
|
1655
|
+
export { BlockScrollStrategy, CloseScrollStrategy, NgpComponentPortal, NgpOverlay, NgpOverlayArrowStateToken, NgpOverlayCooldownManager, NgpOverlayRegistry, NgpPortal, NgpTemplatePortal, NoopScrollStrategy, coerceFlip, coerceOffset, coerceShift, createOverlay, createPortal, injectOverlay, injectOverlayArrowState, injectOverlayContext, ngpOverlayArrow, provideOverlayArrowState, provideOverlayContext };
|
|
1378
1656
|
//# sourceMappingURL=ng-primitives-portal.mjs.map
|