diff --git a/src/material/progress-spinner/BUILD.bazel b/src/material/progress-spinner/BUILD.bazel
index afb70525de7..a93da25d5ef 100644
--- a/src/material/progress-spinner/BUILD.bazel
+++ b/src/material/progress-spinner/BUILD.bazel
@@ -50,6 +50,7 @@ ng_test_library(
),
deps = [
":progress-spinner",
+ "//src/cdk/platform",
"@npm//@angular/platform-browser",
],
)
diff --git a/src/material/progress-spinner/progress-spinner.spec.ts b/src/material/progress-spinner/progress-spinner.spec.ts
index dc0dcaa212c..89288947993 100644
--- a/src/material/progress-spinner/progress-spinner.spec.ts
+++ b/src/material/progress-spinner/progress-spinner.spec.ts
@@ -1,14 +1,16 @@
-import {TestBed, async} from '@angular/core/testing';
-import {Component} from '@angular/core';
+import {TestBed, async, inject} from '@angular/core/testing';
+import {Component, ViewEncapsulation} from '@angular/core';
import {By} from '@angular/platform-browser';
+import {Platform} from '@angular/cdk/platform';
+import {_getShadowRoot} from './progress-spinner';
import {
MatProgressSpinnerModule,
MatProgressSpinner,
MAT_PROGRESS_SPINNER_DEFAULT_OPTIONS,
} from './index';
-
describe('MatProgressSpinner', () => {
+ const supportsShadowDom = typeof document.createElement('div').attachShadow !== 'undefined';
beforeEach(async(() => {
TestBed.configureTestingModule({
@@ -22,6 +24,7 @@ describe('MatProgressSpinner', () => {
ProgressSpinnerCustomDiameter,
SpinnerWithColor,
ProgressSpinnerWithStringValues,
+ IndeterminateSpinnerInShadowDom,
],
}).compileComponents();
}));
@@ -154,6 +157,35 @@ describe('MatProgressSpinner', () => {
.toBe('0 0 25.2 25.2', 'Expected the custom diameter to be applied to the svg viewBox.');
});
+ it('should add a style tag with the indeterminate animation to the document head when using a ' +
+ 'non-default diameter', inject([Platform], (platform: Platform) => {
+ // On Edge and IE we use a fallback animation because the
+ // browser doesn't support animating SVG correctly.
+ if (platform.EDGE || platform.TRIDENT) {
+ return;
+ }
+
+ const fixture = TestBed.createComponent(ProgressSpinnerCustomDiameter);
+ fixture.componentInstance.diameter = 32;
+ fixture.detectChanges();
+
+ expect(document.head.querySelectorAll('style[mat-spinner-animation="32"]').length).toBe(1);
+
+ // Change to something different so we get another tag.
+ fixture.componentInstance.diameter = 64;
+ fixture.detectChanges();
+
+ expect(document.head.querySelectorAll('style[mat-spinner-animation="32"]').length).toBe(1);
+ expect(document.head.querySelectorAll('style[mat-spinner-animation="64"]').length).toBe(1);
+
+ // Change back to the initial one.
+ fixture.componentInstance.diameter = 32;
+ fixture.detectChanges();
+
+ expect(document.head.querySelectorAll('style[mat-spinner-animation="32"]').length).toBe(1);
+ expect(document.head.querySelectorAll('style[mat-spinner-animation="64"]').length).toBe(1);
+ }));
+
it('should allow a custom stroke width', () => {
const fixture = TestBed.createComponent(ProgressSpinnerCustomStrokeWidth);
@@ -321,6 +353,57 @@ describe('MatProgressSpinner', () => {
expect(progressElement.nativeElement.hasAttribute('aria-valuenow')).toBe(false);
});
+ it('should add the indeterminate animation style tag to the Shadow root', () => {
+ // The test is only relevant in browsers that support Shadow DOM.
+ if (!supportsShadowDom) {
+ return;
+ }
+
+ const fixture = TestBed.createComponent(IndeterminateSpinnerInShadowDom);
+ fixture.componentInstance.diameter = 27;
+ fixture.detectChanges();
+
+ const spinner = fixture.debugElement.query(By.css('mat-progress-spinner')).nativeElement;
+ const shadowRoot = _getShadowRoot(spinner, document) as HTMLElement;
+
+ expect(shadowRoot.querySelector('style[mat-spinner-animation="27"]')).toBeTruthy();
+
+ fixture.componentInstance.diameter = 15;
+ fixture.detectChanges();
+
+ expect(shadowRoot.querySelector('style[mat-spinner-animation="27"]')).toBeTruthy();
+ });
+
+ it('should not duplicate style tags inside the Shadow root', () => {
+ // The test is only relevant in browsers that support Shadow DOM.
+ if (!supportsShadowDom) {
+ return;
+ }
+
+ const fixture = TestBed.createComponent(IndeterminateSpinnerInShadowDom);
+ fixture.componentInstance.diameter = 39;
+ fixture.detectChanges();
+
+ const spinner = fixture.debugElement.query(By.css('mat-progress-spinner')).nativeElement;
+ const shadowRoot = _getShadowRoot(spinner, document) as HTMLElement;
+
+ expect(shadowRoot.querySelectorAll('style[mat-spinner-animation="39"]').length).toBe(1);
+
+ // Change to something different so we get another tag.
+ fixture.componentInstance.diameter = 61;
+ fixture.detectChanges();
+
+ expect(shadowRoot.querySelectorAll('style[mat-spinner-animation="39"]').length).toBe(1);
+ expect(shadowRoot.querySelectorAll('style[mat-spinner-animation="61"]').length).toBe(1);
+
+ // Change back to the initial one.
+ fixture.componentInstance.diameter = 39;
+ fixture.detectChanges();
+
+ expect(shadowRoot.querySelectorAll('style[mat-spinner-animation="39"]').length).toBe(1);
+ expect(shadowRoot.querySelectorAll('style[mat-spinner-animation="61"]').length).toBe(1);
+ });
+
});
@@ -360,3 +443,14 @@ class ProgressSpinnerWithColor { color: string = 'primary'; }
`
})
class ProgressSpinnerWithStringValues { }
+
+
+@Component({
+ template: `
+
+ `,
+ encapsulation: ViewEncapsulation.ShadowDom,
+})
+class IndeterminateSpinnerInShadowDom {
+ diameter: number;
+}
diff --git a/src/material/progress-spinner/progress-spinner.ts b/src/material/progress-spinner/progress-spinner.ts
index b1b3375f634..2136d1615de 100644
--- a/src/material/progress-spinner/progress-spinner.ts
+++ b/src/material/progress-spinner/progress-spinner.ts
@@ -124,19 +124,24 @@ const INDETERMINATE_ANIMATION_TEMPLATE = `
encapsulation: ViewEncapsulation.None,
})
export class MatProgressSpinner extends _MatProgressSpinnerMixinBase implements CanColor {
-
private _value = 0;
private _strokeWidth: number;
private _fallbackAnimation = false;
- /** Tracks diameters of existing instances to de-dupe generated styles (default d = 100) */
- private static _diameters = new Set([BASE_SIZE]);
+ /**
+ * Element to which we should add the generated style tags for the indeterminate animation.
+ * For most elements this is the document, but for the ones in the Shadow DOM we need to
+ * use the shadow root.
+ */
+ private _styleRoot: Node;
/**
- * Used for storing all of the generated keyframe animations.
- * @dynamic
+ * Tracks diameters of existing instances to de-dupe generated styles (default d = 100).
+ * We need to keep track of which elements the diameters were attached to, because for
+ * elements in the Shadow DOM the style tags are attached to the shadow root, rather
+ * than the document head.
*/
- private static _styleTag: HTMLStyleElement|null = null;
+ private static _diameters = new WeakMap>();
/** Whether the _mat-animation-noopable class should be applied, disabling animations. */
_noopAnimations: boolean;
@@ -147,8 +152,13 @@ export class MatProgressSpinner extends _MatProgressSpinnerMixinBase implements
set diameter(size: number) {
this._diameter = coerceNumberProperty(size);
- if (!this._fallbackAnimation && !MatProgressSpinner._diameters.has(this._diameter)) {
- this._attachStyleNode();
+ if (!this._fallbackAnimation) {
+ const trackedDiameters = MatProgressSpinner._diameters;
+ const diametersForElement = trackedDiameters.get(this._styleRoot);
+
+ if (!diametersForElement || !diametersForElement.has(this._diameter)) {
+ this._attachStyleNode();
+ }
}
}
private _diameter = BASE_SIZE;
@@ -182,6 +192,16 @@ export class MatProgressSpinner extends _MatProgressSpinnerMixinBase implements
defaults?: MatProgressSpinnerDefaultOptions) {
super(_elementRef);
+
+ const trackedDiameters = MatProgressSpinner._diameters;
+
+ // The base size is already inserted via the component's structural styles. We still
+ // need to track it so we don't end up adding the same styles again.
+ if (!trackedDiameters.has(_document.head)) {
+ trackedDiameters.set(_document.head, new Set([BASE_SIZE]));
+ }
+
+ this._styleRoot = _getShadowRoot(_elementRef.nativeElement, _document) || _document.head;
this._fallbackAnimation = platform.EDGE || platform.TRIDENT;
this._noopAnimations = animationMode === 'NoopAnimations' &&
(!!defaults && !defaults._forceAnimations);
@@ -241,19 +261,22 @@ export class MatProgressSpinner extends _MatProgressSpinnerMixinBase implements
/** Dynamically generates a style tag containing the correct animation for this diameter. */
private _attachStyleNode(): void {
- let styleTag = MatProgressSpinner._styleTag;
+ const styleTag: HTMLStyleElement = this._document.createElement('style');
+ const styleRoot = this._styleRoot;
+ const currentDiameter = this._diameter;
+ const diameters = MatProgressSpinner._diameters;
+ let diametersForElement = diameters.get(styleRoot);
- if (!styleTag) {
- styleTag = this._document.createElement('style');
- this._document.head.appendChild(styleTag);
- MatProgressSpinner._styleTag = styleTag;
+ styleTag.setAttribute('mat-spinner-animation', currentDiameter + '');
+ styleTag.textContent = this._getAnimationText();
+ styleRoot.appendChild(styleTag);
+
+ if (!diametersForElement) {
+ diametersForElement = new Set();
+ diameters.set(styleRoot, diametersForElement);
}
- if (styleTag && styleTag.sheet) {
- (styleTag.sheet as CSSStyleSheet).insertRule(this._getAnimationText(), 0);
- }
-
- MatProgressSpinner._diameters.add(this.diameter);
+ diametersForElement.add(currentDiameter);
}
/** Generates animation styles adjusted for the spinner's diameter. */
@@ -300,3 +323,26 @@ export class MatSpinner extends MatProgressSpinner {
this.mode = 'indeterminate';
}
}
+
+
+/** Gets the shadow root of an element, if supported and the element is inside the Shadow DOM. */
+export function _getShadowRoot(element: HTMLElement, _document: Document): Node | null {
+ // TODO(crisbeto): see whether we should move this into the CDK
+ // feature detection utilities once #15616 gets merged in.
+ if (typeof window !== 'undefined') {
+ const head = _document.head;
+
+ // Check whether the browser supports Shadow DOM.
+ if (head && ((head as any).createShadowRoot || head.attachShadow)) {
+ const rootNode = element.getRootNode ? element.getRootNode() : null;
+
+ // We need to take the `ShadowRoot` off of `window`, because the built-in types are
+ // incorrect. See https://github.com/Microsoft/TypeScript/issues/27929.
+ if (rootNode instanceof (window as any).ShadowRoot) {
+ return rootNode;
+ }
+ }
+ }
+
+ return null;
+}
diff --git a/src/material/progress-spinner/public-api.ts b/src/material/progress-spinner/public-api.ts
index 42fb976850e..b67d2c313a1 100644
--- a/src/material/progress-spinner/public-api.ts
+++ b/src/material/progress-spinner/public-api.ts
@@ -7,5 +7,11 @@
*/
export * from './progress-spinner-module';
-export * from './progress-spinner';
-
+export {
+ MatProgressSpinner,
+ MatSpinner,
+ MAT_PROGRESS_SPINNER_DEFAULT_OPTIONS,
+ ProgressSpinnerMode,
+ MatProgressSpinnerDefaultOptions,
+ MAT_PROGRESS_SPINNER_DEFAULT_OPTIONS_FACTORY,
+} from './progress-spinner';