ng-primitives 0.113.0 → 0.114.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.
package/dialog/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { ScrollStrategy, OverlayRef } from '@angular/cdk/overlay';
2
1
  import * as _angular_core from '@angular/core';
3
2
  import { ViewContainerRef, Injector, Provider, OnDestroy, TemplateRef, Type } from '@angular/core';
3
+ import { ScrollStrategy, NgpOverlayRef } from 'ng-primitives/portal';
4
4
  import * as i1 from 'ng-primitives/internal';
5
5
  import { Subject, Observable } from 'rxjs';
6
6
  import { FocusOrigin } from '@angular/cdk/a11y';
@@ -21,8 +21,6 @@ interface NgpDialogConfig<T = any> {
21
21
  role?: NgpDialogRole;
22
22
  /** Whether this is a modal dialog. Used to set the `aria-modal` attribute. */
23
23
  modal?: boolean;
24
- /** Scroll strategy to be used for the dialog. This determines how the dialog responds to scrolling underneath the panel element. */
25
- scrollStrategy?: ScrollStrategy;
26
24
  /**
27
25
  * Whether the dialog should close when the user navigates. This includes both browser history
28
26
  * navigation (back/forward) and programmatic route changes (e.g. router.navigate()).
@@ -30,8 +28,10 @@ interface NgpDialogConfig<T = any> {
30
28
  closeOnNavigation?: boolean;
31
29
  /** Whether the dialog should close when the user presses the escape key. */
32
30
  closeOnEscape?: boolean;
33
- /** Whether the dialog should close when the user click the overlay. */
31
+ /** Whether the dialog should close when the user clicks the overlay. */
34
32
  closeOnClick?: boolean;
33
+ /** Scroll strategy to be used for the dialog. */
34
+ scrollStrategy?: ScrollStrategy;
35
35
  data?: T;
36
36
  }
37
37
  /**
@@ -54,12 +54,15 @@ declare class NgpDialogDescription implements OnDestroy {
54
54
 
55
55
  declare class NgpDialogOverlay {
56
56
  private readonly dialogRef;
57
+ private startedPointerDownOnOverlay;
57
58
  /**
58
59
  * Whether the dialog should close on backdrop click.
59
60
  * @default `true`
60
61
  */
61
62
  readonly closeOnClick: _angular_core.InputSignalWithTransform<boolean | undefined, unknown>;
62
- protected close(): void;
63
+ protected onPointerDown(event: Event): void;
64
+ protected onClick(event: Event): void;
65
+ protected resetPointerOrigin(): void;
63
66
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<NgpDialogOverlay, never>;
64
67
  static ɵdir: _angular_core.ɵɵDirectiveDeclaration<NgpDialogOverlay, "[ngpDialogOverlay]", ["ngpDialogOverlay"], { "closeOnClick": { "alias": "ngpDialogOverlayCloseOnClick"; "required": false; "isSignal": true; }; }, {}, never, never, true, [{ directive: typeof i1.NgpExitAnimation; inputs: {}; outputs: {}; }]>;
65
68
  }
@@ -75,12 +78,17 @@ declare class NgpDialogTitle implements OnDestroy {
75
78
  static ɵdir: _angular_core.ɵɵDirectiveDeclaration<NgpDialogTitle, "[ngpDialogTitle]", ["ngpDialogTitle"], { "id": { "alias": "id"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
76
79
  }
77
80
 
81
+ /** Minimal portal interface needed by the dialog ref. */
82
+ interface NgpDialogPortalRef {
83
+ getElements(): HTMLElement[];
84
+ detach(immediate?: boolean): Promise<void>;
85
+ }
78
86
  /**
79
87
  * Reference to a dialog opened via the Dialog service.
80
88
  */
81
- declare class NgpDialogRef<T = unknown, R = unknown> {
82
- readonly overlayRef: OverlayRef;
89
+ declare class NgpDialogRef<T = unknown, R = unknown> implements NgpOverlayRef {
83
90
  readonly config: NgpDialogConfig<T>;
91
+ private readonly document;
84
92
  /** Whether the user is allowed to close the dialog. */
85
93
  disableClose: boolean | undefined;
86
94
  /** Whether the escape key is allowed to close the dialog. */
@@ -90,51 +98,97 @@ declare class NgpDialogRef<T = unknown, R = unknown> {
90
98
  focusOrigin?: FocusOrigin;
91
99
  result?: R;
92
100
  }>;
93
- /** Emits when on keyboard events within the dialog. */
94
- readonly keydownEvents: Observable<KeyboardEvent>;
95
- /** Emits on pointer events that happen outside of the dialog. */
96
- readonly outsidePointerEvents: Observable<MouseEvent>;
101
+ /**
102
+ * Observable that emits the dialog result when closed.
103
+ */
104
+ readonly afterClosed: Observable<R | undefined>;
97
105
  /** Data passed from the dialog opener. */
98
106
  readonly data: T;
99
107
  /** Unique ID for the dialog. */
100
108
  readonly id: string;
101
- /** Subscription to external detachments of the dialog. */
102
- private detachSubscription;
103
109
  /** @internal Store the injector */
104
110
  injector: Injector | undefined;
105
111
  /** Whether the dialog is closing. */
106
112
  private closing;
107
- constructor(overlayRef: OverlayRef, config: NgpDialogConfig<T>);
113
+ /** @internal Portal reference for element access and detach. */
114
+ portal: NgpDialogPortalRef | null;
115
+ /** Emits on keyboard events within the dialog. */
116
+ readonly keydownEvents: Observable<KeyboardEvent>;
117
+ /**
118
+ * Emits pointer events (click, auxclick, contextmenu) that happen outside of the dialog.
119
+ * Fed by the NgpOverlayRegistry with CDK-compatible stacking awareness.
120
+ * @internal
121
+ */
122
+ readonly outsidePointerEvents$: Subject<MouseEvent>;
123
+ /** Emits on pointer events that happen outside of the dialog. */
124
+ readonly outsidePointerEvents: Observable<MouseEvent>;
125
+ constructor(config: NgpDialogConfig<T>, document: Document);
126
+ /**
127
+ * Updates the position of the dialog. No-op since dialogs are CSS-centered.
128
+ */
129
+ updatePosition(): this;
130
+ /**
131
+ * NgpOverlayRef implementation — called by the registry for escape-key dismiss.
132
+ */
133
+ hide(options?: {
134
+ immediate?: boolean;
135
+ origin?: FocusOrigin;
136
+ }): void;
137
+ /**
138
+ * NgpOverlayRef implementation — called by the registry for descendant cascade.
139
+ * Skips exit animations and tears down immediately.
140
+ */
141
+ hideImmediate(): Promise<void>;
108
142
  /**
109
143
  * Close the dialog.
110
144
  * @param result Optional result to return to the dialog opener.
111
- * @param options Additional options to customize the closing behavior.
145
+ * @param focusOrigin The origin of the focus event that triggered the close.
112
146
  */
113
147
  close(result?: R, focusOrigin?: FocusOrigin): Promise<void>;
114
- /** Updates the position of the dialog based on the current position strategy. */
115
- updatePosition(): this;
148
+ /**
149
+ * @deprecated Access dialog methods directly instead (keydownEvents, outsidePointerEvents,
150
+ * updatePosition, close). This shim will be removed in the next major version.
151
+ */
152
+ get overlayRef(): {
153
+ keydownEvents: () => Observable<KeyboardEvent>;
154
+ outsidePointerEvents: () => Observable<MouseEvent>;
155
+ updatePosition: () => NgpDialogRef<T, R>;
156
+ dispose: () => Promise<void>;
157
+ detachments: () => Observable<{
158
+ focusOrigin?: FocusOrigin;
159
+ result?: R;
160
+ }>;
161
+ overlayElement: HTMLElement | undefined;
162
+ };
163
+ /**
164
+ * Get the portal elements.
165
+ * @internal
166
+ */
167
+ getElements(): HTMLElement[];
116
168
  }
117
169
  declare function injectDialogRef<T = unknown, R = unknown>(): NgpDialogRef<T, R>;
118
170
 
119
171
  /**
120
- * This is based on the Angular CDK Dialog service.
121
- * https://github.com/angular/components/blob/main/src/cdk/dialog/dialog.ts
172
+ * Originally based on Angular CDK Dialog service.
173
+ * Re-implemented to use ng-primitives/portal instead of @angular/cdk/overlay.
122
174
  */
123
175
  declare class NgpDialogManager implements OnDestroy {
124
176
  private readonly applicationRef;
177
+ private readonly injector;
125
178
  private readonly document;
126
- private readonly overlay;
127
179
  private readonly focusMonitor;
180
+ private readonly viewportRuler;
181
+ private readonly registry;
128
182
  private readonly defaultOptions;
129
183
  private readonly parentDialogManager;
130
- private readonly overlayContainer;
131
184
  private readonly router;
132
- private readonly scrollStrategy;
133
185
  private openDialogsAtThisLevel;
134
186
  private readonly afterAllClosedAtThisLevel;
135
187
  private readonly afterOpenedAtThisLevel;
136
188
  private ariaHiddenElements;
137
189
  private routerSubscription;
190
+ /** Scroll blocking strategy — shared across all dialogs. */
191
+ private scrollStrategy;
138
192
  /** Keeps track of the currently-open dialogs. */
139
193
  get openDialogs(): readonly NgpDialogRef[];
140
194
  /** Stream that emits when a dialog has been opened. */
@@ -169,15 +223,11 @@ declare class NgpDialogManager implements OnDestroy {
169
223
  getDialogById(id: string): NgpDialogRef | undefined;
170
224
  /**
171
225
  * Subscribe to router navigation events so that dialogs with `closeOnNavigation`
172
- * are closed when the user navigates programmatically (e.g. router.navigate()).
173
- * CDK's `disposeOnNavigation` only handles browser popstate events.
226
+ * are closed when the user navigates. This handles both browser popstate events
227
+ * and programmatic route changes (e.g. router.navigate()).
174
228
  */
175
229
  private subscribeToRouterEvents;
176
230
  ngOnDestroy(): void;
177
- /**
178
- * Creates an overlay config from a dialog config.
179
- */
180
- private getOverlayConfig;
181
231
  /**
182
232
  * Creates a custom injector to be used inside the dialog. This allows a component loaded inside
183
233
  * of a dialog to close itself and, optionally, to return a value.
@@ -187,7 +237,17 @@ declare class NgpDialogManager implements OnDestroy {
187
237
  * Removes a dialog from the array of open dialogs.
188
238
  */
189
239
  private removeOpenDialog;
190
- /** Hides all of the content that isn't an overlay from assistive technology. */
240
+ /**
241
+ * Enable scroll blocking when the first dialog opens.
242
+ */
243
+ private enableScrollBlocking;
244
+ /**
245
+ * Disable scroll blocking when the last dialog closes.
246
+ */
247
+ private disableScrollBlocking;
248
+ /**
249
+ * Hides all of the content that isn't a dialog portal from assistive technology.
250
+ */
191
251
  private hideNonDialogContentFromAssistiveTechnology;
192
252
  private getAfterAllClosed;
193
253
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<NgpDialogManager, never>;
@@ -1,17 +1,16 @@
1
1
  import * as i0 from '@angular/core';
2
- import { InjectionToken, inject, input, Directive, booleanAttribute, HostListener, ApplicationRef, ViewContainerRef, isDevMode, TemplateRef, Injector, Injectable, output, signal } from '@angular/core';
2
+ import { InjectionToken, inject, input, Directive, booleanAttribute, ApplicationRef, Injector, ViewContainerRef, isDevMode, Injectable, output, HostListener, signal } from '@angular/core';
3
3
  import { uniqueId, onChange } from 'ng-primitives/utils';
4
4
  import { createStateToken, createStateProvider, createStateInjector, createState } from 'ng-primitives/state';
5
5
  import * as i1 from 'ng-primitives/internal';
6
6
  import { NgpExitAnimationManager, NgpExitAnimation } from 'ng-primitives/internal';
7
- import { hasModifierKey } from '@angular/cdk/keycodes';
8
- import { Subject, defer } from 'rxjs';
7
+ import { Subject, defer, EMPTY, fromEvent } from 'rxjs';
8
+ import { map, filter, takeUntil, startWith } from 'rxjs/operators';
9
9
  import { FocusMonitor } from '@angular/cdk/a11y';
10
- import { Overlay, OverlayContainer, OverlayConfig } from '@angular/cdk/overlay';
11
- import { TemplatePortal, ComponentPortal } from '@angular/cdk/portal';
10
+ import { ViewportRuler } from '@angular/cdk/scrolling';
12
11
  import { DOCUMENT } from '@angular/common';
13
12
  import { Router, NavigationStart } from '@angular/router';
14
- import { startWith } from 'rxjs/operators';
13
+ import { NgpOverlayRegistry, createPortal, BlockScrollStrategy } from 'ng-primitives/portal';
15
14
  import * as i1$1 from 'ng-primitives/focus-trap';
16
15
  import { NgpFocusTrap } from 'ng-primitives/focus-trap';
17
16
 
@@ -97,33 +96,74 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImpor
97
96
  * Reference to a dialog opened via the Dialog service.
98
97
  */
99
98
  class NgpDialogRef {
100
- constructor(overlayRef, config) {
101
- this.overlayRef = overlayRef;
99
+ constructor(config, document) {
102
100
  this.config = config;
101
+ this.document = document;
103
102
  /** Emits when the dialog has been closed. */
104
103
  this.closed = new Subject();
104
+ /**
105
+ * Observable that emits the dialog result when closed.
106
+ */
107
+ this.afterClosed = this.closed.pipe(map(event => event.result));
105
108
  /** Whether the dialog is closing. */
106
109
  this.closing = false;
110
+ /** @internal Portal reference for element access and detach. */
111
+ this.portal = null;
112
+ /**
113
+ * Emits pointer events (click, auxclick, contextmenu) that happen outside of the dialog.
114
+ * Fed by the NgpOverlayRegistry with CDK-compatible stacking awareness.
115
+ * @internal
116
+ */
117
+ this.outsidePointerEvents$ = new Subject();
118
+ /** Emits on pointer events that happen outside of the dialog. */
119
+ this.outsidePointerEvents = this.outsidePointerEvents$.asObservable();
107
120
  this.data = config.data;
108
- this.keydownEvents = overlayRef.keydownEvents();
109
- this.outsidePointerEvents = overlayRef.outsidePointerEvents();
110
121
  this.id = config.id; // By the time the dialog is created we are guaranteed to have an ID.
111
122
  this.closeOnEscape = config.closeOnEscape ?? true;
112
- this.keydownEvents.subscribe(event => {
113
- if (event.key === 'Escape' &&
114
- !this.disableClose &&
115
- this.closeOnEscape !== false &&
116
- !hasModifierKey(event)) {
117
- event.preventDefault();
118
- this.close(undefined, 'keyboard');
119
- }
123
+ // Use defer() so the observable is created on subscribe — by then the portal will be set.
124
+ this.keydownEvents = defer(() => {
125
+ const elements = this.getElements();
126
+ if (!elements.length)
127
+ return EMPTY;
128
+ return fromEvent(this.document, 'keydown').pipe(filter(event => elements.some(el => el.contains(event.target))), takeUntil(this.closed));
120
129
  });
121
- this.detachSubscription = overlayRef.detachments().subscribe(() => this.close());
130
+ }
131
+ /**
132
+ * Updates the position of the dialog. No-op since dialogs are CSS-centered.
133
+ */
134
+ updatePosition() {
135
+ return this;
136
+ }
137
+ /**
138
+ * NgpOverlayRef implementation — called by the registry for escape-key dismiss.
139
+ */
140
+ hide(options) {
141
+ if (this.disableClose) {
142
+ return;
143
+ }
144
+ this.close(undefined, options?.origin);
145
+ }
146
+ /**
147
+ * NgpOverlayRef implementation — called by the registry for descendant cascade.
148
+ * Skips exit animations and tears down immediately.
149
+ */
150
+ async hideImmediate() {
151
+ if (this.closing) {
152
+ return;
153
+ }
154
+ this.closing = true;
155
+ // Detach the portal immediately — no exit animation
156
+ if (this.portal) {
157
+ await this.portal.detach(true);
158
+ this.portal = null;
159
+ }
160
+ this.closed.next({});
161
+ this.closed.complete();
122
162
  }
123
163
  /**
124
164
  * Close the dialog.
125
165
  * @param result Optional result to return to the dialog opener.
126
- * @param options Additional options to customize the closing behavior.
166
+ * @param focusOrigin The origin of the focus event that triggered the close.
127
167
  */
128
168
  async close(result, focusOrigin) {
129
169
  // If the dialog is already closed, do nothing.
@@ -137,15 +177,34 @@ class NgpDialogRef {
137
177
  if (exitAnimationManager) {
138
178
  await exitAnimationManager.exit();
139
179
  }
140
- this.overlayRef.dispose();
141
- this.detachSubscription.unsubscribe();
180
+ // Detach the portal (immediate since exit animation already ran)
181
+ if (this.portal) {
182
+ await this.portal.detach(true);
183
+ this.portal = null;
184
+ }
142
185
  this.closed.next({ focusOrigin, result });
143
186
  this.closed.complete();
144
187
  }
145
- /** Updates the position of the dialog based on the current position strategy. */
146
- updatePosition() {
147
- this.overlayRef.updatePosition();
148
- return this;
188
+ /**
189
+ * @deprecated Access dialog methods directly instead (keydownEvents, outsidePointerEvents,
190
+ * updatePosition, close). This shim will be removed in the next major version.
191
+ */
192
+ get overlayRef() {
193
+ return {
194
+ keydownEvents: () => this.keydownEvents,
195
+ outsidePointerEvents: () => this.outsidePointerEvents,
196
+ updatePosition: () => this.updatePosition(),
197
+ dispose: () => this.close(),
198
+ detachments: () => this.closed.asObservable(),
199
+ overlayElement: this.getElements()[0],
200
+ };
201
+ }
202
+ /**
203
+ * Get the portal elements.
204
+ * @internal
205
+ */
206
+ getElements() {
207
+ return this.portal?.getElements() ?? [];
149
208
  }
150
209
  }
151
210
  function injectDialogRef() {
@@ -155,6 +214,7 @@ function injectDialogRef() {
155
214
  class NgpDialogOverlay {
156
215
  constructor() {
157
216
  this.dialogRef = injectDialogRef();
217
+ this.startedPointerDownOnOverlay = false;
158
218
  /**
159
219
  * Whether the dialog should close on backdrop click.
160
220
  * @default `true`
@@ -165,13 +225,24 @@ class NgpDialogOverlay {
165
225
  transform: booleanAttribute,
166
226
  }]));
167
227
  }
168
- close() {
169
- if (this.closeOnClick() && !this.dialogRef.disableClose) {
228
+ onPointerDown(event) {
229
+ this.startedPointerDownOnOverlay = event.target === event.currentTarget;
230
+ }
231
+ onClick(event) {
232
+ const shouldClose = this.startedPointerDownOnOverlay &&
233
+ event.target === event.currentTarget &&
234
+ this.closeOnClick() &&
235
+ !this.dialogRef.disableClose;
236
+ this.resetPointerOrigin();
237
+ if (shouldClose) {
170
238
  this.dialogRef.close(undefined, 'mouse');
171
239
  }
172
240
  }
241
+ resetPointerOrigin() {
242
+ this.startedPointerDownOnOverlay = false;
243
+ }
173
244
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: NgpDialogOverlay, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
174
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "20.3.9", type: NgpDialogOverlay, isStandalone: true, selector: "[ngpDialogOverlay]", inputs: { closeOnClick: { classPropertyName: "closeOnClick", publicName: "ngpDialogOverlayCloseOnClick", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "click": "close()" } }, exportAs: ["ngpDialogOverlay"], hostDirectives: [{ directive: i1.NgpExitAnimation }], ngImport: i0 }); }
245
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "20.3.9", type: NgpDialogOverlay, isStandalone: true, selector: "[ngpDialogOverlay]", inputs: { closeOnClick: { classPropertyName: "closeOnClick", publicName: "ngpDialogOverlayCloseOnClick", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "pointerdown": "onPointerDown($event)", "click": "onClick($event)", "pointercancel": "resetPointerOrigin()" } }, exportAs: ["ngpDialogOverlay"], hostDirectives: [{ directive: i1.NgpExitAnimation }], ngImport: i0 }); }
175
246
  }
176
247
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: NgpDialogOverlay, decorators: [{
177
248
  type: Directive,
@@ -179,11 +250,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImpor
179
250
  selector: '[ngpDialogOverlay]',
180
251
  exportAs: 'ngpDialogOverlay',
181
252
  hostDirectives: [NgpExitAnimation],
253
+ host: {
254
+ '(pointerdown)': 'onPointerDown($event)',
255
+ '(click)': 'onClick($event)',
256
+ '(pointercancel)': 'resetPointerOrigin()',
257
+ },
182
258
  }]
183
- }], propDecorators: { closeOnClick: [{ type: i0.Input, args: [{ isSignal: true, alias: "ngpDialogOverlayCloseOnClick", required: false }] }], close: [{
184
- type: HostListener,
185
- args: ['click']
186
- }] } });
259
+ }], propDecorators: { closeOnClick: [{ type: i0.Input, args: [{ isSignal: true, alias: "ngpDialogOverlayCloseOnClick", required: false }] }] } });
187
260
 
188
261
  class NgpDialogTitle {
189
262
  constructor() {
@@ -218,27 +291,29 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImpor
218
291
  }], ctorParameters: () => [], propDecorators: { id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: false }] }] } });
219
292
 
220
293
  /**
221
- * This is based on the Angular CDK Dialog service.
222
- * https://github.com/angular/components/blob/main/src/cdk/dialog/dialog.ts
294
+ * Originally based on Angular CDK Dialog service.
295
+ * Re-implemented to use ng-primitives/portal instead of @angular/cdk/overlay.
223
296
  */
224
297
  class NgpDialogManager {
225
298
  constructor() {
226
299
  this.applicationRef = inject(ApplicationRef);
300
+ this.injector = inject(Injector);
227
301
  this.document = inject(DOCUMENT);
228
- this.overlay = inject(Overlay);
229
302
  this.focusMonitor = inject(FocusMonitor);
303
+ this.viewportRuler = inject(ViewportRuler);
304
+ this.registry = inject(NgpOverlayRegistry);
230
305
  this.defaultOptions = injectDialogConfig();
231
306
  this.parentDialogManager = inject(NgpDialogManager, {
232
307
  optional: true,
233
308
  skipSelf: true,
234
309
  });
235
- this.overlayContainer = inject(OverlayContainer);
236
310
  this.router = inject(Router, { optional: true });
237
- this.scrollStrategy = this.defaultOptions.scrollStrategy ?? this.overlay.scrollStrategies.block();
238
311
  this.openDialogsAtThisLevel = [];
239
312
  this.afterAllClosedAtThisLevel = new Subject();
240
313
  this.afterOpenedAtThisLevel = new Subject();
241
314
  this.ariaHiddenElements = new Map();
315
+ /** Scroll blocking strategy — shared across all dialogs. */
316
+ this.scrollStrategy = null;
242
317
  /**
243
318
  * Stream that emits when all open dialog have finished closing.
244
319
  * Will emit on subscribe if there are no open dialogs to begin with.
@@ -275,30 +350,57 @@ class NgpDialogManager {
275
350
  if (config.id && this.getDialogById(config.id) && isDevMode()) {
276
351
  throw Error(`Dialog with id "${config.id}" exists already. The dialog id must be unique.`);
277
352
  }
278
- const overlayConfig = this.getOverlayConfig(config);
279
- const overlayRef = this.overlay.create(overlayConfig);
280
- const dialogRef = new NgpDialogRef(overlayRef, config);
281
- const injector = this.createInjector(config, dialogRef, undefined);
353
+ const dialogRef = new NgpDialogRef(config, this.document);
354
+ const injector = this.createInjector(config, dialogRef);
282
355
  // store the injector in the dialog ref - this is so we can access the exit animation manager
283
356
  dialogRef.injector = injector;
284
357
  const context = {
285
358
  $implicit: dialogRef,
286
359
  close: dialogRef.close.bind(dialogRef),
287
360
  };
288
- if (templateRefOrComponentType instanceof TemplateRef) {
289
- overlayRef.attach(new TemplatePortal(templateRefOrComponentType, config.viewContainerRef, context, injector));
290
- }
291
- else {
292
- overlayRef.attach(new ComponentPortal(templateRefOrComponentType, config.viewContainerRef, injector));
293
- }
294
- // If this is the first dialog that we're opening, hide all the non-overlay content.
361
+ // Create the portal using our portal system
362
+ const portal = createPortal(templateRefOrComponentType, config.viewContainerRef, injector, context);
363
+ // Attach the portal to document.body
364
+ portal.attach(this.document.body);
365
+ // Store the portal reference on the dialog ref for element access and cleanup
366
+ dialogRef.portal = portal;
367
+ // If this is the first dialog that we're opening, hide all the non-overlay content
368
+ // and enable scroll blocking.
295
369
  if (!this.openDialogs.length) {
296
- this.hideNonDialogContentFromAssistiveTechnology();
370
+ this.hideNonDialogContentFromAssistiveTechnology(portal.getElements());
371
+ this.enableScrollBlocking(config);
372
+ }
373
+ // Auto-detect parent overlay: if the trigger element lives inside an existing overlay
374
+ // (e.g. a dialog opened from a popover), register as its child so that clicks inside
375
+ // the dialog don't dismiss the parent overlay.
376
+ // Only inherit parentId from other dialogs — non-dialog overlays (menus, popovers)
377
+ // may close after triggering the dialog open, which would cascade-close the dialog.
378
+ let parentId = activeElement instanceof HTMLElement
379
+ ? this.registry.findContainingOverlay(activeElement)
380
+ : null;
381
+ if (parentId !== null && !this.openDialogs.some(d => d.id === parentId)) {
382
+ parentId = null;
297
383
  }
384
+ // Register with the overlay registry for centralized escape-key routing.
385
+ // outsidePress is false because the NgpDialogOverlay directive handles its own backdrop clicks.
386
+ this.registry.register({
387
+ id: dialogRef.id,
388
+ parentId,
389
+ overlay: dialogRef,
390
+ getElements: () => dialogRef.getElements(),
391
+ triggerElement: activeElement ?? this.document.body,
392
+ dismissPolicy: {
393
+ outsidePress: false,
394
+ escapeKey: config.closeOnEscape ?? true,
395
+ },
396
+ outsidePointerEvents$: dialogRef.outsidePointerEvents$,
397
+ });
298
398
  this.openDialogs.push(dialogRef);
299
399
  this.afterOpened.next(dialogRef);
300
400
  this.subscribeToRouterEvents();
301
401
  dialogRef.closed.subscribe(closeResult => {
402
+ // Deregister from the overlay registry
403
+ this.registry.deregister(dialogRef.id);
302
404
  this.removeOpenDialog(dialogRef, true);
303
405
  // Focus the trigger element after the dialog closes.
304
406
  if (activeElement instanceof HTMLElement && this.document.body.contains(activeElement)) {
@@ -324,8 +426,8 @@ class NgpDialogManager {
324
426
  }
325
427
  /**
326
428
  * Subscribe to router navigation events so that dialogs with `closeOnNavigation`
327
- * are closed when the user navigates programmatically (e.g. router.navigate()).
328
- * CDK's `disposeOnNavigation` only handles browser popstate events.
429
+ * are closed when the user navigates. This handles both browser popstate events
430
+ * and programmatic route changes (e.g. router.navigate()).
329
431
  */
330
432
  subscribeToRouterEvents() {
331
433
  if (this.routerSubscription || !this.router) {
@@ -361,33 +463,19 @@ class NgpDialogManager {
361
463
  this.openDialogsAtThisLevel = [];
362
464
  this.routerSubscription?.unsubscribe();
363
465
  }
364
- /**
365
- * Creates an overlay config from a dialog config.
366
- */
367
- getOverlayConfig(config) {
368
- const state = new OverlayConfig({
369
- positionStrategy: this.overlay.position().global().centerHorizontally().centerVertically(),
370
- scrollStrategy: config.scrollStrategy || this.scrollStrategy,
371
- hasBackdrop: false,
372
- disposeOnNavigation: config.closeOnNavigation,
373
- // required for v21 - the CDK launches overlays using the popover api which means any other overlays
374
- // such as select dropdowns, or tooltips will appear behind the dialog, regardless of z-index
375
- // this disables the use of popovers
376
- usePopover: false,
377
- });
378
- return state;
379
- }
380
466
  /**
381
467
  * Creates a custom injector to be used inside the dialog. This allows a component loaded inside
382
468
  * of a dialog to close itself and, optionally, to return a value.
383
469
  */
384
- createInjector(config, dialogRef, fallbackInjector) {
470
+ createInjector(config, dialogRef) {
385
471
  const userInjector = config.injector || config.viewContainerRef?.injector;
386
472
  const providers = [
387
473
  { provide: NgpDialogRef, useValue: dialogRef },
388
474
  { provide: NgpExitAnimationManager, useClass: NgpExitAnimationManager },
389
475
  ];
390
- return Injector.create({ parent: userInjector || fallbackInjector, providers });
476
+ // Fall back to the service's own injector (root injector) to ensure
477
+ // ApplicationRef and other platform providers are available.
478
+ return Injector.create({ parent: userInjector || this.injector, providers });
391
479
  }
392
480
  /**
393
481
  * Removes a dialog from the array of open dialogs.
@@ -408,27 +496,44 @@ class NgpDialogManager {
408
496
  }
409
497
  });
410
498
  this.ariaHiddenElements.clear();
499
+ this.disableScrollBlocking();
411
500
  if (emitEvent) {
412
501
  this.getAfterAllClosed().next();
413
502
  }
414
503
  }
415
504
  }
416
505
  }
417
- /** Hides all of the content that isn't an overlay from assistive technology. */
418
- hideNonDialogContentFromAssistiveTechnology() {
419
- const overlayContainer = this.overlayContainer.getContainerElement();
420
- // Ensure that the overlay container is attached to the DOM.
421
- if (overlayContainer.parentElement) {
422
- const siblings = overlayContainer.parentElement.children;
423
- for (let i = siblings.length - 1; i > -1; i--) {
424
- const sibling = siblings[i];
425
- if (sibling !== overlayContainer &&
426
- sibling.nodeName !== 'SCRIPT' &&
427
- sibling.nodeName !== 'STYLE' &&
428
- !sibling.hasAttribute('aria-live')) {
429
- this.ariaHiddenElements.set(sibling, sibling.getAttribute('aria-hidden'));
430
- sibling.setAttribute('aria-hidden', 'true');
431
- }
506
+ /**
507
+ * Enable scroll blocking when the first dialog opens.
508
+ */
509
+ enableScrollBlocking(config) {
510
+ if (!this.scrollStrategy) {
511
+ this.scrollStrategy =
512
+ config?.scrollStrategy ?? new BlockScrollStrategy(this.viewportRuler, this.document);
513
+ }
514
+ this.scrollStrategy.enable();
515
+ }
516
+ /**
517
+ * Disable scroll blocking when the last dialog closes.
518
+ */
519
+ disableScrollBlocking() {
520
+ this.scrollStrategy?.disable();
521
+ this.scrollStrategy = null;
522
+ }
523
+ /**
524
+ * Hides all of the content that isn't a dialog portal from assistive technology.
525
+ */
526
+ hideNonDialogContentFromAssistiveTechnology(portalElements) {
527
+ const body = this.document.body;
528
+ const bodyChildren = body.children;
529
+ for (let i = bodyChildren.length - 1; i > -1; i--) {
530
+ const sibling = bodyChildren[i];
531
+ if (!portalElements.includes(sibling) &&
532
+ sibling.nodeName !== 'SCRIPT' &&
533
+ sibling.nodeName !== 'STYLE' &&
534
+ !sibling.hasAttribute('aria-live')) {
535
+ this.ariaHiddenElements.set(sibling, sibling.getAttribute('aria-hidden'));
536
+ sibling.setAttribute('aria-hidden', 'true');
432
537
  }
433
538
  }
434
539
  }