Skip to content

Commit

Permalink
refactor(material-experimental/mdc-progress-bar): remove usage of MDC…
Browse files Browse the repository at this point in the history
… adapter (#25004)

Reworks the MDC progress bar so that it doesn't use MDC's adapter anymore.

(cherry picked from commit daf7f80)
  • Loading branch information
crisbeto committed Jun 1, 2022
1 parent 28cc466 commit 055b25d
Show file tree
Hide file tree
Showing 4 changed files with 57 additions and 131 deletions.
2 changes: 0 additions & 2 deletions src/material-experimental/mdc-progress-bar/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,9 @@ ng_module(
),
assets = [":progress_bar_scss"] + glob(["**/*.html"]),
deps = [
"//src/cdk/bidi",
"//src/material-experimental/mdc-core",
"//src/material/progress-bar",
"@npm//@angular/core",
"@npm//@material/linear-progress",
],
)

Expand Down
9 changes: 7 additions & 2 deletions src/material-experimental/mdc-progress-bar/progress-bar.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,15 @@
More context in the issue: https://github.com/angular/components/issues/22165.
-->
<div class="mdc-linear-progress__buffer" aria-hidden="true">
<div class="mdc-linear-progress__buffer-bar"></div>
<div
class="mdc-linear-progress__buffer-bar"
[style.flex-basis]="_getBufferBarFlexBasis()"></div>
<div class="mdc-linear-progress__buffer-dots"></div>
</div>
<div class="mdc-linear-progress__bar mdc-linear-progress__primary-bar" aria-hidden="true">
<div
class="mdc-linear-progress__bar mdc-linear-progress__primary-bar"
aria-hidden="true"
[style.transform]="_getPrimaryBarTransform()">
<span class="mdc-linear-progress__bar-inner"></span>
</div>
<div class="mdc-linear-progress__bar mdc-linear-progress__secondary-bar" aria-hidden="true">
Expand Down
23 changes: 14 additions & 9 deletions src/material-experimental/mdc-progress-bar/progress-bar.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,20 +96,24 @@ describe('MDC-based MatProgressBar', () => {
expect(getBufferValue()).toBe(100);

progressComponent.value = 40;
fixture.detectChanges();
expect(primaryStyles.transform).toBe('scaleX(0.4)');
expect(getBufferValue()).toBe(100);

progressComponent.value = 35;
progressComponent.bufferValue = 55;
fixture.detectChanges();
expect(primaryStyles.transform).toBe('scaleX(0.35)');
expect(getBufferValue()).toBe(100);

progressComponent.mode = 'buffer';
fixture.detectChanges();
expect(primaryStyles.transform).toBe('scaleX(0.35)');
expect(getBufferValue()).toEqual(55);

progressComponent.value = 60;
progressComponent.bufferValue = 60;
fixture.detectChanges();
expect(primaryStyles.transform).toBe('scaleX(0.6)');
expect(getBufferValue()).toEqual(60);
});
Expand Down Expand Up @@ -239,30 +243,31 @@ describe('MDC-based MatProgressBar', () => {
expect(animationEndSpy).not.toHaveBeenCalled();

// On animation end, output should be emitted.
dispatchFakeEvent(primaryValueBar.nativeElement, 'transitionend');
dispatchFakeEvent(primaryValueBar.nativeElement, 'transitionend', true);
expect(animationEndSpy).toHaveBeenCalledWith({value: 40});
});
});

describe('animation trigger on buffer setting', () => {
let fixture: ComponentFixture<BufferProgressBar>;
let progressElement: DebugElement;
let progressComponent: MatProgressBar;
let primaryValueBar: DebugElement;

beforeEach(() => {
fixture = createComponent(BufferProgressBar);

const progressElement = fixture.debugElement.query(By.css('mat-progress-bar'))!;
progressElement = fixture.debugElement.query(By.css('mat-progress-bar'))!;
progressComponent = progressElement.componentInstance;
primaryValueBar = progressElement.query(By.css('.mdc-linear-progress__primary-bar'))!;
});

it('should bind on transitionend eventListener on primaryBarValue', () => {
spyOn(primaryValueBar.nativeElement, 'addEventListener');
spyOn(progressElement.nativeElement, 'addEventListener');
fixture.detectChanges();

expect(primaryValueBar.nativeElement.addEventListener).toHaveBeenCalled();
expect(primaryValueBar.nativeElement.addEventListener.calls.mostRecent().args[0]).toBe(
expect(progressElement.nativeElement.addEventListener).toHaveBeenCalled();
expect(progressElement.nativeElement.addEventListener.calls.mostRecent().args[0]).toBe(
'transitionend',
);
});
Expand All @@ -277,7 +282,7 @@ describe('MDC-based MatProgressBar', () => {
expect(animationEndSpy).not.toHaveBeenCalled();

// On animation end, output should be emitted.
dispatchFakeEvent(primaryValueBar.nativeElement, 'transitionend');
dispatchFakeEvent(primaryValueBar.nativeElement, 'transitionend', true);
expect(animationEndSpy).toHaveBeenCalledWith({value: 40});
});

Expand All @@ -292,7 +297,7 @@ describe('MDC-based MatProgressBar', () => {
expect(animationEndSpy).not.toHaveBeenCalled();

// On animation end, output should be emitted.
dispatchFakeEvent(primaryValueBar.nativeElement, 'transitionend');
dispatchFakeEvent(primaryValueBar.nativeElement, 'transitionend', true);
expect(animationEndSpy).toHaveBeenCalledWith({value: 40});
});

Expand All @@ -306,7 +311,7 @@ describe('MDC-based MatProgressBar', () => {
progressComponent.value = 30;
progressComponent.bufferValue = 60;
// On animation end, output should be emitted.
dispatchFakeEvent(primaryValueBar.nativeElement, 'transitionend');
dispatchFakeEvent(primaryValueBar.nativeElement, 'transitionend', true);

expect(appRef.tick).not.toHaveBeenCalled();

Expand All @@ -315,7 +320,7 @@ describe('MDC-based MatProgressBar', () => {
progressComponent.value = 40;
progressComponent.bufferValue = 70;
// On animation end, output should be emitted.
dispatchFakeEvent(primaryValueBar.nativeElement, 'transitionend');
dispatchFakeEvent(primaryValueBar.nativeElement, 'transitionend', true);

expect(appRef.tick).toHaveBeenCalled();
expect(animationEndSpy).toHaveBeenCalledWith({value: 40});
Expand Down
154 changes: 36 additions & 118 deletions src/material-experimental/mdc-progress-bar/progress-bar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
EventEmitter,
AfterViewInit,
OnDestroy,
ChangeDetectorRef,
} from '@angular/core';
import {CanColor, mixinColor} from '@angular/material-experimental/mdc-core';
import {ANIMATION_MODULE_TYPE} from '@angular/platform-browser/animations';
Expand All @@ -27,20 +28,12 @@ import {
MAT_PROGRESS_BAR_DEFAULT_OPTIONS,
ProgressAnimationEnd,
} from '@angular/material/progress-bar';
import {
MDCLinearProgressAdapter,
MDCLinearProgressFoundation,
WithMDCResizeObserver,
} from '@material/linear-progress';
import {Subscription, fromEvent, Observable} from 'rxjs';
import {filter} from 'rxjs/operators';
import {Directionality} from '@angular/cdk/bidi';

// Boilerplate for applying mixins to MatProgressBar.
/** @docs-private */
const _MatProgressBarBase = mixinColor(
class {
constructor(public _elementRef: ElementRef) {}
constructor(public _elementRef: ElementRef<HTMLElement>) {}
},
'primary',
);
Expand All @@ -57,10 +50,12 @@ export type ProgressBarMode = 'determinate' | 'indeterminate' | 'buffer' | 'quer
// set tab index to -1 so screen readers will read the aria-label
// Note: there is a known issue with JAWS that does not read progressbar aria labels on FireFox
'tabindex': '-1',
'[attr.aria-valuenow]': '(mode === "indeterminate" || mode === "query") ? null : value',
'[attr.aria-valuenow]': '_isIndeterminate() ? null : value',
'[attr.mode]': 'mode',
'class': 'mat-mdc-progress-bar mdc-linear-progress',
'[class._mat-animation-noopable]': '_isNoopAnimation',
'[class.mdc-linear-progress--animation-ready]': '!_isNoopAnimation',
'[class.mdc-linear-progress--indeterminate]': '_isIndeterminate()',
},
inputs: ['color'],
templateUrl: 'progress-bar.html',
Expand All @@ -75,20 +70,14 @@ export class MatProgressBar
constructor(
elementRef: ElementRef<HTMLElement>,
private _ngZone: NgZone,
@Optional() dir?: Directionality,
private _changeDetectorRef: ChangeDetectorRef,
@Optional() @Inject(ANIMATION_MODULE_TYPE) public _animationMode?: string,
@Optional()
@Inject(MAT_PROGRESS_BAR_DEFAULT_OPTIONS)
defaults?: MatProgressBarDefaultOptions,
) {
super(elementRef);
this._isNoopAnimation = _animationMode === 'NoopAnimations';
if (dir) {
this._dirChangeSubscription = dir.change.subscribe(() => {
this._syncFoundation();
this._foundation?.restartAnimation();
});
}

if (defaults) {
if (defaults.color) {
Expand All @@ -99,56 +88,6 @@ export class MatProgressBar
}
}

/** Implements all of the logic of the MDC progress bar. */
private _foundation: MDCLinearProgressFoundation | undefined;

/** Adapter used by MDC to interact with the DOM. */
private _adapter: MDCLinearProgressAdapter = {
addClass: (className: string) => this._elementRef.nativeElement.classList.add(className),
forceLayout: () => this._elementRef.nativeElement.offsetWidth,
removeAttribute: (name: string) => this._elementRef.nativeElement.removeAttribute(name),
setAttribute: (name: string, value: string) => {
if (name !== 'aria-valuenow') {
this._elementRef.nativeElement.setAttribute(name, value);
}
},
hasClass: (className: string) => this._elementRef.nativeElement.classList.contains(className),
removeClass: (className: string) => this._elementRef.nativeElement.classList.remove(className),
setPrimaryBarStyle: (styleProperty: string, value: string) => {
(this._primaryBar.style as any)[styleProperty] = value;
},
setBufferBarStyle: (styleProperty: string, value: string) => {
(this._bufferBar.style as any)[styleProperty] = value;
},
setStyle: (styleProperty: string, value: string) => {
(this._elementRef.nativeElement.style as any)[styleProperty] = value;
},
getWidth: () => this._elementRef.nativeElement.offsetWidth,
attachResizeObserver: callback => {
const resizeObserverConstructor =
typeof window !== 'undefined' &&
(window as unknown as WithMDCResizeObserver).ResizeObserver;

if (resizeObserverConstructor) {
return this._ngZone.runOutsideAngular(() => {
const observer = new resizeObserverConstructor(callback);

// Internal client users found production errors where `observe` was not a function
// on the constructed `observer`. This should not happen, but adding this check for this
// edge case.
if (typeof observer.observe === 'function') {
observer.observe(this._elementRef.nativeElement);
return observer;
}

return null;
});
}

return null;
},
};

/** Flag that indicates whether NoopAnimations mode is set to true. */
_isNoopAnimation = false;

Expand All @@ -159,7 +98,7 @@ export class MatProgressBar
}
set value(v: number) {
this._value = clamp(v || 0);
this._syncFoundation();
this._changeDetectorRef.markForCheck();
}
private _value = 0;

Expand All @@ -170,26 +109,17 @@ export class MatProgressBar
}
set bufferValue(v: number) {
this._bufferValue = clamp(v || 0);
this._syncFoundation();
this._changeDetectorRef.markForCheck();
}
private _bufferValue = 0;

private _primaryBar: HTMLElement;
private _bufferBar: HTMLElement;

/**
* 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() readonly animationEnd = new EventEmitter<ProgressAnimationEnd>();

/** Reference to animation end subscription to be unsubscribed on destroy. */
private _animationEndSubscription = Subscription.EMPTY;

/** Subscription to when the layout direction changes. */
private _dirChangeSubscription = Subscription.EMPTY;

/**
* Mode of the progress bar.
*
Expand All @@ -205,63 +135,51 @@ export class MatProgressBar
// Note that we don't technically need a getter and a setter here,
// but we use it to match the behavior of the existing mat-progress-bar.
this._mode = value;
this._syncFoundation();
this._changeDetectorRef.markForCheck();
}
private _mode: ProgressBarMode = 'determinate';

ngAfterViewInit() {
const element = this._elementRef.nativeElement;

this._primaryBar = element.querySelector('.mdc-linear-progress__primary-bar') as HTMLElement;
this._bufferBar = element.querySelector('.mdc-linear-progress__buffer-bar') as HTMLElement;

this._foundation = new MDCLinearProgressFoundation(this._adapter);
this._foundation.init();
this._syncFoundation();

// 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._primaryBar, 'transitionend') as Observable<TransitionEvent>
)
.pipe(filter((e: TransitionEvent) => e.target === this._primaryBar))
.subscribe(() => {
if (this.animationEnd.observers.length === 0) {
return;
}

if (this.mode === 'determinate' || this.mode === 'buffer') {
this._ngZone.run(() => this.animationEnd.next({value: this.value}));
}
});
this._elementRef.nativeElement.addEventListener('transitionend', this._transitionendHandler);
});
}

ngOnDestroy() {
if (this._foundation) {
this._foundation.destroy();
}
this._animationEndSubscription.unsubscribe();
this._dirChangeSubscription.unsubscribe();
this._elementRef.nativeElement.removeEventListener('transitionend', this._transitionendHandler);
}

/** Syncs the state of the progress bar with the MDC foundation. */
private _syncFoundation() {
const foundation = this._foundation;
/** Gets the transform style that should be applied to the primary bar. */
_getPrimaryBarTransform(): string {
return `scaleX(${this._isIndeterminate() ? 1 : this.value / 100})`;
}

if (foundation) {
const mode = this.mode;
foundation.setDeterminate(mode !== 'indeterminate' && mode !== 'query');
/** Gets the `flex-basis` value that should be applied to the buffer bar. */
_getBufferBarFlexBasis(): string {
return `${this.mode === 'buffer' ? this.bufferValue : 100}%`;
}

// Divide by 100 because MDC deals with values between 0 and 1.
foundation.setProgress(this.value / 100);
/** Returns whether the progress bar is indeterminate. */
_isIndeterminate(): boolean {
return this.mode === 'indeterminate' || this.mode === 'query';
}

if (mode === 'buffer') {
foundation.setBuffer(this.bufferValue / 100);
}
/** Event handler for `transitionend` events. */
private _transitionendHandler = (event: TransitionEvent) => {
if (
this.animationEnd.observers.length === 0 ||
!event.target ||
!(event.target as HTMLElement).classList.contains('mdc-linear-progress__primary-bar')
) {
return;
}
}

if (this.mode === 'determinate' || this.mode === 'buffer') {
this._ngZone.run(() => this.animationEnd.next({value: this.value}));
}
};
}

/** Clamps a value to be between two numbers, by default 0 and 100. */
Expand Down

0 comments on commit 055b25d

Please sign in to comment.