Skip to content

Commit dadb3e1

Browse files
crisbetojelbourn
authored andcommitted
fix(progress-spinner): animation node inserted into wrong style root when using ngIf with ShadowDom encapsulation (#16982)
If the progress spinner is inside the shadow DOM, we attach the `style` tag with the animation to the shadow root, however since we do the insertion inside the constructor, the spinner might not have been moved into the shadow root yet. These changes move the logic into `ngOnInit` which fires late enough for the node to be in the correct place.
1 parent c8ceae5 commit dadb3e1

File tree

4 files changed

+72
-22
lines changed

4 files changed

+72
-22
lines changed

src/material/progress-spinner/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ ng_test_library(
5151
deps = [
5252
":progress-spinner",
5353
"//src/cdk/platform",
54+
"@npm//@angular/common",
5455
"@npm//@angular/platform-browser",
5556
],
5657
)

src/material/progress-spinner/progress-spinner.spec.ts

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import {TestBed, async, inject} from '@angular/core/testing';
2-
import {Component, ViewEncapsulation} from '@angular/core';
2+
import {Component, ViewEncapsulation, ViewChild, ElementRef} from '@angular/core';
33
import {By} from '@angular/platform-browser';
44
import {Platform} from '@angular/cdk/platform';
5+
import {CommonModule} from '@angular/common';
56
import {_getShadowRoot} from './progress-spinner';
67
import {
78
MatProgressSpinnerModule,
@@ -14,7 +15,7 @@ describe('MatProgressSpinner', () => {
1415

1516
beforeEach(async(() => {
1617
TestBed.configureTestingModule({
17-
imports: [MatProgressSpinnerModule],
18+
imports: [MatProgressSpinnerModule, CommonModule],
1819
declarations: [
1920
BasicProgressSpinner,
2021
IndeterminateProgressSpinner,
@@ -25,6 +26,7 @@ describe('MatProgressSpinner', () => {
2526
SpinnerWithColor,
2627
ProgressSpinnerWithStringValues,
2728
IndeterminateSpinnerInShadowDom,
29+
IndeterminateSpinnerInShadowDomWithNgIf,
2830
],
2931
}).compileComponents();
3032
}));
@@ -404,6 +406,28 @@ describe('MatProgressSpinner', () => {
404406
expect(shadowRoot.querySelectorAll('style[mat-spinner-animation="61"]').length).toBe(1);
405407
});
406408

409+
it('should add the indeterminate animation style tag to the Shadow root if the element is ' +
410+
'inside an ngIf', () => {
411+
// The test is only relevant in browsers that support Shadow DOM.
412+
if (!supportsShadowDom) {
413+
return;
414+
}
415+
416+
const fixture = TestBed.createComponent(IndeterminateSpinnerInShadowDomWithNgIf);
417+
fixture.componentInstance.diameter = 27;
418+
fixture.detectChanges();
419+
420+
const spinner = fixture.componentInstance.spinner.nativeElement;
421+
const shadowRoot = _getShadowRoot(spinner, document) as HTMLElement;
422+
423+
expect(shadowRoot.querySelector('style[mat-spinner-animation="27"]')).toBeTruthy();
424+
425+
fixture.componentInstance.diameter = 15;
426+
fixture.detectChanges();
427+
428+
expect(shadowRoot.querySelector('style[mat-spinner-animation="27"]')).toBeTruthy();
429+
});
430+
407431
});
408432

409433

@@ -454,3 +478,19 @@ class ProgressSpinnerWithStringValues { }
454478
class IndeterminateSpinnerInShadowDom {
455479
diameter: number;
456480
}
481+
482+
@Component({
483+
template: `
484+
<div *ngIf="true">
485+
<mat-progress-spinner mode="indeterminate" [diameter]="diameter"></mat-progress-spinner>
486+
</div>
487+
`,
488+
encapsulation: ViewEncapsulation.ShadowDom,
489+
})
490+
class IndeterminateSpinnerInShadowDomWithNgIf {
491+
@ViewChild(MatProgressSpinner, {read: ElementRef, static: false})
492+
spinner: ElementRef<HTMLElement>;
493+
494+
diameter: number;
495+
}
496+

src/material/progress-spinner/progress-spinner.ts

Lines changed: 27 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
Input,
1919
Optional,
2020
ViewEncapsulation,
21+
OnInit,
2122
} from '@angular/core';
2223
import {CanColor, CanColorCtor, mixinColor} from '@angular/material/core';
2324
import {ANIMATION_MODULE_TYPE} from '@angular/platform-browser/animations';
@@ -123,7 +124,7 @@ const INDETERMINATE_ANIMATION_TEMPLATE = `
123124
changeDetection: ChangeDetectionStrategy.OnPush,
124125
encapsulation: ViewEncapsulation.None,
125126
})
126-
export class MatProgressSpinner extends _MatProgressSpinnerMixinBase implements CanColor {
127+
export class MatProgressSpinner extends _MatProgressSpinnerMixinBase implements OnInit, CanColor {
127128
private _diameter = BASE_SIZE;
128129
private _value = 0;
129130
private _strokeWidth: number;
@@ -153,13 +154,9 @@ export class MatProgressSpinner extends _MatProgressSpinnerMixinBase implements
153154
set diameter(size: number) {
154155
this._diameter = coerceNumberProperty(size);
155156

156-
if (!this._fallbackAnimation) {
157-
const trackedDiameters = MatProgressSpinner._diameters;
158-
const diametersForElement = trackedDiameters.get(this._styleRoot);
159-
160-
if (!diametersForElement || !diametersForElement.has(this._diameter)) {
161-
this._attachStyleNode();
162-
}
157+
// If this is set before `ngOnInit`, the style root may not have been resolved yet.
158+
if (!this._fallbackAnimation && this._styleRoot) {
159+
this._attachStyleNode();
163160
}
164161
}
165162

@@ -201,7 +198,6 @@ export class MatProgressSpinner extends _MatProgressSpinnerMixinBase implements
201198
trackedDiameters.set(_document.head, new Set<number>([BASE_SIZE]));
202199
}
203200

204-
this._styleRoot = _getShadowRoot(_elementRef.nativeElement, _document) || _document.head;
205201
this._fallbackAnimation = platform.EDGE || platform.TRIDENT;
206202
this._noopAnimations = animationMode === 'NoopAnimations' &&
207203
(!!defaults && !defaults._forceAnimations);
@@ -215,13 +211,23 @@ export class MatProgressSpinner extends _MatProgressSpinnerMixinBase implements
215211
this.strokeWidth = defaults.strokeWidth;
216212
}
217213
}
214+
}
215+
216+
ngOnInit() {
217+
const element = this._elementRef.nativeElement;
218+
219+
// Note that we need to look up the root node in ngOnInit, rather than the constructor, because
220+
// Angular seems to create the element outside the shadow root and then moves it inside, if the
221+
// node is inside an `ngIf` and a ShadowDom-encapsulated component.
222+
this._styleRoot = _getShadowRoot(element, this._document) || this._document.head;
223+
this._attachStyleNode();
218224

219225
// On IE and Edge, we can't animate the `stroke-dashoffset`
220226
// reliably so we fall back to a non-spec animation.
221227
const animationClass =
222228
`mat-progress-spinner-indeterminate${this._fallbackAnimation ? '-fallback' : ''}-animation`;
223229

224-
_elementRef.nativeElement.classList.add(animationClass);
230+
element.classList.add(animationClass);
225231
}
226232

227233
/** The radius of the spinner, adjusted for stroke width. */
@@ -261,22 +267,24 @@ export class MatProgressSpinner extends _MatProgressSpinnerMixinBase implements
261267

262268
/** Dynamically generates a style tag containing the correct animation for this diameter. */
263269
private _attachStyleNode(): void {
264-
const styleTag: HTMLStyleElement = this._document.createElement('style');
265270
const styleRoot = this._styleRoot;
266271
const currentDiameter = this._diameter;
267272
const diameters = MatProgressSpinner._diameters;
268273
let diametersForElement = diameters.get(styleRoot);
269274

270-
styleTag.setAttribute('mat-spinner-animation', currentDiameter + '');
271-
styleTag.textContent = this._getAnimationText();
272-
styleRoot.appendChild(styleTag);
275+
if (!diametersForElement || !diametersForElement.has(currentDiameter)) {
276+
const styleTag: HTMLStyleElement = this._document.createElement('style');
277+
styleTag.setAttribute('mat-spinner-animation', currentDiameter + '');
278+
styleTag.textContent = this._getAnimationText();
279+
styleRoot.appendChild(styleTag);
273280

274-
if (!diametersForElement) {
275-
diametersForElement = new Set<number>();
276-
diameters.set(styleRoot, diametersForElement);
277-
}
281+
if (!diametersForElement) {
282+
diametersForElement = new Set<number>();
283+
diameters.set(styleRoot, diametersForElement);
284+
}
278285

279-
diametersForElement.add(currentDiameter);
286+
diametersForElement.add(currentDiameter);
287+
}
280288
}
281289

282290
/** Generates animation styles adjusted for the spinner's diameter. */

tools/public_api_guard/material/progress-spinner.d.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ export declare const MAT_PROGRESS_SPINNER_DEFAULT_OPTIONS: InjectionToken<MatPro
22

33
export declare function MAT_PROGRESS_SPINNER_DEFAULT_OPTIONS_FACTORY(): MatProgressSpinnerDefaultOptions;
44

5-
export declare class MatProgressSpinner extends _MatProgressSpinnerMixinBase implements CanColor {
5+
export declare class MatProgressSpinner extends _MatProgressSpinnerMixinBase implements OnInit, CanColor {
66
readonly _circleRadius: number;
77
readonly _circleStrokeWidth: number;
88
_elementRef: ElementRef<HTMLElement>;
@@ -15,6 +15,7 @@ export declare class MatProgressSpinner extends _MatProgressSpinnerMixinBase imp
1515
strokeWidth: number;
1616
value: number;
1717
constructor(_elementRef: ElementRef<HTMLElement>, platform: Platform, _document: any, animationMode: string, defaults?: MatProgressSpinnerDefaultOptions);
18+
ngOnInit(): void;
1819
}
1920

2021
export interface MatProgressSpinnerDefaultOptions {

0 commit comments

Comments
 (0)