ng-primitives 0.114.1 → 0.115.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/fesm2022/ng-primitives-portal.mjs +83 -15
- package/fesm2022/ng-primitives-portal.mjs.map +1 -1
- package/fesm2022/ng-primitives-tooltip.mjs +192 -13
- package/fesm2022/ng-primitives-tooltip.mjs.map +1 -1
- package/package.json +1 -1
- package/portal/index.d.ts +10 -1
- package/schematics/ng-generate/templates/tooltip/tooltip-trigger.__fileSuffix@dasherize__.ts.template +1 -0
- package/tooltip/index.d.ts +63 -3
|
@@ -106,10 +106,13 @@ class NgpOverlayCooldownManager {
|
|
|
106
106
|
*/
|
|
107
107
|
registerActive(overlayType, overlay, cooldown) {
|
|
108
108
|
const existing = this.activeOverlays.get(overlayType);
|
|
109
|
-
// If there's an existing overlay
|
|
110
|
-
// This ensures
|
|
111
|
-
if (existing && existing !== overlay
|
|
112
|
-
|
|
109
|
+
// If there's an existing overlay of the same type, close it immediately.
|
|
110
|
+
// This ensures only one overlay of each type is open at a time.
|
|
111
|
+
if (existing && existing !== overlay) {
|
|
112
|
+
// Enable instant transition only if cooldown is active
|
|
113
|
+
if (cooldown > 0) {
|
|
114
|
+
existing.instantTransition?.set(true);
|
|
115
|
+
}
|
|
113
116
|
existing.hideImmediate();
|
|
114
117
|
}
|
|
115
118
|
this.activeOverlays.set(overlayType, overlay);
|
|
@@ -308,11 +311,14 @@ class NgpOverlayRegistry {
|
|
|
308
311
|
this.pointerDownTarget = null;
|
|
309
312
|
}
|
|
310
313
|
/**
|
|
311
|
-
* Outside-click dismiss:
|
|
314
|
+
* Outside-click dismiss: checks all overlays (not just the topmost) and
|
|
315
|
+
* dismisses every overlay tree where the click landed outside.
|
|
316
|
+
*
|
|
317
|
+
* This handles the case where a non-dismissable overlay (e.g. a tooltip) is
|
|
318
|
+
* topmost but other overlays (e.g. a popover) should still be dismissed.
|
|
312
319
|
*/
|
|
313
320
|
handleOutsideClick(event) {
|
|
314
|
-
|
|
315
|
-
if (!topmost || this.pendingGuardIds.has(topmost.id)) {
|
|
321
|
+
if (this.entries.length === 0) {
|
|
316
322
|
return;
|
|
317
323
|
}
|
|
318
324
|
// Ignore scrollbar clicks — the click lands outside the viewport's client area.
|
|
@@ -322,18 +328,80 @@ class NgpOverlayRegistry {
|
|
|
322
328
|
(event.clientX >= clientWidth || event.clientY >= clientHeight)) {
|
|
323
329
|
return;
|
|
324
330
|
}
|
|
325
|
-
// Check if the click is inside the topmost overlay
|
|
326
331
|
const path = event.composedPath();
|
|
327
|
-
|
|
328
|
-
const
|
|
329
|
-
const
|
|
330
|
-
|
|
331
|
-
|
|
332
|
+
// Step 1: Build a set of overlay IDs where the click is "inside"
|
|
333
|
+
const insideIds = new Set();
|
|
334
|
+
for (const entry of this.entries) {
|
|
335
|
+
if (!this.isClickOutsideEntry(entry, path)) {
|
|
336
|
+
insideIds.add(entry.id);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
// Step 2: Expand insideIds to include all ancestors of inside entries.
|
|
340
|
+
// If a child contains the click, its entire parent chain is protected.
|
|
341
|
+
for (const id of insideIds) {
|
|
342
|
+
let currentId = this.entries.find(e => e.id === id)?.parentId ?? null;
|
|
343
|
+
while (currentId !== null) {
|
|
344
|
+
if (insideIds.has(currentId))
|
|
345
|
+
break; // already protected
|
|
346
|
+
insideIds.add(currentId);
|
|
347
|
+
currentId = this.entries.find(e => e.id === currentId)?.parentId ?? null;
|
|
348
|
+
}
|
|
332
349
|
}
|
|
333
|
-
//
|
|
350
|
+
// Step 3: For each entry that is outside, find the highest dismissable ancestor
|
|
351
|
+
// and collect unique roots to dismiss.
|
|
352
|
+
const toDismiss = new Map();
|
|
353
|
+
const coveredIds = new Set(); // entries covered by an ancestor we'll dismiss
|
|
354
|
+
// Walk from topmost to oldest
|
|
355
|
+
for (let i = this.entries.length - 1; i >= 0; i--) {
|
|
356
|
+
const entry = this.entries[i];
|
|
357
|
+
// Skip entries where the click was inside
|
|
358
|
+
if (insideIds.has(entry.id))
|
|
359
|
+
continue;
|
|
360
|
+
// Skip entries with pending guards
|
|
361
|
+
if (this.pendingGuardIds.has(entry.id))
|
|
362
|
+
continue;
|
|
363
|
+
// Skip entries already covered by an ancestor we're about to dismiss
|
|
364
|
+
if (coveredIds.has(entry.id))
|
|
365
|
+
continue;
|
|
366
|
+
// Walk up parent chain to find the highest dismissable ancestor
|
|
367
|
+
let highest = entry;
|
|
368
|
+
let currentId = entry.parentId;
|
|
369
|
+
while (currentId !== null) {
|
|
370
|
+
const parent = this.entries.find(e => e.id === currentId);
|
|
371
|
+
if (!parent)
|
|
372
|
+
break;
|
|
373
|
+
if (insideIds.has(parent.id))
|
|
374
|
+
break;
|
|
375
|
+
if (parent.dismissPolicy.outsidePress === false)
|
|
376
|
+
break;
|
|
377
|
+
if (this.pendingGuardIds.has(parent.id))
|
|
378
|
+
break;
|
|
379
|
+
highest = parent;
|
|
380
|
+
currentId = parent.parentId;
|
|
381
|
+
}
|
|
382
|
+
toDismiss.set(highest.id, highest);
|
|
383
|
+
// Mark all descendants as covered
|
|
384
|
+
for (const desc of this.getDescendants(highest.id)) {
|
|
385
|
+
coveredIds.add(desc.id);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
// Derive a proper Element from the composed path for guard evaluation
|
|
334
389
|
const target = path.find((node) => node instanceof Element) ??
|
|
335
390
|
this.document.documentElement;
|
|
336
|
-
|
|
391
|
+
// Step 4: Dismiss each root
|
|
392
|
+
for (const [id, entry] of toDismiss) {
|
|
393
|
+
this.evaluateGuardAndDismiss(id, entry.dismissPolicy.outsidePress, target, () => this.ngZone.run(() => entry.overlay.hide()));
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Check if a click (represented by its composedPath) is outside an entry's
|
|
398
|
+
* elements, trigger, and anchor.
|
|
399
|
+
*/
|
|
400
|
+
isClickOutsideEntry(entry, path) {
|
|
401
|
+
const isInsideElements = entry.getElements().some(el => path.includes(el));
|
|
402
|
+
const isInsideTrigger = path.includes(entry.triggerElement);
|
|
403
|
+
const isInsideAnchor = entry.anchorElement ? path.includes(entry.anchorElement) : false;
|
|
404
|
+
return !(isInsideElements || isInsideTrigger || isInsideAnchor);
|
|
337
405
|
}
|
|
338
406
|
/**
|
|
339
407
|
* Escape-key dismiss: only the topmost overlay responds.
|