diff --git a/src/demo-app/progress-bar/progress-bar-demo.html b/src/demo-app/progress-bar/progress-bar-demo.html
index 62acae8389e..0aec6d4166f 100644
--- a/src/demo-app/progress-bar/progress-bar-demo.html
+++ b/src/demo-app/progress-bar/progress-bar-demo.html
@@ -8,12 +8,15 @@
Value: {{bufferProgressValue}}
+
+
Last animation complete value: {{bufferAnimationEndValue}}
@@ -31,7 +36,8 @@
+ [color]="color" (animationEnd)="bufferAnimationEndValue = $event.value">
+
Indeterminate
diff --git a/src/demo-app/progress-bar/progress-bar-demo.ts b/src/demo-app/progress-bar/progress-bar-demo.ts
index 15aa2c0ef8e..1d9db4663aa 100644
--- a/src/demo-app/progress-bar/progress-bar-demo.ts
+++ b/src/demo-app/progress-bar/progress-bar-demo.ts
@@ -20,6 +20,8 @@ import {Component} from '@angular/core';
export class ProgressBarDemo {
color: string = 'primary';
determinateProgressValue: number = 30;
+ determinateAnimationEndValue: number;
+ bufferAnimationEndValue: number;
bufferProgressValue: number = 30;
bufferBufferValue: number = 40;
diff --git a/src/lib/progress-bar/progress-bar.html b/src/lib/progress-bar/progress-bar.html
index bf2bf2ad9bb..8d394133c4b 100644
--- a/src/lib/progress-bar/progress-bar.html
+++ b/src/lib/progress-bar/progress-bar.html
@@ -11,5 +11,5 @@
-
+
diff --git a/src/lib/progress-bar/progress-bar.spec.ts b/src/lib/progress-bar/progress-bar.spec.ts
index 9b75b4f6590..c277dbc4eed 100644
--- a/src/lib/progress-bar/progress-bar.spec.ts
+++ b/src/lib/progress-bar/progress-bar.spec.ts
@@ -1,15 +1,19 @@
-import {TestBed, ComponentFixture} from '@angular/core/testing';
-import {Component, Type} from '@angular/core';
+import {TestBed, async, ComponentFixture} from '@angular/core/testing';
+import {Component, DebugElement, Type} from '@angular/core';
import {By} from '@angular/platform-browser';
+import {dispatchFakeEvent} from '@angular/cdk/testing';
+import {NoopAnimationsModule} from '@angular/platform-browser/animations';
import {MatProgressBarModule, MAT_PROGRESS_BAR_LOCATION} from './index';
+import {MatProgressBar} from './progress-bar';
describe('MatProgressBar', () => {
let fakePath = '/fake-path';
- function createComponent
(componentType: Type): ComponentFixture {
+ function createComponent(componentType: Type,
+ imports?: Array>): ComponentFixture {
TestBed.configureTestingModule({
- imports: [MatProgressBarModule],
+ imports: imports || [MatProgressBarModule],
declarations: [componentType],
providers: [{
provide: MAT_PROGRESS_BAR_LOCATION,
@@ -20,119 +24,210 @@ describe('MatProgressBar', () => {
return TestBed.createComponent(componentType);
}
- describe('basic progress-bar', () => {
- it('should apply a mode of "determinate" if no mode is provided.', () => {
- const fixture = createComponent(BasicProgressBar);
- fixture.detectChanges();
+ describe('with animation', () => {
+ describe('basic progress-bar', () => {
+ it('should apply a mode of "determinate" if no mode is provided.', () => {
+ const fixture = createComponent(BasicProgressBar);
+ fixture.detectChanges();
+ const progressElement = fixture.debugElement.query(By.css('mat-progress-bar'));
+ expect(progressElement.componentInstance.mode).toBe('determinate');
+ });
- const progressElement = fixture.debugElement.query(By.css('mat-progress-bar'));
- expect(progressElement.componentInstance.mode).toBe('determinate');
+ it('should define default values for value and bufferValue attributes', () => {
+ const fixture = createComponent(BasicProgressBar);
+ fixture.detectChanges();
+ const progressElement = fixture.debugElement.query(By.css('mat-progress-bar'));
+ expect(progressElement.componentInstance.value).toBe(0);
+ expect(progressElement.componentInstance.bufferValue).toBe(0);
+ });
+
+ it('should clamp value and bufferValue between 0 and 100', () => {
+ const fixture = createComponent(BasicProgressBar);
+ fixture.detectChanges();
+
+ const progressElement = fixture.debugElement.query(By.css('mat-progress-bar'));
+ const progressComponent = progressElement.componentInstance;
+
+ progressComponent.value = 50;
+ expect(progressComponent.value).toBe(50);
+
+ progressComponent.value = 999;
+ expect(progressComponent.value).toBe(100);
+
+ progressComponent.value = -10;
+ expect(progressComponent.value).toBe(0);
+
+ progressComponent.bufferValue = -29;
+ expect(progressComponent.bufferValue).toBe(0);
+
+ progressComponent.bufferValue = 9;
+ expect(progressComponent.bufferValue).toBe(9);
+
+ progressComponent.bufferValue = 1320;
+ expect(progressComponent.bufferValue).toBe(100);
+ });
+
+ it('should return the transform attribute for bufferValue and mode', () => {
+ const fixture = createComponent(BasicProgressBar);
+ fixture.detectChanges();
+
+ const progressElement = fixture.debugElement.query(By.css('mat-progress-bar'));
+ const progressComponent = progressElement.componentInstance;
+
+ expect(progressComponent._primaryTransform()).toEqual({transform: 'scaleX(0)'});
+ expect(progressComponent._bufferTransform()).toBe(undefined);
+
+ progressComponent.value = 40;
+ expect(progressComponent._primaryTransform()).toEqual({transform: 'scaleX(0.4)'});
+ expect(progressComponent._bufferTransform()).toBe(undefined);
+
+ progressComponent.value = 35;
+ progressComponent.bufferValue = 55;
+ expect(progressComponent._primaryTransform()).toEqual({transform: 'scaleX(0.35)'});
+ expect(progressComponent._bufferTransform()).toBe(undefined);
+
+ progressComponent.mode = 'buffer';
+ expect(progressComponent._primaryTransform()).toEqual({transform: 'scaleX(0.35)'});
+ expect(progressComponent._bufferTransform()).toEqual({transform: 'scaleX(0.55)'});
+
+
+ progressComponent.value = 60;
+ progressComponent.bufferValue = 60;
+ expect(progressComponent._primaryTransform()).toEqual({transform: 'scaleX(0.6)'});
+ expect(progressComponent._bufferTransform()).toEqual({transform: 'scaleX(0.6)'});
+ });
+
+ it('should prefix SVG references with the current path', () => {
+ const fixture = createComponent(BasicProgressBar);
+ fixture.detectChanges();
+
+ const rect = fixture.debugElement.query(By.css('rect')).nativeElement;
+ expect(rect.getAttribute('fill')).toMatch(/^url\(['"]?\/fake-path#.*['"]?\)$/);
+ });
+
+ it('should account for location hash when prefixing the SVG references', () => {
+ fakePath = '/fake-path#anchor';
+
+ const fixture = createComponent(BasicProgressBar);
+ fixture.detectChanges();
+
+ const rect = fixture.debugElement.query(By.css('rect')).nativeElement;
+ expect(rect.getAttribute('fill')).not.toContain('#anchor#');
+ });
+
+ it('should not be able to tab into the underlying SVG element', () => {
+ const fixture = createComponent(BasicProgressBar);
+ fixture.detectChanges();
+
+ const svg = fixture.debugElement.query(By.css('svg')).nativeElement;
+ expect(svg.getAttribute('focusable')).toBe('false');
+ });
});
- it('should define default values for value and bufferValue attributes', () => {
- const fixture = createComponent(BasicProgressBar);
- fixture.detectChanges();
+ describe('animation trigger on determinate setting', () => {
+ let fixture: ComponentFixture;
+ let progressComponent: MatProgressBar;
+ let primaryValueBar: DebugElement;
- const progressElement = fixture.debugElement.query(By.css('mat-progress-bar'));
- expect(progressElement.componentInstance.value).toBe(0);
- expect(progressElement.componentInstance.bufferValue).toBe(0);
+ beforeEach(() => {
+ fixture = createComponent(BasicProgressBar);
+
+ const progressElement = fixture.debugElement.query(By.css('mat-progress-bar'));
+ progressComponent = progressElement.componentInstance;
+ primaryValueBar = progressElement.query(By.css('.mat-progress-bar-primary'));
+ });
+
+ it('should trigger output event on primary value bar animation end', () => {
+ fixture.detectChanges();
+ spyOn(progressComponent.animationEnd, 'next');
+
+ progressComponent.value = 40;
+ expect(progressComponent.animationEnd.next).not.toHaveBeenCalled();
+
+ // On animation end, output should be emitted.
+ dispatchFakeEvent(primaryValueBar.nativeElement, 'transitionend');
+ expect(progressComponent.animationEnd.next).toHaveBeenCalledWith({ value: 40 });
+ });
});
- it('should clamp value and bufferValue between 0 and 100', () => {
- const fixture = createComponent(BasicProgressBar);
- fixture.detectChanges();
+ describe('animation trigger on buffer setting', () => {
+ let fixture: ComponentFixture;
+ let progressComponent: MatProgressBar;
+ let primaryValueBar: DebugElement;
- const progressElement = fixture.debugElement.query(By.css('mat-progress-bar'));
- const progressComponent = progressElement.componentInstance;
+ beforeEach(() => {
+ fixture = createComponent(BufferProgressBar);
- progressComponent.value = 50;
- expect(progressComponent.value).toBe(50);
+ const progressElement = fixture.debugElement.query(By.css('mat-progress-bar'));
+ progressComponent = progressElement.componentInstance;
+ primaryValueBar = progressElement.query(By.css('.mat-progress-bar-primary'));
+ });
- progressComponent.value = 999;
- expect(progressComponent.value).toBe(100);
+ it('should bind on transitionend eventListener on primaryBarValue', () => {
+ spyOn(primaryValueBar.nativeElement, 'addEventListener');
+ fixture.detectChanges();
- progressComponent.value = -10;
- expect(progressComponent.value).toBe(0);
+ expect(primaryValueBar.nativeElement.addEventListener).toHaveBeenCalled();
+ expect(primaryValueBar.nativeElement.addEventListener
+ .calls.mostRecent().args[0]).toBe('transitionend');
+ });
- progressComponent.bufferValue = -29;
- expect(progressComponent.bufferValue).toBe(0);
+ it('should trigger output event on primary value bar animation end', () => {
+ fixture.detectChanges();
+ spyOn(progressComponent.animationEnd, 'next');
- progressComponent.bufferValue = 9;
- expect(progressComponent.bufferValue).toBe(9);
+ progressComponent.value = 40;
+ expect(progressComponent.animationEnd.next).not.toHaveBeenCalled();
- progressComponent.bufferValue = 1320;
- expect(progressComponent.bufferValue).toBe(100);
- });
+ // On animation end, output should be emitted.
+ dispatchFakeEvent(primaryValueBar.nativeElement, 'transitionend');
+ expect(progressComponent.animationEnd.next).toHaveBeenCalledWith({ value: 40 });
+ });
- it('should return the transform attribute for bufferValue and mode', () => {
- const fixture = createComponent(BasicProgressBar);
- fixture.detectChanges();
+ it('should trigger output event with value not bufferValue', () => {
+ fixture.detectChanges();
+ spyOn(progressComponent.animationEnd, 'next');
- const progressElement = fixture.debugElement.query(By.css('mat-progress-bar'));
- const progressComponent = progressElement.componentInstance;
+ progressComponent.value = 40;
+ progressComponent.bufferValue = 70;
+ expect(progressComponent.animationEnd.next).not.toHaveBeenCalled();
- expect(progressComponent._primaryTransform()).toEqual({transform: 'scaleX(0)'});
- expect(progressComponent._bufferTransform()).toBe(undefined);
-
- progressComponent.value = 40;
- expect(progressComponent._primaryTransform()).toEqual({transform: 'scaleX(0.4)'});
- expect(progressComponent._bufferTransform()).toBe(undefined);
-
- progressComponent.value = 35;
- progressComponent.bufferValue = 55;
- expect(progressComponent._primaryTransform()).toEqual({transform: 'scaleX(0.35)'});
- expect(progressComponent._bufferTransform()).toBe(undefined);
-
- progressComponent.mode = 'buffer';
- expect(progressComponent._primaryTransform()).toEqual({transform: 'scaleX(0.35)'});
- expect(progressComponent._bufferTransform()).toEqual({transform: 'scaleX(0.55)'});
-
-
- progressComponent.value = 60;
- progressComponent.bufferValue = 60;
- expect(progressComponent._primaryTransform()).toEqual({transform: 'scaleX(0.6)'});
- expect(progressComponent._bufferTransform()).toEqual({transform: 'scaleX(0.6)'});
- });
-
- it('should prefix SVG references with the current path', () => {
- const fixture = createComponent(BasicProgressBar);
- fixture.detectChanges();
-
- const rect = fixture.debugElement.query(By.css('rect')).nativeElement;
- expect(rect.getAttribute('fill')).toMatch(/^url\(['"]?\/fake-path#.*['"]?\)$/);
- });
-
- it('should account for location hash when prefixing the SVG references', () => {
- fakePath = '/fake-path#anchor';
-
- const fixture = createComponent(BasicProgressBar);
- fixture.detectChanges();
-
- const rect = fixture.debugElement.query(By.css('rect')).nativeElement;
- expect(rect.getAttribute('fill')).not.toContain('#anchor#');
- });
-
- it('should not be able to tab into the underlying SVG element', () => {
- const fixture = createComponent(BasicProgressBar);
- fixture.detectChanges();
-
- const svg = fixture.debugElement.query(By.css('svg')).nativeElement;
- expect(svg.getAttribute('focusable')).toBe('false');
+ // On animation end, output should be emitted.
+ dispatchFakeEvent(primaryValueBar.nativeElement, 'transitionend');
+ expect(progressComponent.animationEnd.next).toHaveBeenCalledWith({ value: 40 });
+ });
});
});
- describe('buffer progress-bar', () => {
- it('should not modify the mode if a valid mode is provided.', () => {
- const fixture = createComponent(BufferProgressBar);
+ describe('With NoopAnimations', () => {
+ let progressComponent: MatProgressBar;
+ let primaryValueBar: DebugElement;
+ let fixture: ComponentFixture;
+
+ beforeEach(async(() => {
+ fixture = createComponent(BasicProgressBar, [MatProgressBarModule, NoopAnimationsModule]);
+ const progressElement = fixture.debugElement.query(By.css('mat-progress-bar'));
+ progressComponent = progressElement.componentInstance;
+ primaryValueBar = progressElement.query(By.css('.mat-progress-bar-primary'));
+ }));
+
+ it('should not bind transition end listener', () => {
+ spyOn(primaryValueBar.nativeElement, 'addEventListener');
fixture.detectChanges();
- const progressElement = fixture.debugElement.query(By.css('mat-progress-bar'));
- expect(progressElement.componentInstance.mode).toBe('buffer');
+ expect(primaryValueBar.nativeElement.addEventListener).not.toHaveBeenCalled();
+ });
+
+ it('should trigger the animationEnd output on value set', () => {
+ fixture.detectChanges();
+ spyOn(progressComponent.animationEnd, 'next');
+
+ progressComponent.value = 40;
+ expect(progressComponent.animationEnd.next).toHaveBeenCalledWith({ value: 40 });
});
});
});
-
@Component({template: ''})
class BasicProgressBar { }
diff --git a/src/lib/progress-bar/progress-bar.ts b/src/lib/progress-bar/progress-bar.ts
index 506f3d17f46..122995d7640 100644
--- a/src/lib/progress-bar/progress-bar.ts
+++ b/src/lib/progress-bar/progress-bar.ts
@@ -11,11 +11,19 @@ import {
ElementRef,
Inject,
Input,
+ Output,
+ EventEmitter,
Optional,
+ NgZone,
ViewEncapsulation,
+ AfterViewInit,
+ ViewChild,
+ OnDestroy,
InjectionToken,
inject,
} from '@angular/core';
+import {fromEvent, Subscription} from 'rxjs';
+import {filter} from 'rxjs/operators';
import {ANIMATION_MODULE_TYPE} from '@angular/platform-browser/animations';
import {CanColor, mixinColor} from '@angular/material/core';
import {DOCUMENT} from '@angular/common';
@@ -29,6 +37,11 @@ export class MatProgressBarBase {
constructor(public _elementRef: ElementRef) { }
}
+/** Last animation end data. */
+export interface ProgressAnimationEnd {
+ value: number;
+}
+
export const _MatProgressBarMixinBase = mixinColor(MatProgressBarBase, 'primary');
/**
@@ -74,7 +87,7 @@ let progressbarId = 0;
'[attr.aria-valuenow]': 'value',
'[attr.mode]': 'mode',
'class': 'mat-progress-bar',
- '[class._mat-animation-noopable]': `_animationMode === 'NoopAnimations'`,
+ '[class._mat-animation-noopable]': `_isNoopAnimation`,
},
inputs: ['color'],
templateUrl: 'progress-bar.html',
@@ -82,8 +95,9 @@ let progressbarId = 0;
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
})
-export class MatProgressBar extends _MatProgressBarMixinBase implements CanColor {
- constructor(public _elementRef: ElementRef,
+export class MatProgressBar extends _MatProgressBarMixinBase implements CanColor,
+ AfterViewInit, OnDestroy {
+ constructor(public _elementRef: ElementRef, private _ngZone: NgZone,
@Optional() @Inject(ANIMATION_MODULE_TYPE) public _animationMode?: string,
/**
* @deprecated `location` parameter to be made required.
@@ -94,18 +108,30 @@ export class MatProgressBar extends _MatProgressBarMixinBase implements CanColor
// We need to prefix the SVG reference with the current path, otherwise they won't work
// in Safari if the page has a `` tag. Note that we need quotes inside the `url()`,
- // because named route URLs can contain parentheses (see #12338). Also we don't use
- // `Location` from `@angular/common` since we can't tell the difference between whether
+
+ // because named route URLs can contain parentheses (see #12338). Also we don't use since
+ // we can't tell the difference between whether
// the consumer is using the hash location strategy or not, because `Location` normalizes
// both `/#/foo/bar` and `/foo/bar` to the same thing.
const path = location && location.pathname ? location.pathname.split('#')[0] : '';
this._rectangleFillValue = `url('${path}#${this.progressbarId}')`;
+ this._isNoopAnimation = _animationMode === 'NoopAnimations';
}
+ /** Flag that indicates whether NoopAnimations mode is set to true. */
+ _isNoopAnimation = false;
+
/** Value of the progress bar. Defaults to zero. Mirrored to aria-valuenow. */
@Input()
get value(): number { return this._value; }
- set value(v: number) { this._value = clamp(v || 0); }
+ set value(v: number) {
+ this._value = clamp(v || 0);
+
+ // When noop animation is set to true, trigger animationEnd directly.
+ if (this._isNoopAnimation) {
+ this.emitAnimationEnd();
+ }
+ }
private _value: number = 0;
/** Buffer value of the progress bar. Defaults to zero. */
@@ -114,6 +140,18 @@ export class MatProgressBar extends _MatProgressBarMixinBase implements CanColor
set bufferValue(v: number) { this._bufferValue = clamp(v || 0); }
private _bufferValue: number = 0;
+ @ViewChild('primaryValueBar') _primaryValueBar: ElementRef;
+
+ /**
+ * Event emitted when animation of the primary progress bar completes. This event will not
+ * be emitted when animations are disabled, nor will it be emitted for modes with continuous
+ * animations (indeterminate and query).
+ */
+ @Output() animationEnd = new EventEmitter();
+
+ /** Reference to animation end subscription to be unsubscribed on destroy. */
+ private _animationEndSubscription: Subscription = Subscription.EMPTY;
+
/**
* Mode of the progress bar.
*
@@ -145,6 +183,31 @@ export class MatProgressBar extends _MatProgressBarMixinBase implements CanColor
return {transform: `scaleX(${scale})`};
}
}
+
+ ngAfterViewInit() {
+ if (!this._isNoopAnimation) {
+ // Run outside angular so change detection didn't get triggered on every transition end
+ // instead only on the animation that we care about (primary value bar's transitionend)
+ this._ngZone.runOutsideAngular((() => {
+ this._animationEndSubscription =
+ fromEvent(this._primaryValueBar.nativeElement, 'transitionend')
+ .pipe(filter(((e: TransitionEvent) =>
+ e.target === this._primaryValueBar.nativeElement)))
+ .subscribe(_ => this._ngZone.run(() => this.emitAnimationEnd()));
+ }));
+ }
+ }
+
+ ngOnDestroy() {
+ this._animationEndSubscription.unsubscribe();
+ }
+
+ /** Emit an animationEnd event if in determinate or buffer mode. */
+ private emitAnimationEnd(): void {
+ if (this.mode === 'determinate' || this.mode === 'buffer') {
+ this.animationEnd.next({value: this.value});
+ }
+ }
}
/** Clamps a value to be between two numbers, by default 0 and 100. */