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 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
  /**
@@ -75,12 +75,17 @@ declare class NgpDialogTitle implements OnDestroy {
75
75
  static ɵdir: _angular_core.ɵɵDirectiveDeclaration<NgpDialogTitle, "[ngpDialogTitle]", ["ngpDialogTitle"], { "id": { "alias": "id"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
76
76
  }
77
77
 
78
+ /** Minimal portal interface needed by the dialog ref. */
79
+ interface NgpDialogPortalRef {
80
+ getElements(): HTMLElement[];
81
+ detach(immediate?: boolean): Promise<void>;
82
+ }
78
83
  /**
79
84
  * Reference to a dialog opened via the Dialog service.
80
85
  */
81
- declare class NgpDialogRef<T = unknown, R = unknown> {
82
- readonly overlayRef: OverlayRef;
86
+ declare class NgpDialogRef<T = unknown, R = unknown> implements NgpOverlayRef {
83
87
  readonly config: NgpDialogConfig<T>;
88
+ private readonly document;
84
89
  /** Whether the user is allowed to close the dialog. */
85
90
  disableClose: boolean | undefined;
86
91
  /** Whether the escape key is allowed to close the dialog. */
@@ -90,51 +95,97 @@ declare class NgpDialogRef<T = unknown, R = unknown> {
90
95
  focusOrigin?: FocusOrigin;
91
96
  result?: R;
92
97
  }>;
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>;
98
+ /**
99
+ * Observable that emits the dialog result when closed.
100
+ */
101
+ readonly afterClosed: Observable<R | undefined>;
97
102
  /** Data passed from the dialog opener. */
98
103
  readonly data: T;
99
104
  /** Unique ID for the dialog. */
100
105
  readonly id: string;
101
- /** Subscription to external detachments of the dialog. */
102
- private detachSubscription;
103
106
  /** @internal Store the injector */
104
107
  injector: Injector | undefined;
105
108
  /** Whether the dialog is closing. */
106
109
  private closing;
107
- constructor(overlayRef: OverlayRef, config: NgpDialogConfig<T>);
110
+ /** @internal Portal reference for element access and detach. */
111
+ portal: NgpDialogPortalRef | null;
112
+ /** Emits on keyboard events within the dialog. */
113
+ readonly keydownEvents: Observable<KeyboardEvent>;
114
+ /**
115
+ * Emits pointer events (click, auxclick, contextmenu) that happen outside of the dialog.
116
+ * Fed by the NgpOverlayRegistry with CDK-compatible stacking awareness.
117
+ * @internal
118
+ */
119
+ readonly outsidePointerEvents$: Subject<MouseEvent>;
120
+ /** Emits on pointer events that happen outside of the dialog. */
121
+ readonly outsidePointerEvents: Observable<MouseEvent>;
122
+ constructor(config: NgpDialogConfig<T>, document: Document);
123
+ /**
124
+ * Updates the position of the dialog. No-op since dialogs are CSS-centered.
125
+ */
126
+ updatePosition(): this;
127
+ /**
128
+ * NgpOverlayRef implementation — called by the registry for escape-key dismiss.
129
+ */
130
+ hide(options?: {
131
+ immediate?: boolean;
132
+ origin?: FocusOrigin;
133
+ }): void;
134
+ /**
135
+ * NgpOverlayRef implementation — called by the registry for descendant cascade.
136
+ * Skips exit animations and tears down immediately.
137
+ */
138
+ hideImmediate(): Promise<void>;
108
139
  /**
109
140
  * Close the dialog.
110
141
  * @param result Optional result to return to the dialog opener.
111
- * @param options Additional options to customize the closing behavior.
142
+ * @param focusOrigin The origin of the focus event that triggered the close.
112
143
  */
113
144
  close(result?: R, focusOrigin?: FocusOrigin): Promise<void>;
114
- /** Updates the position of the dialog based on the current position strategy. */
115
- updatePosition(): this;
145
+ /**
146
+ * @deprecated Access dialog methods directly instead (keydownEvents, outsidePointerEvents,
147
+ * updatePosition, close). This shim will be removed in the next major version.
148
+ */
149
+ get overlayRef(): {
150
+ keydownEvents: () => Observable<KeyboardEvent>;
151
+ outsidePointerEvents: () => Observable<MouseEvent>;
152
+ updatePosition: () => NgpDialogRef<T, R>;
153
+ dispose: () => Promise<void>;
154
+ detachments: () => Observable<{
155
+ focusOrigin?: FocusOrigin;
156
+ result?: R;
157
+ }>;
158
+ overlayElement: HTMLElement | undefined;
159
+ };
160
+ /**
161
+ * Get the portal elements.
162
+ * @internal
163
+ */
164
+ getElements(): HTMLElement[];
116
165
  }
117
166
  declare function injectDialogRef<T = unknown, R = unknown>(): NgpDialogRef<T, R>;
118
167
 
119
168
  /**
120
- * This is based on the Angular CDK Dialog service.
121
- * https://github.com/angular/components/blob/main/src/cdk/dialog/dialog.ts
169
+ * Originally based on Angular CDK Dialog service.
170
+ * Re-implemented to use ng-primitives/portal instead of @angular/cdk/overlay.
122
171
  */
123
172
  declare class NgpDialogManager implements OnDestroy {
124
173
  private readonly applicationRef;
174
+ private readonly injector;
125
175
  private readonly document;
126
- private readonly overlay;
127
176
  private readonly focusMonitor;
177
+ private readonly viewportRuler;
178
+ private readonly registry;
128
179
  private readonly defaultOptions;
129
180
  private readonly parentDialogManager;
130
- private readonly overlayContainer;
131
181
  private readonly router;
132
- private readonly scrollStrategy;
133
182
  private openDialogsAtThisLevel;
134
183
  private readonly afterAllClosedAtThisLevel;
135
184
  private readonly afterOpenedAtThisLevel;
136
185
  private ariaHiddenElements;
137
186
  private routerSubscription;
187
+ /** Scroll blocking strategy — shared across all dialogs. */
188
+ private scrollStrategy;
138
189
  /** Keeps track of the currently-open dialogs. */
139
190
  get openDialogs(): readonly NgpDialogRef[];
140
191
  /** Stream that emits when a dialog has been opened. */
@@ -169,15 +220,11 @@ declare class NgpDialogManager implements OnDestroy {
169
220
  getDialogById(id: string): NgpDialogRef | undefined;
170
221
  /**
171
222
  * 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.
223
+ * are closed when the user navigates. This handles both browser popstate events
224
+ * and programmatic route changes (e.g. router.navigate()).
174
225
  */
175
226
  private subscribeToRouterEvents;
176
227
  ngOnDestroy(): void;
177
- /**
178
- * Creates an overlay config from a dialog config.
179
- */
180
- private getOverlayConfig;
181
228
  /**
182
229
  * Creates a custom injector to be used inside the dialog. This allows a component loaded inside
183
230
  * of a dialog to close itself and, optionally, to return a value.
@@ -187,7 +234,17 @@ declare class NgpDialogManager implements OnDestroy {
187
234
  * Removes a dialog from the array of open dialogs.
188
235
  */
189
236
  private removeOpenDialog;
190
- /** Hides all of the content that isn't an overlay from assistive technology. */
237
+ /**
238
+ * Enable scroll blocking when the first dialog opens.
239
+ */
240
+ private enableScrollBlocking;
241
+ /**
242
+ * Disable scroll blocking when the last dialog closes.
243
+ */
244
+ private disableScrollBlocking;
245
+ /**
246
+ * Hides all of the content that isn't a dialog portal from assistive technology.
247
+ */
191
248
  private hideNonDialogContentFromAssistiveTechnology;
192
249
  private getAfterAllClosed;
193
250
  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, HostListener, ApplicationRef, Injector, ViewContainerRef, isDevMode, Injectable, output, 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() {
@@ -218,27 +277,29 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImpor
218
277
  }], ctorParameters: () => [], propDecorators: { id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: false }] }] } });
219
278
 
220
279
  /**
221
- * This is based on the Angular CDK Dialog service.
222
- * https://github.com/angular/components/blob/main/src/cdk/dialog/dialog.ts
280
+ * Originally based on Angular CDK Dialog service.
281
+ * Re-implemented to use ng-primitives/portal instead of @angular/cdk/overlay.
223
282
  */
224
283
  class NgpDialogManager {
225
284
  constructor() {
226
285
  this.applicationRef = inject(ApplicationRef);
286
+ this.injector = inject(Injector);
227
287
  this.document = inject(DOCUMENT);
228
- this.overlay = inject(Overlay);
229
288
  this.focusMonitor = inject(FocusMonitor);
289
+ this.viewportRuler = inject(ViewportRuler);
290
+ this.registry = inject(NgpOverlayRegistry);
230
291
  this.defaultOptions = injectDialogConfig();
231
292
  this.parentDialogManager = inject(NgpDialogManager, {
232
293
  optional: true,
233
294
  skipSelf: true,
234
295
  });
235
- this.overlayContainer = inject(OverlayContainer);
236
296
  this.router = inject(Router, { optional: true });
237
- this.scrollStrategy = this.defaultOptions.scrollStrategy ?? this.overlay.scrollStrategies.block();
238
297
  this.openDialogsAtThisLevel = [];
239
298
  this.afterAllClosedAtThisLevel = new Subject();
240
299
  this.afterOpenedAtThisLevel = new Subject();
241
300
  this.ariaHiddenElements = new Map();
301
+ /** Scroll blocking strategy — shared across all dialogs. */
302
+ this.scrollStrategy = null;
242
303
  /**
243
304
  * Stream that emits when all open dialog have finished closing.
244
305
  * Will emit on subscribe if there are no open dialogs to begin with.
@@ -275,30 +336,52 @@ class NgpDialogManager {
275
336
  if (config.id && this.getDialogById(config.id) && isDevMode()) {
276
337
  throw Error(`Dialog with id "${config.id}" exists already. The dialog id must be unique.`);
277
338
  }
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);
339
+ const dialogRef = new NgpDialogRef(config, this.document);
340
+ const injector = this.createInjector(config, dialogRef);
282
341
  // store the injector in the dialog ref - this is so we can access the exit animation manager
283
342
  dialogRef.injector = injector;
284
343
  const context = {
285
344
  $implicit: dialogRef,
286
345
  close: dialogRef.close.bind(dialogRef),
287
346
  };
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.
347
+ // Create the portal using our portal system
348
+ const portal = createPortal(templateRefOrComponentType, config.viewContainerRef, injector, context);
349
+ // Attach the portal to document.body
350
+ portal.attach(this.document.body);
351
+ // Store the portal reference on the dialog ref for element access and cleanup
352
+ dialogRef.portal = portal;
353
+ // If this is the first dialog that we're opening, hide all the non-overlay content
354
+ // and enable scroll blocking.
295
355
  if (!this.openDialogs.length) {
296
- this.hideNonDialogContentFromAssistiveTechnology();
356
+ this.hideNonDialogContentFromAssistiveTechnology(portal.getElements());
357
+ this.enableScrollBlocking(config);
297
358
  }
359
+ // Auto-detect parent overlay: if the trigger element lives inside an existing overlay
360
+ // (e.g. a dialog opened from a popover), register as its child so that clicks inside
361
+ // the dialog don't dismiss the parent overlay.
362
+ const parentId = activeElement instanceof HTMLElement
363
+ ? this.registry.findContainingOverlay(activeElement)
364
+ : null;
365
+ // Register with the overlay registry for centralized escape-key routing.
366
+ // outsidePress is false because the NgpDialogOverlay directive handles its own backdrop clicks.
367
+ this.registry.register({
368
+ id: dialogRef.id,
369
+ parentId,
370
+ overlay: dialogRef,
371
+ getElements: () => dialogRef.getElements(),
372
+ triggerElement: activeElement ?? this.document.body,
373
+ dismissPolicy: {
374
+ outsidePress: false,
375
+ escapeKey: config.closeOnEscape ?? true,
376
+ },
377
+ outsidePointerEvents$: dialogRef.outsidePointerEvents$,
378
+ });
298
379
  this.openDialogs.push(dialogRef);
299
380
  this.afterOpened.next(dialogRef);
300
381
  this.subscribeToRouterEvents();
301
382
  dialogRef.closed.subscribe(closeResult => {
383
+ // Deregister from the overlay registry
384
+ this.registry.deregister(dialogRef.id);
302
385
  this.removeOpenDialog(dialogRef, true);
303
386
  // Focus the trigger element after the dialog closes.
304
387
  if (activeElement instanceof HTMLElement && this.document.body.contains(activeElement)) {
@@ -324,8 +407,8 @@ class NgpDialogManager {
324
407
  }
325
408
  /**
326
409
  * 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.
410
+ * are closed when the user navigates. This handles both browser popstate events
411
+ * and programmatic route changes (e.g. router.navigate()).
329
412
  */
330
413
  subscribeToRouterEvents() {
331
414
  if (this.routerSubscription || !this.router) {
@@ -361,33 +444,19 @@ class NgpDialogManager {
361
444
  this.openDialogsAtThisLevel = [];
362
445
  this.routerSubscription?.unsubscribe();
363
446
  }
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
447
  /**
381
448
  * Creates a custom injector to be used inside the dialog. This allows a component loaded inside
382
449
  * of a dialog to close itself and, optionally, to return a value.
383
450
  */
384
- createInjector(config, dialogRef, fallbackInjector) {
451
+ createInjector(config, dialogRef) {
385
452
  const userInjector = config.injector || config.viewContainerRef?.injector;
386
453
  const providers = [
387
454
  { provide: NgpDialogRef, useValue: dialogRef },
388
455
  { provide: NgpExitAnimationManager, useClass: NgpExitAnimationManager },
389
456
  ];
390
- return Injector.create({ parent: userInjector || fallbackInjector, providers });
457
+ // Fall back to the service's own injector (root injector) to ensure
458
+ // ApplicationRef and other platform providers are available.
459
+ return Injector.create({ parent: userInjector || this.injector, providers });
391
460
  }
392
461
  /**
393
462
  * Removes a dialog from the array of open dialogs.
@@ -408,27 +477,44 @@ class NgpDialogManager {
408
477
  }
409
478
  });
410
479
  this.ariaHiddenElements.clear();
480
+ this.disableScrollBlocking();
411
481
  if (emitEvent) {
412
482
  this.getAfterAllClosed().next();
413
483
  }
414
484
  }
415
485
  }
416
486
  }
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
- }
487
+ /**
488
+ * Enable scroll blocking when the first dialog opens.
489
+ */
490
+ enableScrollBlocking(config) {
491
+ if (!this.scrollStrategy) {
492
+ this.scrollStrategy =
493
+ config?.scrollStrategy ?? new BlockScrollStrategy(this.viewportRuler, this.document);
494
+ }
495
+ this.scrollStrategy.enable();
496
+ }
497
+ /**
498
+ * Disable scroll blocking when the last dialog closes.
499
+ */
500
+ disableScrollBlocking() {
501
+ this.scrollStrategy?.disable();
502
+ this.scrollStrategy = null;
503
+ }
504
+ /**
505
+ * Hides all of the content that isn't a dialog portal from assistive technology.
506
+ */
507
+ hideNonDialogContentFromAssistiveTechnology(portalElements) {
508
+ const body = this.document.body;
509
+ const bodyChildren = body.children;
510
+ for (let i = bodyChildren.length - 1; i > -1; i--) {
511
+ const sibling = bodyChildren[i];
512
+ if (!portalElements.includes(sibling) &&
513
+ sibling.nodeName !== 'SCRIPT' &&
514
+ sibling.nodeName !== 'STYLE' &&
515
+ !sibling.hasAttribute('aria-live')) {
516
+ this.ariaHiddenElements.set(sibling, sibling.getAttribute('aria-hidden'));
517
+ sibling.setAttribute('aria-hidden', 'true');
432
518
  }
433
519
  }
434
520
  }