ng-primitives 0.122.0 → 0.123.1

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.
@@ -93,6 +93,13 @@ function coerceOffset(value) {
93
93
  class NgpOverlayCooldownManager {
94
94
  constructor() {
95
95
  this.lastCloseTimestamps = new Map();
96
+ /**
97
+ * Active overlays per type, stored as a stack ordered oldest-first / topmost-last.
98
+ * Most types only ever hold a single overlay, but when an overlay is nested inside
99
+ * another of the same type (e.g. a popover whose trigger lives inside another
100
+ * popover) the child is pushed on top of its ancestor instead of evicting it.
101
+ * Closing the child then restores the ancestor as the active overlay.
102
+ */
96
103
  this.activeOverlays = new Map();
97
104
  }
98
105
  /**
@@ -117,23 +124,50 @@ class NgpOverlayCooldownManager {
117
124
  }
118
125
  /**
119
126
  * Register an overlay as active for its type.
120
- * Any existing overlay of the same type will be closed immediately.
127
+ *
128
+ * Any existing overlay of the same type is closed immediately so that only one
129
+ * overlay of each type is open at a time - *unless* it is an ancestor of the
130
+ * overlay being registered. A nested overlay (its trigger rendered inside an
131
+ * ancestor overlay's content) is stacked on top of its ancestor instead of
132
+ * evicting it, allowing legitimate nesting to coexist while sibling overlays
133
+ * still replace one another.
134
+ *
121
135
  * @param overlayType The type identifier for the overlay group
122
136
  * @param overlay The overlay instance
123
137
  * @param cooldown The cooldown duration - if > 0, enables instant transitions
124
138
  */
125
139
  registerActive(overlayType, overlay, cooldown) {
126
- const existing = this.activeOverlays.get(overlayType);
127
- // If there's an existing overlay of the same type, close it immediately.
128
- // This ensures only one overlay of each type is open at a time.
129
- if (existing && existing !== overlay) {
140
+ const stack = this.activeOverlays.get(overlayType) ?? [];
141
+ if (stack.includes(overlay)) {
142
+ return;
143
+ }
144
+ // Evict overlays from the top of the stack until we reach the overlay itself
145
+ // or one of its ancestors (or the stack empties). Ancestors are preserved so
146
+ // nested same-type overlays can coexist; peers are closed immediately.
147
+ while (stack.length > 0) {
148
+ const top = stack[stack.length - 1];
149
+ if (overlay.isDescendantOf?.(top)) {
150
+ break;
151
+ }
152
+ if (top.isDescendantOf?.(overlay)) {
153
+ const firstDescendantIndex = stack.findIndex(entry => entry.isDescendantOf?.(overlay));
154
+ stack.splice(firstDescendantIndex, 0, overlay);
155
+ this.activeOverlays.set(overlayType, stack);
156
+ return;
157
+ }
158
+ stack.pop();
130
159
  // Enable instant transition only if cooldown is active
131
160
  if (cooldown > 0) {
132
- existing.instantTransition?.set(true);
161
+ top.instantTransition?.set(true);
133
162
  }
134
- existing.hideImmediate();
163
+ top.hideImmediate();
135
164
  }
136
- this.activeOverlays.set(overlayType, overlay);
165
+ // Push as the topmost active overlay, avoiding a duplicate entry if it is
166
+ // already there (e.g. re-registering after a cancelled destruction).
167
+ if (stack[stack.length - 1] !== overlay) {
168
+ stack.push(overlay);
169
+ }
170
+ this.activeOverlays.set(overlayType, stack);
137
171
  }
138
172
  /**
139
173
  * Unregister an overlay when it closes.
@@ -141,7 +175,15 @@ class NgpOverlayCooldownManager {
141
175
  * @param overlay The overlay instance to remove
142
176
  */
143
177
  unregisterActive(overlayType, overlay) {
144
- if (this.activeOverlays.get(overlayType) === overlay) {
178
+ const stack = this.activeOverlays.get(overlayType);
179
+ if (!stack) {
180
+ return;
181
+ }
182
+ const index = stack.indexOf(overlay);
183
+ if (index !== -1) {
184
+ stack.splice(index, 1);
185
+ }
186
+ if (stack.length === 0) {
145
187
  this.activeOverlays.delete(overlayType);
146
188
  }
147
189
  }
@@ -151,7 +193,8 @@ class NgpOverlayCooldownManager {
151
193
  * @returns true if there's an active overlay, false otherwise
152
194
  */
153
195
  hasActiveOverlay(overlayType) {
154
- return this.activeOverlays.has(overlayType);
196
+ const stack = this.activeOverlays.get(overlayType);
197
+ return stack !== undefined && stack.length > 0;
155
198
  }
156
199
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: NgpOverlayCooldownManager, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
157
200
  static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: NgpOverlayCooldownManager, providedIn: 'root' }); }
@@ -1396,6 +1439,25 @@ class NgpOverlay {
1396
1439
  unregisterChildOverlay(child) {
1397
1440
  this.childOverlays.delete(child);
1398
1441
  }
1442
+ /**
1443
+ * Determine whether this overlay is a descendant of the given overlay - i.e.
1444
+ * its trigger is rendered within the other overlay's content. Walks the parent
1445
+ * overlay chain established through dependency injection.
1446
+ *
1447
+ * Used by the cooldown manager to avoid evicting an ancestor overlay when a
1448
+ * nested overlay of the same type is activated.
1449
+ * @internal
1450
+ */
1451
+ isDescendantOf(other) {
1452
+ let current = this.parentOverlay;
1453
+ while (current) {
1454
+ if (current === other) {
1455
+ return true;
1456
+ }
1457
+ current = current.parentOverlay;
1458
+ }
1459
+ return false;
1460
+ }
1399
1461
  /**
1400
1462
  * Check if the event path includes any child overlay elements (recursively).
1401
1463
  * @internal
@@ -1556,6 +1618,7 @@ class NgpOverlay {
1556
1618
  this.position.set({ x: position.x, y: position.y });
1557
1619
  // Update final placement signal
1558
1620
  this.finalPlacement.set(position.placement);
1621
+ this.transformOrigin.set(this.getTransformOrigin(position.placement));
1559
1622
  // Update arrow position if available
1560
1623
  if (this.arrowElement) {
1561
1624
  this.arrowPosition.set({
@@ -1630,8 +1693,7 @@ class NgpOverlay {
1630
1693
  /**
1631
1694
  * Get the transform origin for the overlay
1632
1695
  */
1633
- getTransformOrigin() {
1634
- const placement = this.config.placement?.() ?? 'top';
1696
+ getTransformOrigin(placement = this.config.placement?.() ?? 'top') {
1635
1697
  const basePlacement = placement.split('-')[0]; // Extract "top", "bottom", etc.
1636
1698
  const alignment = placement.split('-')[1]; // Extract "start" or "end"
1637
1699
  const map = {