dialog.mjs 41 KB

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