@almoamendev/ngx-md3 0.0.16 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -10,6 +10,8 @@ import { ComponentPortal, CdkPortalOutlet } from '@angular/cdk/portal';
10
10
  import * as i2 from '@angular/cdk/scrolling';
11
11
  import { CdkScrollable } from '@angular/cdk/scrolling';
12
12
  import { FormControlName } from '@angular/forms';
13
+ import { CdkDialogContainer, Dialog as Dialog$1 } from '@angular/cdk/dialog';
14
+ import { hasModifierKey } from '@angular/cdk/keycodes';
13
15
  import { Overlay, OverlayConfig } from '@angular/cdk/overlay';
14
16
 
15
17
  class TypeDisplay {
@@ -3087,42 +3089,139 @@ const DIALOG_DATA = new InjectionToken('MD3_DIALOG_DATA');
3087
3089
  const DIALOG_CONFIG = new InjectionToken('MD3_DIALOG_CONFIG');
3088
3090
  const DIALOG_COMPONENT = new InjectionToken('MD3_DIALOG_COMPONENT');
3089
3091
  const DIALOG_EXIT_ANIMATION_FALLBACK_MS = 300;
3092
+ const HIDDEN_CLASS = 'md3-dialog-hidden';
3090
3093
  class DialogRef {
3091
- overlayRef;
3092
- previouslyFocusedElement;
3094
+ cdkRef;
3093
3095
  closed = new Subject();
3094
- isClosed = false;
3096
+ closing = new Subject();
3097
+ closePromise = null;
3098
+ closeStarted = false;
3099
+ closeSettled = false;
3100
+ hidden = false;
3095
3101
  dialogInstance;
3096
3102
  /**
3097
3103
  * Filled by DialogService after the user component is attached. Keeping the
3098
3104
  * instance here lets callers imperatively update inputs when that is useful.
3099
3105
  */
3100
3106
  componentInstance;
3101
- constructor(overlayRef, previouslyFocusedElement) {
3102
- this.overlayRef = overlayRef;
3103
- this.previouslyFocusedElement = previouslyFocusedElement;
3107
+ /** Overlay hosting the dialog, useful to reach the panel and the scrim. */
3108
+ get overlayRef() {
3109
+ return this.cdkRef.overlayRef;
3110
+ }
3111
+ /** Whether the dialog started closing. Such a dialog cannot be hidden or shown anymore. */
3112
+ get isClosing() {
3113
+ return this.closeStarted;
3114
+ }
3115
+ /** Whether the dialog is currently hidden behind another dialog. */
3116
+ get isHidden() {
3117
+ return this.hidden;
3118
+ }
3119
+ constructor(cdkRef, disableCloseEvents) {
3120
+ this.cdkRef = cdkRef;
3121
+ // Closing always goes through this reference so the exit animation can
3122
+ // run before the CDK disposes the overlay. Detachments triggered from
3123
+ // the outside (navigation, scroll strategy) still settle this ref.
3124
+ this.cdkRef.closed.pipe(take(1)).subscribe((result) => this.settle(result));
3125
+ if (!disableCloseEvents) {
3126
+ this.connectCloseEvents();
3127
+ }
3104
3128
  }
3129
+ /** Closes the dialog. The promise resolves once the exit animation is done. */
3105
3130
  close(result) {
3106
- if (this.isClosed) {
3131
+ if (this.closePromise) {
3132
+ return this.closePromise;
3133
+ }
3134
+ this.startClosing();
3135
+ this.closePromise = this.startCloseAnimation().then(() => this.cdkRef.close(result));
3136
+ return this.closePromise;
3137
+ }
3138
+ /**
3139
+ * Hides the dialog with the closing animation while keeping it alive, so
3140
+ * its content, form state and subscriptions survive until show() is called.
3141
+ * The promise resolves once the dialog is out of sight.
3142
+ */
3143
+ hide() {
3144
+ if (this.hidden || this.closeStarted) {
3145
+ return Promise.resolve();
3146
+ }
3147
+ this.hidden = true;
3148
+ this.toggleHiddenState(true);
3149
+ this.dialogInstance?.isActive.set(false);
3150
+ return this.waitForSurfaceAnimation();
3151
+ }
3152
+ /**
3153
+ * Brings a hidden dialog back with the opening animation. Focus moves back
3154
+ * into the dialog even when it was never hidden, which is what makes it
3155
+ * usable again once the dialog above it closes.
3156
+ */
3157
+ show() {
3158
+ if (this.closeStarted) {
3107
3159
  return;
3108
3160
  }
3109
- this.isClosed = true;
3110
- this.startCloseAnimation().then(() => {
3111
- // Disposing the overlay removes the wrapper, the backdrop, and the
3112
- // dynamically attached content component in one operation.
3113
- this.overlayRef.dispose();
3114
- this.previouslyFocusedElement?.focus();
3115
- this.closed.next(result);
3116
- this.closed.complete();
3161
+ if (this.hidden) {
3162
+ this.hidden = false;
3163
+ this.toggleHiddenState(false);
3164
+ this.dialogInstance?.isActive.set(true);
3165
+ }
3166
+ this.dialogInstance?._recaptureFocus();
3167
+ }
3168
+ /** Emits when the dialog starts closing, before the exit animation runs. */
3169
+ beforeClosed() {
3170
+ return this.closing.asObservable();
3171
+ }
3172
+ afterClosed() {
3173
+ return this.closed.asObservable();
3174
+ }
3175
+ connectCloseEvents() {
3176
+ // The CDK config keeps `disableClose` on, so the scrim and Escape key
3177
+ // are handled here instead and the dialog can animate out.
3178
+ this.cdkRef.backdropClick.pipe(take(1)).subscribe(() => {
3179
+ this.cdkRef.containerInstance._closeInteractionType = 'mouse';
3180
+ this.close();
3181
+ });
3182
+ this.cdkRef.keydownEvents.pipe(filter((event) => event.key === 'Escape' && !hasModifierKey(event)), take(1)).subscribe((event) => {
3183
+ event.preventDefault();
3184
+ this.cdkRef.containerInstance._closeInteractionType = 'keyboard';
3185
+ this.close();
3117
3186
  });
3118
3187
  }
3188
+ toggleHiddenState(isHidden) {
3189
+ const overlayRef = this.cdkRef.overlayRef;
3190
+ const panel = overlayRef.overlayElement;
3191
+ const backdrop = overlayRef.backdropElement;
3192
+ panel.classList.toggle(HIDDEN_CLASS, isHidden);
3193
+ backdrop?.classList.toggle(HIDDEN_CLASS, isHidden);
3194
+ // `inert` keeps a hidden dialog out of the tab order and away from
3195
+ // assistive technology without removing it from the DOM.
3196
+ if (isHidden) {
3197
+ panel.setAttribute('inert', '');
3198
+ }
3199
+ else {
3200
+ panel.removeAttribute('inert');
3201
+ }
3202
+ }
3203
+ startClosing() {
3204
+ if (this.closeStarted) {
3205
+ return;
3206
+ }
3207
+ this.closeStarted = true;
3208
+ this.closing.next();
3209
+ this.closing.complete();
3210
+ }
3119
3211
  startCloseAnimation() {
3120
- let panel = this.overlayRef.overlayElement;
3121
- let backdrop = this.overlayRef.backdropElement;
3122
- let animatedElement = panel.querySelector('.md3-dialog-container') ?? panel;
3123
- panel.classList.add('md3-dialog-closing');
3124
- backdrop?.classList.add('md3-dialog-closing');
3212
+ const overlayRef = this.cdkRef.overlayRef;
3213
+ const wasVisible = !this.hidden;
3214
+ overlayRef.overlayElement.classList.add('md3-dialog-closing');
3215
+ overlayRef.backdropElement?.classList.add('md3-dialog-closing');
3125
3216
  this.dialogInstance?.isActive.set(false);
3217
+ // A hidden dialog already played the exit animation, so there is
3218
+ // nothing left to transition and it can be disposed right away.
3219
+ return wasVisible ? this.waitForSurfaceAnimation() : Promise.resolve();
3220
+ }
3221
+ /** Resolves when the surface finished animating, with a timeout as a safety net. */
3222
+ waitForSurfaceAnimation() {
3223
+ const panel = this.cdkRef.overlayRef.overlayElement;
3224
+ const surface = panel.querySelector('.md3-dialog-container') ?? panel;
3126
3225
  return new Promise((resolve) => {
3127
3226
  let isResolved = false;
3128
3227
  let timeoutId = setTimeout(done, DIALOG_EXIT_ANIMATION_FALLBACK_MS);
@@ -3132,82 +3231,182 @@ class DialogRef {
3132
3231
  }
3133
3232
  isResolved = true;
3134
3233
  clearTimeout(timeoutId);
3135
- animatedElement.removeEventListener('transitionend', onTransitionEnd);
3234
+ surface.removeEventListener('transitionend', onTransitionEnd);
3136
3235
  resolve();
3137
3236
  }
3138
3237
  function onTransitionEnd(event) {
3139
- if (event.target === animatedElement) {
3238
+ if (event.target === surface) {
3140
3239
  done();
3141
3240
  }
3142
3241
  }
3143
- animatedElement.addEventListener('transitionend', onTransitionEnd);
3242
+ surface.addEventListener('transitionend', onTransitionEnd);
3144
3243
  });
3145
3244
  }
3146
- afterClosed() {
3147
- return this.closed.asObservable();
3245
+ /** Emits the result once the CDK reference is closed. Focus is restored by the container. */
3246
+ settle(result) {
3247
+ if (this.closeSettled) {
3248
+ return;
3249
+ }
3250
+ // Covers detachments that did not go through close(), so anything
3251
+ // waiting on beforeClosed() is still notified.
3252
+ this.startClosing();
3253
+ this.closeSettled = true;
3254
+ this.closed.next(result);
3255
+ this.closed.complete();
3148
3256
  }
3149
3257
  }
3150
3258
 
3151
- class Dialog {
3152
- portalOutlet = viewChild(CdkPortalOutlet, /* @ts-ignore */
3153
- ...(ngDevMode ? [{ debugName: "portalOutlet" }] : /* istanbul ignore next */ []));
3259
+ /**
3260
+ * Material 3 dialog shell. It extends the CDK dialog container, so focus
3261
+ * trapping, focus restoration and the ARIA attributes are handled by the CDK
3262
+ * while this component only owns the MD3 surface and its animation.
3263
+ */
3264
+ class Dialog extends CdkDialogContainer {
3154
3265
  isActive = signal(false, /* @ts-ignore */
3155
3266
  ...(ngDevMode ? [{ debugName: "isActive" }] : /* istanbul ignore next */ []));
3156
3267
  config = inject(DIALOG_CONFIG, { optional: true }) ?? {};
3157
- get role() {
3158
- return this.config.role ?? 'dialog';
3159
- }
3160
- ngAfterContentInit() {
3268
+ /**
3269
+ * Started by DialogService, either right after the dialog is created or
3270
+ * once the dialog it replaces has finished animating out. The animation
3271
+ * runs on the next frame so the surface has a real from/to state.
3272
+ */
3273
+ startEnterAnimation() {
3161
3274
  requestAnimationFrame(() => {
3162
3275
  this.isActive.set(true);
3163
3276
  });
3164
3277
  }
3165
3278
  /**
3166
- * The service creates this wrapper first, then calls this method to mount
3167
- * the user supplied component into the dialog container.
3279
+ * @deprecated The CDK dialog service attaches the content. Kept for
3280
+ * backwards compatibility and will be removed in a future release.
3168
3281
  */
3169
3282
  attachContent(component, injector) {
3170
- const portal = new ComponentPortal(component, null, injector);
3171
- return this.portalOutlet().attachComponentPortal(portal);
3283
+ return this.attachComponentPortal(new ComponentPortal(component, null, injector));
3172
3284
  }
3173
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: Dialog, deps: [], target: i0.ɵɵFactoryTarget.Component });
3174
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "22.1.0", type: Dialog, isStandalone: true, selector: "md3-dialog", viewQueries: [{ propertyName: "portalOutlet", first: true, predicate: CdkPortalOutlet, descendants: true, isSignal: true }], ngImport: i0, template: "<section\n class=\"md3-dialog-container\"\n [class.md3-active]=\"isActive()\"\n [class.md-scheme-dark]=\"config.scheme == 'dark'\"\n [class.md-scheme-light]=\"config.scheme == 'light'\"\n [attr.dir]=\"config.direction ?? null\"\n tabindex=\"-1\"\n [attr.role]=\"role\"\n [attr.aria-modal]=\"true\"\n [attr.aria-label]=\"config.ariaLabel ?? null\"\n [attr.aria-labelledby]=\"config.ariaLabelledBy ?? null\"\n [attr.aria-describedby]=\"config.ariaDescribedBy ?? null\">\n <ng-template cdkPortalOutlet></ng-template>\n</section>\n", styles: ["::ng-deep .cdk-overlay-container{position:fixed;z-index:1000;inset:0;pointer-events:none;font-size:1rem}::ng-deep .cdk-overlay-container .md3-dialog-scrim{position:absolute;inset:0;pointer-events:auto;background-color:rgb(var(--md-scheme-scrim));transition:all var(--md-motion-expressive-slow-spatial-duration) var(--md-motion-expressive-slow-spatial-easing);opacity:.32}::ng-deep .cdk-overlay-container .md3-dialog-scrim.md3-dialog-opening{opacity:0}::ng-deep .cdk-overlay-container .md3-dialog-scrim.md3-dialog-closing{opacity:0}::ng-deep .cdk-overlay-container .cdk-global-overlay-wrapper{position:absolute;display:flex;inset:0;pointer-events:none;padding:3.5rem 1.5rem;overflow:hidden}::ng-deep .cdk-overlay-container .cdk-global-overlay-wrapper .md3-dialog-panel{position:absolute;pointer-events:auto;max-width:35em}::ng-deep .cdk-overlay-container .cdk-global-overlay-wrapper .md3-dialog-panel.md3-dialog-closing{pointer-events:none}:host{display:block;max-width:100%;min-width:17.5em}:host .md3-dialog-container{display:block;width:100%;max-height:inherit;overflow:auto;color:rgb(var(--md-scheme-on-surface));background-color:rgb(var(--md-scheme-surface-container-high));border-radius:var(--md-border-radius-xlarge);box-shadow:var(--md-shadow-6dp);transform-origin:center;transition:all var(--md-motion-expressive-default-spatial-duration) var(--md-motion-expressive-default-spatial-easing);opacity:0;transform:scaleY(.8);margin-top:-8rem}:host .md3-dialog-container.md3-active{opacity:1;transform:scaleY(1);margin-top:0}\n"], dependencies: [{ kind: "directive", type: CdkPortalOutlet, selector: "[cdkPortalOutlet]", inputs: ["cdkPortalOutlet"], outputs: ["attached"], exportAs: ["cdkPortalOutlet"] }] });
3285
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: Dialog, deps: null, target: i0.ɵɵFactoryTarget.Component });
3286
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "22.1.0", type: Dialog, isStandalone: true, selector: "md3-dialog", usesInheritance: true, ngImport: i0, template: "<!-- The role and aria attributes live on the host element, applied by the CDK dialog container. -->\n<section\n class=\"md3-dialog-container\"\n [class.md3-active]=\"isActive()\"\n [class.md-scheme-dark]=\"config.scheme == 'dark'\"\n [class.md-scheme-light]=\"config.scheme == 'light'\"\n [attr.dir]=\"config.direction ?? null\">\n <ng-template cdkPortalOutlet></ng-template>\n</section>\n", styles: ["::ng-deep .cdk-overlay-container{position:fixed;z-index:1000;inset:0;pointer-events:none;font-size:1rem}::ng-deep .cdk-overlay-container .md3-dialog-scrim{position:absolute;inset:0;pointer-events:auto;background-color:rgb(var(--md-scheme-scrim));transition:all var(--md-motion-expressive-slow-spatial-duration) var(--md-motion-expressive-slow-spatial-easing);opacity:.32}::ng-deep .cdk-overlay-container .md3-dialog-scrim.md3-dialog-opening{opacity:0}::ng-deep .cdk-overlay-container .md3-dialog-scrim.md3-dialog-closing{opacity:0}::ng-deep .cdk-overlay-container .md3-dialog-scrim.md3-dialog-hidden{opacity:0}::ng-deep .cdk-overlay-container .md3-dialog-scrim.md3-dialog-scrim-kept{opacity:.32;transition:none}::ng-deep .cdk-overlay-container .md3-dialog-scrim.md3-dialog-scrim-instant{transition:none}::ng-deep .cdk-overlay-container .cdk-global-overlay-wrapper{position:absolute;display:flex;inset:0;pointer-events:none;padding:3.5rem 1.5rem;overflow:hidden}::ng-deep .cdk-overlay-container .cdk-global-overlay-wrapper .md3-dialog-panel{position:absolute;pointer-events:auto;max-width:35em}::ng-deep .cdk-overlay-container .cdk-global-overlay-wrapper .md3-dialog-panel.md3-dialog-closing,::ng-deep .cdk-overlay-container .cdk-global-overlay-wrapper .md3-dialog-panel.md3-dialog-hidden{pointer-events:none}:host{display:block;max-width:100%;min-width:17.5em;outline:0}:host .md3-dialog-container{display:block;width:100%;max-height:inherit;overflow:auto;color:rgb(var(--md-scheme-on-surface));background-color:rgb(var(--md-scheme-surface-container-high));border-radius:var(--md-border-radius-xlarge);box-shadow:var(--md-shadow-6dp);transform-origin:center;transition:all var(--md-motion-expressive-default-spatial-duration) var(--md-motion-expressive-default-spatial-easing);opacity:0;transform:scaleY(.8);margin-top:-8rem}:host .md3-dialog-container.md3-active{opacity:1;transform:scaleY(1);margin-top:0}\n"], dependencies: [{ kind: "directive", type: CdkPortalOutlet, selector: "[cdkPortalOutlet]", inputs: ["cdkPortalOutlet"], outputs: ["attached"], exportAs: ["cdkPortalOutlet"] }] });
3175
3287
  }
3176
3288
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: Dialog, decorators: [{
3177
3289
  type: Component,
3178
3290
  args: [{ selector: 'md3-dialog', imports: [
3179
3291
  CdkPortalOutlet,
3180
- ], template: "<section\n class=\"md3-dialog-container\"\n [class.md3-active]=\"isActive()\"\n [class.md-scheme-dark]=\"config.scheme == 'dark'\"\n [class.md-scheme-light]=\"config.scheme == 'light'\"\n [attr.dir]=\"config.direction ?? null\"\n tabindex=\"-1\"\n [attr.role]=\"role\"\n [attr.aria-modal]=\"true\"\n [attr.aria-label]=\"config.ariaLabel ?? null\"\n [attr.aria-labelledby]=\"config.ariaLabelledBy ?? null\"\n [attr.aria-describedby]=\"config.ariaDescribedBy ?? null\">\n <ng-template cdkPortalOutlet></ng-template>\n</section>\n", styles: ["::ng-deep .cdk-overlay-container{position:fixed;z-index:1000;inset:0;pointer-events:none;font-size:1rem}::ng-deep .cdk-overlay-container .md3-dialog-scrim{position:absolute;inset:0;pointer-events:auto;background-color:rgb(var(--md-scheme-scrim));transition:all var(--md-motion-expressive-slow-spatial-duration) var(--md-motion-expressive-slow-spatial-easing);opacity:.32}::ng-deep .cdk-overlay-container .md3-dialog-scrim.md3-dialog-opening{opacity:0}::ng-deep .cdk-overlay-container .md3-dialog-scrim.md3-dialog-closing{opacity:0}::ng-deep .cdk-overlay-container .cdk-global-overlay-wrapper{position:absolute;display:flex;inset:0;pointer-events:none;padding:3.5rem 1.5rem;overflow:hidden}::ng-deep .cdk-overlay-container .cdk-global-overlay-wrapper .md3-dialog-panel{position:absolute;pointer-events:auto;max-width:35em}::ng-deep .cdk-overlay-container .cdk-global-overlay-wrapper .md3-dialog-panel.md3-dialog-closing{pointer-events:none}:host{display:block;max-width:100%;min-width:17.5em}:host .md3-dialog-container{display:block;width:100%;max-height:inherit;overflow:auto;color:rgb(var(--md-scheme-on-surface));background-color:rgb(var(--md-scheme-surface-container-high));border-radius:var(--md-border-radius-xlarge);box-shadow:var(--md-shadow-6dp);transform-origin:center;transition:all var(--md-motion-expressive-default-spatial-duration) var(--md-motion-expressive-default-spatial-easing);opacity:0;transform:scaleY(.8);margin-top:-8rem}:host .md3-dialog-container.md3-active{opacity:1;transform:scaleY(1);margin-top:0}\n"] }]
3181
- }], propDecorators: { portalOutlet: [{ type: i0.ViewChild, args: [i0.forwardRef(() => CdkPortalOutlet), { isSignal: true }] }] } });
3292
+ ], template: "<!-- The role and aria attributes live on the host element, applied by the CDK dialog container. -->\n<section\n class=\"md3-dialog-container\"\n [class.md3-active]=\"isActive()\"\n [class.md-scheme-dark]=\"config.scheme == 'dark'\"\n [class.md-scheme-light]=\"config.scheme == 'light'\"\n [attr.dir]=\"config.direction ?? null\">\n <ng-template cdkPortalOutlet></ng-template>\n</section>\n", styles: ["::ng-deep .cdk-overlay-container{position:fixed;z-index:1000;inset:0;pointer-events:none;font-size:1rem}::ng-deep .cdk-overlay-container .md3-dialog-scrim{position:absolute;inset:0;pointer-events:auto;background-color:rgb(var(--md-scheme-scrim));transition:all var(--md-motion-expressive-slow-spatial-duration) var(--md-motion-expressive-slow-spatial-easing);opacity:.32}::ng-deep .cdk-overlay-container .md3-dialog-scrim.md3-dialog-opening{opacity:0}::ng-deep .cdk-overlay-container .md3-dialog-scrim.md3-dialog-closing{opacity:0}::ng-deep .cdk-overlay-container .md3-dialog-scrim.md3-dialog-hidden{opacity:0}::ng-deep .cdk-overlay-container .md3-dialog-scrim.md3-dialog-scrim-kept{opacity:.32;transition:none}::ng-deep .cdk-overlay-container .md3-dialog-scrim.md3-dialog-scrim-instant{transition:none}::ng-deep .cdk-overlay-container .cdk-global-overlay-wrapper{position:absolute;display:flex;inset:0;pointer-events:none;padding:3.5rem 1.5rem;overflow:hidden}::ng-deep .cdk-overlay-container .cdk-global-overlay-wrapper .md3-dialog-panel{position:absolute;pointer-events:auto;max-width:35em}::ng-deep .cdk-overlay-container .cdk-global-overlay-wrapper .md3-dialog-panel.md3-dialog-closing,::ng-deep .cdk-overlay-container .cdk-global-overlay-wrapper .md3-dialog-panel.md3-dialog-hidden{pointer-events:none}:host{display:block;max-width:100%;min-width:17.5em;outline:0}:host .md3-dialog-container{display:block;width:100%;max-height:inherit;overflow:auto;color:rgb(var(--md-scheme-on-surface));background-color:rgb(var(--md-scheme-surface-container-high));border-radius:var(--md-border-radius-xlarge);box-shadow:var(--md-shadow-6dp);transform-origin:center;transition:all var(--md-motion-expressive-default-spatial-duration) var(--md-motion-expressive-default-spatial-easing);opacity:0;transform:scaleY(.8);margin-top:-8rem}:host .md3-dialog-container.md3-active{opacity:1;transform:scaleY(1);margin-top:0}\n"] }]
3293
+ }] });
3182
3294
 
3295
+ const OPENING_CLASS = 'md3-dialog-opening';
3296
+ const SCRIM_KEPT_CLASS = 'md3-dialog-scrim-kept';
3297
+ const SCRIM_INSTANT_CLASS = 'md3-dialog-scrim-instant';
3298
+ /**
3299
+ * Head start given to a dialog that leaves the screen before the dialog taking
3300
+ * its place animates in. Long enough to read as one dialog replacing another,
3301
+ * short enough to still feel like a single gesture.
3302
+ */
3303
+ const DIALOG_HANDOVER_DELAY_MS = 160;
3183
3304
  class DialogService {
3305
+ cdkDialog = inject(Dialog$1);
3184
3306
  overlay = inject(Overlay);
3185
3307
  injector = inject(Injector);
3186
3308
  document = inject(DOCUMENT);
3309
+ /** Open dialogs, from the first one opened to the one currently on top. */
3310
+ refs = [];
3311
+ /**
3312
+ * Page scrolling is blocked here instead of per overlay, so stacked dialogs
3313
+ * cannot unblock the page while another dialog is still open.
3314
+ */
3315
+ scrollBlock = this.overlay.scrollStrategies.block();
3316
+ /** Element that was focused before the first dialog of the stack opened. */
3317
+ rootTrigger = null;
3318
+ get openDialogs() {
3319
+ return this.refs;
3320
+ }
3187
3321
  open(component, config = {}) {
3188
3322
  const dialogConfig = this.mergeConfig(config);
3189
- const previouslyFocusedElement = this.getFocusedElement();
3190
- const overlayRef = this.createOverlay();
3191
- const dialogRef = new DialogRef(overlayRef, previouslyFocusedElement);
3192
- const injector = this.createInjector(component, dialogConfig, dialogRef);
3193
- // The overlay hosts the MD3 dialog shell. The shell is responsible for
3194
- // the surface markup and receives the dynamic content through a portal.
3195
- const dialogPortal = new ComponentPortal(Dialog, dialogConfig.viewContainerRef ?? null, injector);
3196
- const dialogComponentRef = overlayRef.attach(dialogPortal);
3197
- const contentComponentRef = dialogComponentRef.instance.attachContent(component, injector);
3198
- this.bindDataToInputs(contentComponentRef, dialogConfig);
3199
- dialogRef.componentInstance = contentComponentRef.instance;
3200
- dialogRef.dialogInstance = dialogComponentRef.instance;
3201
- this.startOpenAnimation(overlayRef);
3202
- this.connectCloseEvents(overlayRef, dialogRef, dialogConfig);
3203
- this.focusDialog(overlayRef);
3323
+ if (this.refs.length === 0) {
3324
+ this.rootTrigger = this.getFocusedElement();
3325
+ }
3326
+ // The dialog that is on screen starts leaving first, so the two dialogs
3327
+ // never animate on top of each other.
3328
+ const previousRef = this.topRef();
3329
+ if (previousRef) {
3330
+ this.dismissPrevious(previousRef, dialogConfig);
3331
+ }
3332
+ let dialogRef;
3333
+ const cdkRef = this.cdkDialog.open(component, {
3334
+ // The MD3 shell replaces the default CDK container, so the overlay,
3335
+ // focus trap and focus restoration come from the CDK while the shell
3336
+ // only renders the surface.
3337
+ container: {
3338
+ type: Dialog,
3339
+ providers: () => [
3340
+ { provide: DIALOG_CONFIG, useValue: dialogConfig },
3341
+ ],
3342
+ },
3343
+ // These tokens make the dialog controllable from the dynamic
3344
+ // component without coupling that component to the service.
3345
+ providers: (cdkDialogRef) => {
3346
+ dialogRef = new DialogRef(cdkDialogRef, dialogConfig.disableCloseEvents);
3347
+ return [
3348
+ { provide: DialogRef, useValue: dialogRef },
3349
+ { provide: DIALOG_DATA, useValue: dialogConfig.data },
3350
+ { provide: DIALOG_CONFIG, useValue: dialogConfig },
3351
+ { provide: DIALOG_COMPONENT, useValue: component },
3352
+ ];
3353
+ },
3354
+ data: dialogConfig.data,
3355
+ role: dialogConfig.role,
3356
+ ariaModal: true,
3357
+ ariaLabel: dialogConfig.ariaLabel || null,
3358
+ ariaLabelledBy: dialogConfig.ariaLabelledBy || null,
3359
+ ariaDescribedBy: dialogConfig.ariaDescribedBy || null,
3360
+ direction: dialogConfig.direction ?? undefined,
3361
+ viewContainerRef: dialogConfig.viewContainerRef,
3362
+ injector: dialogConfig.injector,
3363
+ hasBackdrop: true,
3364
+ backdropClass: ['md3-dialog-scrim', OPENING_CLASS],
3365
+ panelClass: ['md3-dialog-panel', OPENING_CLASS],
3366
+ positionStrategy: this.overlay.position()
3367
+ .global()
3368
+ .centerHorizontally()
3369
+ .centerVertically(),
3370
+ // Scrolling is blocked by the service for the whole stack.
3371
+ scrollStrategy: this.overlay.scrollStrategies.noop(),
3372
+ // Focus goes back to whatever opened the stack, even when the dialog
3373
+ // that was open underneath has been closed in the meantime.
3374
+ restoreFocus: this.rootTrigger ?? true,
3375
+ // Scrim and Escape handling lives in DialogRef so the dialog can
3376
+ // play its exit animation before the overlay is disposed.
3377
+ disableClose: true,
3378
+ });
3379
+ dialogRef.dialogInstance = cdkRef.containerInstance;
3380
+ dialogRef.componentInstance = cdkRef.componentInstance ?? undefined;
3381
+ if (cdkRef.componentRef) {
3382
+ this.bindDataToInputs(cdkRef.componentRef, dialogConfig);
3383
+ }
3384
+ this.refs.push(dialogRef);
3385
+ this.updateScrollBlock();
3386
+ if (previousRef) {
3387
+ setTimeout(() => this.startReplacingAnimation(dialogRef, previousRef), DIALOG_HANDOVER_DELAY_MS);
3388
+ }
3389
+ else {
3390
+ this.startOpenAnimation(dialogRef);
3391
+ }
3392
+ // Restoring starts with the exit animation, so the dialog underneath is
3393
+ // already coming back while this one is fading out.
3394
+ dialogRef.beforeClosed().subscribe(() => this.restorePreviousDialog(dialogRef));
3395
+ dialogRef.afterClosed().subscribe(() => this.removeRef(dialogRef));
3204
3396
  return dialogRef;
3205
3397
  }
3398
+ /** Closes every open dialog, including the hidden ones. */
3399
+ closeAll() {
3400
+ // Closed from the bottom up so a closing dialog never brings back a
3401
+ // hidden dialog that is about to close as well.
3402
+ return Promise.all([...this.refs].map((ref) => ref.close())).then(() => undefined);
3403
+ }
3206
3404
  mergeConfig(config) {
3207
3405
  return {
3208
3406
  data: config.data,
3209
3407
  bindDataToInputs: config.bindDataToInputs ?? false,
3210
3408
  disableCloseEvents: config.disableCloseEvents ?? false,
3409
+ previousDialog: config.previousDialog ?? 'close',
3211
3410
  role: config.role ?? 'dialog',
3212
3411
  ariaLabel: config.ariaLabel ?? '',
3213
3412
  ariaLabelledBy: config.ariaLabelledBy ?? '',
@@ -3218,20 +3417,17 @@ class DialogService {
3218
3417
  injector: config.injector ?? this.injector,
3219
3418
  };
3220
3419
  }
3221
- createOverlay() {
3222
- const overlayConfig = new OverlayConfig({
3223
- hasBackdrop: true,
3224
- backdropClass: ['md3-dialog-scrim', 'md3-dialog-opening'],
3225
- panelClass: ['md3-dialog-panel', 'md3-dialog-opening'],
3226
- positionStrategy: this.overlay.position()
3227
- .global()
3228
- .centerHorizontally()
3229
- .centerVertically(),
3230
- scrollStrategy: this.overlay.scrollStrategies.block(),
3231
- });
3232
- return this.overlay.create(overlayConfig);
3420
+ /**
3421
+ * Hides or closes the dialog that is on screen. Its scrim stays up until
3422
+ * the dialog taking over is ready for it, so the page behind never
3423
+ * brightens between the two dialogs.
3424
+ */
3425
+ dismissPrevious(previousRef, config) {
3426
+ previousRef.overlayRef.backdropElement?.classList.add(SCRIM_KEPT_CLASS);
3427
+ return config.previousDialog === 'hide' ? previousRef.hide() : this.closeAll();
3233
3428
  }
3234
- startOpenAnimation(overlayRef) {
3429
+ startOpenAnimation(dialogRef) {
3430
+ const overlayRef = dialogRef.overlayRef;
3235
3431
  const panel = overlayRef.overlayElement;
3236
3432
  const backdrop = overlayRef.backdropElement;
3237
3433
  // The overlay is inserted already rendered. Keep an initial class for
@@ -3240,22 +3436,53 @@ class DialogService {
3240
3436
  requestAnimationFrame(() => {
3241
3437
  setTimeout(() => {
3242
3438
  panel.getBoundingClientRect();
3243
- panel.classList.remove('md3-dialog-opening');
3244
- backdrop?.classList.remove('md3-dialog-opening');
3439
+ panel.classList.remove(OPENING_CLASS);
3440
+ backdrop?.classList.remove(OPENING_CLASS);
3245
3441
  }, 100);
3246
3442
  });
3443
+ dialogRef.dialogInstance?.startEnterAnimation();
3247
3444
  }
3248
- createInjector(component, config, dialogRef) {
3249
- return Injector.create({
3250
- parent: config.injector,
3251
- providers: [
3252
- // These tokens make the dialog controllable from the dynamic
3253
- // component without coupling that component to the service.
3254
- { provide: DialogRef, useValue: dialogRef },
3255
- { provide: DIALOG_DATA, useValue: config.data },
3256
- { provide: DIALOG_CONFIG, useValue: config },
3257
- { provide: DIALOG_COMPONENT, useValue: component },
3258
- ],
3445
+ /** Opening animation for a dialog that took the place of another one. */
3446
+ startReplacingAnimation(dialogRef, previousRef) {
3447
+ if (dialogRef.isClosing) {
3448
+ return;
3449
+ }
3450
+ const panel = dialogRef.overlayRef.overlayElement;
3451
+ const backdrop = dialogRef.overlayRef.backdropElement;
3452
+ this.beginScrimHandover(previousRef, dialogRef);
3453
+ panel.getBoundingClientRect();
3454
+ panel.classList.remove(OPENING_CLASS);
3455
+ backdrop?.classList.remove(OPENING_CLASS);
3456
+ this.endScrimHandover(previousRef, dialogRef);
3457
+ dialogRef.dialogInstance?.startEnterAnimation();
3458
+ }
3459
+ /** Opening animation for a dialog coming back from behind a closing one. */
3460
+ startRestoringAnimation(closingRef, previousRef) {
3461
+ if (previousRef.isClosing) {
3462
+ return;
3463
+ }
3464
+ this.beginScrimHandover(closingRef, previousRef);
3465
+ previousRef.show();
3466
+ this.endScrimHandover(closingRef, previousRef);
3467
+ }
3468
+ /**
3469
+ * Moves both scrims to their new value in a single frame, without a
3470
+ * transition on either side, so the dim behind the dialogs never changes.
3471
+ */
3472
+ beginScrimHandover(fromRef, toRef) {
3473
+ const from = fromRef.overlayRef.backdropElement;
3474
+ const to = toRef.overlayRef.backdropElement;
3475
+ from?.classList.add(SCRIM_INSTANT_CLASS);
3476
+ from?.classList.remove(SCRIM_KEPT_CLASS);
3477
+ to?.classList.add(SCRIM_INSTANT_CLASS);
3478
+ }
3479
+ /** Gives the scrims their transitions back once the handover is painted. */
3480
+ endScrimHandover(fromRef, toRef) {
3481
+ const from = fromRef.overlayRef.backdropElement;
3482
+ const to = toRef.overlayRef.backdropElement;
3483
+ requestAnimationFrame(() => {
3484
+ from?.classList.remove(SCRIM_INSTANT_CLASS);
3485
+ to?.classList.remove(SCRIM_INSTANT_CLASS);
3259
3486
  });
3260
3487
  }
3261
3488
  bindDataToInputs(componentRef, config) {
@@ -3268,34 +3495,58 @@ class DialogService {
3268
3495
  componentRef.setInput(inputName, inputValue);
3269
3496
  });
3270
3497
  }
3271
- connectCloseEvents(overlayRef, dialogRef, config) {
3272
- if (config.disableCloseEvents) {
3498
+ canBindDataToInputs(data) {
3499
+ return typeof data === 'object' && data !== null && !Array.isArray(data);
3500
+ }
3501
+ /** Last dialog of the stack that is not closing yet. */
3502
+ topRef() {
3503
+ for (let index = this.refs.length - 1; index >= 0; index--) {
3504
+ if (!this.refs[index].isClosing) {
3505
+ return this.refs[index];
3506
+ }
3507
+ }
3508
+ return undefined;
3509
+ }
3510
+ restorePreviousDialog(ref) {
3511
+ const index = this.refs.indexOf(ref);
3512
+ if (index < 1) {
3513
+ return;
3514
+ }
3515
+ for (let previousIndex = index - 1; previousIndex >= 0; previousIndex--) {
3516
+ const previousRef = this.refs[previousIndex];
3517
+ if (previousRef.isClosing) {
3518
+ continue;
3519
+ }
3520
+ // The closing dialog gets the same head start it was given when it
3521
+ // took the screen, and keeps the scrim until then.
3522
+ ref.overlayRef.backdropElement?.classList.add(SCRIM_KEPT_CLASS);
3523
+ setTimeout(() => this.startRestoringAnimation(ref, previousRef), DIALOG_HANDOVER_DELAY_MS);
3273
3524
  return;
3274
3525
  }
3275
- overlayRef.backdropClick().pipe(take(1)).subscribe(() => dialogRef.close());
3276
- overlayRef.keydownEvents().pipe(filter((event) => event.key === 'Escape'), take(1)).subscribe(() => dialogRef.close());
3277
- }
3278
- focusDialog(overlayRef) {
3279
- queueMicrotask(() => {
3280
- const surface = overlayRef.overlayElement.querySelector('.md3-dialog-container');
3281
- const focusTarget = surface?.querySelector([
3282
- 'button:not([disabled])',
3283
- '[href]',
3284
- 'input:not([disabled])',
3285
- 'select:not([disabled])',
3286
- 'textarea:not([disabled])',
3287
- '[tabindex]:not([tabindex="-1"])',
3288
- ].join(', '));
3289
- (focusTarget ?? surface)?.focus();
3290
- });
3526
+ }
3527
+ removeRef(ref) {
3528
+ const index = this.refs.indexOf(ref);
3529
+ if (index === -1) {
3530
+ return;
3531
+ }
3532
+ this.refs.splice(index, 1);
3533
+ if (this.refs.length === 0) {
3534
+ this.rootTrigger = null;
3535
+ }
3536
+ this.updateScrollBlock();
3537
+ }
3538
+ updateScrollBlock() {
3539
+ if (this.refs.length > 0) {
3540
+ this.scrollBlock.enable();
3541
+ }
3542
+ else {
3543
+ this.scrollBlock.disable();
3544
+ }
3291
3545
  }
3292
3546
  getFocusedElement() {
3293
3547
  const activeElement = this.document.activeElement;
3294
3548
  return activeElement instanceof HTMLElement ? activeElement : null;
3295
3549
  }
3296
- canBindDataToInputs(data) {
3297
- return typeof data === 'object' && data !== null && !Array.isArray(data);
3298
- }
3299
3550
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: DialogService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
3300
3551
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: DialogService, providedIn: 'root' });
3301
3552
  }