Skip to content

fix(progress-spinner): non-default diameter indeterminate animation not working inside Shadow DOM #16177

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 4, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/material/progress-spinner/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ ng_test_library(
),
deps = [
":progress-spinner",
"//src/cdk/platform",
"@npm//@angular/platform-browser",
],
)
Expand Down
100 changes: 97 additions & 3 deletions src/material/progress-spinner/progress-spinner.spec.ts
Original file line number Diff line number Diff line change
@@ -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({
Expand All @@ -22,6 +24,7 @@ describe('MatProgressSpinner', () => {
ProgressSpinnerCustomDiameter,
SpinnerWithColor,
ProgressSpinnerWithStringValues,
IndeterminateSpinnerInShadowDom,
],
}).compileComponents();
}));
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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);
});

});


Expand Down Expand Up @@ -360,3 +443,14 @@ class ProgressSpinnerWithColor { color: string = 'primary'; }
`
})
class ProgressSpinnerWithStringValues { }


@Component({
template: `
<mat-progress-spinner mode="indeterminate" [diameter]="diameter"></mat-progress-spinner>
`,
encapsulation: ViewEncapsulation.ShadowDom,
})
class IndeterminateSpinnerInShadowDom {
diameter: number;
}
84 changes: 65 additions & 19 deletions src/material/progress-spinner/progress-spinner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<number>([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<Node, Set<number>>();

/** Whether the _mat-animation-noopable class should be applied, disabling animations. */
_noopAnimations: boolean;
Expand All @@ -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;
Expand Down Expand Up @@ -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<number>([BASE_SIZE]));
}

this._styleRoot = _getShadowRoot(_elementRef.nativeElement, _document) || _document.head;
this._fallbackAnimation = platform.EDGE || platform.TRIDENT;
this._noopAnimations = animationMode === 'NoopAnimations' &&
(!!defaults && !defaults._forceAnimations);
Expand Down Expand Up @@ -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;

if (!styleTag) {
styleTag = this._document.createElement('style');
this._document.head.appendChild(styleTag);
MatProgressSpinner._styleTag = 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);

styleTag.setAttribute('mat-spinner-animation', currentDiameter + '');
styleTag.textContent = this._getAnimationText();
styleRoot.appendChild(styleTag);

if (!diametersForElement) {
diametersForElement = new Set<number>();
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. */
Expand Down Expand Up @@ -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;
}
10 changes: 8 additions & 2 deletions src/material/progress-spinner/public-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';