+
+
+ Start
+ End
+ Left
+ Right
+ Center
+
+
+
+
+ Top
+ Bottom
+
+
+
+
+
+
Show button on snack bar
+
+ Snack bar action label
+
+
+
+
+
+
+
Auto hide after duration
+
+ Auto hide duration in ms
+
+
+
+
+
+ Add extra class to container
+
+
+
+
+
+
+
+
+
+
+ Template snack bar: {{message}}
+
diff --git a/src/dev-app/mdc-snack-bar/mdc-snack-bar-demo.scss b/src/dev-app/mdc-snack-bar/mdc-snack-bar-demo.scss
new file mode 100644
index 000000000000..b8453c032fa4
--- /dev/null
+++ b/src/dev-app/mdc-snack-bar/mdc-snack-bar-demo.scss
@@ -0,0 +1,17 @@
+.demo-party .mdc-snackbar__surface {
+ animation: demo-party 5000ms infinite;
+}
+
+@keyframes demo-party {
+ 0% { background: #00f; }
+ 10% { background: #8e44ad; }
+ 20% { background: #1abc9c; }
+ 30% { background: #d35400; }
+ 40% { background: #00f; }
+ 50% { background: #34495e; }
+ 60% { background: #00f; }
+ 70% { background: #2980b9; }
+ 80% { background: #f1c40f; }
+ 90% { background: #2980b9; }
+ 100% { background: #0ff; }
+}
diff --git a/src/dev-app/mdc-snack-bar/mdc-snack-bar-demo.ts b/src/dev-app/mdc-snack-bar/mdc-snack-bar-demo.ts
new file mode 100644
index 000000000000..da8eaba2787c
--- /dev/null
+++ b/src/dev-app/mdc-snack-bar/mdc-snack-bar-demo.ts
@@ -0,0 +1,57 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+
+import {Directionality} from '@angular/cdk/bidi';
+import {Component, TemplateRef, ViewChild, ViewEncapsulation} from '@angular/core';
+import {MatSnackBar} from '@angular/material-experimental/mdc-snack-bar';
+import {
+ MatSnackBarConfig,
+ MatSnackBarHorizontalPosition,
+ MatSnackBarVerticalPosition
+} from '@angular/material/snack-bar';
+
+@Component({
+ selector: 'mdc-snack-bar-demo',
+ templateUrl: 'mdc-snack-bar-demo.html',
+ styleUrls: ['mdc-snack-bar-demo.css'],
+ encapsulation: ViewEncapsulation.None,
+})
+export class MdcSnackBarDemo {
+
+ @ViewChild('template') template: TemplateRef;
+ message = 'Snack Bar opened.';
+ actionButtonLabel = 'Retry';
+ action = false;
+ setAutoHide = true;
+ autoHide = 10000;
+ addExtraClass = false;
+ horizontalPosition: MatSnackBarHorizontalPosition = 'center';
+ verticalPosition: MatSnackBarVerticalPosition = 'bottom';
+
+ constructor(public snackBar: MatSnackBar, private _dir: Directionality) {}
+
+ open() {
+ const config = this._createConfig();
+ this.snackBar.open(this.message, this.action ? this.actionButtonLabel : undefined, config);
+ }
+
+ openTemplate() {
+ const config = this._createConfig();
+ this.snackBar.openFromTemplate(this.template, config);
+ }
+
+ private _createConfig() {
+ const config = new MatSnackBarConfig();
+ config.verticalPosition = this.verticalPosition;
+ config.horizontalPosition = this.horizontalPosition;
+ config.duration = this.setAutoHide ? this.autoHide : 0;
+ config.panelClass = this.addExtraClass ? ['demo-party'] : undefined;
+ config.direction = this._dir.value;
+ return config;
+ }
+}
diff --git a/src/dev-app/mdc-snackbar/BUILD.bazel b/src/dev-app/mdc-snackbar/BUILD.bazel
deleted file mode 100644
index 4cbccd840809..000000000000
--- a/src/dev-app/mdc-snackbar/BUILD.bazel
+++ /dev/null
@@ -1,22 +0,0 @@
-load("@io_bazel_rules_sass//:defs.bzl", "sass_binary")
-load("//tools:defaults.bzl", "ng_module")
-
-package(default_visibility = ["//visibility:public"])
-
-ng_module(
- name = "mdc-snackbar",
- srcs = glob(["**/*.ts"]),
- assets = [
- "mdc-snackbar-demo.html",
- ":mdc_snackbar_demo_scss",
- ],
- deps = [
- "//src/material-experimental/mdc-snackbar",
- "@npm//@angular/router",
- ],
-)
-
-sass_binary(
- name = "mdc_snackbar_demo_scss",
- src = "mdc-snackbar-demo.scss",
-)
diff --git a/src/dev-app/mdc-snackbar/mdc-snackbar-demo-module.ts b/src/dev-app/mdc-snackbar/mdc-snackbar-demo-module.ts
deleted file mode 100644
index 86fd91914f33..000000000000
--- a/src/dev-app/mdc-snackbar/mdc-snackbar-demo-module.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-/**
- * @license
- * Copyright Google LLC All Rights Reserved.
- *
- * Use of this source code is governed by an MIT-style license that can be
- * found in the LICENSE file at https://angular.io/license
- */
-
-import {NgModule} from '@angular/core';
-import {MatSnackbarModule} from '@angular/material-experimental/mdc-snackbar';
-import {RouterModule} from '@angular/router';
-import {MdcSnackbarDemo} from './mdc-snackbar-demo';
-
-@NgModule({
- imports: [
- MatSnackbarModule,
- RouterModule.forChild([{path: '', component: MdcSnackbarDemo}]),
- ],
- declarations: [MdcSnackbarDemo],
-})
-export class MdcSnackbarDemoModule {
-}
diff --git a/src/dev-app/mdc-snackbar/mdc-snackbar-demo.html b/src/dev-app/mdc-snackbar/mdc-snackbar-demo.html
deleted file mode 100644
index 320ef68256ab..000000000000
--- a/src/dev-app/mdc-snackbar/mdc-snackbar-demo.html
+++ /dev/null
@@ -1,2 +0,0 @@
-
-Not yet implemented.
diff --git a/src/dev-app/mdc-snackbar/mdc-snackbar-demo.scss b/src/dev-app/mdc-snackbar/mdc-snackbar-demo.scss
deleted file mode 100644
index 1c31683b9987..000000000000
--- a/src/dev-app/mdc-snackbar/mdc-snackbar-demo.scss
+++ /dev/null
@@ -1 +0,0 @@
-// TODO: copy in demo styles from existing mat-snackbar demo.
diff --git a/src/dev-app/mdc-snackbar/mdc-snackbar-demo.ts b/src/dev-app/mdc-snackbar/mdc-snackbar-demo.ts
deleted file mode 100644
index 757be572361b..000000000000
--- a/src/dev-app/mdc-snackbar/mdc-snackbar-demo.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-/**
- * @license
- * Copyright Google LLC All Rights Reserved.
- *
- * Use of this source code is governed by an MIT-style license that can be
- * found in the LICENSE file at https://angular.io/license
- */
-
-import {Component} from '@angular/core';
-
-@Component({
- selector: 'mdc-snackbar-demo',
- templateUrl: 'mdc-snackbar-demo.html',
- styleUrls: ['mdc-snackbar-demo.css'],
-})
-export class MdcSnackbarDemo {
-}
diff --git a/src/dev-app/snack-bar/snack-bar-demo.ts b/src/dev-app/snack-bar/snack-bar-demo.ts
index 03ae9d239733..66463ccade07 100644
--- a/src/dev-app/snack-bar/snack-bar-demo.ts
+++ b/src/dev-app/snack-bar/snack-bar-demo.ts
@@ -7,7 +7,7 @@
*/
import {Directionality} from '@angular/cdk/bidi';
-import {Component, TemplateRef, ViewChild} from '@angular/core';
+import {Component, TemplateRef, ViewChild, ViewEncapsulation} from '@angular/core';
import {
MatSnackBar,
MatSnackBarConfig,
@@ -20,6 +20,7 @@ import {
selector: 'snack-bar-demo',
styleUrls: ['snack-bar-demo.css'],
templateUrl: 'snack-bar-demo.html',
+ encapsulation: ViewEncapsulation.None,
})
export class SnackBarDemo {
@ViewChild('template') template: TemplateRef;
diff --git a/src/material-experimental/config.bzl b/src/material-experimental/config.bzl
index df669c819a26..ced17d98678e 100644
--- a/src/material-experimental/config.bzl
+++ b/src/material-experimental/config.bzl
@@ -24,7 +24,7 @@ entryPoints = [
"mdc-slide-toggle/testing",
"mdc-slider",
"mdc-slider/testing",
- "mdc-snackbar",
+ "mdc-snack-bar",
"mdc-table",
"mdc-tabs",
"popover-edit",
diff --git a/src/material-experimental/mdc-snack-bar/BUILD.bazel b/src/material-experimental/mdc-snack-bar/BUILD.bazel
new file mode 100644
index 000000000000..a4c6cccd3cb9
--- /dev/null
+++ b/src/material-experimental/mdc-snack-bar/BUILD.bazel
@@ -0,0 +1,83 @@
+load(
+ "//tools:defaults.bzl",
+ "ng_module",
+ "ng_test_library",
+ "ng_web_test_suite",
+ "sass_binary",
+ "sass_library",
+)
+
+package(default_visibility = ["//visibility:public"])
+
+ng_module(
+ name = "mdc-snack-bar",
+ srcs = glob(
+ ["**/*.ts"],
+ exclude = ["**/*.spec.ts"],
+ ),
+ assets = [
+ ":simple_snack_bar_scss",
+ ":snack_bar_container_scss",
+ ] + glob(["**/*.html"]),
+ module_name = "@angular/material-experimental/mdc-snack-bar",
+ deps = [
+ "//src/material/core",
+ "//src/material/snack-bar",
+ "@npm//@angular/core",
+ "@npm//@material/snackbar",
+ ],
+)
+
+sass_library(
+ name = "mdc_snack_bar_scss_lib",
+ srcs = glob(["**/_*.scss"]),
+ deps = [
+ "//src/material-experimental/mdc-helpers:mdc_helpers_scss_lib",
+ "//src/material/core:core_scss_lib",
+ ],
+)
+
+sass_binary(
+ name = "simple_snack_bar_scss",
+ src = "simple-snack-bar.scss",
+)
+
+sass_binary(
+ name = "snack_bar_container_scss",
+ src = "snack-bar-container.scss",
+ include_paths = [
+ "external/npm/node_modules",
+ ],
+ deps = [
+ "//src/material-experimental/mdc-helpers:mdc_helpers_scss_lib",
+ "//src/material-experimental/mdc-helpers:mdc_scss_deps_lib",
+ ],
+)
+
+###########
+# Testing
+###########
+
+ng_test_library(
+ name = "unit_test_sources",
+ srcs = glob(
+ ["**/*.spec.ts"],
+ exclude = ["**/*.e2e.spec.ts"],
+ ),
+ deps = [
+ ":mdc-snack-bar",
+ "//src/cdk/a11y",
+ "//src/cdk/overlay",
+ "@npm//@angular/common",
+ "@npm//@angular/platform-browser",
+ ],
+)
+
+ng_web_test_suite(
+ name = "unit_tests",
+ static_files = ["@npm//:node_modules/@material/snackbar/dist/mdc.snackbar.js"],
+ deps = [
+ ":unit_test_sources",
+ "//src/material-experimental:mdc_require_config.js",
+ ],
+)
diff --git a/src/material-experimental/mdc-snack-bar/_snack-bar-theme.scss b/src/material-experimental/mdc-snack-bar/_snack-bar-theme.scss
new file mode 100644
index 000000000000..600fcaa79bdc
--- /dev/null
+++ b/src/material-experimental/mdc-snack-bar/_snack-bar-theme.scss
@@ -0,0 +1,38 @@
+@import '../mdc-helpers/mdc-helpers';
+@import '@material/snackbar/mixins.import';
+
+@mixin mat-mdc-snack-bar-color($config-or-theme) {
+ $config: mat-get-color-config($config-or-theme);
+ @include mat-using-mdc-theme($config) {
+ @include mdc-snackbar-core-styles($query: $mat-theme-styles-query);
+ }
+}
+
+@mixin mat-mdc-snack-bar-typography($config-or-theme) {
+ $config: mat-get-typography-config($config-or-theme);
+ @include mat-using-mdc-typography($config) {
+ @include mdc-snackbar-core-styles($query: $mat-typography-styles-query);
+ }
+}
+
+@mixin mat-mdc-snack-bar-density($config-or-theme) {}
+
+@mixin mat-mdc-snack-bar-theme($theme-or-color-config) {
+ $theme: _mat-legacy-get-theme($theme-or-color-config);
+ @include _mat-check-duplicate-theme-styles($theme, 'mat-mdc-snack-bar') {
+ $color: mat-get-color-config($theme);
+ $density: mat-get-density-config($theme);
+ $typography: mat-get-typography-config($theme);
+
+ @if $color != null {
+ @include mat-mdc-snack-bar-color($color);
+ }
+ @if $density != null {
+ @include mat-mdc-snack-bar-density($density);
+ }
+ @if $typography != null {
+ @include mat-mdc-snack-bar-typography($typography);
+ }
+ }
+}
+
diff --git a/src/material-experimental/mdc-snackbar/index.ts b/src/material-experimental/mdc-snack-bar/index.ts
similarity index 100%
rename from src/material-experimental/mdc-snackbar/index.ts
rename to src/material-experimental/mdc-snack-bar/index.ts
diff --git a/src/material-experimental/mdc-snack-bar/module.ts b/src/material-experimental/mdc-snack-bar/module.ts
new file mode 100644
index 000000000000..6160969ce416
--- /dev/null
+++ b/src/material-experimental/mdc-snack-bar/module.ts
@@ -0,0 +1,52 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+
+import {OverlayModule} from '@angular/cdk/overlay';
+import {PortalModule} from '@angular/cdk/portal';
+import {CommonModule} from '@angular/common';
+import {NgModule} from '@angular/core';
+import {MatButtonModule} from '@angular/material/button';
+import {MatCommonModule} from '@angular/material/core';
+
+import {MatSimpleSnackBar} from './simple-snack-bar';
+import {MatSnackBarContainer} from './snack-bar-container';
+import {
+ MatSnackBarAction,
+ MatSnackBarActions,
+ MatSnackBarLabel
+} from './snack-bar-content';
+
+@NgModule({
+ imports: [
+ OverlayModule,
+ PortalModule,
+ CommonModule,
+ MatButtonModule,
+ MatCommonModule,
+ ],
+ exports: [
+ MatCommonModule,
+ MatSnackBarContainer,
+ MatSnackBarLabel,
+ MatSnackBarActions,
+ MatSnackBarAction,
+ ],
+ declarations: [
+ MatSimpleSnackBar,
+ MatSnackBarContainer,
+ MatSnackBarLabel,
+ MatSnackBarActions,
+ MatSnackBarAction,
+ ],
+ entryComponents: [
+ MatSimpleSnackBar,
+ MatSnackBarContainer,
+ ],
+})
+export class MatSnackBarModule {
+}
diff --git a/src/material-experimental/mdc-snack-bar/public-api.ts b/src/material-experimental/mdc-snack-bar/public-api.ts
new file mode 100644
index 000000000000..f854131a3e8d
--- /dev/null
+++ b/src/material-experimental/mdc-snack-bar/public-api.ts
@@ -0,0 +1,21 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+
+export * from './simple-snack-bar';
+export * from './snack-bar-container';
+export * from './snack-bar-content';
+export * from './snack-bar';
+export * from './module';
+
+export {
+ MatSnackBarConfig,
+ MatSnackBarRef,
+ SimpleSnackBar,
+ MAT_SNACK_BAR_DATA,
+ MAT_SNACK_BAR_DEFAULT_OPTIONS,
+} from '@angular/material/snack-bar';
diff --git a/src/material-experimental/mdc-snack-bar/simple-snack-bar.html b/src/material-experimental/mdc-snack-bar/simple-snack-bar.html
new file mode 100644
index 000000000000..a6d6bbb2321a
--- /dev/null
+++ b/src/material-experimental/mdc-snack-bar/simple-snack-bar.html
@@ -0,0 +1,9 @@
+
+ {{data.message}}
+
+
+
+
+
diff --git a/src/material-experimental/mdc-snack-bar/simple-snack-bar.scss b/src/material-experimental/mdc-snack-bar/simple-snack-bar.scss
new file mode 100644
index 000000000000..4766bd5697f4
--- /dev/null
+++ b/src/material-experimental/mdc-snack-bar/simple-snack-bar.scss
@@ -0,0 +1,5 @@
+// The label and actions expect to be in a flex container. Since this component adds another
+// wrapping layer to the mdc-snackbar__surface, it should also include flex display.
+.mat-mdc-simple-snack-bar {
+ display: flex;
+}
diff --git a/src/material-experimental/mdc-snack-bar/simple-snack-bar.ts b/src/material-experimental/mdc-snack-bar/simple-snack-bar.ts
new file mode 100644
index 000000000000..c15c3e0fc578
--- /dev/null
+++ b/src/material-experimental/mdc-snack-bar/simple-snack-bar.ts
@@ -0,0 +1,34 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+
+import {ChangeDetectionStrategy, Component, Inject, ViewEncapsulation} from '@angular/core';
+import {
+ MAT_SNACK_BAR_DATA,
+ TextOnlySnackBar,
+ MatSnackBarRef,
+ SimpleSnackBar
+} from '@angular/material/snack-bar';
+
+@Component({
+ selector: 'mat-simple-snack-bar',
+ templateUrl: 'simple-snack-bar.html',
+ styleUrls: ['simple-snack-bar.css'],
+ exportAs: 'matSnackBar',
+ encapsulation: ViewEncapsulation.None,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ host: {
+ 'class': 'mat-mdc-simple-snack-bar',
+ }
+})
+export class MatSimpleSnackBar implements TextOnlySnackBar {
+ constructor(
+ public snackBarRef: MatSnackBarRef,
+ @Inject(MAT_SNACK_BAR_DATA) public data: {message: string, action: string}) {
+ }
+}
+
diff --git a/src/material-experimental/mdc-snack-bar/snack-bar-container.html b/src/material-experimental/mdc-snack-bar/snack-bar-container.html
new file mode 100644
index 000000000000..4172dd285f5a
--- /dev/null
+++ b/src/material-experimental/mdc-snack-bar/snack-bar-container.html
@@ -0,0 +1,9 @@
+
+
+
+
+
+
diff --git a/src/material-experimental/mdc-snack-bar/snack-bar-container.scss b/src/material-experimental/mdc-snack-bar/snack-bar-container.scss
new file mode 100644
index 000000000000..3a8466a6c403
--- /dev/null
+++ b/src/material-experimental/mdc-snack-bar/snack-bar-container.scss
@@ -0,0 +1,16 @@
+@import '@material/snackbar/mixins.import';
+@import '../mdc-helpers/mdc-helpers';
+
+@include mdc-snackbar-core-styles($query: $mat-base-styles-query);
+
+// MDC sets the position as fixed and sets the container on the bottom center of the page (or
+// otherwise can be set to be "leading"). Our overlay handles a more advanced configuration
+// of positions, so we'll defer logic there.
+.mat-mdc-snack-bar-container {
+ position: static;
+}
+
+// These elements need to have full width using flex layout.
+.mat-mdc-snack-bar-handset, .mat-mdc-snack-bar-container, .mat-mdc-snack-bar-label {
+ flex: 1;
+}
diff --git a/src/material-experimental/mdc-snack-bar/snack-bar-container.ts b/src/material-experimental/mdc-snack-bar/snack-bar-container.ts
new file mode 100644
index 000000000000..b194623ac9ef
--- /dev/null
+++ b/src/material-experimental/mdc-snack-bar/snack-bar-container.ts
@@ -0,0 +1,186 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+
+import {
+ BasePortalOutlet,
+ CdkPortalOutlet,
+ ComponentPortal,
+ TemplatePortal
+} from '@angular/cdk/portal';
+import {
+ AfterViewChecked,
+ ChangeDetectionStrategy,
+ Component,
+ ComponentRef,
+ ElementRef,
+ EmbeddedViewRef,
+ OnDestroy,
+ ViewChild,
+ ViewEncapsulation
+} from '@angular/core';
+import {MatSnackBarConfig, SnackBarContainer} from '@angular/material/snack-bar';
+import {MDCSnackbarAdapter, MDCSnackbarFoundation} from '@material/snackbar';
+import {Observable, Subject} from 'rxjs';
+
+/**
+ * The MDC label class that should wrap the label content of the snack bar.
+ * @docs-private
+ */
+const MDC_SNACKBAR_LABEL_CLASS = 'mdc-snackbar__label';
+
+/**
+ * Internal component that wraps user-provided snack bar content.
+ * @docs-private
+ */
+@Component({
+ selector: 'mat-mdc-snack-bar-container',
+ templateUrl: 'snack-bar-container.html',
+ styleUrls: ['snack-bar-container.css'],
+ // In Ivy embedded views will be change detected from their declaration place, rather than
+ // where they were stamped out. This means that we can't have the snack bar container be OnPush,
+ // because it might cause snack bars that were opened from a template not to be out of date.
+ // tslint:disable-next-line:validate-decorators
+ changeDetection: ChangeDetectionStrategy.Default,
+ encapsulation: ViewEncapsulation.None,
+ host: {
+ '[attr.role]': '_role',
+ 'class': 'mdc-snackbar mat-mdc-snack-bar-container',
+ '[class.mat-snack-bar-container]': 'false',
+ // Mark this element with a 'mat-exit' attribute to indicate that the snackbar has
+ // been dismissed and will soon be removed from the DOM. This is used by the snackbar
+ // test harness.
+ '[attr.mat-exit]': `_exiting ? '' : null`,
+ }
+})
+export class MatSnackBarContainer extends BasePortalOutlet
+ implements SnackBarContainer, AfterViewChecked, OnDestroy {
+ /** Subject for notifying that the snack bar has exited from view. */
+ readonly _onExit: Subject = new Subject();
+
+ /** Subject for notifying that the snack bar has finished entering the view. */
+ readonly _onEnter: Subject = new Subject();
+
+ /** ARIA role for the snack bar container. */
+ _role: 'alert' | 'status' | null;
+
+ /** Whether the snack bar is currently exiting. */
+ _exiting = false;
+
+ private _mdcAdapter: MDCSnackbarAdapter = {
+ addClass: (className: string) => this._setClass(className, true),
+ removeClass: (className: string) => this._setClass(className, false),
+ announce: () => {},
+ notifyClosed: () => {
+ this._onExit.next();
+ this._mdcFoundation.destroy();
+ },
+ notifyClosing: () => {},
+ notifyOpened: () => this._onEnter.next(),
+ notifyOpening: () => {},
+ };
+
+ _mdcFoundation = new MDCSnackbarFoundation(this._mdcAdapter);
+
+ /** The portal outlet inside of this container into which the snack bar content will be loaded. */
+ @ViewChild(CdkPortalOutlet, {static: true}) _portalOutlet: CdkPortalOutlet;
+
+ /** Element that acts as the MDC surface container which should contain the label and actions. */
+ @ViewChild('surface', {static: true}) _surface: ElementRef;
+
+ /**
+ * Element that will have the `mdc-snackbar__label` class applied if the attached component
+ * or template does not have it. This ensures that the appropriate structure, typography, and
+ * color is applied to the attached view.
+ */
+ @ViewChild('label', {static: true}) _label: ElementRef;
+
+ constructor(
+ private _elementRef: ElementRef,
+ public snackBarConfig: MatSnackBarConfig) {
+ super();
+
+ // Based on the ARIA spec, `alert` and `status` roles have an
+ // implicit `assertive` and `polite` politeness respectively.
+ if (snackBarConfig.politeness === 'assertive' && !snackBarConfig.announcementMessage) {
+ this._role = 'alert';
+ } else if (snackBarConfig.politeness === 'off') {
+ this._role = null;
+ } else {
+ this._role = 'status';
+ }
+
+ // `MatSnackBar` will use the config's timeout to determine when the snack bar should be closed.
+ // Set this to `-1` to mark it as indefinitely open so that MDC does not close itself.
+ this._mdcFoundation.setTimeoutMs(-1);
+ }
+
+ ngAfterViewChecked() {
+ // Check to see if the attached component or template uses the MDC template structure,
+ // specifically the MDC label. If not, the container should apply the MDC label class to this
+ // component's label container, which will apply MDC's label styles to the attached view.
+ if (!this._label.nativeElement.querySelector(`.${MDC_SNACKBAR_LABEL_CLASS}`)) {
+ this._label.nativeElement.classList.add(MDC_SNACKBAR_LABEL_CLASS);
+ } else {
+ this._label.nativeElement.classList.remove(MDC_SNACKBAR_LABEL_CLASS);
+ }
+ }
+
+ /** Makes sure the exit callbacks have been invoked when the element is destroyed. */
+ ngOnDestroy() {
+ this._mdcFoundation.close();
+ }
+
+ enter() {
+ this._mdcFoundation.open();
+ }
+
+ exit(): Observable {
+ this._exiting = true;
+ this._mdcFoundation.close();
+ return this._onExit.asObservable();
+ }
+
+ /** Attach a component portal as content to this snack bar container. */
+ attachComponentPortal(portal: ComponentPortal): ComponentRef {
+ this._assertNotAttached();
+ this._applySnackBarClasses();
+ return this._portalOutlet.attachComponentPortal(portal);
+ }
+
+ /** Attach a template portal as content to this snack bar container. */
+ attachTemplatePortal(portal: TemplatePortal): EmbeddedViewRef {
+ this._assertNotAttached();
+ this._applySnackBarClasses();
+ return this._portalOutlet.attachTemplatePortal(portal);
+ }
+
+ private _setClass(cssClass: string, active: boolean) {
+ const classList = this._elementRef.nativeElement.classList;
+ active ? classList.add(cssClass) : classList.remove(cssClass);
+ }
+
+ /** Applies the user-configured CSS classes to the snack bar. */
+ private _applySnackBarClasses() {
+ const panelClasses = this.snackBarConfig.panelClass;
+ if (panelClasses) {
+ if (Array.isArray(panelClasses)) {
+ // Note that we can't use a spread here, because IE doesn't support multiple arguments.
+ panelClasses.forEach(cssClass => this._setClass(cssClass, true));
+ } else {
+ this._setClass(panelClasses, true);
+ }
+ }
+ }
+
+ /** Asserts that no content is already attached to the container. */
+ private _assertNotAttached() {
+ if (this._portalOutlet.hasAttached()) {
+ throw Error('Attempting to attach snack bar content after content is already attached');
+ }
+ }
+}
diff --git a/src/material-experimental/mdc-snack-bar/snack-bar-content.ts b/src/material-experimental/mdc-snack-bar/snack-bar-content.ts
new file mode 100644
index 000000000000..d492eb356e95
--- /dev/null
+++ b/src/material-experimental/mdc-snack-bar/snack-bar-content.ts
@@ -0,0 +1,36 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+
+import {Directive} from '@angular/core';
+
+/** Directive that should be applied to the text element to be rendered in the snack bar. */
+@Directive({
+ selector: `[matSnackBarLabel]`,
+ host: {
+ 'class': 'mat-mdc-snack-bar-label mdc-snackbar__label',
+ }
+})
+export class MatSnackBarLabel {}
+
+/** Directive that should be applied to the element containing the snack bar's action buttons. */
+@Directive({
+ selector: `[matSnackBarActions]`,
+ host: {
+ 'class': 'mat-mdc-snack-bar-actions mdc-snackbar__actions',
+ }
+})
+export class MatSnackBarActions {}
+
+/** Directive that should be applied to each of the snack bar's action buttons. */
+@Directive({
+ selector: `[matSnackBarAction]`,
+ host: {
+ 'class': 'mat-mdc-snack-bar-action mdc-snackbar__action',
+ }
+})
+export class MatSnackBarAction {}
diff --git a/src/material-experimental/mdc-snack-bar/snack-bar.spec.ts b/src/material-experimental/mdc-snack-bar/snack-bar.spec.ts
new file mode 100644
index 000000000000..17e2f40a4bfc
--- /dev/null
+++ b/src/material-experimental/mdc-snack-bar/snack-bar.spec.ts
@@ -0,0 +1,894 @@
+import {LiveAnnouncer} from '@angular/cdk/a11y';
+import {OverlayContainer} from '@angular/cdk/overlay';
+import {CommonModule} from '@angular/common';
+import {
+ Component,
+ Directive,
+ Inject,
+ NgModule,
+ TemplateRef,
+ ViewChild,
+ ViewContainerRef,
+} from '@angular/core';
+import {ComponentFixture, fakeAsync, flush, inject, TestBed, tick} from '@angular/core/testing';
+import {NoopAnimationsModule} from '@angular/platform-browser/animations';
+import {
+ MAT_SNACK_BAR_DATA,
+ MAT_SNACK_BAR_DEFAULT_OPTIONS,
+ MatSimpleSnackBar,
+ MatSnackBar,
+ MatSnackBarConfig, MatSnackBarContainer,
+ MatSnackBarModule,
+ MatSnackBarRef,
+} from './index';
+
+describe('MatSnackBar', () => {
+ let snackBar: MatSnackBar;
+ let liveAnnouncer: LiveAnnouncer;
+ let overlayContainer: OverlayContainer;
+ let overlayContainerElement: HTMLElement;
+
+ let testViewContainerRef: ViewContainerRef;
+ let viewContainerFixture: ComponentFixture;
+
+ let simpleMessage = 'Burritos are here!';
+ let simpleActionLabel = 'pickup';
+
+ beforeEach(fakeAsync(() => {
+ TestBed.configureTestingModule({
+ imports: [MatSnackBarModule, SnackBarTestModule, NoopAnimationsModule],
+ }).compileComponents();
+ }));
+
+ beforeEach(inject([MatSnackBar, LiveAnnouncer, OverlayContainer],
+ (sb: MatSnackBar, la: LiveAnnouncer, oc: OverlayContainer) => {
+ snackBar = sb;
+ liveAnnouncer = la;
+ overlayContainer = oc;
+ overlayContainerElement = oc.getContainerElement();
+ }));
+
+ afterEach(() => {
+ overlayContainer.ngOnDestroy();
+ liveAnnouncer.ngOnDestroy();
+ });
+
+ beforeEach(() => {
+ viewContainerFixture = TestBed.createComponent(ComponentWithChildViewContainer);
+
+ viewContainerFixture.detectChanges();
+ testViewContainerRef = viewContainerFixture.componentInstance.childViewContainer;
+ });
+
+ it('should have the role of `alert` with an `assertive` politeness if no announcement message ' +
+ 'is provided', () => {
+ snackBar.openFromComponent(BurritosNotification,
+ {announcementMessage: '', politeness: 'assertive'});
+
+ viewContainerFixture.detectChanges();
+
+ const containerElement = overlayContainerElement.querySelector('mat-mdc-snack-bar-container')!;
+ expect(containerElement.getAttribute('role'))
+ .toBe('alert', 'Expected snack bar container to have role="alert"');
+ });
+
+ it('should have the role of `status` with an `assertive` politeness if an announcement message ' +
+ 'is provided', () => {
+ snackBar.openFromComponent(BurritosNotification,
+ {announcementMessage: 'Yay Burritos', politeness: 'assertive'});
+ viewContainerFixture.detectChanges();
+
+ const containerElement = overlayContainerElement.querySelector('mat-mdc-snack-bar-container')!;
+ expect(containerElement.getAttribute('role'))
+ .toBe('status', 'Expected snack bar container to have role="status"');
+ });
+
+ it('should have the role of `status` with a `polite` politeness', () => {
+ snackBar.openFromComponent(BurritosNotification, {politeness: 'polite'});
+ viewContainerFixture.detectChanges();
+
+ const containerElement = overlayContainerElement.querySelector('mat-mdc-snack-bar-container')!;
+ expect(containerElement.getAttribute('role'))
+ .toBe('status', 'Expected snack bar container to have role="status"');
+ });
+
+ it('should remove the role if the politeness is turned off', () => {
+ snackBar.openFromComponent(BurritosNotification, {politeness: 'off'});
+ viewContainerFixture.detectChanges();
+
+ const containerElement = overlayContainerElement.querySelector('mat-mdc-snack-bar-container')!;
+ expect(containerElement.getAttribute('role')).toBeFalsy('Expected role to be removed');
+ });
+
+ it('should have exactly one MDC label element when opened through simple snack bar', () => {
+ let config: MatSnackBarConfig = {viewContainerRef: testViewContainerRef};
+ snackBar.open(simpleMessage, simpleActionLabel, config);
+ viewContainerFixture.detectChanges();
+
+ expect(overlayContainerElement.querySelectorAll('.mdc-snackbar__label').length).toBe(1);
+ });
+
+ it('should open and close a snackbar without a ViewContainerRef', fakeAsync(() => {
+ let snackBarRef = snackBar.open('Snack time!', 'Chew');
+ viewContainerFixture.detectChanges();
+
+ let messageElement = overlayContainerElement.querySelector('mat-mdc-snack-bar-container')!;
+ expect(messageElement.textContent).toContain('Snack time!',
+ 'Expected snack bar to show a message without a ViewContainerRef');
+
+ snackBarRef.dismiss();
+ viewContainerFixture.detectChanges();
+ flush();
+
+ expect(overlayContainerElement.childNodes.length)
+ .toBe(0, 'Expected snack bar to be dismissed without a ViewContainerRef');
+ }));
+
+ it('should open a simple message with a button', () => {
+ let config: MatSnackBarConfig = {viewContainerRef: testViewContainerRef};
+ let snackBarRef = snackBar.open(simpleMessage, simpleActionLabel, config);
+
+ viewContainerFixture.detectChanges();
+
+ expect(snackBarRef.instance instanceof MatSimpleSnackBar)
+ .toBe(true, 'Expected the snack bar content component to be SimpleSnackBar');
+ expect(snackBarRef.instance.snackBarRef)
+ .toBe(snackBarRef,
+ 'Expected the snack bar reference to be placed in the component instance');
+
+ let messageElement = overlayContainerElement.querySelector('mat-mdc-snack-bar-container')!;
+ expect(messageElement.textContent)
+ .toContain(simpleMessage, `Expected the snack bar message to be '${simpleMessage}'`);
+
+ let buttonElement = overlayContainerElement.querySelector('button.mat-button')!;
+ expect(buttonElement.tagName)
+ .toBe('BUTTON', 'Expected snack bar action label to be a