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

This commit is contained in:
Kristiyan Kostadinov
2019-06-04 18:41:21 +02:00
committed by Joey Perrott
parent 5a3e206a6b
commit c11653334b
4 changed files with 170 additions and 23 deletions
@@ -50,6 +50,7 @@ ng_test_library(
),
deps = [
":progress-spinner",
"//src/cdk/platform",
"@npm//@angular/platform-browser",
],
)
@@ -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: `
<mat-progress-spinner mode="indeterminate" [diameter]="diameter"></mat-progress-spinner>
`,
encapsulation: ViewEncapsulation.ShadowDom,
})
class IndeterminateSpinnerInShadowDom {
diameter: number;
}
@@ -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;
@@ -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<number>([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<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. */
@@ -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;
}
+8 -2
View File
@@ -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';