UNPKG

37 kBJavaScriptView Raw
1import * as i1 from '@angular/cdk/a11y';
2import { A11yModule } from '@angular/cdk/a11y';
3import * as i1$1 from '@angular/cdk/overlay';
4import { Overlay, OverlayConfig, OverlayRef, OverlayModule } from '@angular/cdk/overlay';
5import { _getFocusedElementPierceShadowDom } from '@angular/cdk/platform';
6import * as i3 from '@angular/cdk/portal';
7import { BasePortalOutlet, CdkPortalOutlet, ComponentPortal, TemplatePortal, PortalModule } from '@angular/cdk/portal';
8import { DOCUMENT } from '@angular/common';
9import * as i0 from '@angular/core';
10import { Component, ViewEncapsulation, ChangeDetectionStrategy, Optional, Inject, ViewChild, InjectionToken, Injector, TemplateRef, Injectable, SkipSelf, NgModule } from '@angular/core';
11import { ESCAPE, hasModifierKey } from '@angular/cdk/keycodes';
12import { Subject, defer, of } from 'rxjs';
13import { Directionality } from '@angular/cdk/bidi';
14import { startWith } from 'rxjs/operators';
15
16/** Configuration for opening a modal dialog. */
17class DialogConfig {
18 constructor() {
19 /** The ARIA role of the dialog element. */
20 this.role = 'dialog';
21 /** Optional CSS class or classes applied to the overlay panel. */
22 this.panelClass = '';
23 /** Whether the dialog has a backdrop. */
24 this.hasBackdrop = true;
25 /** Optional CSS class or classes applied to the overlay backdrop. */
26 this.backdropClass = '';
27 /** Whether the dialog closes with the escape key or pointer events outside the panel element. */
28 this.disableClose = false;
29 /** Width of the dialog. */
30 this.width = '';
31 /** Height of the dialog. */
32 this.height = '';
33 /** Data being injected into the child component. */
34 this.data = null;
35 /** ID of the element that describes the dialog. */
36 this.ariaDescribedBy = null;
37 /** ID of the element that labels the dialog. */
38 this.ariaLabelledBy = null;
39 /** Dialog label applied via `aria-label` */
40 this.ariaLabel = null;
41 /** Whether this is a modal dialog. Used to set the `aria-modal` attribute. */
42 this.ariaModal = true;
43 /**
44 * Where the dialog should focus on open.
45 * @breaking-change 14.0.0 Remove boolean option from autoFocus. Use string or
46 * AutoFocusTarget instead.
47 */
48 this.autoFocus = 'first-tabbable';
49 /**
50 * Whether the dialog should restore focus to the previously-focused element upon closing.
51 * Has the following behavior based on the type that is passed in:
52 * - `boolean` - when true, will return focus to the element that was focused before the dialog
53 * was opened, otherwise won't restore focus at all.
54 * - `string` - focus will be restored to the first element that matches the CSS selector.
55 * - `HTMLElement` - focus will be restored to the specific element.
56 */
57 this.restoreFocus = true;
58 /**
59 * Whether the dialog should close when the user navigates backwards or forwards through browser
60 * history. This does not apply to navigation via anchor element unless using URL-hash based
61 * routing (`HashLocationStrategy` in the Angular router).
62 */
63 this.closeOnNavigation = true;
64 /**
65 * Whether the dialog should close when the dialog service is destroyed. This is useful if
66 * another service is wrapping the dialog and is managing the destruction instead.
67 */
68 this.closeOnDestroy = true;
69 /**
70 * Whether the dialog should close when the underlying overlay is detached. This is useful if
71 * another service is wrapping the dialog and is managing the destruction instead. E.g. an
72 * external detachment can happen as a result of a scroll strategy triggering it or when the
73 * browser location changes.
74 */
75 this.closeOnOverlayDetachments = true;
76 }
77}
78
79function throwDialogContentAlreadyAttachedError() {
80 throw Error('Attempting to attach dialog content after content is already attached');
81}
82/**
83 * Internal component that wraps user-provided dialog content.
84 * @docs-private
85 */
86class CdkDialogContainer extends BasePortalOutlet {
87 constructor(_elementRef, _focusTrapFactory, _document, _config, _interactivityChecker, _ngZone, _overlayRef, _focusMonitor) {
88 super();
89 this._elementRef = _elementRef;
90 this._focusTrapFactory = _focusTrapFactory;
91 this._config = _config;
92 this._interactivityChecker = _interactivityChecker;
93 this._ngZone = _ngZone;
94 this._overlayRef = _overlayRef;
95 this._focusMonitor = _focusMonitor;
96 /** Element that was focused before the dialog was opened. Save this to restore upon close. */
97 this._elementFocusedBeforeDialogWasOpened = null;
98 /**
99 * Type of interaction that led to the dialog being closed. This is used to determine
100 * whether the focus style will be applied when returning focus to its original location
101 * after the dialog is closed.
102 */
103 this._closeInteractionType = null;
104 /**
105 * Attaches a DOM portal to the dialog container.
106 * @param portal Portal to be attached.
107 * @deprecated To be turned into a method.
108 * @breaking-change 10.0.0
109 */
110 this.attachDomPortal = (portal) => {
111 if (this._portalOutlet.hasAttached() && (typeof ngDevMode === 'undefined' || ngDevMode)) {
112 throwDialogContentAlreadyAttachedError();
113 }
114 const result = this._portalOutlet.attachDomPortal(portal);
115 this._contentAttached();
116 return result;
117 };
118 this._ariaLabelledBy = this._config.ariaLabelledBy || null;
119 this._document = _document;
120 }
121 _contentAttached() {
122 this._initializeFocusTrap();
123 this._handleBackdropClicks();
124 this._captureInitialFocus();
125 }
126 /**
127 * Can be used by child classes to customize the initial focus
128 * capturing behavior (e.g. if it's tied to an animation).
129 */
130 _captureInitialFocus() {
131 this._trapFocus();
132 }
133 ngOnDestroy() {
134 this._restoreFocus();
135 }
136 /**
137 * Attach a ComponentPortal as content to this dialog container.
138 * @param portal Portal to be attached as the dialog content.
139 */
140 attachComponentPortal(portal) {
141 if (this._portalOutlet.hasAttached() && (typeof ngDevMode === 'undefined' || ngDevMode)) {
142 throwDialogContentAlreadyAttachedError();
143 }
144 const result = this._portalOutlet.attachComponentPortal(portal);
145 this._contentAttached();
146 return result;
147 }
148 /**
149 * Attach a TemplatePortal as content to this dialog container.
150 * @param portal Portal to be attached as the dialog content.
151 */
152 attachTemplatePortal(portal) {
153 if (this._portalOutlet.hasAttached() && (typeof ngDevMode === 'undefined' || ngDevMode)) {
154 throwDialogContentAlreadyAttachedError();
155 }
156 const result = this._portalOutlet.attachTemplatePortal(portal);
157 this._contentAttached();
158 return result;
159 }
160 // TODO(crisbeto): this shouldn't be exposed, but there are internal references to it.
161 /** Captures focus if it isn't already inside the dialog. */
162 _recaptureFocus() {
163 if (!this._containsFocus()) {
164 this._trapFocus();
165 }
166 }
167 /**
168 * Focuses the provided element. If the element is not focusable, it will add a tabIndex
169 * attribute to forcefully focus it. The attribute is removed after focus is moved.
170 * @param element The element to focus.
171 */
172 _forceFocus(element, options) {
173 if (!this._interactivityChecker.isFocusable(element)) {
174 element.tabIndex = -1;
175 // The tabindex attribute should be removed to avoid navigating to that element again
176 this._ngZone.runOutsideAngular(() => {
177 const callback = () => {
178 element.removeEventListener('blur', callback);
179 element.removeEventListener('mousedown', callback);
180 element.removeAttribute('tabindex');
181 };
182 element.addEventListener('blur', callback);
183 element.addEventListener('mousedown', callback);
184 });
185 }
186 element.focus(options);
187 }
188 /**
189 * Focuses the first element that matches the given selector within the focus trap.
190 * @param selector The CSS selector for the element to set focus to.
191 */
192 _focusByCssSelector(selector, options) {
193 let elementToFocus = this._elementRef.nativeElement.querySelector(selector);
194 if (elementToFocus) {
195 this._forceFocus(elementToFocus, options);
196 }
197 }
198 /**
199 * Moves the focus inside the focus trap. When autoFocus is not set to 'dialog', if focus
200 * cannot be moved then focus will go to the dialog container.
201 */
202 _trapFocus() {
203 const element = this._elementRef.nativeElement;
204 // If were to attempt to focus immediately, then the content of the dialog would not yet be
205 // ready in instances where change detection has to run first. To deal with this, we simply
206 // wait for the microtask queue to be empty when setting focus when autoFocus isn't set to
207 // dialog. If the element inside the dialog can't be focused, then the container is focused
208 // so the user can't tab into other elements behind it.
209 switch (this._config.autoFocus) {
210 case false:
211 case 'dialog':
212 // Ensure that focus is on the dialog container. It's possible that a different
213 // component tried to move focus while the open animation was running. See:
214 // https://github.com/angular/components/issues/16215. Note that we only want to do this
215 // if the focus isn't inside the dialog already, because it's possible that the consumer
216 // turned off `autoFocus` in order to move focus themselves.
217 if (!this._containsFocus()) {
218 element.focus();
219 }
220 break;
221 case true:
222 case 'first-tabbable':
223 this._focusTrap.focusInitialElementWhenReady().then(focusedSuccessfully => {
224 // If we weren't able to find a focusable element in the dialog, then focus the dialog
225 // container instead.
226 if (!focusedSuccessfully) {
227 this._focusDialogContainer();
228 }
229 });
230 break;
231 case 'first-heading':
232 this._focusByCssSelector('h1, h2, h3, h4, h5, h6, [role="heading"]');
233 break;
234 default:
235 this._focusByCssSelector(this._config.autoFocus);
236 break;
237 }
238 }
239 /** Restores focus to the element that was focused before the dialog opened. */
240 _restoreFocus() {
241 const focusConfig = this._config.restoreFocus;
242 let focusTargetElement = null;
243 if (typeof focusConfig === 'string') {
244 focusTargetElement = this._document.querySelector(focusConfig);
245 }
246 else if (typeof focusConfig === 'boolean') {
247 focusTargetElement = focusConfig ? this._elementFocusedBeforeDialogWasOpened : null;
248 }
249 else if (focusConfig) {
250 focusTargetElement = focusConfig;
251 }
252 // We need the extra check, because IE can set the `activeElement` to null in some cases.
253 if (this._config.restoreFocus &&
254 focusTargetElement &&
255 typeof focusTargetElement.focus === 'function') {
256 const activeElement = _getFocusedElementPierceShadowDom();
257 const element = this._elementRef.nativeElement;
258 // Make sure that focus is still inside the dialog or is on the body (usually because a
259 // non-focusable element like the backdrop was clicked) before moving it. It's possible that
260 // the consumer moved it themselves before the animation was done, in which case we shouldn't
261 // do anything.
262 if (!activeElement ||
263 activeElement === this._document.body ||
264 activeElement === element ||
265 element.contains(activeElement)) {
266 if (this._focusMonitor) {
267 this._focusMonitor.focusVia(focusTargetElement, this._closeInteractionType);
268 this._closeInteractionType = null;
269 }
270 else {
271 focusTargetElement.focus();
272 }
273 }
274 }
275 if (this._focusTrap) {
276 this._focusTrap.destroy();
277 }
278 }
279 /** Focuses the dialog container. */
280 _focusDialogContainer() {
281 // Note that there is no focus method when rendering on the server.
282 if (this._elementRef.nativeElement.focus) {
283 this._elementRef.nativeElement.focus();
284 }
285 }
286 /** Returns whether focus is inside the dialog. */
287 _containsFocus() {
288 const element = this._elementRef.nativeElement;
289 const activeElement = _getFocusedElementPierceShadowDom();
290 return element === activeElement || element.contains(activeElement);
291 }
292 /** Sets up the focus trap. */
293 _initializeFocusTrap() {
294 this._focusTrap = this._focusTrapFactory.create(this._elementRef.nativeElement);
295 // Save the previously focused element. This element will be re-focused
296 // when the dialog closes.
297 if (this._document) {
298 this._elementFocusedBeforeDialogWasOpened = _getFocusedElementPierceShadowDom();
299 }
300 }
301 /** Sets up the listener that handles clicks on the dialog backdrop. */
302 _handleBackdropClicks() {
303 // Clicking on the backdrop will move focus out of dialog.
304 // Recapture it if closing via the backdrop is disabled.
305 this._overlayRef.backdropClick().subscribe(() => {
306 if (this._config.disableClose) {
307 this._recaptureFocus();
308 }
309 });
310 }
311 static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: CdkDialogContainer, deps: [{ token: i0.ElementRef }, { token: i1.FocusTrapFactory }, { token: DOCUMENT, optional: true }, { token: DialogConfig }, { token: i1.InteractivityChecker }, { token: i0.NgZone }, { token: i1$1.OverlayRef }, { token: i1.FocusMonitor }], target: i0.ɵɵFactoryTarget.Component }); }
312 static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.0.0", type: CdkDialogContainer, selector: "cdk-dialog-container", host: { attributes: { "tabindex": "-1" }, properties: { "attr.id": "_config.id || null", "attr.role": "_config.role", "attr.aria-modal": "_config.ariaModal", "attr.aria-labelledby": "_config.ariaLabel ? null : _ariaLabelledBy", "attr.aria-label": "_config.ariaLabel", "attr.aria-describedby": "_config.ariaDescribedBy || null" }, classAttribute: "cdk-dialog-container" }, viewQueries: [{ propertyName: "_portalOutlet", first: true, predicate: CdkPortalOutlet, descendants: true, static: true }], usesInheritance: true, ngImport: i0, template: "<ng-template cdkPortalOutlet></ng-template>\n", styles: [".cdk-dialog-container{display:block;width:100%;height:100%;min-height:inherit;max-height:inherit}"], dependencies: [{ kind: "directive", type: i3.CdkPortalOutlet, selector: "[cdkPortalOutlet]", inputs: ["cdkPortalOutlet"], outputs: ["attached"], exportAs: ["cdkPortalOutlet"] }], changeDetection: i0.ChangeDetectionStrategy.Default, encapsulation: i0.ViewEncapsulation.None }); }
313}
314i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: CdkDialogContainer, decorators: [{
315 type: Component,
316 args: [{ selector: 'cdk-dialog-container', encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.Default, host: {
317 'class': 'cdk-dialog-container',
318 'tabindex': '-1',
319 '[attr.id]': '_config.id || null',
320 '[attr.role]': '_config.role',
321 '[attr.aria-modal]': '_config.ariaModal',
322 '[attr.aria-labelledby]': '_config.ariaLabel ? null : _ariaLabelledBy',
323 '[attr.aria-label]': '_config.ariaLabel',
324 '[attr.aria-describedby]': '_config.ariaDescribedBy || null',
325 }, template: "<ng-template cdkPortalOutlet></ng-template>\n", styles: [".cdk-dialog-container{display:block;width:100%;height:100%;min-height:inherit;max-height:inherit}"] }]
326 }], ctorParameters: function () { return [{ type: i0.ElementRef }, { type: i1.FocusTrapFactory }, { type: undefined, decorators: [{
327 type: Optional
328 }, {
329 type: Inject,
330 args: [DOCUMENT]
331 }] }, { type: undefined, decorators: [{
332 type: Inject,
333 args: [DialogConfig]
334 }] }, { type: i1.InteractivityChecker }, { type: i0.NgZone }, { type: i1$1.OverlayRef }, { type: i1.FocusMonitor }]; }, propDecorators: { _portalOutlet: [{
335 type: ViewChild,
336 args: [CdkPortalOutlet, { static: true }]
337 }] } });
338
339/**
340 * Reference to a dialog opened via the Dialog service.
341 */
342class DialogRef {
343 constructor(overlayRef, config) {
344 this.overlayRef = overlayRef;
345 this.config = config;
346 /** Emits when the dialog has been closed. */
347 this.closed = new Subject();
348 this.disableClose = config.disableClose;
349 this.backdropClick = overlayRef.backdropClick();
350 this.keydownEvents = overlayRef.keydownEvents();
351 this.outsidePointerEvents = overlayRef.outsidePointerEvents();
352 this.id = config.id; // By the time the dialog is created we are guaranteed to have an ID.
353 this.keydownEvents.subscribe(event => {
354 if (event.keyCode === ESCAPE && !this.disableClose && !hasModifierKey(event)) {
355 event.preventDefault();
356 this.close(undefined, { focusOrigin: 'keyboard' });
357 }
358 });
359 this.backdropClick.subscribe(() => {
360 if (!this.disableClose) {
361 this.close(undefined, { focusOrigin: 'mouse' });
362 }
363 });
364 this._detachSubscription = overlayRef.detachments().subscribe(() => {
365 // Check specifically for `false`, because we want `undefined` to be treated like `true`.
366 if (config.closeOnOverlayDetachments !== false) {
367 this.close();
368 }
369 });
370 }
371 /**
372 * Close the dialog.
373 * @param result Optional result to return to the dialog opener.
374 * @param options Additional options to customize the closing behavior.
375 */
376 close(result, options) {
377 if (this.containerInstance) {
378 const closedSubject = this.closed;
379 this.containerInstance._closeInteractionType = options?.focusOrigin || 'program';
380 // Drop the detach subscription first since it can be triggered by the
381 // `dispose` call and override the result of this closing sequence.
382 this._detachSubscription.unsubscribe();
383 this.overlayRef.dispose();
384 closedSubject.next(result);
385 closedSubject.complete();
386 this.componentInstance = this.containerInstance = null;
387 }
388 }
389 /** Updates the position of the dialog based on the current position strategy. */
390 updatePosition() {
391 this.overlayRef.updatePosition();
392 return this;
393 }
394 /**
395 * Updates the dialog's width and height.
396 * @param width New width of the dialog.
397 * @param height New height of the dialog.
398 */
399 updateSize(width = '', height = '') {
400 this.overlayRef.updateSize({ width, height });
401 return this;
402 }
403 /** Add a CSS class or an array of classes to the overlay pane. */
404 addPanelClass(classes) {
405 this.overlayRef.addPanelClass(classes);
406 return this;
407 }
408 /** Remove a CSS class or an array of classes from the overlay pane. */
409 removePanelClass(classes) {
410 this.overlayRef.removePanelClass(classes);
411 return this;
412 }
413}
414
415/** Injection token for the Dialog's ScrollStrategy. */
416const DIALOG_SCROLL_STRATEGY = new InjectionToken('DialogScrollStrategy');
417/** Injection token for the Dialog's Data. */
418const DIALOG_DATA = new InjectionToken('DialogData');
419/** Injection token that can be used to provide default options for the dialog module. */
420const DEFAULT_DIALOG_CONFIG = new InjectionToken('DefaultDialogConfig');
421/** @docs-private */
422function DIALOG_SCROLL_STRATEGY_PROVIDER_FACTORY(overlay) {
423 return () => overlay.scrollStrategies.block();
424}
425/** @docs-private */
426const DIALOG_SCROLL_STRATEGY_PROVIDER = {
427 provide: DIALOG_SCROLL_STRATEGY,
428 deps: [Overlay],
429 useFactory: DIALOG_SCROLL_STRATEGY_PROVIDER_FACTORY,
430};
431
432/** Unique id for the created dialog. */
433let uniqueId = 0;
434class Dialog {
435 /** Keeps track of the currently-open dialogs. */
436 get openDialogs() {
437 return this._parentDialog ? this._parentDialog.openDialogs : this._openDialogsAtThisLevel;
438 }
439 /** Stream that emits when a dialog has been opened. */
440 get afterOpened() {
441 return this._parentDialog ? this._parentDialog.afterOpened : this._afterOpenedAtThisLevel;
442 }
443 constructor(_overlay, _injector, _defaultOptions, _parentDialog, _overlayContainer, scrollStrategy) {
444 this._overlay = _overlay;
445 this._injector = _injector;
446 this._defaultOptions = _defaultOptions;
447 this._parentDialog = _parentDialog;
448 this._overlayContainer = _overlayContainer;
449 this._openDialogsAtThisLevel = [];
450 this._afterAllClosedAtThisLevel = new Subject();
451 this._afterOpenedAtThisLevel = new Subject();
452 this._ariaHiddenElements = new Map();
453 /**
454 * Stream that emits when all open dialog have finished closing.
455 * Will emit on subscribe if there are no open dialogs to begin with.
456 */
457 this.afterAllClosed = defer(() => this.openDialogs.length
458 ? this._getAfterAllClosed()
459 : this._getAfterAllClosed().pipe(startWith(undefined)));
460 this._scrollStrategy = scrollStrategy;
461 }
462 open(componentOrTemplateRef, config) {
463 const defaults = (this._defaultOptions || new DialogConfig());
464 config = { ...defaults, ...config };
465 config.id = config.id || `cdk-dialog-${uniqueId++}`;
466 if (config.id &&
467 this.getDialogById(config.id) &&
468 (typeof ngDevMode === 'undefined' || ngDevMode)) {
469 throw Error(`Dialog with id "${config.id}" exists already. The dialog id must be unique.`);
470 }
471 const overlayConfig = this._getOverlayConfig(config);
472 const overlayRef = this._overlay.create(overlayConfig);
473 const dialogRef = new DialogRef(overlayRef, config);
474 const dialogContainer = this._attachContainer(overlayRef, dialogRef, config);
475 dialogRef.containerInstance = dialogContainer;
476 this._attachDialogContent(componentOrTemplateRef, dialogRef, dialogContainer, config);
477 // If this is the first dialog that we're opening, hide all the non-overlay content.
478 if (!this.openDialogs.length) {
479 this._hideNonDialogContentFromAssistiveTechnology();
480 }
481 this.openDialogs.push(dialogRef);
482 dialogRef.closed.subscribe(() => this._removeOpenDialog(dialogRef, true));
483 this.afterOpened.next(dialogRef);
484 return dialogRef;
485 }
486 /**
487 * Closes all of the currently-open dialogs.
488 */
489 closeAll() {
490 reverseForEach(this.openDialogs, dialog => dialog.close());
491 }
492 /**
493 * Finds an open dialog by its id.
494 * @param id ID to use when looking up the dialog.
495 */
496 getDialogById(id) {
497 return this.openDialogs.find(dialog => dialog.id === id);
498 }
499 ngOnDestroy() {
500 // Make one pass over all the dialogs that need to be untracked, but should not be closed. We
501 // want to stop tracking the open dialog even if it hasn't been closed, because the tracking
502 // determines when `aria-hidden` is removed from elements outside the dialog.
503 reverseForEach(this._openDialogsAtThisLevel, dialog => {
504 // Check for `false` specifically since we want `undefined` to be interpreted as `true`.
505 if (dialog.config.closeOnDestroy === false) {
506 this._removeOpenDialog(dialog, false);
507 }
508 });
509 // Make a second pass and close the remaining dialogs. We do this second pass in order to
510 // correctly dispatch the `afterAllClosed` event in case we have a mixed array of dialogs
511 // that should be closed and dialogs that should not.
512 reverseForEach(this._openDialogsAtThisLevel, dialog => dialog.close());
513 this._afterAllClosedAtThisLevel.complete();
514 this._afterOpenedAtThisLevel.complete();
515 this._openDialogsAtThisLevel = [];
516 }
517 /**
518 * Creates an overlay config from a dialog config.
519 * @param config The dialog configuration.
520 * @returns The overlay configuration.
521 */
522 _getOverlayConfig(config) {
523 const state = new OverlayConfig({
524 positionStrategy: config.positionStrategy ||
525 this._overlay.position().global().centerHorizontally().centerVertically(),
526 scrollStrategy: config.scrollStrategy || this._scrollStrategy(),
527 panelClass: config.panelClass,
528 hasBackdrop: config.hasBackdrop,
529 direction: config.direction,
530 minWidth: config.minWidth,
531 minHeight: config.minHeight,
532 maxWidth: config.maxWidth,
533 maxHeight: config.maxHeight,
534 width: config.width,
535 height: config.height,
536 disposeOnNavigation: config.closeOnNavigation,
537 });
538 if (config.backdropClass) {
539 state.backdropClass = config.backdropClass;
540 }
541 return state;
542 }
543 /**
544 * Attaches a dialog container to a dialog's already-created overlay.
545 * @param overlay Reference to the dialog's underlying overlay.
546 * @param config The dialog configuration.
547 * @returns A promise resolving to a ComponentRef for the attached container.
548 */
549 _attachContainer(overlay, dialogRef, config) {
550 const userInjector = config.injector || config.viewContainerRef?.injector;
551 const providers = [
552 { provide: DialogConfig, useValue: config },
553 { provide: DialogRef, useValue: dialogRef },
554 { provide: OverlayRef, useValue: overlay },
555 ];
556 let containerType;
557 if (config.container) {
558 if (typeof config.container === 'function') {
559 containerType = config.container;
560 }
561 else {
562 containerType = config.container.type;
563 providers.push(...config.container.providers(config));
564 }
565 }
566 else {
567 containerType = CdkDialogContainer;
568 }
569 const containerPortal = new ComponentPortal(containerType, config.viewContainerRef, Injector.create({ parent: userInjector || this._injector, providers }), config.componentFactoryResolver);
570 const containerRef = overlay.attach(containerPortal);
571 return containerRef.instance;
572 }
573 /**
574 * Attaches the user-provided component to the already-created dialog container.
575 * @param componentOrTemplateRef The type of component being loaded into the dialog,
576 * or a TemplateRef to instantiate as the content.
577 * @param dialogRef Reference to the dialog being opened.
578 * @param dialogContainer Component that is going to wrap the dialog content.
579 * @param config Configuration used to open the dialog.
580 */
581 _attachDialogContent(componentOrTemplateRef, dialogRef, dialogContainer, config) {
582 if (componentOrTemplateRef instanceof TemplateRef) {
583 const injector = this._createInjector(config, dialogRef, dialogContainer, undefined);
584 let context = { $implicit: config.data, dialogRef };
585 if (config.templateContext) {
586 context = {
587 ...context,
588 ...(typeof config.templateContext === 'function'
589 ? config.templateContext()
590 : config.templateContext),
591 };
592 }
593 dialogContainer.attachTemplatePortal(new TemplatePortal(componentOrTemplateRef, null, context, injector));
594 }
595 else {
596 const injector = this._createInjector(config, dialogRef, dialogContainer, this._injector);
597 const contentRef = dialogContainer.attachComponentPortal(new ComponentPortal(componentOrTemplateRef, config.viewContainerRef, injector, config.componentFactoryResolver));
598 dialogRef.componentInstance = contentRef.instance;
599 }
600 }
601 /**
602 * Creates a custom injector to be used inside the dialog. This allows a component loaded inside
603 * of a dialog to close itself and, optionally, to return a value.
604 * @param config Config object that is used to construct the dialog.
605 * @param dialogRef Reference to the dialog being opened.
606 * @param dialogContainer Component that is going to wrap the dialog content.
607 * @param fallbackInjector Injector to use as a fallback when a lookup fails in the custom
608 * dialog injector, if the user didn't provide a custom one.
609 * @returns The custom injector that can be used inside the dialog.
610 */
611 _createInjector(config, dialogRef, dialogContainer, fallbackInjector) {
612 const userInjector = config.injector || config.viewContainerRef?.injector;
613 const providers = [
614 { provide: DIALOG_DATA, useValue: config.data },
615 { provide: DialogRef, useValue: dialogRef },
616 ];
617 if (config.providers) {
618 if (typeof config.providers === 'function') {
619 providers.push(...config.providers(dialogRef, config, dialogContainer));
620 }
621 else {
622 providers.push(...config.providers);
623 }
624 }
625 if (config.direction &&
626 (!userInjector ||
627 !userInjector.get(Directionality, null, { optional: true }))) {
628 providers.push({
629 provide: Directionality,
630 useValue: { value: config.direction, change: of() },
631 });
632 }
633 return Injector.create({ parent: userInjector || fallbackInjector, providers });
634 }
635 /**
636 * Removes a dialog from the array of open dialogs.
637 * @param dialogRef Dialog to be removed.
638 * @param emitEvent Whether to emit an event if this is the last dialog.
639 */
640 _removeOpenDialog(dialogRef, emitEvent) {
641 const index = this.openDialogs.indexOf(dialogRef);
642 if (index > -1) {
643 this.openDialogs.splice(index, 1);
644 // If all the dialogs were closed, remove/restore the `aria-hidden`
645 // to a the siblings and emit to the `afterAllClosed` stream.
646 if (!this.openDialogs.length) {
647 this._ariaHiddenElements.forEach((previousValue, element) => {
648 if (previousValue) {
649 element.setAttribute('aria-hidden', previousValue);
650 }
651 else {
652 element.removeAttribute('aria-hidden');
653 }
654 });
655 this._ariaHiddenElements.clear();
656 if (emitEvent) {
657 this._getAfterAllClosed().next();
658 }
659 }
660 }
661 }
662 /** Hides all of the content that isn't an overlay from assistive technology. */
663 _hideNonDialogContentFromAssistiveTechnology() {
664 const overlayContainer = this._overlayContainer.getContainerElement();
665 // Ensure that the overlay container is attached to the DOM.
666 if (overlayContainer.parentElement) {
667 const siblings = overlayContainer.parentElement.children;
668 for (let i = siblings.length - 1; i > -1; i--) {
669 const sibling = siblings[i];
670 if (sibling !== overlayContainer &&
671 sibling.nodeName !== 'SCRIPT' &&
672 sibling.nodeName !== 'STYLE' &&
673 !sibling.hasAttribute('aria-live')) {
674 this._ariaHiddenElements.set(sibling, sibling.getAttribute('aria-hidden'));
675 sibling.setAttribute('aria-hidden', 'true');
676 }
677 }
678 }
679 }
680 _getAfterAllClosed() {
681 const parent = this._parentDialog;
682 return parent ? parent._getAfterAllClosed() : this._afterAllClosedAtThisLevel;
683 }
684 static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: Dialog, deps: [{ token: i1$1.Overlay }, { token: i0.Injector }, { token: DEFAULT_DIALOG_CONFIG, optional: true }, { token: Dialog, optional: true, skipSelf: true }, { token: i1$1.OverlayContainer }, { token: DIALOG_SCROLL_STRATEGY }], target: i0.ɵɵFactoryTarget.Injectable }); }
685 static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: Dialog }); }
686}
687i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: Dialog, decorators: [{
688 type: Injectable
689 }], ctorParameters: function () { return [{ type: i1$1.Overlay }, { type: i0.Injector }, { type: DialogConfig, decorators: [{
690 type: Optional
691 }, {
692 type: Inject,
693 args: [DEFAULT_DIALOG_CONFIG]
694 }] }, { type: Dialog, decorators: [{
695 type: Optional
696 }, {
697 type: SkipSelf
698 }] }, { type: i1$1.OverlayContainer }, { type: undefined, decorators: [{
699 type: Inject,
700 args: [DIALOG_SCROLL_STRATEGY]
701 }] }]; } });
702/**
703 * Executes a callback against all elements in an array while iterating in reverse.
704 * Useful if the array is being modified as it is being iterated.
705 */
706function reverseForEach(items, callback) {
707 let i = items.length;
708 while (i--) {
709 callback(items[i]);
710 }
711}
712
713class DialogModule {
714 static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: DialogModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
715 static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "16.0.0", ngImport: i0, type: DialogModule, declarations: [CdkDialogContainer], imports: [OverlayModule, PortalModule, A11yModule], exports: [
716 // Re-export the PortalModule so that people extending the `CdkDialogContainer`
717 // don't have to remember to import it or be faced with an unhelpful error.
718 PortalModule,
719 CdkDialogContainer] }); }
720 static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: DialogModule, providers: [Dialog, DIALOG_SCROLL_STRATEGY_PROVIDER], imports: [OverlayModule, PortalModule, A11yModule,
721 // Re-export the PortalModule so that people extending the `CdkDialogContainer`
722 // don't have to remember to import it or be faced with an unhelpful error.
723 PortalModule] }); }
724}
725i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: DialogModule, decorators: [{
726 type: NgModule,
727 args: [{
728 imports: [OverlayModule, PortalModule, A11yModule],
729 exports: [
730 // Re-export the PortalModule so that people extending the `CdkDialogContainer`
731 // don't have to remember to import it or be faced with an unhelpful error.
732 PortalModule,
733 CdkDialogContainer,
734 ],
735 declarations: [CdkDialogContainer],
736 providers: [Dialog, DIALOG_SCROLL_STRATEGY_PROVIDER],
737 }]
738 }] });
739
740/**
741 * Generated bundle index. Do not edit.
742 */
743
744export { CdkDialogContainer, DEFAULT_DIALOG_CONFIG, DIALOG_DATA, DIALOG_SCROLL_STRATEGY, DIALOG_SCROLL_STRATEGY_PROVIDER, DIALOG_SCROLL_STRATEGY_PROVIDER_FACTORY, Dialog, DialogConfig, DialogModule, DialogRef, throwDialogContentAlreadyAttachedError };
745//# sourceMappingURL=dialog.mjs.map