mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
fix(progress-spinner): non-default diameter indeterminate animation not working inside Shadow DOM (#16177)
This commit is contained in:
committed by
Joey Perrott
parent
5a3e206a6b
commit
c11653334b
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user