dialog.mjs 36 KB

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