From 31d5b7d49fd73dd9ef1af6c4d67cdfd5c8332829 Mon Sep 17 00:00:00 2001 From: andrewseguin Date: Tue, 23 Jun 2020 19:05:38 -0700 Subject: [PATCH 1/8] feat(material-experimental/snack-bar): add MDC-based snack-bar --- .github/CODEOWNERS | 5 +- src/dev-app/BUILD.bazel | 2 +- src/dev-app/dev-app/dev-app-layout.ts | 2 +- src/dev-app/dev-app/routes.ts | 4 +- src/dev-app/mdc-snack-bar/BUILD.bazel | 29 + .../mdc-snack-bar-demo-module.ts | 36 + .../mdc-snack-bar/mdc-snack-bar-demo.html | 59 ++ .../mdc-snack-bar/mdc-snack-bar-demo.scss | 17 + .../mdc-snack-bar/mdc-snack-bar-demo.ts | 57 ++ src/dev-app/mdc-snackbar/BUILD.bazel | 22 - .../mdc-snackbar/mdc-snackbar-demo-module.ts | 22 - .../mdc-snackbar/mdc-snackbar-demo.html | 2 - .../mdc-snackbar/mdc-snackbar-demo.scss | 1 - src/dev-app/mdc-snackbar/mdc-snackbar-demo.ts | 17 - src/dev-app/snack-bar/snack-bar-demo.ts | 3 +- src/material-experimental/config.bzl | 2 +- .../mdc-snack-bar/BUILD.bazel | 83 ++ .../mdc-snack-bar/_snack-bar-theme.scss | 38 + .../{mdc-snackbar => mdc-snack-bar}/index.ts | 0 .../mdc-snack-bar/module.ts | 74 ++ .../mdc-snack-bar/public-api.ts | 20 + .../mdc-snack-bar/simple-snack-bar.html | 9 + .../mdc-snack-bar/simple-snack-bar.scss | 5 + .../mdc-snack-bar/simple-snack-bar.ts | 34 + .../mdc-snack-bar/snack-bar-container.html | 9 + .../mdc-snack-bar/snack-bar-container.scss | 16 + .../mdc-snack-bar/snack-bar-container.ts | 174 ++++ .../mdc-snack-bar/snack-bar.spec.ts | 876 ++++++++++++++++++ .../mdc-snack-bar/snack-bar.ts | 23 + .../tsconfig-build.json | 0 .../mdc-snackbar/BUILD.bazel | 32 - .../mdc-snackbar/_snackbar-theme.scss | 26 - .../mdc-snackbar/module.ts | 19 - .../mdc-snackbar/public-api.ts | 10 - .../mdc-snackbar/snackbar.html | 1 - .../mdc-snackbar/snackbar.scss | 1 - .../mdc-snackbar/snackbar.ts | 20 - .../mdc-theming/BUILD.bazel | 1 + .../mdc-theming/_all-theme.scss | 2 + src/material/snack-bar/simple-snack-bar.ts | 15 +- src/material/snack-bar/snack-bar-container.ts | 16 +- src/material/snack-bar/snack-bar-ref.ts | 6 +- src/material/snack-bar/snack-bar.spec.ts | 47 +- src/material/snack-bar/snack-bar.ts | 43 +- .../public_api_guard/material/snack-bar.d.ts | 31 +- 45 files changed, 1677 insertions(+), 234 deletions(-) create mode 100644 src/dev-app/mdc-snack-bar/BUILD.bazel create mode 100644 src/dev-app/mdc-snack-bar/mdc-snack-bar-demo-module.ts create mode 100644 src/dev-app/mdc-snack-bar/mdc-snack-bar-demo.html create mode 100644 src/dev-app/mdc-snack-bar/mdc-snack-bar-demo.scss create mode 100644 src/dev-app/mdc-snack-bar/mdc-snack-bar-demo.ts delete mode 100644 src/dev-app/mdc-snackbar/BUILD.bazel delete mode 100644 src/dev-app/mdc-snackbar/mdc-snackbar-demo-module.ts delete mode 100644 src/dev-app/mdc-snackbar/mdc-snackbar-demo.html delete mode 100644 src/dev-app/mdc-snackbar/mdc-snackbar-demo.scss delete mode 100644 src/dev-app/mdc-snackbar/mdc-snackbar-demo.ts create mode 100644 src/material-experimental/mdc-snack-bar/BUILD.bazel create mode 100644 src/material-experimental/mdc-snack-bar/_snack-bar-theme.scss rename src/material-experimental/{mdc-snackbar => mdc-snack-bar}/index.ts (100%) create mode 100644 src/material-experimental/mdc-snack-bar/module.ts create mode 100644 src/material-experimental/mdc-snack-bar/public-api.ts create mode 100644 src/material-experimental/mdc-snack-bar/simple-snack-bar.html create mode 100644 src/material-experimental/mdc-snack-bar/simple-snack-bar.scss create mode 100644 src/material-experimental/mdc-snack-bar/simple-snack-bar.ts create mode 100644 src/material-experimental/mdc-snack-bar/snack-bar-container.html create mode 100644 src/material-experimental/mdc-snack-bar/snack-bar-container.scss create mode 100644 src/material-experimental/mdc-snack-bar/snack-bar-container.ts create mode 100644 src/material-experimental/mdc-snack-bar/snack-bar.spec.ts create mode 100644 src/material-experimental/mdc-snack-bar/snack-bar.ts rename src/material-experimental/{mdc-snackbar => mdc-snack-bar}/tsconfig-build.json (100%) delete mode 100644 src/material-experimental/mdc-snackbar/BUILD.bazel delete mode 100644 src/material-experimental/mdc-snackbar/_snackbar-theme.scss delete mode 100644 src/material-experimental/mdc-snackbar/module.ts delete mode 100644 src/material-experimental/mdc-snackbar/public-api.ts delete mode 100644 src/material-experimental/mdc-snackbar/snackbar.html delete mode 100644 src/material-experimental/mdc-snackbar/snackbar.scss delete mode 100644 src/material-experimental/mdc-snackbar/snackbar.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 2fcd03dbf3a5..4010ecdb61c3 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -111,7 +111,7 @@ /src/material-experimental/mdc-progress-spinner/** @andrewseguin /src/material-experimental/mdc-progress-bar/** @andrewseguin /src/material-experimental/mdc-radio/** @mmalerba -/src/material-experimental/mdc-snackbar/** @opozo +/src/material-experimental/mdc-snack-bar/** @andrewseguin /src/material-experimental/mdc-slide-toggle/** @crisbeto /src/material-experimental/mdc-slider/** @devversion /src/material-experimental/mdc-tabs/** @crisbeto @@ -176,8 +176,9 @@ /src/dev-app/mdc-menu/** @crisbeto /src/dev-app/mdc-progress-bar/** @crisbeto /src/dev-app/mdc-radio/** @mmalerba -/src/dev-app/mdc-snackbar/** @opozo +/src/dev-app/mdc-snack-bar/** @andrewseguin /src/dev-app/mdc-sidenav/** @crisbeto +/src/dev-app/mdc-snack-bar/** @andrewseguin /src/dev-app/mdc-slide-toggle/** @crisbeto /src/dev-app/mdc-slider/** @devversion /src/dev-app/mdc-table/** @andrewseguin diff --git a/src/dev-app/BUILD.bazel b/src/dev-app/BUILD.bazel index f7649dbc04b6..cb333071b9ed 100644 --- a/src/dev-app/BUILD.bazel +++ b/src/dev-app/BUILD.bazel @@ -57,7 +57,7 @@ ng_module( "//src/dev-app/mdc-sidenav", "//src/dev-app/mdc-slide-toggle", "//src/dev-app/mdc-slider", - "//src/dev-app/mdc-snackbar", + "//src/dev-app/mdc-snack-bar", "//src/dev-app/mdc-table", "//src/dev-app/mdc-tabs", "//src/dev-app/menu", diff --git a/src/dev-app/dev-app/dev-app-layout.ts b/src/dev-app/dev-app/dev-app-layout.ts index cda5577e38ca..42e8749143d1 100644 --- a/src/dev-app/dev-app/dev-app-layout.ts +++ b/src/dev-app/dev-app/dev-app-layout.ts @@ -85,7 +85,7 @@ export class DevAppLayout { {name: 'MDC Sidenav', route: '/mdc-sidenav'}, {name: 'MDC Slide Toggle', route: '/mdc-slide-toggle'}, {name: 'MDC Slider', route: '/mdc-slider'}, - {name: 'MDC Snackbar', route: '/mdc-snackbar'}, + {name: 'MDC Snack Bar', route: '/mdc-snack-bar'}, {name: 'MDC Table', route: '/mdc-table'}, ]; diff --git a/src/dev-app/dev-app/routes.ts b/src/dev-app/dev-app/routes.ts index b326d2061e6e..89739756a3bc 100644 --- a/src/dev-app/dev-app/routes.ts +++ b/src/dev-app/dev-app/routes.ts @@ -78,8 +78,8 @@ export const DEV_APP_ROUTES: Routes = [ {path: 'mdc-radio', loadChildren: 'mdc-radio/mdc-radio-demo-module#MdcRadioDemoModule'}, {path: 'mdc-sidenav', loadChildren: 'mdc-sidenav/mdc-sidenav-demo-module#MdcSidenavDemoModule'}, { - path: 'mdc-snackbar', - loadChildren: 'mdc-snackbar/mdc-snackbar-demo-module#MdcSnackbarDemoModule' + path: 'mdc-snack-bar', + loadChildren: 'mdc-snack-bar/mdc-snack-bar-demo-module#MdcSnackBarDemoModule' }, { path: 'mdc-slide-toggle', diff --git a/src/dev-app/mdc-snack-bar/BUILD.bazel b/src/dev-app/mdc-snack-bar/BUILD.bazel new file mode 100644 index 000000000000..8f42efacb079 --- /dev/null +++ b/src/dev-app/mdc-snack-bar/BUILD.bazel @@ -0,0 +1,29 @@ +load("@io_bazel_rules_sass//:defs.bzl", "sass_binary") +load("//tools:defaults.bzl", "ng_module") + +package(default_visibility = ["//visibility:public"]) + +ng_module( + name = "mdc-snack-bar", + srcs = glob(["**/*.ts"]), + assets = [ + "mdc-snack-bar-demo.html", + ":mdc_snack_bar_demo_scss", + ], + deps = [ + "//src/cdk/bidi", + "//src/material-experimental/mdc-snack-bar", + "//src/material/button", + "//src/material/checkbox", + "//src/material/form-field", + "//src/material/input", + "//src/material/select", + "@npm//@angular/forms", + "@npm//@angular/router", + ], +) + +sass_binary( + name = "mdc_snack_bar_demo_scss", + src = "mdc-snack-bar-demo.scss", +) diff --git a/src/dev-app/mdc-snack-bar/mdc-snack-bar-demo-module.ts b/src/dev-app/mdc-snack-bar/mdc-snack-bar-demo-module.ts new file mode 100644 index 000000000000..df8f627ee8dd --- /dev/null +++ b/src/dev-app/mdc-snack-bar/mdc-snack-bar-demo-module.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 {NgModule} from '@angular/core'; +import {MatSnackBarModule} from '@angular/material-experimental/mdc-snack-bar'; +import {RouterModule} from '@angular/router'; +import {MdcSnackBarDemo} from './mdc-snack-bar-demo'; +import {CommonModule} from '@angular/common'; +import {FormsModule} from '@angular/forms'; +import {MatButtonModule} from '@angular/material/button'; +import {MatCheckboxModule} from '@angular/material/checkbox'; +import {MatFormFieldModule} from '@angular/material/form-field'; +import {MatInputModule} from '@angular/material/input'; +import {MatSelectModule} from '@angular/material/select'; + +@NgModule({ + imports: [ + MatSnackBarModule, + CommonModule, + FormsModule, + MatButtonModule, + MatCheckboxModule, + MatFormFieldModule, + MatInputModule, + MatSelectModule, + RouterModule.forChild([{path: '', component: MdcSnackBarDemo}]), + ], + declarations: [MdcSnackBarDemo], +}) +export class MdcSnackBarDemoModule { +} diff --git a/src/dev-app/mdc-snack-bar/mdc-snack-bar-demo.html b/src/dev-app/mdc-snack-bar/mdc-snack-bar-demo.html new file mode 100644 index 000000000000..e8ef3ca05cc0 --- /dev/null +++ b/src/dev-app/mdc-snack-bar/mdc-snack-bar-demo.html @@ -0,0 +1,59 @@ +

SnackBar demo

+
+
+ Message: +
+
+
Position in page:
+ + + 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..d661ee564f4f --- /dev/null +++ b/src/material-experimental/mdc-snack-bar/module.ts @@ -0,0 +1,74 @@ +/** + * @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 {Directive, 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'; + +/** 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 {} + +@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..a93d08aff488 --- /dev/null +++ b/src/material-experimental/mdc-snack-bar/public-api.ts @@ -0,0 +1,20 @@ +/** + * @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'; +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..3a27be92c57e --- /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, + MatSimpleSnackBarInterface, + 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]': 'true', + } +}) +export class MatSimpleSnackBar implements MatSimpleSnackBarInterface { + 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..8221437469c7 --- /dev/null +++ b/src/material-experimental/mdc-snack-bar/snack-bar-container.ts @@ -0,0 +1,174 @@ +/** + * @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, MatSnackBarContainerInterface} from '@angular/material/snack-bar'; +import {MDCSnackbarAdapter, MDCSnackbarFoundation} from '@material/snackbar'; +import {Subject} from 'rxjs'; + +/** + * 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, + animations: [], + host: { + '[attr.role]': '_role', + '[class.mdc-snackbar]': 'true', + '[class.mat-mdc-snack-bar-container]': 'true', + '[class.mat-snack-bar-container]': 'false', + } +}) +export class MatSnackBarContainer extends BasePortalOutlet + implements MatSnackBarContainerInterface, 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; + + private _mdcAdapter: MDCSnackbarAdapter = { + addClass: (className: string) => this._setClass(className, true), + removeClass: (className: string) => this._setClass(className, false), + announce: () => {}, + notifyClosed: () => this._onExit.next(), + notifyClosing: () => {}, + notifyOpened: () => this._onEnter.next(), + notifyOpening: () => {}, + }; + + private _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. + const attachedViewHasMdcLabel = + !!this._label.nativeElement.querySelector('.mdc-snackbar__label'); + this._label.nativeElement.classList.toggle('mdc-snackbar__label', !attachedViewHasMdcLabel); + } + + /** Makes sure the exit callbacks have been invoked when the element is destroyed. */ + ngOnDestroy() { + this._mdcFoundation.close(); + } + + enter() { + this._mdcFoundation.open(); + } + + exit() { + // Mark this element with an '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. + this._elementRef.nativeElement.setAttribute('mat-exit', ''); + + this._mdcFoundation.close(); + return this._onExit; + } + + /** 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.spec.ts b/src/material-experimental/mdc-snack-bar/snack-bar.spec.ts new file mode 100644 index 000000000000..aef3c934e94a --- /dev/null +++ b/src/material-experimental/mdc-snack-bar/snack-bar.spec.ts @@ -0,0 +1,876 @@ +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, + 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 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