diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 7b7715893236..862dd89182e3 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -177,6 +177,7 @@
/src/dev-app/mdc-list/** @mmalerba
/src/dev-app/mdc-menu/** @crisbeto
/src/dev-app/mdc-progress-bar/** @crisbeto
+/src/dev-app/mdc-progress-spinner/** @annieyw @mmalerba
/src/dev-app/mdc-radio/** @mmalerba
/src/dev-app/mdc-snack-bar/** @andrewseguin
/src/dev-app/mdc-sidenav/** @crisbeto
@@ -236,6 +237,7 @@
/src/e2e-app/mdc-input/** @devversion
/src/e2e-app/mdc-menu/** @crisbeto
/src/e2e-app/mdc-progress-bar/** @crisbeto
+/src/e2e-app/mdc-progress-spinner/** @annieyw @mmalerba
/src/e2e-app/mdc-radio/** @mmalerba
/src/e2e-app/mdc-slider/** @andrewseguin
/src/e2e-app/mdc-slide-toggle/** @crisbeto
diff --git a/rollup-globals.bzl b/rollup-globals.bzl
index 71cd302e5584..ca76ac87d5be 100644
--- a/rollup-globals.bzl
+++ b/rollup-globals.bzl
@@ -41,6 +41,7 @@ ROLLUP_GLOBALS = {
"@material/auto-init": "mdc.autoInit",
"@material/base": "mdc.base",
"@material/checkbox": "mdc.checkbox",
+ "@material/circular-progress": "mdc.circularProgress",
"@material/chips": "mdc.chips",
"@material/dialog": "mdc.dialog",
"@material/dom": "mdc.dom",
diff --git a/src/dev-app/BUILD.bazel b/src/dev-app/BUILD.bazel
index 31b817794910..52c3217bb4a5 100644
--- a/src/dev-app/BUILD.bazel
+++ b/src/dev-app/BUILD.bazel
@@ -54,6 +54,7 @@ ng_module(
"//src/dev-app/mdc-list",
"//src/dev-app/mdc-menu",
"//src/dev-app/mdc-progress-bar",
+ "//src/dev-app/mdc-progress-spinner",
"//src/dev-app/mdc-radio",
"//src/dev-app/mdc-sidenav",
"//src/dev-app/mdc-slide-toggle",
@@ -130,6 +131,7 @@ filegroup(
"@npm//:node_modules/@material/base/dist/mdc.base.js",
"@npm//:node_modules/@material/checkbox/dist/mdc.checkbox.js",
"@npm//:node_modules/@material/chips/dist/mdc.chips.js",
+ "@npm//:node_modules/@material/circular-progress/dist/mdc.circularProgress.js",
"@npm//:node_modules/@material/data-table/dist/mdc.dataTable.js",
"@npm//:node_modules/@material/dialog/dist/mdc.dialog.js",
"@npm//:node_modules/@material/dom/dist/mdc.dom.js",
diff --git a/src/dev-app/dev-app/dev-app-layout.ts b/src/dev-app/dev-app/dev-app-layout.ts
index daf5413d0b9a..7c96d56ec0ef 100644
--- a/src/dev-app/dev-app/dev-app-layout.ts
+++ b/src/dev-app/dev-app/dev-app-layout.ts
@@ -83,6 +83,7 @@ export class DevAppLayout {
{name: 'MDC Menu', route: '/mdc-menu'},
{name: 'MDC Radio', route: '/mdc-radio'},
{name: 'MDC Progress Bar', route: '/mdc-progress-bar'},
+ {name: 'MDC Progress Spinner', route: '/mdc-progress-spinner'},
{name: 'MDC Tabs', route: '/mdc-tabs'},
{name: 'MDC Sidenav', route: '/mdc-sidenav'},
{name: 'MDC Slide Toggle', route: '/mdc-slide-toggle'},
diff --git a/src/dev-app/dev-app/routes.ts b/src/dev-app/dev-app/routes.ts
index 8d0d05679590..89f1d59a161b 100644
--- a/src/dev-app/dev-app/routes.ts
+++ b/src/dev-app/dev-app/routes.ts
@@ -83,6 +83,11 @@ export const DEV_APP_ROUTES: Routes = [
{path: 'mdc-input', loadChildren: 'mdc-input/mdc-input-demo-module#MdcInputDemoModule'},
{path: 'mdc-list', loadChildren: 'mdc-list/mdc-list-demo-module#MdcListDemoModule'},
{path: 'mdc-menu', loadChildren: 'mdc-menu/mdc-menu-demo-module#MdcMenuDemoModule'},
+ {
+ path: 'mdc-progress-spinner',
+ loadChildren:
+ 'mdc-progress-spinner/mdc-progress-spinner-demo-module#MdcProgressSpinnerDemoModule'
+ },
{path: 'mdc-radio', loadChildren: 'mdc-radio/mdc-radio-demo-module#MdcRadioDemoModule'},
{path: 'mdc-sidenav', loadChildren: 'mdc-sidenav/mdc-sidenav-demo-module#MdcSidenavDemoModule'},
{
diff --git a/src/dev-app/mdc-progress-spinner/BUILD.bazel b/src/dev-app/mdc-progress-spinner/BUILD.bazel
new file mode 100644
index 000000000000..851ca3cd6941
--- /dev/null
+++ b/src/dev-app/mdc-progress-spinner/BUILD.bazel
@@ -0,0 +1,25 @@
+load("//tools:defaults.bzl", "ng_module", "sass_binary")
+
+package(default_visibility = ["//visibility:public"])
+
+ng_module(
+ name = "mdc-progress-spinner",
+ srcs = glob(["**/*.ts"]),
+ assets = [
+ "mdc-progress-spinner-demo.html",
+ ":mdc_progress_spinner_demo_scss",
+ ],
+ deps = [
+ "//src/material-experimental/mdc-progress-spinner",
+ "//src/material/button",
+ "//src/material/button-toggle",
+ "//src/material/checkbox",
+ "@npm//@angular/forms",
+ "@npm//@angular/router",
+ ],
+)
+
+sass_binary(
+ name = "mdc_progress_spinner_demo_scss",
+ src = "mdc-progress-spinner-demo.scss",
+)
diff --git a/src/dev-app/mdc-progress-spinner/mdc-progress-spinner-demo-module.ts b/src/dev-app/mdc-progress-spinner/mdc-progress-spinner-demo-module.ts
new file mode 100644
index 000000000000..351381e161a0
--- /dev/null
+++ b/src/dev-app/mdc-progress-spinner/mdc-progress-spinner-demo-module.ts
@@ -0,0 +1,29 @@
+/**
+ * @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 {MatProgressSpinnerModule} from '@angular/material-experimental/mdc-progress-spinner';
+import {RouterModule} from '@angular/router';
+import {MdcProgressSpinnerDemo} from './mdc-progress-spinner-demo';
+import {MatButtonModule} from '@angular/material/button';
+import {MatCheckboxModule} from '@angular/material/checkbox';
+import {MatButtonToggleModule} from '@angular/material/button-toggle';
+import {FormsModule} from '@angular/forms';
+
+@NgModule({
+ imports: [
+ MatButtonModule,
+ MatCheckboxModule,
+ MatButtonToggleModule,
+ FormsModule,
+ MatProgressSpinnerModule,
+ RouterModule.forChild([{path: '', component: MdcProgressSpinnerDemo}]),
+ ],
+ declarations: [MdcProgressSpinnerDemo],
+})
+export class MdcProgressSpinnerDemoModule {}
diff --git a/src/dev-app/mdc-progress-spinner/mdc-progress-spinner-demo.html b/src/dev-app/mdc-progress-spinner/mdc-progress-spinner-demo.html
new file mode 100644
index 000000000000..9d2f05fc1f53
--- /dev/null
+++ b/src/dev-app/mdc-progress-spinner/mdc-progress-spinner-demo.html
@@ -0,0 +1,32 @@
+
Determinate
+
+
+
Value: {{progressValue}}
+
+
+
Is determinate
+
+
+
+
+
+
+
+
+Indeterminate
+
+
+ Primary Color
+ Accent Color
+ Warn Color
+
+
+
+
+
+
+
+
diff --git a/src/dev-app/mdc-progress-spinner/mdc-progress-spinner-demo.scss b/src/dev-app/mdc-progress-spinner/mdc-progress-spinner-demo.scss
new file mode 100644
index 000000000000..d0dec7c4d5ab
--- /dev/null
+++ b/src/dev-app/mdc-progress-spinner/mdc-progress-spinner-demo.scss
@@ -0,0 +1,12 @@
+.demo-progress-spinner {
+ width: 100%;
+
+ .mat-mdc-progress-spinner,
+ .mat-mdc-spinner {
+ display: inline-block;
+ }
+}
+
+.demo-progress-spinner-controls {
+ margin: 10px 0;
+}
diff --git a/src/dev-app/mdc-progress-spinner/mdc-progress-spinner-demo.ts b/src/dev-app/mdc-progress-spinner/mdc-progress-spinner-demo.ts
new file mode 100644
index 000000000000..e5177f137285
--- /dev/null
+++ b/src/dev-app/mdc-progress-spinner/mdc-progress-spinner-demo.ts
@@ -0,0 +1,26 @@
+/**
+ * @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';
+import {ThemePalette} from '@angular/material/core';
+
+
+@Component({
+ selector: 'mdc-progress-spinner-demo',
+ templateUrl: 'mdc-progress-spinner-demo.html',
+ styleUrls: ['mdc-progress-spinner-demo.css'],
+})
+export class MdcProgressSpinnerDemo {
+ progressValue = 60;
+ color: ThemePalette = 'primary';
+ isDeterminate = true;
+
+ step(val: number) {
+ this.progressValue = Math.max(0, Math.min(100, val + this.progressValue));
+ }
+}
diff --git a/src/e2e-app/BUILD.bazel b/src/e2e-app/BUILD.bazel
index 454af893fda4..8b6285cb62cf 100644
--- a/src/e2e-app/BUILD.bazel
+++ b/src/e2e-app/BUILD.bazel
@@ -40,6 +40,7 @@ ng_module(
"//src/material-experimental/mdc-input",
"//src/material-experimental/mdc-menu",
"//src/material-experimental/mdc-progress-bar",
+ "//src/material-experimental/mdc-progress-spinner",
"//src/material-experimental/mdc-radio",
"//src/material-experimental/mdc-slide-toggle",
"//src/material-experimental/mdc-slider",
diff --git a/src/e2e-app/devserver-configure.js b/src/e2e-app/devserver-configure.js
index 9906a958623d..4b5b619a7d11 100644
--- a/src/e2e-app/devserver-configure.js
+++ b/src/e2e-app/devserver-configure.js
@@ -13,6 +13,7 @@ require.config({
'@material/base': '@material/base/dist/mdc.base',
'@material/checkbox': '@material/checkbox/dist/mdc.checkbox',
'@material/chips': '@material/chips/dist/mdc.chips',
+ '@material/circular-progress': '@material/circular-progress/dist/mdc.circularProgress',
'@material/dialog': '@material/dialog/dist/mdc.dialog',
'@material/dom': '@material/dom/dist/mdc.dom',
'@material/drawer': '@material/drawer/dist/mdc.drawer',
diff --git a/src/e2e-app/e2e-app/e2e-app-layout.html b/src/e2e-app/e2e-app/e2e-app-layout.html
index 36147be3f023..1cedee067943 100644
--- a/src/e2e-app/e2e-app/e2e-app-layout.html
+++ b/src/e2e-app/e2e-app/e2e-app-layout.html
@@ -34,6 +34,7 @@
MDC Table
MDC Tabs
MDC Progress bar
+ MDC Progress spinner
diff --git a/src/e2e-app/e2e-app/routes.ts b/src/e2e-app/e2e-app/routes.ts
index a88f9fb64b84..e699e23e181d 100644
--- a/src/e2e-app/e2e-app/routes.ts
+++ b/src/e2e-app/e2e-app/routes.ts
@@ -23,6 +23,7 @@ import {MdcSliderE2e} from '../mdc-slider/mdc-slider-e2e';
import {MdcTableE2e} from '../mdc-table/mdc-table-e2e';
import {MdcTabsE2e} from '../mdc-tabs/mdc-tabs-e2e';
import {MdcProgressBarE2E} from '../mdc-progress-bar/mdc-progress-bar-e2e';
+import {MdcProgressSpinnerE2e} from '../mdc-progress-spinner/mdc-progress-spinner-e2e';
import {MenuE2E} from '../menu/menu-e2e';
import {ProgressBarE2E} from '../progress-bar/progress-bar-e2e';
import {ProgressSpinnerE2E} from '../progress-spinner/progress-spinner-e2e';
@@ -61,6 +62,7 @@ export const E2E_APP_ROUTES: Routes = [
{path: 'mdc-tabs', component: MdcTabsE2e},
{path: 'mdc-table', component: MdcTableE2e},
{path: 'mdc-progress-bar', component: MdcProgressBarE2E},
+ {path: 'mdc-progress-spinner', component: MdcProgressSpinnerE2e},
{path: 'menu', component: MenuE2E},
{path: 'progress-bar', component: ProgressBarE2E},
{path: 'progress-spinner', component: ProgressSpinnerE2E},
diff --git a/src/e2e-app/main-module.ts b/src/e2e-app/main-module.ts
index b2bb0cc28d35..30a8e3e36b5f 100644
--- a/src/e2e-app/main-module.ts
+++ b/src/e2e-app/main-module.ts
@@ -40,6 +40,7 @@ import {TabsE2eModule} from './tabs/tabs-e2e-module';
import {ToolbarE2eModule} from './toolbar/toolbar-e2e-module';
import {VirtualScrollE2eModule} from './virtual-scroll/virtual-scroll-e2e-module';
import {MdcProgressBarE2eModule} from './mdc-progress-bar/mdc-progress-bar-e2e-module';
+import {MdcProgressSpinnerE2eModule} from './mdc-progress-spinner/mdc-progress-spinner-module';
@NgModule({
imports: [
@@ -72,6 +73,7 @@ import {MdcProgressBarE2eModule} from './mdc-progress-bar/mdc-progress-bar-e2e-m
MdcTableE2eModule,
MdcTabsE2eModule,
MdcProgressBarE2eModule,
+ MdcProgressSpinnerE2eModule,
MenuE2eModule,
ProgressBarE2eModule,
ProgressSpinnerE2eModule,
diff --git a/src/e2e-app/mdc-progress-spinner/mdc-progress-spinner-e2e.html b/src/e2e-app/mdc-progress-spinner/mdc-progress-spinner-e2e.html
new file mode 100644
index 000000000000..e2672d5bd949
--- /dev/null
+++ b/src/e2e-app/mdc-progress-spinner/mdc-progress-spinner-e2e.html
@@ -0,0 +1,3 @@
+
+
+
diff --git a/src/e2e-app/mdc-progress-spinner/mdc-progress-spinner-e2e.ts b/src/e2e-app/mdc-progress-spinner/mdc-progress-spinner-e2e.ts
new file mode 100644
index 000000000000..ad20e871b974
--- /dev/null
+++ b/src/e2e-app/mdc-progress-spinner/mdc-progress-spinner-e2e.ts
@@ -0,0 +1,19 @@
+/**
+ * @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-progress-spinner-e2e',
+ templateUrl: 'mdc-progress-spinner-e2e.html'
+})
+export class MdcProgressSpinnerE2e {
+ value = 65;
+ diameter = 37;
+ strokeWidth = 6;
+}
diff --git a/src/e2e-app/mdc-progress-spinner/mdc-progress-spinner-module.ts b/src/e2e-app/mdc-progress-spinner/mdc-progress-spinner-module.ts
new file mode 100644
index 000000000000..78da71c0da20
--- /dev/null
+++ b/src/e2e-app/mdc-progress-spinner/mdc-progress-spinner-module.ts
@@ -0,0 +1,17 @@
+/**
+ * @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 {MatProgressSpinnerModule} from '@angular/material-experimental/mdc-progress-spinner';
+import {MdcProgressSpinnerE2e} from './mdc-progress-spinner-e2e';
+
+@NgModule({
+ imports: [MatProgressSpinnerModule],
+ declarations: [MdcProgressSpinnerE2e]
+})
+export class MdcProgressSpinnerE2eModule {}
diff --git a/src/material-experimental/config.bzl b/src/material-experimental/config.bzl
index d94533d7e327..65b7ce391822 100644
--- a/src/material-experimental/config.bzl
+++ b/src/material-experimental/config.bzl
@@ -17,6 +17,8 @@ entryPoints = [
"mdc-menu/testing",
"mdc-progress-bar",
"mdc-progress-bar/testing",
+ "mdc-progress-spinner",
+ "mdc-progress-spinner/testing",
"mdc-radio",
"mdc-select",
"mdc-sidenav",
diff --git a/src/material-experimental/mdc-progress-spinner/BUILD.bazel b/src/material-experimental/mdc-progress-spinner/BUILD.bazel
new file mode 100644
index 000000000000..73419190adc9
--- /dev/null
+++ b/src/material-experimental/mdc-progress-spinner/BUILD.bazel
@@ -0,0 +1,95 @@
+load("//src/e2e-app:test_suite.bzl", "e2e_test_suite")
+load(
+ "//tools:defaults.bzl",
+ "ng_e2e_test_library",
+ "ng_module",
+ "ng_test_library",
+ "ng_web_test_suite",
+ "sass_binary",
+ "sass_library",
+)
+
+package(default_visibility = ["//visibility:public"])
+
+ng_module(
+ name = "mdc-progress-spinner",
+ srcs = glob(
+ ["**/*.ts"],
+ exclude = [
+ "**/*.spec.ts",
+ ],
+ ),
+ assets = [":progress_spinner_scss"] + glob(["**/*.html"]),
+ module_name = "@angular/material-experimental/mdc-progress-spinner",
+ deps = [
+ "//src/cdk/platform",
+ "//src/material/core",
+ "//src/material/progress-spinner",
+ "@npm//@angular/common",
+ "@npm//@angular/core",
+ "@npm//@material/circular-progress",
+ ],
+)
+
+sass_library(
+ name = "mdc_progress_spinner_scss_lib",
+ srcs = glob(["**/_*.scss"]),
+ deps = [
+ "//src/material-experimental/mdc-helpers:mdc_helpers_scss_lib",
+ "//src/material-experimental/mdc-helpers:mdc_scss_deps_lib",
+ ],
+)
+
+sass_binary(
+ name = "progress_spinner_scss",
+ src = "progress-spinner.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",
+ ],
+)
+
+ng_test_library(
+ name = "progress_spinner_tests_lib",
+ srcs = glob(
+ ["**/*.spec.ts"],
+ exclude = ["**/*.e2e.spec.ts"],
+ ),
+ deps = [
+ ":mdc-progress-spinner",
+ "//src/cdk/platform",
+ "//src/material/progress-spinner",
+ "@npm//@angular/common",
+ "@npm//@angular/platform-browser",
+ ],
+)
+
+ng_web_test_suite(
+ name = "unit_tests",
+ static_files = [
+ "@npm//:node_modules/@material/circular-progress/dist/mdc.circularProgress.js",
+ ],
+ deps = [
+ ":progress_spinner_tests_lib",
+ "//src/material-experimental:mdc_require_config.js",
+ ],
+)
+
+ng_e2e_test_library(
+ name = "e2e_test_sources",
+ srcs = glob(["**/*.e2e.spec.ts"]),
+ deps = [
+ "//src/cdk/testing/private/e2e",
+ ],
+)
+
+e2e_test_suite(
+ name = "e2e_tests",
+ deps = [
+ ":e2e_test_sources",
+ "//src/cdk/testing/private/e2e",
+ ],
+)
diff --git a/src/material-experimental/mdc-progress-spinner/README.md b/src/material-experimental/mdc-progress-spinner/README.md
new file mode 100644
index 000000000000..3c70c4e81c91
--- /dev/null
+++ b/src/material-experimental/mdc-progress-spinner/README.md
@@ -0,0 +1,88 @@
+This is prototype of an alternate version of `` built on top of
+[MDC Web](https://github.com/material-components/material-components-web). It demonstrates how
+Angular Material could use MDC Web under the hood while still exposing the same API Angular users as
+the existing ``. This component is experimental and should not be used in
+production.
+
+## How to use
+Assuming your application is already up and running using Angular Material, you can add this
+component by following these steps:
+
+1. Install Angular Material Experimental & MDC WEB:
+
+ ```bash
+ npm i material-components-web @angular/material-experimental
+ ```
+
+2. In your `angular.json`, make sure `node_modules/` is listed as a Sass include path. This is
+ needed for the Sass compiler to be able to find the MDC Web Sass files.
+
+ ```json
+ ...
+ "styles": [
+ "src/styles.scss"
+ ],
+ "stylePreprocessorOptions": {
+ "includePaths": [
+ "node_modules/"
+ ]
+ },
+ ...
+ ```
+
+3. Import the experimental `MatProgressSpinnerModule` and add it to the module that declares your
+ component:
+
+ ```ts
+ import {MatProgressSpinnerModule} from '@angular/material-experimental/mdc-progress-spinner';
+
+ @NgModule({
+ declarations: [MyComponent],
+ imports: [MatProgressSpinnerModule],
+ })
+ export class MyModule {}
+ ```
+
+4. Add use `` in your component's template, just like you would the normal
+ ``:
+
+ ```html
+
+ ```
+
+5. Add the theme and typography mixins to your Sass. (There is currently no pre-built CSS option for
+ the experimental ``):
+
+ ```scss
+ @import '~@angular/material/theming';
+ @import '~@angular/material-experimental/mdc-progress-spinner';
+
+ $my-primary: mat-palette($mat-indigo);
+ $my-accent: mat-palette($mat-pink, A200, A100, A400);
+ $my-theme: mat-light-theme((
+ color: (
+ primary: $my-primary,
+ accent: $my-accent
+ )
+ ));
+
+ @include mat-mdc-progress-spinner-theme($my-theme);
+ @include mat-mdc-progress-spinner-typography();
+ ```
+
+## Replacing the standard progress spinner in an existing app
+Because the experimental API mirrors the API for the standard progress spinner, it can easily be swapped
+in by just changing the import paths. There is currently no schematic for this, but you can run the
+following string replace across your TypeScript files:
+
+```bash
+grep -lr --include="*.ts" --exclude-dir="node_modules" \
+ --exclude="*.d.ts" "['\"]@angular/material/progress-spinner['\"]" | xargs sed -i \
+ "s/['\"]@angular\/material\/progress-spinner['\"]/'@angular\/material-experimental\/mdc-progress-spinner'/g"
+```
+
+CSS styles and tests that depend on implementation details of mat-progress-spinner (such as getting
+elements from the template by class name) will need to be manually updated.
+
+There are some small visual differences between this progress and the standard `mat-progress-spinner`.
+This progress spinner has slightly different animation timings and easing curves.
diff --git a/src/material-experimental/mdc-progress-spinner/_progress-spinner-theme.scss b/src/material-experimental/mdc-progress-spinner/_progress-spinner-theme.scss
new file mode 100644
index 000000000000..8dd80b3b71a4
--- /dev/null
+++ b/src/material-experimental/mdc-progress-spinner/_progress-spinner-theme.scss
@@ -0,0 +1,46 @@
+@import '@material/circular-progress/mixins.import';
+@import '../mdc-helpers/mdc-helpers';
+
+@mixin _mat-mdc-progress-spinner-color($color) {
+ @include mdc-circular-progress-color($color, $query: $mat-theme-styles-query);
+}
+
+@mixin mat-mdc-progress-spinner-color($config-or-theme) {
+ $config: mat-get-color-config($config-or-theme);
+ @include mat-using-mdc-theme($config) {
+ .mat-mdc-progress-spinner {
+ @include _mat-mdc-progress-spinner-color(primary);
+
+ &.mat-accent {
+ @include _mat-mdc-progress-spinner-color(secondary);
+ }
+
+ &.mat-warn {
+ @include _mat-mdc-progress-spinner-color(error);
+ }
+ }
+ }
+}
+
+@mixin mat-mdc-progress-spinner-typography($config-or-theme) {}
+
+@mixin mat-mdc-progress-spinner-density($config-or-theme) {}
+
+@mixin mat-mdc-progress-spinner-theme($theme-or-color-config) {
+ $theme: _mat-legacy-get-theme($theme-or-color-config);
+ @include _mat-check-duplicate-theme-styles($theme, 'mat-mdc-progress-spinner') {
+ $color: mat-get-color-config($theme);
+ $density: mat-get-density-config($theme);
+ $typography: mat-get-typography-config($theme);
+
+ @if $color != null {
+ @include mat-mdc-progress-spinner-color($color);
+ }
+ @if $density != null {
+ @include mat-mdc-progress-spinner-density($density);
+ }
+ @if $typography != null {
+ @include mat-mdc-progress-spinner-typography($typography);
+ }
+ }
+}
diff --git a/src/material-experimental/mdc-progress-spinner/index.ts b/src/material-experimental/mdc-progress-spinner/index.ts
new file mode 100644
index 000000000000..676ca90f1ffa
--- /dev/null
+++ b/src/material-experimental/mdc-progress-spinner/index.ts
@@ -0,0 +1,9 @@
+/**
+ * @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 './public-api';
diff --git a/src/material-experimental/mdc-progress-spinner/module.ts b/src/material-experimental/mdc-progress-spinner/module.ts
new file mode 100644
index 000000000000..b7cfff51bc78
--- /dev/null
+++ b/src/material-experimental/mdc-progress-spinner/module.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
+ */
+
+import {NgModule} from '@angular/core';
+import {MatCommonModule} from '@angular/material/core';
+import {MatProgressSpinner, MatSpinner} from './progress-spinner';
+import {CommonModule} from '@angular/common';
+
+@NgModule({
+ imports: [CommonModule],
+ exports: [MatProgressSpinner, MatSpinner, MatCommonModule],
+ declarations: [MatProgressSpinner, MatSpinner],
+})
+export class MatProgressSpinnerModule {
+}
diff --git a/src/material-experimental/mdc-progress-spinner/progress-spinner.e2e.spec.ts b/src/material-experimental/mdc-progress-spinner/progress-spinner.e2e.spec.ts
new file mode 100644
index 000000000000..e7347d20a384
--- /dev/null
+++ b/src/material-experimental/mdc-progress-spinner/progress-spinner.e2e.spec.ts
@@ -0,0 +1,18 @@
+import {browser, by, element} from 'protractor';
+
+describe('MDC-based progress-spinner', () => {
+ beforeEach(async () => await browser.get('/mdc-progress-spinner'));
+
+ it('should render a determinate progress spinner', async () => {
+ expect(await element(by.css('mat-progress-spinner')).isPresent()).toBe(true);
+ });
+
+ it('should render an indeterminate progress spinner', async () => {
+ expect(await element(by.css('mat-progress-spinner[mode="indeterminate"]')).isPresent())
+ .toBe(true);
+ });
+
+ it('should render a spinner', async () => {
+ expect(await element(by.css('mat-spinner')).isPresent()).toBe(true);
+ });
+});
diff --git a/src/material-experimental/mdc-progress-spinner/progress-spinner.html b/src/material-experimental/mdc-progress-spinner/progress-spinner.html
new file mode 100644
index 000000000000..ec5cf42f6dc3
--- /dev/null
+++ b/src/material-experimental/mdc-progress-spinner/progress-spinner.html
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/material-experimental/mdc-progress-spinner/progress-spinner.scss b/src/material-experimental/mdc-progress-spinner/progress-spinner.scss
new file mode 100644
index 000000000000..610abd1ec850
--- /dev/null
+++ b/src/material-experimental/mdc-progress-spinner/progress-spinner.scss
@@ -0,0 +1,9 @@
+@import '@material/circular-progress/mixins.import';
+@import '../mdc-helpers/mdc-helpers';
+
+
+@include mdc-circular-progress-core-styles($query: $mat-base-styles-without-animation-query);
+
+:not(._mat-animation-noopable) {
+ @include mdc-circular-progress-core-styles($query: animation);
+}
diff --git a/src/material-experimental/mdc-progress-spinner/progress-spinner.spec.ts b/src/material-experimental/mdc-progress-spinner/progress-spinner.spec.ts
new file mode 100644
index 000000000000..46516577c9d1
--- /dev/null
+++ b/src/material-experimental/mdc-progress-spinner/progress-spinner.spec.ts
@@ -0,0 +1,385 @@
+import {async, TestBed} from '@angular/core/testing';
+import {
+ MatProgressSpinner,
+ MatProgressSpinnerModule
+} from '@angular/material-experimental/mdc-progress-spinner';
+import {CommonModule} from '@angular/common';
+import {By} from '@angular/platform-browser';
+import {MAT_PROGRESS_SPINNER_DEFAULT_OPTIONS} from '@angular/material/progress-spinner';
+import {Component, ElementRef, ViewChild, ViewEncapsulation} from '@angular/core';
+
+describe('MDC-based MatProgressSpinner', () => {
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [MatProgressSpinnerModule, CommonModule],
+ declarations: [
+ BasicProgressSpinner,
+ IndeterminateProgressSpinner,
+ ProgressSpinnerWithValueAndBoundMode,
+ ProgressSpinnerWithColor,
+ ProgressSpinnerCustomStrokeWidth,
+ ProgressSpinnerCustomDiameter,
+ SpinnerWithColor,
+ ProgressSpinnerWithStringValues,
+ IndeterminateSpinnerInShadowDom,
+ IndeterminateSpinnerInShadowDomWithNgIf,
+ ],
+ }).compileComponents();
+ }));
+
+ it('should apply a mode of "determinate" if no mode is provided.', () => {
+ let fixture = TestBed.createComponent(BasicProgressSpinner);
+ fixture.detectChanges();
+
+ let progressElement = fixture.debugElement.query(By.css('mat-progress-spinner'))!;
+ expect(progressElement.componentInstance.mode).toBe('determinate');
+ });
+
+ it('should not modify the mode if a valid mode is provided.', () => {
+ let fixture = TestBed.createComponent(IndeterminateProgressSpinner);
+ fixture.detectChanges();
+
+ let progressElement = fixture.debugElement.query(By.css('mat-progress-spinner'))!;
+ expect(progressElement.componentInstance.mode).toBe('indeterminate');
+ });
+
+ it('should define a default value of zero for the value attribute', () => {
+ let fixture = TestBed.createComponent(BasicProgressSpinner);
+ fixture.detectChanges();
+
+ let progressElement = fixture.debugElement.query(By.css('mat-progress-spinner'))!;
+ expect(progressElement.componentInstance.value).toBe(0);
+ });
+
+ it('should set the value to 0 when the mode is set to indeterminate', () => {
+ let fixture = TestBed.createComponent(ProgressSpinnerWithValueAndBoundMode);
+ let progressElement = fixture.debugElement.query(By.css('mat-progress-spinner'))!;
+ fixture.componentInstance.mode = 'determinate';
+ fixture.detectChanges();
+
+ expect(progressElement.componentInstance.value).toBe(50);
+ fixture.componentInstance.mode = 'indeterminate';
+ fixture.detectChanges();
+ expect(progressElement.componentInstance.value).toBe(0);
+ });
+
+ it('should retain the value if it updates while indeterminate', () => {
+ let fixture = TestBed.createComponent(ProgressSpinnerWithValueAndBoundMode);
+ let progressElement = fixture.debugElement.query(By.css('mat-progress-spinner'))!;
+
+ fixture.componentInstance.mode = 'determinate';
+ fixture.detectChanges();
+ expect(progressElement.componentInstance.value).toBe(50);
+
+ fixture.componentInstance.mode = 'indeterminate';
+ fixture.detectChanges();
+ expect(progressElement.componentInstance.value).toBe(0);
+
+ fixture.componentInstance.value = 75;
+ fixture.detectChanges();
+ expect(progressElement.componentInstance.value).toBe(0);
+
+ fixture.componentInstance.mode = 'determinate';
+ fixture.detectChanges();
+ expect(progressElement.componentInstance.value).toBe(75);
+ });
+
+ it('should clamp the value of the progress between 0 and 100', () => {
+ let fixture = TestBed.createComponent(BasicProgressSpinner);
+ fixture.detectChanges();
+
+ let progressElement = fixture.debugElement.query(By.css('mat-progress-spinner'))!;
+ let progressComponent = progressElement.componentInstance;
+
+ progressComponent.value = 50;
+ expect(progressComponent.value).toBe(50);
+
+ progressComponent.value = 0;
+ expect(progressComponent.value).toBe(0);
+
+ progressComponent.value = 100;
+ expect(progressComponent.value).toBe(100);
+
+ progressComponent.value = 999;
+ expect(progressComponent.value).toBe(100);
+
+ progressComponent.value = -10;
+ expect(progressComponent.value).toBe(0);
+ });
+
+ it('should default to a stroke width that is 10% of the diameter', () => {
+ const fixture = TestBed.createComponent(ProgressSpinnerCustomDiameter);
+ const spinner = fixture.debugElement.query(By.directive(MatProgressSpinner))!;
+
+ fixture.componentInstance.diameter = 67;
+ fixture.detectChanges();
+
+ expect(spinner.componentInstance.strokeWidth).toBe(6.7);
+ });
+
+ it('should allow a custom diameter', () => {
+ const fixture = TestBed.createComponent(ProgressSpinnerCustomDiameter);
+ const spinner = fixture.debugElement.query(By.css('mat-progress-spinner'))!.nativeElement;
+ const svgElement = fixture.nativeElement.querySelector('svg');
+
+ fixture.componentInstance.diameter = 32;
+ fixture.detectChanges();
+
+ expect(parseInt(spinner.style.width))
+ .toBe(32, 'Expected the custom diameter to be applied to the host element width.');
+ expect(parseInt(spinner.style.height))
+ .toBe(32, 'Expected the custom diameter to be applied to the host element height.');
+ expect(parseInt(svgElement.clientWidth))
+ .toBe(32, 'Expected the custom diameter to be applied to the svg element width.');
+ expect(parseInt(svgElement.clientHeight))
+ .toBe(32, 'Expected the custom diameter to be applied to the svg element height.');
+ expect(svgElement.getAttribute('viewBox'))
+ .toBe('0 0 25.2 25.2', 'Expected the custom diameter to be applied to the svg viewBox.');
+ });
+
+ it('should allow a custom stroke width', () => {
+ const fixture = TestBed.createComponent(ProgressSpinnerCustomStrokeWidth);
+
+ fixture.componentInstance.strokeWidth = 40;
+ fixture.detectChanges();
+
+ const circleElement = fixture.nativeElement.querySelector('circle');
+ const svgElement = fixture.nativeElement.querySelector('svg');
+
+ expect(parseInt(circleElement.style.strokeWidth)).toBe(40, 'Expected the custom stroke ' +
+ 'width to be applied to the circle element as a percentage of the element size.');
+ expect(svgElement.getAttribute('viewBox'))
+ .toBe('0 0 130 130', 'Expected the viewBox to be adjusted based on the stroke width.');
+ });
+
+ it('should expand the host element if the stroke width is greater than the default', () => {
+ const fixture = TestBed.createComponent(ProgressSpinnerCustomStrokeWidth);
+ const element = fixture.debugElement.nativeElement.querySelector('.mat-mdc-progress-spinner');
+
+ fixture.componentInstance.strokeWidth = 40;
+ fixture.detectChanges();
+
+ expect(element.style.width).toBe('100px');
+ expect(element.style.height).toBe('100px');
+ });
+
+ it('should not collapse the host element if the stroke width is less than the default', () => {
+ const fixture = TestBed.createComponent(ProgressSpinnerCustomStrokeWidth);
+ const element = fixture.debugElement.nativeElement.querySelector('.mat-mdc-progress-spinner');
+
+ fixture.componentInstance.strokeWidth = 5;
+ fixture.detectChanges();
+
+ expect(element.style.width).toBe('100px');
+ expect(element.style.height).toBe('100px');
+ });
+
+ it('should set the color class on the mat-spinner', () => {
+ let fixture = TestBed.createComponent(SpinnerWithColor);
+ fixture.detectChanges();
+
+ let progressElement = fixture.debugElement.query(By.css('mat-spinner'))!;
+
+ expect(progressElement.nativeElement.classList).toContain('mat-primary');
+
+ fixture.componentInstance.color = 'accent';
+ fixture.detectChanges();
+
+ expect(progressElement.nativeElement.classList).toContain('mat-accent');
+ expect(progressElement.nativeElement.classList).not.toContain('mat-primary');
+ });
+
+ it('should set the color class on the mat-progress-spinner', () => {
+ let fixture = TestBed.createComponent(ProgressSpinnerWithColor);
+ fixture.detectChanges();
+
+ let progressElement = fixture.debugElement.query(By.css('mat-progress-spinner'))!;
+
+ expect(progressElement.nativeElement.classList).toContain('mat-primary');
+
+ fixture.componentInstance.color = 'accent';
+ fixture.detectChanges();
+
+ expect(progressElement.nativeElement.classList).toContain('mat-accent');
+ expect(progressElement.nativeElement.classList).not.toContain('mat-primary');
+ });
+
+ it('should remove the underlying SVG element from the tab order explicitly', () => {
+ const fixture = TestBed.createComponent(BasicProgressSpinner);
+
+ fixture.detectChanges();
+
+ expect(fixture.nativeElement.querySelector('svg').getAttribute('focusable')).toBe('false');
+ });
+
+ it('should handle the number inputs being passed in as strings', () => {
+ const fixture = TestBed.createComponent(ProgressSpinnerWithStringValues);
+ const spinner = fixture.debugElement.query(By.directive(MatProgressSpinner))!;
+ const svgElement = spinner.nativeElement.querySelector('svg');
+
+ fixture.detectChanges();
+
+ expect(spinner.componentInstance.diameter).toBe(37);
+ expect(spinner.componentInstance.strokeWidth).toBe(11);
+ expect(spinner.componentInstance.value).toBe(25);
+
+ expect(spinner.nativeElement.style.width).toBe('37px');
+ expect(spinner.nativeElement.style.height).toBe('37px');
+
+ expect(svgElement.clientWidth).toBe(37);
+ expect(svgElement.clientHeight).toBe(37);
+ expect(svgElement.getAttribute('viewBox')).toBe('0 0 38 38');
+ });
+
+ it('should update the element size when changed dynamically', () => {
+ let fixture = TestBed.createComponent(BasicProgressSpinner);
+ let spinner = fixture.debugElement.query(By.directive(MatProgressSpinner))!;
+ spinner.componentInstance.diameter = 32;
+ fixture.detectChanges();
+ expect(spinner.nativeElement.style.width).toBe('32px');
+ expect(spinner.nativeElement.style.height).toBe('32px');
+ });
+
+ it('should be able to set a default diameter', () => {
+ TestBed
+ .resetTestingModule()
+ .configureTestingModule({
+ imports: [MatProgressSpinnerModule],
+ declarations: [BasicProgressSpinner],
+ providers: [{
+ provide: MAT_PROGRESS_SPINNER_DEFAULT_OPTIONS,
+ useValue: {diameter: 23}
+ }]
+ })
+ .compileComponents();
+
+ const fixture = TestBed.createComponent(BasicProgressSpinner);
+ fixture.detectChanges();
+
+ const progressElement = fixture.debugElement.query(By.css('mat-progress-spinner'))!;
+ expect(progressElement.componentInstance.diameter).toBe(23);
+ });
+
+ it('should be able to set a default stroke width', () => {
+ TestBed
+ .resetTestingModule()
+ .configureTestingModule({
+ imports: [MatProgressSpinnerModule],
+ declarations: [BasicProgressSpinner],
+ providers: [{
+ provide: MAT_PROGRESS_SPINNER_DEFAULT_OPTIONS,
+ useValue: {strokeWidth: 7}
+ }]
+ })
+ .compileComponents();
+
+ const fixture = TestBed.createComponent(BasicProgressSpinner);
+ fixture.detectChanges();
+
+ const progressElement = fixture.debugElement.query(By.css('mat-progress-spinner'))!;
+ expect(progressElement.componentInstance.strokeWidth).toBe(7);
+ });
+
+ it('should set `aria-valuenow` to the current value in determinate mode', () => {
+ const fixture = TestBed.createComponent(ProgressSpinnerWithValueAndBoundMode);
+ const progressElement = fixture.debugElement.query(By.css('mat-progress-spinner'))!;
+ fixture.componentInstance.mode = 'determinate';
+ fixture.componentInstance.value = 37;
+ fixture.detectChanges();
+
+ expect(progressElement.nativeElement.getAttribute('aria-valuenow')).toBe('0.37');
+ });
+
+ it('should clear `aria-valuenow` in indeterminate mode', () => {
+ const fixture = TestBed.createComponent(ProgressSpinnerWithValueAndBoundMode);
+ const progressElement = fixture.debugElement.query(By.css('mat-progress-spinner'))!;
+ fixture.componentInstance.mode = 'determinate';
+ fixture.componentInstance.value = 89;
+ fixture.detectChanges();
+
+ expect(progressElement.nativeElement.hasAttribute('aria-valuenow')).toBe(true);
+
+ fixture.componentInstance.mode = 'indeterminate';
+ fixture.detectChanges();
+
+ expect(progressElement.nativeElement.hasAttribute('aria-valuenow')).toBe(false);
+ });
+});
+
+
+@Component({template: ''})
+class BasicProgressSpinner {
+}
+
+@Component({template: ''})
+class ProgressSpinnerCustomStrokeWidth {
+ strokeWidth: number;
+}
+
+@Component({template: ''})
+class ProgressSpinnerCustomDiameter {
+ diameter: number;
+}
+
+@Component({template: ''})
+class IndeterminateProgressSpinner {
+}
+
+@Component({
+ template: ''
+})
+class ProgressSpinnerWithValueAndBoundMode {
+ mode = 'indeterminate';
+ value = 50;
+}
+
+@Component({
+ template: `
+ `
+})
+class SpinnerWithColor {
+ color: string = 'primary';
+}
+
+@Component({
+ template: `
+ `
+})
+class ProgressSpinnerWithColor {
+ color: string = 'primary';
+}
+
+@Component({
+ template: `
+
+ `
+})
+class ProgressSpinnerWithStringValues {
+}
+
+
+@Component({
+ template: `
+
+ `,
+ encapsulation: ViewEncapsulation.ShadowDom,
+})
+class IndeterminateSpinnerInShadowDom {
+ diameter: number;
+}
+
+@Component({
+ template: `
+
+
+
+ `,
+ encapsulation: ViewEncapsulation.ShadowDom,
+})
+class IndeterminateSpinnerInShadowDomWithNgIf {
+ @ViewChild(MatProgressSpinner, {read: ElementRef})
+ spinner: ElementRef;
+
+ diameter: number;
+}
+
diff --git a/src/material-experimental/mdc-progress-spinner/progress-spinner.ts b/src/material-experimental/mdc-progress-spinner/progress-spinner.ts
new file mode 100644
index 000000000000..aeef9a5f4676
--- /dev/null
+++ b/src/material-experimental/mdc-progress-spinner/progress-spinner.ts
@@ -0,0 +1,242 @@
+/**
+ * @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 {
+ AfterViewInit,
+ ChangeDetectionStrategy,
+ Component,
+ ElementRef,
+ Inject,
+ Input,
+ OnDestroy,
+ Optional,
+ ViewChild,
+ ViewEncapsulation
+} from '@angular/core';
+import {
+ MDCCircularProgressAdapter,
+ MDCCircularProgressFoundation
+} from '@material/circular-progress';
+import {CanColor, CanColorCtor, mixinColor} from '@angular/material/core';
+import {ANIMATION_MODULE_TYPE} from '@angular/platform-browser/animations';
+import {
+ MAT_PROGRESS_SPINNER_DEFAULT_OPTIONS,
+ MatProgressSpinnerDefaultOptions
+} from '@angular/material/progress-spinner';
+import {coerceNumberProperty, NumberInput} from '@angular/cdk/coercion';
+
+// Boilerplate for applying mixins to MatProgressBar.
+class MatProgressSpinnerBase {
+ constructor(public _elementRef: ElementRef) {
+ }
+}
+
+const _MatProgressSpinnerMixinBase: CanColorCtor & typeof MatProgressSpinnerBase =
+ mixinColor(MatProgressSpinnerBase, 'primary');
+
+/** Possible mode for a progress spinner. */
+export type ProgressSpinnerMode = 'determinate' | 'indeterminate';
+
+/**
+ * Base reference size of the spinner.
+ */
+const BASE_SIZE = 100;
+
+/**
+ * Base reference stroke width of the spinner.
+ */
+const BASE_STROKE_WIDTH = 10;
+
+@Component({
+ selector: 'mat-progress-spinner, mat-spinner',
+ exportAs: 'matProgressSpinner',
+ host: {
+ 'role': 'progressbar',
+ 'class': 'mat-mdc-progress-spinner mdc-circular-progress',
+ '[class._mat-animation-noopable]': `_noopAnimations`,
+ '[style.width.px]': 'diameter',
+ '[style.height.px]': 'diameter',
+ '[attr.aria-valuemin]': '0',
+ '[attr.aria-valuemax]': '100',
+ '[attr.aria-valuenow]': 'mode === "determinate" ? value : null',
+ '[attr.mode]': 'mode',
+ },
+ inputs: ['color'],
+ templateUrl: 'progress-spinner.html',
+ styleUrls: ['progress-spinner.css'],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ encapsulation: ViewEncapsulation.None,
+})
+export class MatProgressSpinner extends _MatProgressSpinnerMixinBase implements AfterViewInit,
+ OnDestroy, CanColor {
+
+ /** Whether the _mat-animation-noopable class should be applied, disabling animations. */
+ _noopAnimations: boolean;
+
+ /** Implements all of the logic of the MDC circular progress. */
+ _foundation: MDCCircularProgressFoundation;
+
+ /** The element of the determinate spinner. */
+ @ViewChild('determinateSpinner') _determinateCircle: ElementRef;
+
+ /** Adapter used by MDC to interact with the DOM. */
+ // TODO: switch to class when MDC removes object spread in foundation
+ // https://github.com/material-components/material-components-web/pull/6256
+ private _adapter: MDCCircularProgressAdapter = {
+ addClass: (className: string) => this._elementRef.nativeElement.classList.add(className),
+ hasClass: (className: string) => this._elementRef.nativeElement.classList.contains(className),
+ removeClass: (className: string) => this._elementRef.nativeElement.classList.remove(className),
+ removeAttribute: (name: string) => this._elementRef.nativeElement.removeAttribute(name),
+ setAttribute: (name: string, value: string) =>
+ this._elementRef.nativeElement.setAttribute(name, value),
+ getDeterminateCircleAttribute: (attributeName: string) =>
+ this._determinateCircle.nativeElement.getAttribute(attributeName),
+ setDeterminateCircleAttribute: (attributeName: string, value: string) =>
+ this._determinateCircle.nativeElement.setAttribute(attributeName, value),
+ };
+
+ constructor(public _elementRef: ElementRef,
+ @Optional() @Inject(ANIMATION_MODULE_TYPE) animationMode: string,
+ @Inject(MAT_PROGRESS_SPINNER_DEFAULT_OPTIONS)
+ defaults?: MatProgressSpinnerDefaultOptions) {
+ super(_elementRef);
+ this._noopAnimations = animationMode === 'NoopAnimations' &&
+ (!!defaults && !defaults._forceAnimations);
+
+ if (defaults) {
+ if (defaults.diameter) {
+ this.diameter = defaults.diameter;
+ }
+
+ if (defaults.strokeWidth) {
+ this.strokeWidth = defaults.strokeWidth;
+ }
+ }
+ }
+
+ private _mode: ProgressSpinnerMode = this._elementRef.nativeElement.nodeName.toLowerCase() ===
+ 'mat-spinner' ? 'indeterminate' : 'determinate';
+
+ /**
+ * Mode of the progress bar.
+ *
+ * Input must be one of these values: determinate, indeterminate, buffer, query, defaults to
+ * 'determinate'.
+ * Mirrored to mode attribute.
+ */
+ @Input()
+ get mode(): ProgressSpinnerMode { return this._mode; }
+
+ set mode(value: ProgressSpinnerMode) {
+ this._mode = value;
+ this._syncFoundation();
+ }
+
+ private _value = 0;
+
+ /** Value of the progress bar. Defaults to zero. Mirrored to aria-valuenow. */
+ @Input()
+ get value(): number {
+ return this.mode === 'determinate' ? this._value : 0;
+ }
+
+ set value(v: number) {
+ this._value = Math.max(0, Math.min(100, coerceNumberProperty(v)));
+ this._syncFoundation();
+ }
+
+ private _diameter = BASE_SIZE;
+
+ /** The diameter of the progress spinner (will set width and height of svg). */
+ @Input()
+ get diameter(): number {
+ return this._diameter;
+ }
+
+ set diameter(size: number) {
+ this._diameter = coerceNumberProperty(size);
+ this._syncFoundation();
+ }
+
+ private _strokeWidth: number;
+
+ /** Stroke width of the progress spinner. */
+ @Input()
+ get strokeWidth(): number {
+ return this._strokeWidth ?? this.diameter / 10;
+ }
+
+ set strokeWidth(value: number) {
+ this._strokeWidth = coerceNumberProperty(value);
+ }
+
+ /** The radius of the spinner, adjusted for stroke width. */
+ _circleRadius(): number {
+ return (this.diameter - BASE_STROKE_WIDTH) / 2;
+ }
+
+ /** The view box of the spinner's svg element. */
+ _viewBox() {
+ const viewBox = this._circleRadius() * 2 + this.strokeWidth;
+ return `0 0 ${viewBox} ${viewBox}`;
+ }
+
+ /** The stroke circumference of the svg circle. */
+ _strokeCircumference(): number {
+ return 2 * Math.PI * this._circleRadius();
+ }
+
+ /** The dash offset of the svg circle. */
+ _strokeDashOffset() {
+ if (this.mode === 'determinate') {
+ return this._strokeCircumference() * (100 - this._value) / 100;
+ }
+ return null;
+ }
+
+ /** Stroke width of the circle in percent. */
+ _circleStrokeWidth() {
+ return this.strokeWidth / this.diameter * 100;
+ }
+
+ ngAfterViewInit() {
+ this._foundation = new MDCCircularProgressFoundation(this._adapter);
+ this._foundation.init();
+ this._syncFoundation();
+ }
+
+ ngOnDestroy() {
+ if (this._foundation) {
+ this._foundation.destroy();
+ }
+ }
+
+ /** Syncs the state of the progress spinner with the MDC foundation. */
+ private _syncFoundation() {
+ const foundation = this._foundation;
+
+ if (foundation) {
+ const mode = this.mode;
+ foundation.setProgress(this.value / 100);
+ foundation.setDeterminate(mode === 'determinate');
+ }
+ }
+
+ static ngAcceptInputType_diameter: NumberInput;
+ static ngAcceptInputType_strokeWidth: NumberInput;
+ static ngAcceptInputType_value: NumberInput;
+}
+
+/**
+ * `` component.
+ *
+ * This is a component definition to be used as a convenience reference to create an
+ * indeterminate `` instance.
+ */
+// tslint:disable-next-line:variable-name
+export const MatSpinner = MatProgressSpinner;
diff --git a/src/material-experimental/mdc-progress-spinner/public-api.ts b/src/material-experimental/mdc-progress-spinner/public-api.ts
new file mode 100644
index 000000000000..6aa2230d1349
--- /dev/null
+++ b/src/material-experimental/mdc-progress-spinner/public-api.ts
@@ -0,0 +1,10 @@
+/**
+ * @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 './progress-spinner';
+export * from './module';
diff --git a/src/material-experimental/mdc-progress-spinner/testing/BUILD.bazel b/src/material-experimental/mdc-progress-spinner/testing/BUILD.bazel
new file mode 100644
index 000000000000..d878e9403268
--- /dev/null
+++ b/src/material-experimental/mdc-progress-spinner/testing/BUILD.bazel
@@ -0,0 +1,38 @@
+load("//tools:defaults.bzl", "ng_module", "ng_test_library", "ng_web_test_suite")
+
+package(default_visibility = ["//visibility:public"])
+
+ng_module(
+ name = "testing",
+ srcs = glob(
+ ["**/*.ts"],
+ exclude = ["**/*.spec.ts"],
+ ),
+ module_name = "@angular/material-experimental/mdc-progress-spinner/testing",
+ deps = [
+ "//src/cdk/coercion",
+ "//src/cdk/testing",
+ "//src/material/progress-spinner/testing",
+ ],
+)
+
+ng_test_library(
+ name = "unit_tests_lib",
+ srcs = glob(["**/*.spec.ts"]),
+ deps = [
+ ":testing",
+ "//src/material-experimental/mdc-progress-spinner",
+ "//src/material/progress-spinner/testing:harness_tests_lib",
+ ],
+)
+
+ng_web_test_suite(
+ name = "unit_tests",
+ static_files = [
+ "@npm//:node_modules/@material/circular-progress/dist/mdc.circularProgress.js",
+ ],
+ deps = [
+ ":unit_tests_lib",
+ "//src/material-experimental:mdc_require_config.js",
+ ],
+)
diff --git a/src/material-experimental/mdc-progress-spinner/testing/index.ts b/src/material-experimental/mdc-progress-spinner/testing/index.ts
new file mode 100644
index 000000000000..676ca90f1ffa
--- /dev/null
+++ b/src/material-experimental/mdc-progress-spinner/testing/index.ts
@@ -0,0 +1,9 @@
+/**
+ * @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 './public-api';
diff --git a/src/material-experimental/mdc-progress-spinner/testing/progress-spinner-harness.spec.ts b/src/material-experimental/mdc-progress-spinner/testing/progress-spinner-harness.spec.ts
new file mode 100644
index 000000000000..ea15d90dadbc
--- /dev/null
+++ b/src/material-experimental/mdc-progress-spinner/testing/progress-spinner-harness.spec.ts
@@ -0,0 +1,7 @@
+import {runHarnessTests} from '@angular/material/progress-spinner/testing/shared.spec';
+import {MatProgressSpinnerHarness} from './progress-spinner-harness';
+import {MatProgressSpinnerModule} from '../index';
+
+describe('MDC-based MatProgressSpinnerHarness', () => {
+ runHarnessTests(MatProgressSpinnerModule, MatProgressSpinnerHarness);
+});
diff --git a/src/material-experimental/mdc-progress-spinner/testing/progress-spinner-harness.ts b/src/material-experimental/mdc-progress-spinner/testing/progress-spinner-harness.ts
new file mode 100644
index 000000000000..1ab7e56ad0fb
--- /dev/null
+++ b/src/material-experimental/mdc-progress-spinner/testing/progress-spinner-harness.ts
@@ -0,0 +1,42 @@
+/**
+ * @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 {coerceNumberProperty} from '@angular/cdk/coercion';
+import {ComponentHarness, HarnessPredicate} from '@angular/cdk/testing';
+import {ProgressSpinnerMode} from '@angular/material/progress-spinner';
+import {ProgressSpinnerHarnessFilters} from '@angular/material/progress-spinner/testing';
+
+/** Harness for interacting with a MDC based mat-progress-spinner in tests. */
+export class MatProgressSpinnerHarness extends ComponentHarness {
+ /** The selector for the host element of a `MatProgressSpinner` instance. */
+ static hostSelector = 'mat-progress-spinner,mat-spinner';
+
+ /**
+ * Gets a `HarnessPredicate` that can be used to search for a `MatProgressSpinnerHarness` that
+ * meets certain criteria.
+ * @param options Options for filtering which progress spinner instances are considered a match.
+ * @return a `HarnessPredicate` configured with the given options.
+ */
+ static with(options: ProgressSpinnerHarnessFilters = {}):
+ HarnessPredicate {
+ return new HarnessPredicate(MatProgressSpinnerHarness, options);
+ }
+
+ /** Gets the progress spinner's value. */
+ async getValue(): Promise {
+ const host = await this.host();
+ const ariaValue = await host.getAttribute('aria-valuenow');
+ return ariaValue ? coerceNumberProperty(ariaValue) : null;
+ }
+
+ /** Gets the progress spinner's mode. */
+ async getMode(): Promise {
+ const modeAttr = (await this.host()).getAttribute('mode');
+ return await modeAttr as ProgressSpinnerMode;
+ }
+}
diff --git a/src/material-experimental/mdc-progress-spinner/testing/public-api.ts b/src/material-experimental/mdc-progress-spinner/testing/public-api.ts
new file mode 100644
index 000000000000..77f91ded9e77
--- /dev/null
+++ b/src/material-experimental/mdc-progress-spinner/testing/public-api.ts
@@ -0,0 +1,9 @@
+/**
+ * @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 './progress-spinner-harness';
diff --git a/src/material-experimental/mdc-theming/BUILD.bazel b/src/material-experimental/mdc-theming/BUILD.bazel
index 87ff3709bbf5..095b88a2f07f 100644
--- a/src/material-experimental/mdc-theming/BUILD.bazel
+++ b/src/material-experimental/mdc-theming/BUILD.bazel
@@ -25,6 +25,7 @@ sass_library(
"//src/material-experimental/mdc-list:mdc_list_scss_lib",
"//src/material-experimental/mdc-menu:mdc_menu_scss_lib",
"//src/material-experimental/mdc-progress-bar:mdc_progress_bar_scss_lib",
+ "//src/material-experimental/mdc-progress-spinner:mdc_progress_spinner_scss_lib",
"//src/material-experimental/mdc-radio:mdc_radio_scss_lib",
"//src/material-experimental/mdc-slide-toggle:mdc_slide_toggle_scss_lib",
"//src/material-experimental/mdc-slider:mdc_slider_scss_lib",
diff --git a/src/material-experimental/mdc_require_config.js b/src/material-experimental/mdc_require_config.js
index bff597ceb6a1..f86b99b8cef0 100644
--- a/src/material-experimental/mdc_require_config.js
+++ b/src/material-experimental/mdc_require_config.js
@@ -7,6 +7,7 @@ require.config({
'@material/base': '/base/npm/node_modules/@material/base/dist/mdc.base',
'@material/checkbox': '/base/npm/node_modules/@material/checkbox/dist/mdc.checkbox',
'@material/chips': '/base/npm/node_modules/@material/chips/dist/mdc.chips',
+ '@material/circular-progress': '/base/npm/node_modules/@material/circular-progress/dist/mdc.circularProgress',
'@material/dialog': '/base/npm/node_modules/@material/dialog/dist/mdc.dialog',
'@material/dom': '/base/npm/node_modules/@material/dom/dist/mdc.dom',
'@material/drawer': '/base/npm/node_modules/@material/drawer/dist/mdc.drawer',
diff --git a/src/universal-app/kitchen-sink-mdc/kitchen-sink-mdc.html b/src/universal-app/kitchen-sink-mdc/kitchen-sink-mdc.html
index 9af4d662b63b..42abd2cfd384 100644
--- a/src/universal-app/kitchen-sink-mdc/kitchen-sink-mdc.html
+++ b/src/universal-app/kitchen-sink-mdc/kitchen-sink-mdc.html
@@ -195,3 +195,9 @@ Prefix and Suffix
Name
+
+MDC Progress spinner
+
+
+
+
diff --git a/src/universal-app/kitchen-sink-mdc/kitchen-sink-mdc.ts b/src/universal-app/kitchen-sink-mdc/kitchen-sink-mdc.ts
index 7c3273c7e847..c36edb3b970b 100644
--- a/src/universal-app/kitchen-sink-mdc/kitchen-sink-mdc.ts
+++ b/src/universal-app/kitchen-sink-mdc/kitchen-sink-mdc.ts
@@ -14,6 +14,7 @@ import {MatTabsModule} from '@angular/material-experimental/mdc-tabs';
import {MatTableModule} from '@angular/material-experimental/mdc-table';
import {MatIconModule} from '@angular/material/icon';
import {MatSnackBarModule, MatSnackBar} from '@angular/material-experimental/mdc-snack-bar';
+import {MatProgressSpinnerModule} from '@angular/material-experimental/mdc-progress-spinner';
@Component({
selector: 'kitchen-sink-mdc',
@@ -39,6 +40,7 @@ export class KitchenSinkMdc {
MatTableModule,
MatProgressBarModule,
MatSnackBarModule,
+ MatProgressSpinnerModule,
],
declarations: [KitchenSinkMdc],
exports: [KitchenSinkMdc],
diff --git a/tools/system-config-tmpl.js b/tools/system-config-tmpl.js
index 6e8cdae10997..ca531ffe7962 100644
--- a/tools/system-config-tmpl.js
+++ b/tools/system-config-tmpl.js
@@ -42,6 +42,7 @@ var pathMapping = {
'@material/base': 'node:@material/base/dist/mdc.base.js',
'@material/checkbox': 'node:@material/checkbox/dist/mdc.checkbox.js',
'@material/chips': 'node:@material/chips/dist/mdc.chips.js',
+ '@material/circular-progress': 'node:@material/circular-progress/dist/mdc.circularProgress.js',
'@material/dialog': 'node:@material/dialog/dist/mdc.dialog.js',
'@material/dom': 'node:@material/dom/dist/mdc.dom.js',
'@material/drawer': 'node:@material/drawer/dist/mdc.drawer.js',