@almoamendev/ngx-md3 0.0.16 → 0.2.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 {
@@ -557,7 +559,7 @@ class SideSheet {
557
559
  return this.portalOutlet.attachComponentPortal(portal);
558
560
  }
559
561
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: SideSheet, deps: [{ token: LayoutService }, { token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Component });
560
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "22.1.0", type: SideSheet, isStandalone: true, selector: "md3-side-sheet", host: { properties: { "class.md3-active": "isActive()", "class.md3-hidden": "isHidden()", "class.md3-start": "side() === \"start\"", "class.md3-end": "side() === \"end\"", "class.md3-inset": "isInset()", "class.md3-show-divider": "showDivider()" } }, viewQueries: [{ propertyName: "portalOutlet", first: true, predicate: CdkPortalOutlet, descendants: true, static: true }], ngImport: i0, template: "<div class=\"md3-side-sheet-scrim\" (click)=\"close()\"></div>\n<div class=\"md3-side-sheet-container\">\n <ng-template cdkPortalOutlet></ng-template>\n</div>\n", styles: [":host{--background-color: rgb(var(--md-scheme-surface-container-low));--foreground-color: rgb(var(--md-scheme-on-surface));--border-radius: var(--md-border-radius-large);--elevation: var(--md-shadow-0dp);--hidden-offset: 80%;position:relative;height:100%;width:100%;min-height:0;pointer-events:none;z-index:1}:host .md3-side-sheet-scrim{position:fixed;inset:0;width:100vw;height:100vh;z-index:-1;pointer-events:none;display:none;background-color:rgb(var(--md-scheme-scrim));opacity:0;transition:all var(--md-motion-standard-default-spatial-duration) var(--md-motion-standard-default-spatial-easing)}:host .md3-side-sheet-container{flex:1 1 auto;position:relative;max-width:25em;min-width:16em;height:100%;overflow:hidden;pointer-events:all;background-color:var(--background-color);color:var(--foreground-color);box-shadow:var(--elevation);opacity:0;transform:translate(var(--hidden-offset));transition:all var(--md-motion-standard-default-spatial-duration) var(--md-motion-standard-default-spatial-easing)}:host .md3-side-sheet-container>::ng-deep *{display:grid;grid-template-areas:\"header\" \"body\" \"actions\";grid-template-rows:auto minmax(0,1fr) auto;height:100%;overflow:hidden}:host.md3-hidden .md3-side-sheet-container{pointer-events:none;opacity:0;transform:translate(var(--hidden-offset))}:host.md3-active .md3-side-sheet-container{pointer-events:all;opacity:1;transform:translate(0)}:host.md3-show-divider .md3-side-sheet-container:before{content:\"\";position:absolute;height:100%;width:.0625em;top:0;bottom:0;opacity:0;background-color:rgb(var(--md-scheme-outline));pointer-events:none}:host.md3-start{--hidden-offset: -80%}:host.md3-start.md3-style-modal{inset-inline-start:0}:host.md3-start .md3-side-sheet-container{border-start-end-radius:var(--border-radius);border-end-end-radius:var(--border-radius)}:host.md3-start.md3-show-divider .md3-side-sheet-container:before{inset-inline-end:0}:host.md3-end{--hidden-offset: 80%}:host.md3-end.md3-style-modal{inset-inline-end:0}:host.md3-end .md3-side-sheet-container{border-start-start-radius:var(--border-radius);border-end-start-radius:var(--border-radius)}:host.md3-end.md3-show-divider .md3-side-sheet-container:before{inset-inline-start:0}:host.md3-style-standard .md3-side-sheet-container{--background-color: rgb(var(--md-scheme-surface));--border-radius: var(--md-border-radius-none)}:host.md3-style-standard.md3-show-divider .md3-side-sheet-container:before{opacity:1}:host.md3-style-default.md3-inset{padding:1em}:host.md3-style-default.md3-inset .md3-side-sheet-container{border-radius:var(--border-radius)}:host.md3-style-modal{--elevation: var(--md-shadow-1dp);position:fixed;top:0;bottom:0;z-index:500}:host.md3-style-modal .md3-side-sheet-scrim{display:block}:host.md3-style-modal.md3-active .md3-side-sheet-scrim{pointer-events:all;opacity:.32}:host.md3-style-modal.md3-inset{padding:1em}:host.md3-style-modal.md3-inset .md3-side-sheet-container{border-radius:var(--border-radius)}:host.md3-style-modal.md3-end .md3-side-sheet-container{margin-inline-start:auto}:host ::ng-deep md3-nav-group [md3-nav-item]{padding-inline:1.25em;margin-bottom:.5em}:host ::ng-deep md3-nav-group [md3-nav-item] .md3-item-container{padding:0 1em;flex-direction:row;align-items:center;justify-content:start;width:fit-content;height:3.5em}:host ::ng-deep md3-nav-group [md3-nav-item] .md3-item-container:before,:host ::ng-deep md3-nav-group [md3-nav-item] .md3-item-container:after{inset:0;height:auto;width:auto;border-radius:var(--md-border-radius-xlarge)}:host ::ng-deep md3-nav-group [md3-nav-item] .md3-item-container .md3-icon-container{padding:0}:host ::ng-deep md3-nav-group [md3-nav-item] .md3-item-container .md3-label{font-size:.875em;line-height:1.428571em;letter-spacing:.007143em;max-height:2.857143em;padding-inline:0}:host ::ng-deep md3-nav-group [md3-nav-item] .md3-item-container .md3-badge-container{position:relative;inset:auto}:host ::ng-deep md3-nav-group [md3-nav-item] .md3-item-container .md3-badge-container.md3-small{position:absolute;height:1.875em;width:1.625em}\n"], dependencies: [{ kind: "directive", type: CdkPortalOutlet, selector: "[cdkPortalOutlet]", inputs: ["cdkPortalOutlet"], outputs: ["attached"], exportAs: ["cdkPortalOutlet"] }] });
562
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "22.1.0", type: SideSheet, isStandalone: true, selector: "md3-side-sheet", host: { properties: { "class.md3-active": "isActive()", "class.md3-hidden": "isHidden()", "class.md3-start": "side() === \"start\"", "class.md3-end": "side() === \"end\"", "class.md3-inset": "isInset()", "class.md3-show-divider": "showDivider()" } }, viewQueries: [{ propertyName: "portalOutlet", first: true, predicate: CdkPortalOutlet, descendants: true, static: true }], ngImport: i0, template: "<div class=\"md3-side-sheet-scrim\" (click)=\"close()\"></div>\n<div class=\"md3-side-sheet-container\">\n <ng-template cdkPortalOutlet></ng-template>\n</div>\n", styles: [":host{--background-color: rgb(var(--md-scheme-surface-container-low));--foreground-color: rgb(var(--md-scheme-on-surface));--border-radius: var(--md-border-radius-large);--elevation: var(--md-shadow-0dp);--hidden-offset: 80%;position:relative;height:100%;width:100%;min-height:0;pointer-events:none;z-index:1}:host .md3-side-sheet-scrim{position:fixed;inset:0;width:100vw;height:100vh;z-index:-1;pointer-events:none;display:none;background-color:rgb(var(--md-scheme-scrim));opacity:0;transition:all var(--md-motion-standard-default-spatial-duration) var(--md-motion-standard-default-spatial-easing)}:host .md3-side-sheet-container{flex:1 1 auto;position:relative;max-width:16em;min-width:16em;height:100%;overflow:hidden;pointer-events:all;background-color:var(--background-color);color:var(--foreground-color);box-shadow:var(--elevation);opacity:0;transform:translate(var(--hidden-offset));transition:all var(--md-motion-standard-default-spatial-duration) var(--md-motion-standard-default-spatial-easing)}:host .md3-side-sheet-container>::ng-deep *{display:grid;grid-template-areas:\"header\" \"body\" \"actions\";grid-template-rows:auto minmax(0,1fr) auto;height:100%;overflow:hidden}:host.md3-hidden .md3-side-sheet-container{pointer-events:none;opacity:0;transform:translate(var(--hidden-offset))}:host.md3-active .md3-side-sheet-container{pointer-events:all;opacity:1;transform:translate(0)}:host.md3-show-divider .md3-side-sheet-container:before{content:\"\";position:absolute;height:100%;width:.0625em;top:0;bottom:0;opacity:0;background-color:rgb(var(--md-scheme-outline));pointer-events:none}:host.md3-start{--hidden-offset: -80%}:host.md3-start.md3-style-modal{inset-inline-start:0}:host.md3-start .md3-side-sheet-container{border-start-end-radius:var(--border-radius);border-end-end-radius:var(--border-radius)}:host.md3-start.md3-show-divider .md3-side-sheet-container:before{inset-inline-end:0}:host.md3-end{--hidden-offset: 80%}:host.md3-end.md3-style-modal{inset-inline-end:0}:host.md3-end .md3-side-sheet-container{border-start-start-radius:var(--border-radius);border-end-start-radius:var(--border-radius)}:host.md3-end.md3-show-divider .md3-side-sheet-container:before{inset-inline-start:0}:host.md3-style-standard .md3-side-sheet-container{--background-color: rgb(var(--md-scheme-surface));--border-radius: var(--md-border-radius-none)}:host.md3-style-standard.md3-show-divider .md3-side-sheet-container:before{opacity:1}:host.md3-style-default.md3-inset{padding:1em}:host.md3-style-default.md3-inset .md3-side-sheet-container{border-radius:var(--border-radius)}:host.md3-style-modal{--elevation: var(--md-shadow-1dp);position:fixed;top:0;bottom:0;z-index:500}:host.md3-style-modal .md3-side-sheet-container{max-width:25em}:host.md3-style-modal .md3-side-sheet-scrim{display:block}:host.md3-style-modal.md3-active .md3-side-sheet-scrim{pointer-events:all;opacity:.32}:host.md3-style-modal.md3-inset{padding:1em}:host.md3-style-modal.md3-inset .md3-side-sheet-container{border-radius:var(--border-radius)}:host.md3-style-modal.md3-end .md3-side-sheet-container{margin-inline-start:auto}:host ::ng-deep md3-nav-group [md3-nav-item]{padding-inline:1.25em;margin-bottom:.5em}:host ::ng-deep md3-nav-group [md3-nav-item] .md3-item-container{padding:0 1em;flex-direction:row;align-items:center;justify-content:start;width:fit-content;height:3.5em}:host ::ng-deep md3-nav-group [md3-nav-item] .md3-item-container:before,:host ::ng-deep md3-nav-group [md3-nav-item] .md3-item-container:after{inset:0;height:auto;width:auto;border-radius:var(--md-border-radius-xlarge)}:host ::ng-deep md3-nav-group [md3-nav-item] .md3-item-container .md3-icon-container{padding:0}:host ::ng-deep md3-nav-group [md3-nav-item] .md3-item-container .md3-label{font-size:.875em;line-height:1.428571em;letter-spacing:.007143em;max-height:2.857143em;padding-inline:0}:host ::ng-deep md3-nav-group [md3-nav-item] .md3-item-container .md3-badge-container{position:relative;inset:auto}:host ::ng-deep md3-nav-group [md3-nav-item] .md3-item-container .md3-badge-container.md3-small{position:absolute;height:1.875em;width:1.625em}\n"], dependencies: [{ kind: "directive", type: CdkPortalOutlet, selector: "[cdkPortalOutlet]", inputs: ["cdkPortalOutlet"], outputs: ["attached"], exportAs: ["cdkPortalOutlet"] }] });
561
563
  }
562
564
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: SideSheet, decorators: [{
563
565
  type: Component,
@@ -570,7 +572,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImpor
570
572
  '[class.md3-end]': 'side() === "end"',
571
573
  '[class.md3-inset]': 'isInset()',
572
574
  '[class.md3-show-divider]': 'showDivider()',
573
- }, template: "<div class=\"md3-side-sheet-scrim\" (click)=\"close()\"></div>\n<div class=\"md3-side-sheet-container\">\n <ng-template cdkPortalOutlet></ng-template>\n</div>\n", styles: [":host{--background-color: rgb(var(--md-scheme-surface-container-low));--foreground-color: rgb(var(--md-scheme-on-surface));--border-radius: var(--md-border-radius-large);--elevation: var(--md-shadow-0dp);--hidden-offset: 80%;position:relative;height:100%;width:100%;min-height:0;pointer-events:none;z-index:1}:host .md3-side-sheet-scrim{position:fixed;inset:0;width:100vw;height:100vh;z-index:-1;pointer-events:none;display:none;background-color:rgb(var(--md-scheme-scrim));opacity:0;transition:all var(--md-motion-standard-default-spatial-duration) var(--md-motion-standard-default-spatial-easing)}:host .md3-side-sheet-container{flex:1 1 auto;position:relative;max-width:25em;min-width:16em;height:100%;overflow:hidden;pointer-events:all;background-color:var(--background-color);color:var(--foreground-color);box-shadow:var(--elevation);opacity:0;transform:translate(var(--hidden-offset));transition:all var(--md-motion-standard-default-spatial-duration) var(--md-motion-standard-default-spatial-easing)}:host .md3-side-sheet-container>::ng-deep *{display:grid;grid-template-areas:\"header\" \"body\" \"actions\";grid-template-rows:auto minmax(0,1fr) auto;height:100%;overflow:hidden}:host.md3-hidden .md3-side-sheet-container{pointer-events:none;opacity:0;transform:translate(var(--hidden-offset))}:host.md3-active .md3-side-sheet-container{pointer-events:all;opacity:1;transform:translate(0)}:host.md3-show-divider .md3-side-sheet-container:before{content:\"\";position:absolute;height:100%;width:.0625em;top:0;bottom:0;opacity:0;background-color:rgb(var(--md-scheme-outline));pointer-events:none}:host.md3-start{--hidden-offset: -80%}:host.md3-start.md3-style-modal{inset-inline-start:0}:host.md3-start .md3-side-sheet-container{border-start-end-radius:var(--border-radius);border-end-end-radius:var(--border-radius)}:host.md3-start.md3-show-divider .md3-side-sheet-container:before{inset-inline-end:0}:host.md3-end{--hidden-offset: 80%}:host.md3-end.md3-style-modal{inset-inline-end:0}:host.md3-end .md3-side-sheet-container{border-start-start-radius:var(--border-radius);border-end-start-radius:var(--border-radius)}:host.md3-end.md3-show-divider .md3-side-sheet-container:before{inset-inline-start:0}:host.md3-style-standard .md3-side-sheet-container{--background-color: rgb(var(--md-scheme-surface));--border-radius: var(--md-border-radius-none)}:host.md3-style-standard.md3-show-divider .md3-side-sheet-container:before{opacity:1}:host.md3-style-default.md3-inset{padding:1em}:host.md3-style-default.md3-inset .md3-side-sheet-container{border-radius:var(--border-radius)}:host.md3-style-modal{--elevation: var(--md-shadow-1dp);position:fixed;top:0;bottom:0;z-index:500}:host.md3-style-modal .md3-side-sheet-scrim{display:block}:host.md3-style-modal.md3-active .md3-side-sheet-scrim{pointer-events:all;opacity:.32}:host.md3-style-modal.md3-inset{padding:1em}:host.md3-style-modal.md3-inset .md3-side-sheet-container{border-radius:var(--border-radius)}:host.md3-style-modal.md3-end .md3-side-sheet-container{margin-inline-start:auto}:host ::ng-deep md3-nav-group [md3-nav-item]{padding-inline:1.25em;margin-bottom:.5em}:host ::ng-deep md3-nav-group [md3-nav-item] .md3-item-container{padding:0 1em;flex-direction:row;align-items:center;justify-content:start;width:fit-content;height:3.5em}:host ::ng-deep md3-nav-group [md3-nav-item] .md3-item-container:before,:host ::ng-deep md3-nav-group [md3-nav-item] .md3-item-container:after{inset:0;height:auto;width:auto;border-radius:var(--md-border-radius-xlarge)}:host ::ng-deep md3-nav-group [md3-nav-item] .md3-item-container .md3-icon-container{padding:0}:host ::ng-deep md3-nav-group [md3-nav-item] .md3-item-container .md3-label{font-size:.875em;line-height:1.428571em;letter-spacing:.007143em;max-height:2.857143em;padding-inline:0}:host ::ng-deep md3-nav-group [md3-nav-item] .md3-item-container .md3-badge-container{position:relative;inset:auto}:host ::ng-deep md3-nav-group [md3-nav-item] .md3-item-container .md3-badge-container.md3-small{position:absolute;height:1.875em;width:1.625em}\n"] }]
575
+ }, template: "<div class=\"md3-side-sheet-scrim\" (click)=\"close()\"></div>\n<div class=\"md3-side-sheet-container\">\n <ng-template cdkPortalOutlet></ng-template>\n</div>\n", styles: [":host{--background-color: rgb(var(--md-scheme-surface-container-low));--foreground-color: rgb(var(--md-scheme-on-surface));--border-radius: var(--md-border-radius-large);--elevation: var(--md-shadow-0dp);--hidden-offset: 80%;position:relative;height:100%;width:100%;min-height:0;pointer-events:none;z-index:1}:host .md3-side-sheet-scrim{position:fixed;inset:0;width:100vw;height:100vh;z-index:-1;pointer-events:none;display:none;background-color:rgb(var(--md-scheme-scrim));opacity:0;transition:all var(--md-motion-standard-default-spatial-duration) var(--md-motion-standard-default-spatial-easing)}:host .md3-side-sheet-container{flex:1 1 auto;position:relative;max-width:16em;min-width:16em;height:100%;overflow:hidden;pointer-events:all;background-color:var(--background-color);color:var(--foreground-color);box-shadow:var(--elevation);opacity:0;transform:translate(var(--hidden-offset));transition:all var(--md-motion-standard-default-spatial-duration) var(--md-motion-standard-default-spatial-easing)}:host .md3-side-sheet-container>::ng-deep *{display:grid;grid-template-areas:\"header\" \"body\" \"actions\";grid-template-rows:auto minmax(0,1fr) auto;height:100%;overflow:hidden}:host.md3-hidden .md3-side-sheet-container{pointer-events:none;opacity:0;transform:translate(var(--hidden-offset))}:host.md3-active .md3-side-sheet-container{pointer-events:all;opacity:1;transform:translate(0)}:host.md3-show-divider .md3-side-sheet-container:before{content:\"\";position:absolute;height:100%;width:.0625em;top:0;bottom:0;opacity:0;background-color:rgb(var(--md-scheme-outline));pointer-events:none}:host.md3-start{--hidden-offset: -80%}:host.md3-start.md3-style-modal{inset-inline-start:0}:host.md3-start .md3-side-sheet-container{border-start-end-radius:var(--border-radius);border-end-end-radius:var(--border-radius)}:host.md3-start.md3-show-divider .md3-side-sheet-container:before{inset-inline-end:0}:host.md3-end{--hidden-offset: 80%}:host.md3-end.md3-style-modal{inset-inline-end:0}:host.md3-end .md3-side-sheet-container{border-start-start-radius:var(--border-radius);border-end-start-radius:var(--border-radius)}:host.md3-end.md3-show-divider .md3-side-sheet-container:before{inset-inline-start:0}:host.md3-style-standard .md3-side-sheet-container{--background-color: rgb(var(--md-scheme-surface));--border-radius: var(--md-border-radius-none)}:host.md3-style-standard.md3-show-divider .md3-side-sheet-container:before{opacity:1}:host.md3-style-default.md3-inset{padding:1em}:host.md3-style-default.md3-inset .md3-side-sheet-container{border-radius:var(--border-radius)}:host.md3-style-modal{--elevation: var(--md-shadow-1dp);position:fixed;top:0;bottom:0;z-index:500}:host.md3-style-modal .md3-side-sheet-container{max-width:25em}:host.md3-style-modal .md3-side-sheet-scrim{display:block}:host.md3-style-modal.md3-active .md3-side-sheet-scrim{pointer-events:all;opacity:.32}:host.md3-style-modal.md3-inset{padding:1em}:host.md3-style-modal.md3-inset .md3-side-sheet-container{border-radius:var(--border-radius)}:host.md3-style-modal.md3-end .md3-side-sheet-container{margin-inline-start:auto}:host ::ng-deep md3-nav-group [md3-nav-item]{padding-inline:1.25em;margin-bottom:.5em}:host ::ng-deep md3-nav-group [md3-nav-item] .md3-item-container{padding:0 1em;flex-direction:row;align-items:center;justify-content:start;width:fit-content;height:3.5em}:host ::ng-deep md3-nav-group [md3-nav-item] .md3-item-container:before,:host ::ng-deep md3-nav-group [md3-nav-item] .md3-item-container:after{inset:0;height:auto;width:auto;border-radius:var(--md-border-radius-xlarge)}:host ::ng-deep md3-nav-group [md3-nav-item] .md3-item-container .md3-icon-container{padding:0}:host ::ng-deep md3-nav-group [md3-nav-item] .md3-item-container .md3-label{font-size:.875em;line-height:1.428571em;letter-spacing:.007143em;max-height:2.857143em;padding-inline:0}:host ::ng-deep md3-nav-group [md3-nav-item] .md3-item-container .md3-badge-container{position:relative;inset:auto}:host ::ng-deep md3-nav-group [md3-nav-item] .md3-item-container .md3-badge-container.md3-small{position:absolute;height:1.875em;width:1.625em}\n"] }]
574
576
  }], ctorParameters: () => [{ type: LayoutService }, { type: i0.ElementRef }], propDecorators: { portalOutlet: [{
575
577
  type: ViewChild,
576
578
  args: [CdkPortalOutlet, { static: true }]
@@ -603,37 +605,45 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImpor
603
605
 
604
606
  class SheetsService {
605
607
  injector = inject(Injector);
608
+ /**
609
+ * Outlets registered per side, from the page scaffold up to whatever took
610
+ * them over last. Sheets always open in the outlet on top, so a full screen
611
+ * dialog can host them while it is up and hand them back when it closes.
612
+ */
606
613
  states = new Map();
607
614
  registerSideSheetOutlet(side, outlet) {
608
- const currentState = this.states.get(side);
609
- if (currentState?.outlet === outlet) {
615
+ const stack = this.getOutletStack(side);
616
+ if (stack.some((state) => state.outlet === outlet)) {
610
617
  return;
611
618
  }
612
- if (currentState) {
613
- this.disposeOutletState(currentState);
614
- }
615
619
  if (outlet.hasAttached()) {
616
620
  outlet.detach();
617
621
  }
618
622
  const stackPortal = new ComponentPortal(SideSheetStack, null, this.injector);
619
623
  const stackComponentRef = outlet.attachComponentPortal(stackPortal);
620
- this.states.set(side, {
624
+ stack.push({
621
625
  outlet,
622
626
  stackComponentRef,
623
627
  refs: [],
624
628
  });
625
629
  }
626
630
  unregisterSideSheetOutlet(side, outlet) {
627
- const state = this.states.get(side);
628
- if (!state || state.outlet !== outlet) {
631
+ const stack = this.states.get(side);
632
+ const index = stack?.findIndex((state) => state.outlet === outlet) ?? -1;
633
+ if (!stack || index === -1) {
629
634
  return;
630
635
  }
631
- this.disposeOutletState(state);
632
- this.states.delete(side);
636
+ // Sheets opened in this outlet go with it, and the outlet underneath
637
+ // becomes the one new sheets open in.
638
+ this.disposeOutletState(stack[index]);
639
+ stack.splice(index, 1);
640
+ if (stack.length === 0) {
641
+ this.states.delete(side);
642
+ }
633
643
  }
634
644
  openSideSheet(component, config = {}) {
635
645
  const sheetConfig = this.mergeConfig(config);
636
- const state = this.states.get(sheetConfig.side);
646
+ const state = this.activeState(sheetConfig.side);
637
647
  if (!state) {
638
648
  throw new Error(`No ${sheetConfig.side} side sheet outlet is registered.`);
639
649
  }
@@ -653,7 +663,19 @@ class SheetsService {
653
663
  return sheetRef;
654
664
  }
655
665
  closeSideSheet(side = 'end', result) {
656
- this.getVisibleRef(this.states.get(side))?.close(result);
666
+ this.getVisibleRef(this.activeState(side))?.close(result);
667
+ }
668
+ /** Outlet that sheets currently open in: the last one that registered. */
669
+ activeState(side) {
670
+ const stack = this.states.get(side);
671
+ return stack?.[stack.length - 1];
672
+ }
673
+ getOutletStack(side) {
674
+ const stack = this.states.get(side) ?? [];
675
+ if (!this.states.has(side)) {
676
+ this.states.set(side, stack);
677
+ }
678
+ return stack;
657
679
  }
658
680
  mergeConfig(config) {
659
681
  return {
@@ -3087,42 +3109,144 @@ const DIALOG_DATA = new InjectionToken('MD3_DIALOG_DATA');
3087
3109
  const DIALOG_CONFIG = new InjectionToken('MD3_DIALOG_CONFIG');
3088
3110
  const DIALOG_COMPONENT = new InjectionToken('MD3_DIALOG_COMPONENT');
3089
3111
  const DIALOG_EXIT_ANIMATION_FALLBACK_MS = 300;
3112
+ const HIDDEN_CLASS = 'md3-dialog-hidden';
3090
3113
  class DialogRef {
3091
- overlayRef;
3092
- previouslyFocusedElement;
3114
+ cdkRef;
3115
+ isFullScreen;
3093
3116
  closed = new Subject();
3094
- isClosed = false;
3117
+ closing = new Subject();
3118
+ closePromise = null;
3119
+ closeStarted = false;
3120
+ closeSettled = false;
3121
+ hidden = false;
3122
+ /** Shell hosting the dialog: the regular dialog or the full screen one. */
3095
3123
  dialogInstance;
3096
3124
  /**
3097
3125
  * Filled by DialogService after the user component is attached. Keeping the
3098
3126
  * instance here lets callers imperatively update inputs when that is useful.
3099
3127
  */
3100
3128
  componentInstance;
3101
- constructor(overlayRef, previouslyFocusedElement) {
3102
- this.overlayRef = overlayRef;
3103
- this.previouslyFocusedElement = previouslyFocusedElement;
3104
- }
3129
+ /** Overlay hosting the dialog, useful to reach the panel and the scrim. */
3130
+ get overlayRef() {
3131
+ return this.cdkRef.overlayRef;
3132
+ }
3133
+ /** Whether the dialog started closing. Such a dialog cannot be hidden or shown anymore. */
3134
+ get isClosing() {
3135
+ return this.closeStarted;
3136
+ }
3137
+ /** Whether the dialog is currently hidden behind another dialog. */
3138
+ get isHidden() {
3139
+ return this.hidden;
3140
+ }
3141
+ constructor(cdkRef, disableCloseEvents,
3142
+ /** Whether this reference belongs to a full screen dialog. */
3143
+ isFullScreen = false) {
3144
+ this.cdkRef = cdkRef;
3145
+ this.isFullScreen = isFullScreen;
3146
+ // Closing always goes through this reference so the exit animation can
3147
+ // run before the CDK disposes the overlay. Detachments triggered from
3148
+ // the outside (navigation, scroll strategy) still settle this ref.
3149
+ this.cdkRef.closed.pipe(take(1)).subscribe((result) => this.settle(result));
3150
+ if (!disableCloseEvents) {
3151
+ this.connectCloseEvents();
3152
+ }
3153
+ }
3154
+ /** Closes the dialog. The promise resolves once the exit animation is done. */
3105
3155
  close(result) {
3106
- if (this.isClosed) {
3156
+ if (this.closePromise) {
3157
+ return this.closePromise;
3158
+ }
3159
+ this.startClosing();
3160
+ this.closePromise = this.startCloseAnimation().then(() => this.cdkRef.close(result));
3161
+ return this.closePromise;
3162
+ }
3163
+ /**
3164
+ * Hides the dialog with the closing animation while keeping it alive, so
3165
+ * its content, form state and subscriptions survive until show() is called.
3166
+ * The promise resolves once the dialog is out of sight.
3167
+ */
3168
+ hide() {
3169
+ if (this.hidden || this.closeStarted) {
3170
+ return Promise.resolve();
3171
+ }
3172
+ this.hidden = true;
3173
+ this.toggleHiddenState(true);
3174
+ this.dialogInstance?.setActive(false);
3175
+ return this.waitForSurfaceAnimation();
3176
+ }
3177
+ /**
3178
+ * Brings a hidden dialog back with the opening animation. Focus moves back
3179
+ * into the dialog even when it was never hidden, which is what makes it
3180
+ * usable again once the dialog above it closes.
3181
+ */
3182
+ show() {
3183
+ if (this.closeStarted) {
3107
3184
  return;
3108
3185
  }
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();
3186
+ if (this.hidden) {
3187
+ this.hidden = false;
3188
+ this.toggleHiddenState(false);
3189
+ this.dialogInstance?.setActive(true);
3190
+ }
3191
+ this.dialogInstance?.recaptureFocus();
3192
+ }
3193
+ /** Emits when the dialog starts closing, before the exit animation runs. */
3194
+ beforeClosed() {
3195
+ return this.closing.asObservable();
3196
+ }
3197
+ afterClosed() {
3198
+ return this.closed.asObservable();
3199
+ }
3200
+ connectCloseEvents() {
3201
+ // The CDK config keeps `disableClose` on, so the scrim and Escape key
3202
+ // are handled here instead and the dialog can animate out.
3203
+ this.cdkRef.backdropClick.pipe(take(1)).subscribe(() => {
3204
+ this.cdkRef.containerInstance._closeInteractionType = 'mouse';
3205
+ this.close();
3206
+ });
3207
+ this.cdkRef.keydownEvents.pipe(filter((event) => event.key === 'Escape' && !hasModifierKey(event)), take(1)).subscribe((event) => {
3208
+ event.preventDefault();
3209
+ this.cdkRef.containerInstance._closeInteractionType = 'keyboard';
3210
+ this.close();
3117
3211
  });
3118
3212
  }
3213
+ toggleHiddenState(isHidden) {
3214
+ const overlayRef = this.cdkRef.overlayRef;
3215
+ const panel = overlayRef.overlayElement;
3216
+ const backdrop = overlayRef.backdropElement;
3217
+ panel.classList.toggle(HIDDEN_CLASS, isHidden);
3218
+ backdrop?.classList.toggle(HIDDEN_CLASS, isHidden);
3219
+ // `inert` keeps a hidden dialog out of the tab order and away from
3220
+ // assistive technology without removing it from the DOM.
3221
+ if (isHidden) {
3222
+ panel.setAttribute('inert', '');
3223
+ }
3224
+ else {
3225
+ panel.removeAttribute('inert');
3226
+ }
3227
+ }
3228
+ startClosing() {
3229
+ if (this.closeStarted) {
3230
+ return;
3231
+ }
3232
+ this.closeStarted = true;
3233
+ this.closing.next();
3234
+ this.closing.complete();
3235
+ }
3119
3236
  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');
3125
- this.dialogInstance?.isActive.set(false);
3237
+ const overlayRef = this.cdkRef.overlayRef;
3238
+ const wasVisible = !this.hidden;
3239
+ overlayRef.overlayElement.classList.add('md3-dialog-closing');
3240
+ overlayRef.backdropElement?.classList.add('md3-dialog-closing');
3241
+ this.dialogInstance?.setActive(false);
3242
+ // A hidden dialog already played the exit animation, so there is
3243
+ // nothing left to transition and it can be disposed right away.
3244
+ return wasVisible ? this.waitForSurfaceAnimation() : Promise.resolve();
3245
+ }
3246
+ /** Resolves when the surface finished animating, with a timeout as a safety net. */
3247
+ waitForSurfaceAnimation() {
3248
+ const panel = this.cdkRef.overlayRef.overlayElement;
3249
+ const surface = this.dialogInstance?.surfaceElement ?? panel;
3126
3250
  return new Promise((resolve) => {
3127
3251
  let isResolved = false;
3128
3252
  let timeoutId = setTimeout(done, DIALOG_EXIT_ANIMATION_FALLBACK_MS);
@@ -3132,82 +3256,327 @@ class DialogRef {
3132
3256
  }
3133
3257
  isResolved = true;
3134
3258
  clearTimeout(timeoutId);
3135
- animatedElement.removeEventListener('transitionend', onTransitionEnd);
3259
+ surface.removeEventListener('transitionend', onTransitionEnd);
3136
3260
  resolve();
3137
3261
  }
3138
3262
  function onTransitionEnd(event) {
3139
- if (event.target === animatedElement) {
3263
+ if (event.target === surface) {
3140
3264
  done();
3141
3265
  }
3142
3266
  }
3143
- animatedElement.addEventListener('transitionend', onTransitionEnd);
3267
+ surface.addEventListener('transitionend', onTransitionEnd);
3144
3268
  });
3145
3269
  }
3146
- afterClosed() {
3147
- return this.closed.asObservable();
3270
+ /** Emits the result once the CDK reference is closed. Focus is restored by the container. */
3271
+ settle(result) {
3272
+ if (this.closeSettled) {
3273
+ return;
3274
+ }
3275
+ // Covers detachments that did not go through close(), so anything
3276
+ // waiting on beforeClosed() is still notified.
3277
+ this.startClosing();
3278
+ this.closeSettled = true;
3279
+ this.closed.next(result);
3280
+ this.closed.complete();
3148
3281
  }
3149
3282
  }
3150
3283
 
3151
- class Dialog {
3152
- portalOutlet = viewChild(CdkPortalOutlet, /* @ts-ignore */
3153
- ...(ngDevMode ? [{ debugName: "portalOutlet" }] : /* istanbul ignore next */ []));
3284
+ /**
3285
+ * Material 3 dialog shell. It extends the CDK dialog container, so focus
3286
+ * trapping, focus restoration and the ARIA attributes are handled by the CDK
3287
+ * while this component only owns the MD3 surface and its animation.
3288
+ */
3289
+ class Dialog extends CdkDialogContainer {
3290
+ surface = viewChild('surface', /* @ts-ignore */
3291
+ ...(ngDevMode ? [{ debugName: "surface" }] : /* istanbul ignore next */ []));
3154
3292
  isActive = signal(false, /* @ts-ignore */
3155
3293
  ...(ngDevMode ? [{ debugName: "isActive" }] : /* istanbul ignore next */ []));
3156
3294
  config = inject(DIALOG_CONFIG, { optional: true }) ?? {};
3157
- get role() {
3158
- return this.config.role ?? 'dialog';
3295
+ get surfaceElement() {
3296
+ return this.surface()?.nativeElement ?? null;
3159
3297
  }
3160
- ngAfterContentInit() {
3298
+ /**
3299
+ * Started by DialogService, either right after the dialog is created or
3300
+ * once the dialog it replaces has had its head start. The animation runs on
3301
+ * the next frame so the surface has a real from/to state.
3302
+ */
3303
+ startEnterAnimation() {
3161
3304
  requestAnimationFrame(() => {
3162
3305
  this.isActive.set(true);
3163
3306
  });
3164
3307
  }
3165
- /**
3166
- * The service creates this wrapper first, then calls this method to mount
3167
- * the user supplied component into the dialog container.
3168
- */
3169
- attachContent(component, injector) {
3170
- const portal = new ComponentPortal(component, null, injector);
3171
- return this.portalOutlet().attachComponentPortal(portal);
3308
+ setActive(value) {
3309
+ this.isActive.set(value);
3310
+ }
3311
+ recaptureFocus() {
3312
+ this._recaptureFocus();
3172
3313
  }
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"] }] });
3314
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: Dialog, deps: null, target: i0.ɵɵFactoryTarget.Component });
3315
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "22.1.0", type: Dialog, isStandalone: true, selector: "md3-dialog", viewQueries: [{ propertyName: "surface", first: true, predicate: ["surface"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<!-- The role and aria attributes live on the host element, applied by the CDK dialog container. -->\n<section\n #surface\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
3316
  }
3176
3317
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: Dialog, decorators: [{
3177
3318
  type: Component,
3178
3319
  args: [{ selector: 'md3-dialog', imports: [
3179
3320
  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 }] }] } });
3321
+ ], template: "<!-- The role and aria attributes live on the host element, applied by the CDK dialog container. -->\n<section\n #surface\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"] }]
3322
+ }], propDecorators: { surface: [{ type: i0.ViewChild, args: ['surface', { isSignal: true }] }] } });
3323
+
3324
+ /**
3325
+ * Material 3 full screen dialog shell. It covers the viewport and lays its
3326
+ * content out like the scaffold does, with a main pane and a side sheet outlet
3327
+ * on each side, so side sheets opened while it is up land inside the dialog
3328
+ * instead of behind it.
3329
+ */
3330
+ class FullScreenDialog extends CdkDialogContainer {
3331
+ surface = viewChild('surface', /* @ts-ignore */
3332
+ ...(ngDevMode ? [{ debugName: "surface" }] : /* istanbul ignore next */ []));
3333
+ startOutlet = viewChild('startOutlet', { ...(ngDevMode ? { debugName: "startOutlet" } : /* istanbul ignore next */ {}), read: CdkPortalOutlet });
3334
+ endOutlet = viewChild('endOutlet', { ...(ngDevMode ? { debugName: "endOutlet" } : /* istanbul ignore next */ {}), read: CdkPortalOutlet });
3335
+ sheets = inject(SheetsService);
3336
+ isActive = signal(false, /* @ts-ignore */
3337
+ ...(ngDevMode ? [{ debugName: "isActive" }] : /* istanbul ignore next */ []));
3338
+ config = inject(DIALOG_CONFIG, { optional: true }) ?? {};
3339
+ get surfaceElement() {
3340
+ return this.surface()?.nativeElement ?? null;
3341
+ }
3342
+ ngAfterViewInit() {
3343
+ const startOutlet = this.startOutlet();
3344
+ const endOutlet = this.endOutlet();
3345
+ // Taking the outlets over is what sends the next side sheet inside the
3346
+ // dialog. The scaffold gets them back when this dialog is destroyed.
3347
+ if (startOutlet) {
3348
+ this.sheets.registerSideSheetOutlet('start', startOutlet);
3349
+ }
3350
+ if (endOutlet) {
3351
+ this.sheets.registerSideSheetOutlet('end', endOutlet);
3352
+ }
3353
+ }
3354
+ ngOnDestroy() {
3355
+ const startOutlet = this.startOutlet();
3356
+ const endOutlet = this.endOutlet();
3357
+ if (startOutlet) {
3358
+ this.sheets.unregisterSideSheetOutlet('start', startOutlet);
3359
+ }
3360
+ if (endOutlet) {
3361
+ this.sheets.unregisterSideSheetOutlet('end', endOutlet);
3362
+ }
3363
+ super.ngOnDestroy();
3364
+ }
3365
+ startEnterAnimation() {
3366
+ requestAnimationFrame(() => {
3367
+ this.isActive.set(true);
3368
+ });
3369
+ }
3370
+ setActive(value) {
3371
+ this.isActive.set(value);
3372
+ }
3373
+ recaptureFocus() {
3374
+ this._recaptureFocus();
3375
+ }
3376
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: FullScreenDialog, deps: null, target: i0.ɵɵFactoryTarget.Component });
3377
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "22.1.0", type: FullScreenDialog, isStandalone: true, selector: "md3-fullscreen-dialog", viewQueries: [{ propertyName: "surface", first: true, predicate: ["surface"], descendants: true, isSignal: true }, { propertyName: "startOutlet", first: true, predicate: ["startOutlet"], descendants: true, read: CdkPortalOutlet, isSignal: true }, { propertyName: "endOutlet", first: true, predicate: ["endOutlet"], descendants: true, read: CdkPortalOutlet, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<!-- The role and aria attributes live on the host element, applied by the CDK dialog container. -->\n<section\n #surface\n class=\"md3-fullscreen-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 <div class=\"md3-panes-container md3-scrollable\">\n <!--\n The CDK attaches the dialog content to the first portal outlet of\n this template, so the main pane has to come first. The panes are\n placed by grid area, which makes the DOM order irrelevant visually.\n -->\n <div class=\"md3-pane md3-pane-main\">\n <ng-template cdkPortalOutlet></ng-template>\n </div>\n <div class=\"md3-pane md3-pane-start\">\n <ng-template #startOutlet cdkPortalOutlet></ng-template>\n </div>\n <div class=\"md3-pane md3-pane-end\">\n <ng-template #endOutlet cdkPortalOutlet></ng-template>\n </div>\n </div>\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 .cdk-global-overlay-wrapper{position:absolute;display:flex;inset:0;pointer-events:none;overflow:hidden}::ng-deep .cdk-overlay-container .cdk-global-overlay-wrapper:has(>.md3-fullscreen-dialog-panel){padding:0}::ng-deep .cdk-overlay-container .cdk-global-overlay-wrapper .md3-fullscreen-dialog-panel{width:100%;height:100%;max-width:none;pointer-events:auto}::ng-deep .cdk-overlay-container .cdk-global-overlay-wrapper .md3-fullscreen-dialog-panel.md3-dialog-closing,::ng-deep .cdk-overlay-container .cdk-global-overlay-wrapper .md3-fullscreen-dialog-panel.md3-dialog-hidden{pointer-events:none}:host{display:block;height:100%;width:100%;outline:0}:host .md3-fullscreen-dialog-container{display:grid;grid-template-rows:minmax(0,1fr);height:100%;min-height:0;overflow:hidden;color:rgb(var(--md-scheme-on-surface));background-color:rgb(var(--md-scheme-surface));transition:all var(--md-motion-expressive-default-spatial-duration) var(--md-motion-expressive-default-spatial-easing);opacity:0;transform:translateY(1.5em)}:host .md3-fullscreen-dialog-container.md3-active{opacity:1;transform:none}:host .md3-panes-container{display:grid;grid-template-areas:\"start main end\";grid-template-columns:auto minmax(0,1fr) auto;grid-template-rows:minmax(0,1fr);min-width:0;min-height:0;overflow:hidden}:host .md3-panes-container .md3-pane{justify-self:stretch;align-self:stretch;min-width:0;min-height:0;height:100%;overflow:hidden}:host .md3-panes-container .md3-pane.md3-pane-start{grid-area:start}:host .md3-panes-container .md3-pane.md3-pane-main{position:relative;grid-area:main;z-index:1;display:grid;grid-template-rows:minmax(0,1fr)}:host .md3-panes-container .md3-pane.md3-pane-main>::ng-deep *{display:grid;grid-template-areas:\"header\" \"body\";grid-template-rows:auto minmax(0,1fr) auto;min-height:0;height:100%;overflow-x:hidden;overflow-y:auto;scrollbar-width:thin;scrollbar-color:rgb(var(--md-scheme-outline-variant)) transparent}:host .md3-panes-container .md3-pane.md3-pane-main>::ng-deep *::-webkit-scrollbar{width:.5rem;height:.5rem}:host .md3-panes-container .md3-pane.md3-pane-main>::ng-deep *::-webkit-scrollbar-track{background:transparent}:host .md3-panes-container .md3-pane.md3-pane-main>::ng-deep *>md3-dialog-body{grid-area:body;margin:0;padding:1em 1.5em;overflow-y:visible;height:auto}:host .md3-panes-container .md3-pane.md3-pane-end{grid-area:end}\n"], dependencies: [{ kind: "directive", type: CdkPortalOutlet, selector: "[cdkPortalOutlet]", inputs: ["cdkPortalOutlet"], outputs: ["attached"], exportAs: ["cdkPortalOutlet"] }] });
3378
+ }
3379
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: FullScreenDialog, decorators: [{
3380
+ type: Component,
3381
+ args: [{ selector: 'md3-fullscreen-dialog', imports: [
3382
+ CdkPortalOutlet,
3383
+ ], template: "<!-- The role and aria attributes live on the host element, applied by the CDK dialog container. -->\n<section\n #surface\n class=\"md3-fullscreen-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 <div class=\"md3-panes-container md3-scrollable\">\n <!--\n The CDK attaches the dialog content to the first portal outlet of\n this template, so the main pane has to come first. The panes are\n placed by grid area, which makes the DOM order irrelevant visually.\n -->\n <div class=\"md3-pane md3-pane-main\">\n <ng-template cdkPortalOutlet></ng-template>\n </div>\n <div class=\"md3-pane md3-pane-start\">\n <ng-template #startOutlet cdkPortalOutlet></ng-template>\n </div>\n <div class=\"md3-pane md3-pane-end\">\n <ng-template #endOutlet cdkPortalOutlet></ng-template>\n </div>\n </div>\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 .cdk-global-overlay-wrapper{position:absolute;display:flex;inset:0;pointer-events:none;overflow:hidden}::ng-deep .cdk-overlay-container .cdk-global-overlay-wrapper:has(>.md3-fullscreen-dialog-panel){padding:0}::ng-deep .cdk-overlay-container .cdk-global-overlay-wrapper .md3-fullscreen-dialog-panel{width:100%;height:100%;max-width:none;pointer-events:auto}::ng-deep .cdk-overlay-container .cdk-global-overlay-wrapper .md3-fullscreen-dialog-panel.md3-dialog-closing,::ng-deep .cdk-overlay-container .cdk-global-overlay-wrapper .md3-fullscreen-dialog-panel.md3-dialog-hidden{pointer-events:none}:host{display:block;height:100%;width:100%;outline:0}:host .md3-fullscreen-dialog-container{display:grid;grid-template-rows:minmax(0,1fr);height:100%;min-height:0;overflow:hidden;color:rgb(var(--md-scheme-on-surface));background-color:rgb(var(--md-scheme-surface));transition:all var(--md-motion-expressive-default-spatial-duration) var(--md-motion-expressive-default-spatial-easing);opacity:0;transform:translateY(1.5em)}:host .md3-fullscreen-dialog-container.md3-active{opacity:1;transform:none}:host .md3-panes-container{display:grid;grid-template-areas:\"start main end\";grid-template-columns:auto minmax(0,1fr) auto;grid-template-rows:minmax(0,1fr);min-width:0;min-height:0;overflow:hidden}:host .md3-panes-container .md3-pane{justify-self:stretch;align-self:stretch;min-width:0;min-height:0;height:100%;overflow:hidden}:host .md3-panes-container .md3-pane.md3-pane-start{grid-area:start}:host .md3-panes-container .md3-pane.md3-pane-main{position:relative;grid-area:main;z-index:1;display:grid;grid-template-rows:minmax(0,1fr)}:host .md3-panes-container .md3-pane.md3-pane-main>::ng-deep *{display:grid;grid-template-areas:\"header\" \"body\";grid-template-rows:auto minmax(0,1fr) auto;min-height:0;height:100%;overflow-x:hidden;overflow-y:auto;scrollbar-width:thin;scrollbar-color:rgb(var(--md-scheme-outline-variant)) transparent}:host .md3-panes-container .md3-pane.md3-pane-main>::ng-deep *::-webkit-scrollbar{width:.5rem;height:.5rem}:host .md3-panes-container .md3-pane.md3-pane-main>::ng-deep *::-webkit-scrollbar-track{background:transparent}:host .md3-panes-container .md3-pane.md3-pane-main>::ng-deep *>md3-dialog-body{grid-area:body;margin:0;padding:1em 1.5em;overflow-y:visible;height:auto}:host .md3-panes-container .md3-pane.md3-pane-end{grid-area:end}\n"] }]
3384
+ }], propDecorators: { surface: [{ type: i0.ViewChild, args: ['surface', { isSignal: true }] }], startOutlet: [{ type: i0.ViewChild, args: ['startOutlet', { ...{ read: CdkPortalOutlet }, isSignal: true }] }], endOutlet: [{ type: i0.ViewChild, args: ['endOutlet', { ...{ read: CdkPortalOutlet }, isSignal: true }] }] } });
3182
3385
 
3386
+ const PANEL_CLASS = 'md3-dialog-panel';
3387
+ const FULL_SCREEN_PANEL_CLASS = 'md3-fullscreen-dialog-panel';
3388
+ const OPENING_CLASS = 'md3-dialog-opening';
3389
+ const SCRIM_KEPT_CLASS = 'md3-dialog-scrim-kept';
3390
+ const SCRIM_INSTANT_CLASS = 'md3-dialog-scrim-instant';
3391
+ /**
3392
+ * Head start given to a dialog that leaves the screen before the dialog taking
3393
+ * its place animates in. Long enough to read as one dialog replacing another,
3394
+ * short enough to still feel like a single gesture.
3395
+ */
3396
+ const DIALOG_HANDOVER_DELAY_MS = 160;
3183
3397
  class DialogService {
3398
+ cdkDialog = inject(Dialog$1);
3184
3399
  overlay = inject(Overlay);
3185
3400
  injector = inject(Injector);
3186
3401
  document = inject(DOCUMENT);
3402
+ /** Open dialogs, from the first one opened to the one currently on top. */
3403
+ refs = [];
3404
+ /**
3405
+ * Page scrolling is blocked here instead of per overlay, so stacked dialogs
3406
+ * cannot unblock the page while another dialog is still open.
3407
+ */
3408
+ scrollBlock = this.overlay.scrollStrategies.block();
3409
+ /** Element that was focused before the first dialog of the stack opened. */
3410
+ rootTrigger = null;
3411
+ get openDialogs() {
3412
+ return this.refs;
3413
+ }
3414
+ /** The full screen dialog that is open, if there is one. */
3415
+ get fullScreenDialog() {
3416
+ return this.refs.find((ref) => ref.isFullScreen && !ref.isClosing);
3417
+ }
3187
3418
  open(component, config = {}) {
3188
3419
  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);
3420
+ if (this.refs.length === 0) {
3421
+ this.rootTrigger = this.getFocusedElement();
3422
+ }
3423
+ // The dialog that is on screen starts leaving first, so the two dialogs
3424
+ // never animate on top of each other. A full screen dialog is not part
3425
+ // of this: regular dialogs open on top of it and leave it alone.
3426
+ const previousRef = this.topRegularRef();
3427
+ if (previousRef) {
3428
+ this.dismissPrevious(previousRef, dialogConfig.previousDialog);
3429
+ }
3430
+ let dialogRef;
3431
+ const cdkRef = this.cdkDialog.open(component, {
3432
+ // The MD3 shell replaces the default CDK container, so the overlay,
3433
+ // focus trap and focus restoration come from the CDK while the shell
3434
+ // only renders the surface.
3435
+ container: {
3436
+ type: Dialog,
3437
+ providers: () => [
3438
+ { provide: DIALOG_CONFIG, useValue: dialogConfig },
3439
+ ],
3440
+ },
3441
+ // These tokens make the dialog controllable from the dynamic
3442
+ // component without coupling that component to the service.
3443
+ providers: (cdkDialogRef) => {
3444
+ dialogRef = new DialogRef(cdkDialogRef, dialogConfig.disableCloseEvents);
3445
+ return [
3446
+ { provide: DialogRef, useValue: dialogRef },
3447
+ { provide: DIALOG_DATA, useValue: dialogConfig.data },
3448
+ { provide: DIALOG_CONFIG, useValue: dialogConfig },
3449
+ { provide: DIALOG_COMPONENT, useValue: component },
3450
+ ];
3451
+ },
3452
+ data: dialogConfig.data,
3453
+ role: dialogConfig.role,
3454
+ ariaModal: true,
3455
+ ariaLabel: dialogConfig.ariaLabel || null,
3456
+ ariaLabelledBy: dialogConfig.ariaLabelledBy || null,
3457
+ ariaDescribedBy: dialogConfig.ariaDescribedBy || null,
3458
+ direction: dialogConfig.direction ?? undefined,
3459
+ viewContainerRef: dialogConfig.viewContainerRef,
3460
+ injector: dialogConfig.injector,
3461
+ hasBackdrop: true,
3462
+ backdropClass: ['md3-dialog-scrim', OPENING_CLASS],
3463
+ panelClass: [PANEL_CLASS, OPENING_CLASS],
3464
+ positionStrategy: this.overlay.position()
3465
+ .global()
3466
+ .centerHorizontally()
3467
+ .centerVertically(),
3468
+ // Scrolling is blocked by the service for the whole stack.
3469
+ scrollStrategy: this.overlay.scrollStrategies.noop(),
3470
+ // Focus goes back to whatever opened the stack, even when the dialog
3471
+ // that was open underneath has been closed in the meantime.
3472
+ restoreFocus: this.rootTrigger ?? true,
3473
+ // Scrim and Escape handling lives in DialogRef so the dialog can
3474
+ // play its exit animation before the overlay is disposed.
3475
+ disableClose: true,
3476
+ });
3477
+ this.registerDialog(dialogRef, cdkRef, dialogConfig, previousRef);
3478
+ return dialogRef;
3479
+ }
3480
+ /**
3481
+ * Opens a dialog that covers the whole screen. Only one can be open at a
3482
+ * time, so it replaces whatever is open, and while it is up the side sheet
3483
+ * outlets belong to it: side sheets open inside the dialog, not behind it.
3484
+ */
3485
+ openFullScreen(component, config = {}) {
3486
+ const dialogConfig = this.mergeConfig(config);
3487
+ if (this.refs.length === 0) {
3488
+ this.rootTrigger = this.getFocusedElement();
3489
+ }
3490
+ // A full screen dialog is a context of its own: the dialog it replaces
3491
+ // and anything stacked on that dialog goes with it.
3492
+ const previousRef = this.topRef();
3493
+ if (previousRef) {
3494
+ // Everything open belongs to the context being replaced, including
3495
+ // a full screen dialog that is already up.
3496
+ this.closeAll();
3497
+ }
3498
+ let dialogRef;
3499
+ const cdkRef = this.cdkDialog.open(component, {
3500
+ container: {
3501
+ type: FullScreenDialog,
3502
+ providers: () => [
3503
+ { provide: DIALOG_CONFIG, useValue: dialogConfig },
3504
+ ],
3505
+ },
3506
+ providers: (cdkDialogRef) => {
3507
+ dialogRef = new DialogRef(cdkDialogRef, dialogConfig.disableCloseEvents, true);
3508
+ return [
3509
+ { provide: DialogRef, useValue: dialogRef },
3510
+ { provide: DIALOG_DATA, useValue: dialogConfig.data },
3511
+ { provide: DIALOG_CONFIG, useValue: dialogConfig },
3512
+ { provide: DIALOG_COMPONENT, useValue: component },
3513
+ ];
3514
+ },
3515
+ data: dialogConfig.data,
3516
+ role: dialogConfig.role,
3517
+ ariaModal: true,
3518
+ ariaLabel: dialogConfig.ariaLabel || null,
3519
+ ariaLabelledBy: dialogConfig.ariaLabelledBy || null,
3520
+ ariaDescribedBy: dialogConfig.ariaDescribedBy || null,
3521
+ direction: dialogConfig.direction ?? undefined,
3522
+ viewContainerRef: dialogConfig.viewContainerRef,
3523
+ injector: dialogConfig.injector,
3524
+ // The dialog covers the page on its own, so there is nothing left
3525
+ // for a scrim to dim.
3526
+ hasBackdrop: false,
3527
+ panelClass: [FULL_SCREEN_PANEL_CLASS, OPENING_CLASS],
3528
+ // The CDK writes the size on the panel and the global strategy then
3529
+ // drops its centering margins, which is how the panel ends up
3530
+ // covering the whole wrapper.
3531
+ width: '100%',
3532
+ height: '100%',
3533
+ positionStrategy: this.overlay.position().global(),
3534
+ scrollStrategy: this.overlay.scrollStrategies.noop(),
3535
+ restoreFocus: this.rootTrigger ?? true,
3536
+ disableClose: true,
3537
+ });
3538
+ this.registerDialog(dialogRef, cdkRef, dialogConfig, previousRef);
3204
3539
  return dialogRef;
3205
3540
  }
3541
+ /** Wires a freshly created dialog into the stack and starts its animation. */
3542
+ registerDialog(dialogRef, cdkRef, dialogConfig, previousRef) {
3543
+ dialogRef.dialogInstance = cdkRef.containerInstance;
3544
+ dialogRef.componentInstance = cdkRef.componentInstance ?? undefined;
3545
+ if (cdkRef.componentRef) {
3546
+ this.bindDataToInputs(cdkRef.componentRef, dialogConfig);
3547
+ }
3548
+ this.refs.push(dialogRef);
3549
+ this.updateScrollBlock();
3550
+ if (previousRef) {
3551
+ setTimeout(() => this.startReplacingAnimation(dialogRef, previousRef), DIALOG_HANDOVER_DELAY_MS);
3552
+ }
3553
+ else {
3554
+ this.startOpenAnimation(dialogRef);
3555
+ }
3556
+ // Restoring starts with the exit animation, so the dialog underneath is
3557
+ // already coming back while this one is fading out.
3558
+ dialogRef.beforeClosed().subscribe(() => this.restorePreviousDialog(dialogRef));
3559
+ dialogRef.afterClosed().subscribe(() => this.removeRef(dialogRef));
3560
+ }
3561
+ /** Closes every open dialog, including the hidden and the full screen ones. */
3562
+ closeAll() {
3563
+ // Closed from the bottom up so a closing dialog never brings back a
3564
+ // hidden dialog that is about to close as well.
3565
+ return Promise.all([...this.refs].map((ref) => ref.close())).then(() => undefined);
3566
+ }
3567
+ /** Closes the regular dialogs and leaves a full screen dialog alone. */
3568
+ closeRegularDialogs() {
3569
+ const closing = this.refs
3570
+ .filter((ref) => !ref.isFullScreen)
3571
+ .map((ref) => ref.close());
3572
+ return Promise.all(closing).then(() => undefined);
3573
+ }
3206
3574
  mergeConfig(config) {
3207
3575
  return {
3208
3576
  data: config.data,
3209
3577
  bindDataToInputs: config.bindDataToInputs ?? false,
3210
3578
  disableCloseEvents: config.disableCloseEvents ?? false,
3579
+ previousDialog: config.previousDialog ?? 'close',
3211
3580
  role: config.role ?? 'dialog',
3212
3581
  ariaLabel: config.ariaLabel ?? '',
3213
3582
  ariaLabelledBy: config.ariaLabelledBy ?? '',
@@ -3218,20 +3587,17 @@ class DialogService {
3218
3587
  injector: config.injector ?? this.injector,
3219
3588
  };
3220
3589
  }
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);
3590
+ /**
3591
+ * Hides or closes the dialog that is on screen. Its scrim stays up until
3592
+ * the dialog taking over is ready for it, so the page behind never
3593
+ * brightens between the two dialogs.
3594
+ */
3595
+ dismissPrevious(previousRef, behavior) {
3596
+ previousRef.overlayRef.backdropElement?.classList.add(SCRIM_KEPT_CLASS);
3597
+ return behavior === 'hide' ? previousRef.hide() : this.closeRegularDialogs();
3233
3598
  }
3234
- startOpenAnimation(overlayRef) {
3599
+ startOpenAnimation(dialogRef) {
3600
+ const overlayRef = dialogRef.overlayRef;
3235
3601
  const panel = overlayRef.overlayElement;
3236
3602
  const backdrop = overlayRef.backdropElement;
3237
3603
  // The overlay is inserted already rendered. Keep an initial class for
@@ -3240,22 +3606,58 @@ class DialogService {
3240
3606
  requestAnimationFrame(() => {
3241
3607
  setTimeout(() => {
3242
3608
  panel.getBoundingClientRect();
3243
- panel.classList.remove('md3-dialog-opening');
3244
- backdrop?.classList.remove('md3-dialog-opening');
3609
+ panel.classList.remove(OPENING_CLASS);
3610
+ backdrop?.classList.remove(OPENING_CLASS);
3245
3611
  }, 100);
3246
3612
  });
3613
+ dialogRef.dialogInstance?.startEnterAnimation();
3247
3614
  }
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
- ],
3615
+ /** Opening animation for a dialog that took the place of another one. */
3616
+ startReplacingAnimation(dialogRef, previousRef) {
3617
+ if (dialogRef.isClosing) {
3618
+ return;
3619
+ }
3620
+ const panel = dialogRef.overlayRef.overlayElement;
3621
+ const backdrop = dialogRef.overlayRef.backdropElement;
3622
+ this.beginScrimHandover(previousRef, dialogRef);
3623
+ panel.getBoundingClientRect();
3624
+ panel.classList.remove(OPENING_CLASS);
3625
+ backdrop?.classList.remove(OPENING_CLASS);
3626
+ this.endScrimHandover(previousRef, dialogRef);
3627
+ dialogRef.dialogInstance?.startEnterAnimation();
3628
+ }
3629
+ /** Opening animation for a dialog coming back from behind a closing one. */
3630
+ startRestoringAnimation(closingRef, previousRef) {
3631
+ if (previousRef.isClosing) {
3632
+ return;
3633
+ }
3634
+ this.beginScrimHandover(closingRef, previousRef);
3635
+ previousRef.show();
3636
+ this.endScrimHandover(closingRef, previousRef);
3637
+ }
3638
+ /**
3639
+ * Moves both scrims to their new value in a single frame, without a
3640
+ * transition on either side, so the dim behind the dialogs never changes.
3641
+ */
3642
+ beginScrimHandover(fromRef, toRef) {
3643
+ const from = fromRef.overlayRef.backdropElement;
3644
+ const to = toRef.overlayRef.backdropElement;
3645
+ // Nothing to hand the scrim over to, e.g. a full screen dialog: the
3646
+ // outgoing scrim fades out on its own instead.
3647
+ if (!to) {
3648
+ return;
3649
+ }
3650
+ from?.classList.add(SCRIM_INSTANT_CLASS);
3651
+ from?.classList.remove(SCRIM_KEPT_CLASS);
3652
+ to?.classList.add(SCRIM_INSTANT_CLASS);
3653
+ }
3654
+ /** Gives the scrims their transitions back once the handover is painted. */
3655
+ endScrimHandover(fromRef, toRef) {
3656
+ const from = fromRef.overlayRef.backdropElement;
3657
+ const to = toRef.overlayRef.backdropElement;
3658
+ requestAnimationFrame(() => {
3659
+ from?.classList.remove(SCRIM_INSTANT_CLASS);
3660
+ to?.classList.remove(SCRIM_INSTANT_CLASS);
3259
3661
  });
3260
3662
  }
3261
3663
  bindDataToInputs(componentRef, config) {
@@ -3268,34 +3670,69 @@ class DialogService {
3268
3670
  componentRef.setInput(inputName, inputValue);
3269
3671
  });
3270
3672
  }
3271
- connectCloseEvents(overlayRef, dialogRef, config) {
3272
- if (config.disableCloseEvents) {
3673
+ canBindDataToInputs(data) {
3674
+ return typeof data === 'object' && data !== null && !Array.isArray(data);
3675
+ }
3676
+ /** Last dialog of the stack that is not closing yet. */
3677
+ topRef() {
3678
+ for (let index = this.refs.length - 1; index >= 0; index--) {
3679
+ if (!this.refs[index].isClosing) {
3680
+ return this.refs[index];
3681
+ }
3682
+ }
3683
+ return undefined;
3684
+ }
3685
+ /**
3686
+ * Same, without the full screen dialog: it hosts the dialogs opened on top
3687
+ * of it, so it is never the one that gets hidden or closed to make room.
3688
+ */
3689
+ topRegularRef() {
3690
+ const ref = this.topRef();
3691
+ return ref?.isFullScreen ? undefined : ref;
3692
+ }
3693
+ restorePreviousDialog(ref) {
3694
+ const index = this.refs.indexOf(ref);
3695
+ if (index < 1) {
3273
3696
  return;
3274
3697
  }
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
- });
3698
+ for (let previousIndex = index - 1; previousIndex >= 0; previousIndex--) {
3699
+ const previousRef = this.refs[previousIndex];
3700
+ if (previousRef.isClosing) {
3701
+ continue;
3702
+ }
3703
+ // The closing dialog gets the same head start it was given when it
3704
+ // took the screen, and keeps the scrim until then. Nothing to keep
3705
+ // when the dialog underneath has no scrim of its own.
3706
+ if (previousRef.overlayRef.backdropElement) {
3707
+ ref.overlayRef.backdropElement?.classList.add(SCRIM_KEPT_CLASS);
3708
+ }
3709
+ setTimeout(() => this.startRestoringAnimation(ref, previousRef), DIALOG_HANDOVER_DELAY_MS);
3710
+ return;
3711
+ }
3712
+ }
3713
+ removeRef(ref) {
3714
+ const index = this.refs.indexOf(ref);
3715
+ if (index === -1) {
3716
+ return;
3717
+ }
3718
+ this.refs.splice(index, 1);
3719
+ if (this.refs.length === 0) {
3720
+ this.rootTrigger = null;
3721
+ }
3722
+ this.updateScrollBlock();
3723
+ }
3724
+ updateScrollBlock() {
3725
+ if (this.refs.length > 0) {
3726
+ this.scrollBlock.enable();
3727
+ }
3728
+ else {
3729
+ this.scrollBlock.disable();
3730
+ }
3291
3731
  }
3292
3732
  getFocusedElement() {
3293
3733
  const activeElement = this.document.activeElement;
3294
3734
  return activeElement instanceof HTMLElement ? activeElement : null;
3295
3735
  }
3296
- canBindDataToInputs(data) {
3297
- return typeof data === 'object' && data !== null && !Array.isArray(data);
3298
- }
3299
3736
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: DialogService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
3300
3737
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: DialogService, providedIn: 'root' });
3301
3738
  }
@@ -3319,13 +3756,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImpor
3319
3756
 
3320
3757
  class DialogBody {
3321
3758
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: DialogBody, deps: [], target: i0.ɵɵFactoryTarget.Component });
3322
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "22.1.0", type: DialogBody, isStandalone: true, selector: "md3-dialog-body", ngImport: i0, template: "<md3-type-body size=\"medium\">\n <ng-content></ng-content>\n</md3-type-body>", styles: [":host{display:block;margin:1em 1.5em 1.5em;color:rgb(var(--md-scheme-on-surface-variant))}:host .md3-text-body.md3-text-medium{letter-spacing:.017857em}\n"], dependencies: [{ kind: "directive", type: TypeBody, selector: "md3-type-body, [md3-type-body]", inputs: ["size", "color", "emphasized"] }] });
3759
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "22.1.0", type: DialogBody, isStandalone: true, selector: "md3-dialog-body", host: { classAttribute: "md3-scrollable" }, ngImport: i0, template: "<md3-type-body size=\"medium\">\n <ng-content></ng-content>\n</md3-type-body>", styles: [":host{display:block;margin:1em 1.5em 1.5em;color:rgb(var(--md-scheme-on-surface-variant))}:host .md3-text-body.md3-text-medium{letter-spacing:.017857em}\n"], dependencies: [{ kind: "directive", type: TypeBody, selector: "md3-type-body, [md3-type-body]", inputs: ["size", "color", "emphasized"] }] });
3323
3760
  }
3324
3761
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: DialogBody, decorators: [{
3325
3762
  type: Component,
3326
3763
  args: [{ selector: 'md3-dialog-body', imports: [
3327
3764
  TypeBody
3328
- ], template: "<md3-type-body size=\"medium\">\n <ng-content></ng-content>\n</md3-type-body>", styles: [":host{display:block;margin:1em 1.5em 1.5em;color:rgb(var(--md-scheme-on-surface-variant))}:host .md3-text-body.md3-text-medium{letter-spacing:.017857em}\n"] }]
3765
+ ], host: {
3766
+ 'class': 'md3-scrollable',
3767
+ }, template: "<md3-type-body size=\"medium\">\n <ng-content></ng-content>\n</md3-type-body>", styles: [":host{display:block;margin:1em 1.5em 1.5em;color:rgb(var(--md-scheme-on-surface-variant))}:host .md3-text-body.md3-text-medium{letter-spacing:.017857em}\n"] }]
3329
3768
  }] });
3330
3769
 
3331
3770
  class DialogActions {
@@ -3337,6 +3776,38 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImpor
3337
3776
  args: [{ selector: 'md3-dialog-actions', imports: [], template: "<div class=\"md3-actions\">\n <ng-content></ng-content>\n</div>", styles: [":host{display:block;margin:1.5em}:host .md3-actions{display:flex;gap:1em;flex-direction:row-reverse}\n"] }]
3338
3777
  }] });
3339
3778
 
3779
+ /**
3780
+ * Header of a full screen dialog: a leading icon button to leave the dialog,
3781
+ * the headline, and a trailing action. It sticks to the top of the dialog while
3782
+ * the content scrolls underneath.
3783
+ */
3784
+ class FullScreenDialogHeader {
3785
+ title = input('', { ...(ngDevMode ? { debugName: "title" } : /* istanbul ignore next */ {}), alias: 'title' });
3786
+ // context values
3787
+ buttonContextSize = signal('small', /* @ts-ignore */
3788
+ ...(ngDevMode ? [{ debugName: "buttonContextSize" }] : /* istanbul ignore next */ []));
3789
+ buttonContextWidth = signal('default', /* @ts-ignore */
3790
+ ...(ngDevMode ? [{ debugName: "buttonContextWidth" }] : /* istanbul ignore next */ []));
3791
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: FullScreenDialogHeader, deps: [], target: i0.ɵɵFactoryTarget.Component });
3792
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.0", type: FullScreenDialogHeader, isStandalone: true, selector: "md3-fullscreen-dialog-header", inputs: { title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null } }, providers: [
3793
+ {
3794
+ provide: MD3_BUTTON_CONTEXT,
3795
+ useExisting: FullScreenDialogHeader,
3796
+ },
3797
+ ], ngImport: i0, template: "<ng-content select=\"[md3-icon-button][md3-header-leading]\"></ng-content>\n<div class=\"md3-title-container\">\n <md3-type-title size=\"large\">{{ title() }}</md3-type-title>\n</div>\n<div class=\"md3-actions-container\">\n <ng-content select=\"[md3-header-trailing]\"></ng-content>\n</div>\n", styles: [":host{--foreground-color: rgb(var(--md-scheme-on-surface));position:sticky;top:0;z-index:3;grid-area:header;display:flex;align-items:center;gap:.5em;height:4em;padding:0 1em;color:var(--foreground-color);background-color:rgb(var(--md-scheme-surface))}:host ::ng-deep [md3-header-leading]:first-child{margin-inline-start:-.5em}:host .md3-title-container{flex:1 1 auto;min-width:0;color:inherit}:host .md3-title-container:first-child{padding-inline-start:.5em}:host .md3-actions-container{display:flex;flex:0 0 auto;align-items:center;gap:.5em}\n"], dependencies: [{ kind: "directive", type: TypeTitle, selector: "md3-type-title, [md3-type-title]", inputs: ["size", "color", "emphasized"] }] });
3798
+ }
3799
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: FullScreenDialogHeader, decorators: [{
3800
+ type: Component,
3801
+ args: [{ selector: 'md3-fullscreen-dialog-header', imports: [
3802
+ TypeTitle,
3803
+ ], providers: [
3804
+ {
3805
+ provide: MD3_BUTTON_CONTEXT,
3806
+ useExisting: FullScreenDialogHeader,
3807
+ },
3808
+ ], template: "<ng-content select=\"[md3-icon-button][md3-header-leading]\"></ng-content>\n<div class=\"md3-title-container\">\n <md3-type-title size=\"large\">{{ title() }}</md3-type-title>\n</div>\n<div class=\"md3-actions-container\">\n <ng-content select=\"[md3-header-trailing]\"></ng-content>\n</div>\n", styles: [":host{--foreground-color: rgb(var(--md-scheme-on-surface));position:sticky;top:0;z-index:3;grid-area:header;display:flex;align-items:center;gap:.5em;height:4em;padding:0 1em;color:var(--foreground-color);background-color:rgb(var(--md-scheme-surface))}:host ::ng-deep [md3-header-leading]:first-child{margin-inline-start:-.5em}:host .md3-title-container{flex:1 1 auto;min-width:0;color:inherit}:host .md3-title-container:first-child{padding-inline-start:.5em}:host .md3-actions-container{display:flex;flex:0 0 auto;align-items:center;gap:.5em}\n"] }]
3809
+ }], propDecorators: { title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }] } });
3810
+
3340
3811
  const MENU_DATA = new InjectionToken('MD3_MENU_DATA');
3341
3812
  const MENU_CONFIG = new InjectionToken('MD3_MENU_CONFIG');
3342
3813
  const MENU_COMPONENT = new InjectionToken('MD3_MENU_COMPONENT');
@@ -4367,5 +4838,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImpor
4367
4838
  * Generated bundle index. Do not edit.
4368
4839
  */
4369
4840
 
4370
- export { AppBar, AppBarLogo, Avatar, Badge, Button, ButtonGroup, Card, Checkbox, ChipAvatar, Chips, CircularProgressIndicator, DIALOG_COMPONENT, DIALOG_CONFIG, DIALOG_DATA, Dialog, DialogActions, DialogBody, DialogHeader, DialogRef, DialogService, Divider, FloatingActionButton, Grid, GridItem, IconButton, IconElement, InputElement, LayoutService, LinearProgressIndicator, List, ListItem, ListItemPrimaryAction, ListLeading, ListSlot, LoadingIndicator, MENU_COMPONENT, MENU_CONFIG, MENU_DATA, MaterialIcon, Menu, MenuGroup, MenuItem, MenuRef, MenuService, NavigationBar, NavigationGroup, NavigationItem, NavigationRail, RadioButton, SIDE_SHEET_COMPONENT, SIDE_SHEET_CONFIG, SIDE_SHEET_DATA, SNACKBAR_ACTION_LABEL, SNACKBAR_CONFIG, SNACKBAR_MESSAGE, Scaffold, ScaffoldBar, ScaffoldPane, ScaffoldRail, SheetsService, SideSheetActions, SideSheetBody, SideSheetHeader, SideSheetRef, Slider, Snackbar, SnackbarRef, SnackbarService, SplitButton, StateComponent, SupportingText, Switch, TextField, TypeBody, TypeDisplay, TypeHeadline, TypeLabel, TypeTitle };
4841
+ export { AppBar, AppBarLogo, Avatar, Badge, Button, ButtonGroup, Card, Checkbox, ChipAvatar, Chips, CircularProgressIndicator, DIALOG_COMPONENT, DIALOG_CONFIG, DIALOG_DATA, Dialog, DialogActions, DialogBody, DialogHeader, DialogRef, DialogService, Divider, FloatingActionButton, FullScreenDialog, FullScreenDialogHeader, Grid, GridItem, IconButton, IconElement, InputElement, LayoutService, LinearProgressIndicator, List, ListItem, ListItemPrimaryAction, ListLeading, ListSlot, LoadingIndicator, MENU_COMPONENT, MENU_CONFIG, MENU_DATA, MaterialIcon, Menu, MenuGroup, MenuItem, MenuRef, MenuService, NavigationBar, NavigationGroup, NavigationItem, NavigationRail, RadioButton, SIDE_SHEET_COMPONENT, SIDE_SHEET_CONFIG, SIDE_SHEET_DATA, SNACKBAR_ACTION_LABEL, SNACKBAR_CONFIG, SNACKBAR_MESSAGE, Scaffold, ScaffoldBar, ScaffoldPane, ScaffoldRail, SheetsService, SideSheetActions, SideSheetBody, SideSheetHeader, SideSheetRef, Slider, Snackbar, SnackbarRef, SnackbarService, SplitButton, StateComponent, SupportingText, Switch, TextField, TypeBody, TypeDisplay, TypeHeadline, TypeLabel, TypeTitle };
4371
4842
  //# sourceMappingURL=almoamendev-ngx-md3.mjs.map