Skip to content

Commit

Permalink
feat(progress-spinner): add support for custom diameters
Browse files Browse the repository at this point in the history
  • Loading branch information
kara committed Oct 5, 2017
1 parent 282c87f commit 0504e41
Show file tree
Hide file tree
Showing 7 changed files with 164 additions and 36 deletions.
4 changes: 2 additions & 2 deletions src/demo-app/progress-spinner/progress-spinner-demo.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ <h1>Determinate</h1>

<div class="demo-progress-spinner">
<mat-progress-spinner [mode]="isDeterminate ? 'determinate' : 'indeterminate'"
[value]="progressValue" color="primary" [strokeWidth]="1"></mat-progress-spinner>
[value]="progressValue" color="primary" [strokeWidth]="1" [diameter]="32"></mat-progress-spinner>
<mat-progress-spinner [mode]="isDeterminate ? 'determinate' : 'indeterminate'"
[value]="progressValue" color="accent"></mat-progress-spinner>
[value]="progressValue" color="accent" [diameter]="50"></mat-progress-spinner>
</div>

<h1>Indeterminate</h1>
Expand Down
15 changes: 11 additions & 4 deletions src/lib/progress-spinner/progress-spinner-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,21 @@
* found in the LICENSE file at https://angular.io/license
*/
import {NgModule} from '@angular/core';
import {PlatformModule} from '@angular/cdk/platform';
import {MatCommonModule} from '@angular/material/core';
import {MatProgressSpinner, MatSpinner} from './progress-spinner';


@NgModule({
imports: [MatCommonModule],
exports: [MatProgressSpinner, MatSpinner, MatCommonModule],
declarations: [MatProgressSpinner, MatSpinner],
imports: [MatCommonModule, PlatformModule],
exports: [
MatProgressSpinner,
MatSpinner,
MatCommonModule
],
declarations: [
MatProgressSpinner,
MatSpinner
],
})
class MatProgressSpinnerModule {}

Expand Down
12 changes: 8 additions & 4 deletions src/lib/progress-spinner/progress-spinner.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,21 @@
element containing the SVG. `focusable="false"` prevents IE from allowing the user to
tab into the SVG element.
-->

<svg
width="100"
height="100"
viewBox="0 0 100 100"
[style.width.px]="_elementSize"
[style.height.px]="_elementSize"
[attr.viewBox]="_viewBox"
preserveAspectRatio="xMidYMid meet"
focusable="false">

<circle
cx="50%"
cy="50%"
[attr.r]="_circleRadius"
[style.stroke-dashoffset.px]="_getStrokeDashOffset()"
[style.animation-name]="'mat-progress-spinner-stroke-rotate-' + diameter"
[style.stroke-dashoffset.px]="_strokeDashOffset"
[style.stroke-dasharray.px]="_strokeCircumference"
[style.transform.rotate]="'360deg'"
[style.stroke-width.px]="strokeWidth"></circle>
</svg>
21 changes: 9 additions & 12 deletions src/lib/progress-spinner/progress-spinner.scss
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@
$mat-progress-spinner-stroke-rotate-fallback-duration: 10 * 1000ms !default;
$mat-progress-spinner-stroke-rotate-fallback-ease: cubic-bezier(0.87, 0.03, 0.33, 1) !default;

$_mat-progress-spinner-radius: 45px;
$_mat-progress-spinner-circumference: $pi * $_mat-progress-spinner-radius * 2;

$_mat-progress-spinner-default-radius: 45px;
$_mat-progress-spinner-default-circumference: $pi * $_mat-progress-spinner-default-radius * 2;

.mat-progress-spinner {
display: block;
Expand All @@ -24,8 +23,6 @@ $_mat-progress-spinner-circumference: $pi * $_mat-progress-spinner-radius * 2;

circle {
fill: transparent;
stroke-dasharray: $_mat-progress-spinner-circumference;
stroke-dashoffset: $_mat-progress-spinner-circumference;
transform-origin: center;
transition: stroke-dashoffset 225ms linear;
}
Expand All @@ -35,10 +32,11 @@ $_mat-progress-spinner-circumference: $pi * $_mat-progress-spinner-radius * 2;
linear infinite;

circle {
// Note: we multiply the duration by 8, because the animation is spread out in 8 stages.
animation: mat-progress-spinner-stroke-rotate $swift-ease-in-out-duration * 8
$ease-in-out-curve-function infinite;
transition-property: stroke;
// Note: we multiply the duration by 8, because the animation is spread out in 8 stages.
animation-duration: $swift-ease-in-out-duration * 8;
animation-timing-function: $ease-in-out-curve-function;
animation-iteration-count: infinite;
}
}

Expand All @@ -49,7 +47,6 @@ $_mat-progress-spinner-circumference: $pi * $_mat-progress-spinner-radius * 2;
infinite;

circle {
stroke-dashoffset: (1 - 0.8) * $_mat-progress-spinner-circumference;
transition-property: stroke;
}
}
Expand All @@ -63,11 +60,11 @@ $_mat-progress-spinner-circumference: $pi * $_mat-progress-spinner-radius * 2;
}

@at-root {
$start: (1 - 0.05) * $_mat-progress-spinner-circumference; // start the animation at 5%
$end: (1 - 0.8) * $_mat-progress-spinner-circumference; // end the animation at 80%
$start: (1 - 0.05) * $_mat-progress-spinner-default-circumference; // start the animation at 5%
$end: (1 - 0.8) * $_mat-progress-spinner-default-circumference; // end the animation at 80%
$fallback-iterations: 4;

@keyframes mat-progress-spinner-stroke-rotate {
@keyframes mat-progress-spinner-stroke-rotate-100 {
/*
stylelint-disable declaration-block-single-line-max-declarations,
declaration-block-semicolon-space-after
Expand Down
26 changes: 26 additions & 0 deletions src/lib/progress-spinner/progress-spinner.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ describe('MatProgressSpinner', () => {
ProgressSpinnerWithValueAndBoundMode,
ProgressSpinnerWithColor,
ProgressSpinnerCustomStrokeWidth,
ProgressSpinnerCustomDiameter,
SpinnerWithColor,
],
}).compileComponents();
Expand Down Expand Up @@ -79,6 +80,26 @@ describe('MatProgressSpinner', () => {
expect(progressComponent.value).toBe(0);
});

it('should allow a custom diameter', () => {
const fixture = TestBed.createComponent(ProgressSpinnerCustomDiameter);
const spinner = fixture.debugElement.query(By.css('mat-progress-spinner')).nativeElement;
const svgElement = fixture.nativeElement.querySelector('svg');

fixture.componentInstance.diameter = 32;
fixture.detectChanges();

expect(parseInt(spinner.style.width))
.toBe(32, 'Expected the custom diameter to be applied to the host element width.');
expect(parseInt(spinner.style.height))
.toBe(32, 'Expected the custom diameter to be applied to the host element height.');
expect(parseInt(svgElement.style.width))
.toBe(32, 'Expected the custom diameter to be applied to the svg element width.');
expect(parseInt(svgElement.style.height))
.toBe(32, 'Expected the custom diameter to be applied to the svg element height.');
expect(svgElement.getAttribute('viewBox'))
.toBe('0 0 32 32', 'Expected the custom diameter to be applied to the svg viewBox.');
});

it('should allow a custom stroke width', () => {
const fixture = TestBed.createComponent(ProgressSpinnerCustomStrokeWidth);
const circleElement = fixture.nativeElement.querySelector('circle');
Expand Down Expand Up @@ -161,6 +182,11 @@ class ProgressSpinnerCustomStrokeWidth {
strokeWidth: number;
}

@Component({template: '<mat-progress-spinner [diameter]="diameter"></mat-progress-spinner>'})
class ProgressSpinnerCustomDiameter {
diameter: number;
}

@Component({template: '<mat-progress-spinner mode="indeterminate"></mat-progress-spinner>'})
class IndeterminateProgressSpinner { }

Expand Down
118 changes: 104 additions & 14 deletions src/lib/progress-spinner/progress-spinner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,47 @@ import {
SimpleChanges,
OnChanges,
ViewEncapsulation,
Optional,
Inject,
} from '@angular/core';
import {CanColor, mixinColor} from '@angular/material/core';
import {Platform} from '@angular/cdk/platform';
import {DOCUMENT} from '@angular/common';

/** Possible mode for a progress spinner. */
export type ProgressSpinnerMode = 'determinate' | 'indeterminate';

// Boilerplate for applying mixins to MdProgressSpinner.
// Boilerplate for applying mixins to MatProgressSpinner.
/** @docs-private */
export class MatProgressSpinnerBase {
constructor(public _renderer: Renderer2, public _elementRef: ElementRef) {}
}
export const _MatProgressSpinnerMixinBase = mixinColor(MatProgressSpinnerBase, 'primary');

const INDETERMINATE_ANIMATION_TEMPLATE = `
@keyframes mat-progress-spinner-stroke-rotate-DIAMETER {
0% { stroke-dashoffset: START_VALUE; transform: rotate(0); }
12.5% { stroke-dashoffset: END_VALUE; transform: rotate(0); }
12.51% { stroke-dashoffset: END_VALUE; transform: rotateX(180deg) rotate(72.5deg); }
25% { stroke-dashoffset: START_VALUE; transform: rotateX(180deg) rotate(72.5deg); }
25.1% { stroke-dashoffset: START_VALUE; transform: rotate(270deg); }
37.5% { stroke-dashoffset: END_VALUE; transform: rotate(270deg); }
37.51% { stroke-dashoffset: END_VALUE; transform: rotateX(180deg) rotate(161.5deg); }
50% { stroke-dashoffset: START_VALUE; transform: rotateX(180deg) rotate(161.5deg); }
50.01% { stroke-dashoffset: START_VALUE; transform: rotate(180deg); }
62.5% { stroke-dashoffset: END_VALUE; transform: rotate(180deg); }
62.51% { stroke-dashoffset: END_VALUE; transform: rotateX(180deg) rotate(251.5deg); }
75% { stroke-dashoffset: START_VALUE; transform: rotateX(180deg) rotate(251.5deg); }
75.01% { stroke-dashoffset: START_VALUE; transform: rotate(90deg); }
87.5% { stroke-dashoffset: END_VALUE; transform: rotate(90deg); }
87.51% { stroke-dashoffset: END_VALUE; transform: rotateX(180deg) rotate(341.5deg); }
100% { stroke-dashoffset: START_VALUE; transform: rotateX(180deg) rotate(341.5deg); }
}
`;

/**
* <mat-progress-spinner> component.
*/
Expand All @@ -53,13 +80,30 @@ export const _MatProgressSpinnerMixinBase = mixinColor(MatProgressSpinnerBase, '
encapsulation: ViewEncapsulation.None,
preserveWhitespaces: false,
})
export class MatProgressSpinner extends _MatProgressSpinnerMixinBase implements CanColor, OnChanges {
export class MatProgressSpinner extends _MatProgressSpinnerMixinBase implements CanColor,
OnChanges {

private _value: number;
private readonly _baseSize = 100;
private readonly _baseStrokeWidth = 10;
private _fallbackAnimation = false;

/** The width and height of the host element. Will grow with stroke width. **/
_elementSize = this._baseSize;
_circleRadius = 45;

/** Tracks diameters of existing instances to de-dupe generated styles (default d = 100) */
static diameters = new Set<number>([100]);

/** The diameter of the progress spinner (will set width and height of svg). */
@Input()
get diameter(): number {
return this._diameter;
}

set diameter(size: number) {
this._setDiameterAndInitStyles(size);
}
_diameter = this._baseSize;

/** Stroke width of the progress spinner. */
@Input() strokeWidth: number = 10;
Expand All @@ -78,31 +122,76 @@ export class MatProgressSpinner extends _MatProgressSpinnerMixinBase implements
}
}

constructor(renderer: Renderer2, elementRef: ElementRef, platform: Platform) {
super(renderer, elementRef);
constructor(public _renderer: Renderer2, public _elementRef: ElementRef,
platform: Platform, @Optional() @Inject(DOCUMENT) private _document: any) {
super(_renderer, _elementRef);

// On IE and Edge we can't animate the `stroke-dashoffset`
this._fallbackAnimation = platform.EDGE || platform.TRIDENT;

// On IE and Edge, we can't animate the `stroke-dashoffset`
// reliably so we fall back to a non-spec animation.
const animationClass = (platform.EDGE || platform.TRIDENT) ?
const animationClass = this._fallbackAnimation ?
'mat-progress-spinner-indeterminate-fallback-animation' :
'mat-progress-spinner-indeterminate-animation';

renderer.addClass(elementRef.nativeElement, animationClass);
_renderer.addClass(_elementRef.nativeElement, animationClass);
}

ngOnChanges(changes: SimpleChanges) {
if (changes.strokeWidth) {
this._elementSize = this._baseSize + Math.max(this.strokeWidth - this._baseStrokeWidth, 0);
if (changes.strokeWidth || changes.diameter) {
this._elementSize =
this._diameter + Math.max(this.strokeWidth - this._baseStrokeWidth, 0);
}
}

_getStrokeDashOffset() {
/** The radius of the spinner, adjusted for stroke width. */
get _circleRadius() {
return (this.diameter - this._baseStrokeWidth) / 2;
}

/** The view box of the spinner's svg element. */
get _viewBox() {
return `0 0 ${this._elementSize} ${this._elementSize}`;
}

/** The stroke circumference of the svg circle. */
get _strokeCircumference(): number {
return 2 * Math.PI * this._circleRadius;
}

/** The dash offset of the svg circle. */
get _strokeDashOffset() {
if (this.mode === 'determinate') {
return 2 * Math.PI * this._circleRadius * (100 - this._value) / 100;
return this._strokeCircumference * (100 - this._value) / 100;
}

return null;
}

/** Sets the diameter and adds diameter-specific styles if necessary. */
private _setDiameterAndInitStyles(size: number): void {
this._diameter = size;
if (!MatProgressSpinner.diameters.has(this.diameter) && !this._fallbackAnimation) {
this._attachStyleNode();
}
}

/** Dynamically generates a style tag containing the correct animation for this diameter. */
private _attachStyleNode(): void {
const styleTag = this._renderer.createElement('style');
styleTag.textContent = this._getAnimationText();
this._renderer.appendChild(this._document.head, styleTag);
MatProgressSpinner.diameters.add(this.diameter);
}

/** Generates animation styles adjusted for the spinner's diameter. */
private _getAnimationText(): string {
return INDETERMINATE_ANIMATION_TEMPLATE
// Animation should begin at 5% and end at 80%
.replace(/START_VALUE/g, `${0.95 * this._strokeCircumference}`)
.replace(/END_VALUE/g, `${0.2 * this._strokeCircumference}`)
.replace(/DIAMETER/g, `${this.diameter}`);
}
}


Expand Down Expand Up @@ -130,8 +219,9 @@ export class MatProgressSpinner extends _MatProgressSpinnerMixinBase implements
preserveWhitespaces: false,
})
export class MatSpinner extends MatProgressSpinner {
constructor(renderer: Renderer2, elementRef: ElementRef, platform: Platform) {
super(renderer, elementRef, platform);
constructor(renderer: Renderer2, elementRef: ElementRef, platform: Platform,
@Optional() @Inject(DOCUMENT) document: any) {
super(renderer, elementRef, platform, document);
this.mode = 'indeterminate';
}
}
4 changes: 4 additions & 0 deletions src/universal-app/kitchen-sink/kitchen-sink.html
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,10 @@ <h2>Progress bar</h2>
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
<mat-progress-bar mode="query"></mat-progress-bar>

<h2>Progress spinner</h2>

<mat-progress-spinner mode="indeterminate" [diameter]="32"></mat-progress-spinner>
<mat-progress-spinner mode="determinate" [value]="60"></mat-progress-spinner>

<h2>Radio buttons</h2>

Expand Down

0 comments on commit 0504e41

Please sign in to comment.