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
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
|
|
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
|
-
/**
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
readonly
|
|
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
|
-
|
|
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
|
|
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
|
-
/**
|
|
115
|
-
|
|
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
|
-
*
|
|
121
|
-
*
|
|
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
|
|
173
|
-
*
|
|
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
|
-
/**
|
|
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,
|
|
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 {
|
|
8
|
-
import {
|
|
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 {
|
|
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 {
|
|
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(
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
141
|
-
this.
|
|
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
|
-
/**
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
*
|
|
222
|
-
*
|
|
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
|
|
279
|
-
const
|
|
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
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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
|
|
328
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
-
/**
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
if (
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
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
|
}
|