Skip to content

Commit 5bb8e7c

Browse files
committed
refactor(material-experimental/mdc-progress-bar): remove usage of MDC adapter
Reworks the MDC progress bar so that it doesn't use MDC's adapter anymore.
1 parent d0f803c commit 5bb8e7c

File tree

4 files changed

+58
-132
lines changed

4 files changed

+58
-132
lines changed

src/material-experimental/mdc-progress-bar/BUILD.bazel

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,9 @@ ng_module(
2121
),
2222
assets = [":progress_bar_scss"] + glob(["**/*.html"]),
2323
deps = [
24-
"//src/cdk/bidi",
2524
"//src/material-experimental/mdc-core",
2625
"//src/material/progress-bar",
2726
"@npm//@angular/core",
28-
"@npm//@material/linear-progress",
2927
],
3028
)
3129

src/material-experimental/mdc-progress-bar/progress-bar.html

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,15 @@
33
More context in the issue: https://github.com/angular/components/issues/22165.
44
-->
55
<div class="mdc-linear-progress__buffer" aria-hidden="true">
6-
<div class="mdc-linear-progress__buffer-bar"></div>
6+
<div
7+
class="mdc-linear-progress__buffer-bar"
8+
[style.flex-basis]="_getBufferBarFlexBasis()"></div>
79
<div class="mdc-linear-progress__buffer-dots"></div>
810
</div>
9-
<div class="mdc-linear-progress__bar mdc-linear-progress__primary-bar" aria-hidden="true">
11+
<div
12+
class="mdc-linear-progress__bar mdc-linear-progress__primary-bar"
13+
aria-hidden="true"
14+
[style.transform]="_getPrimaryBarTransform()">
1015
<span class="mdc-linear-progress__bar-inner"></span>
1116
</div>
1217
<div class="mdc-linear-progress__bar mdc-linear-progress__secondary-bar" aria-hidden="true">

src/material-experimental/mdc-progress-bar/progress-bar.spec.ts

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -96,20 +96,24 @@ describe('MDC-based MatProgressBar', () => {
9696
expect(getBufferValue()).toBe(100);
9797

9898
progressComponent.value = 40;
99+
fixture.detectChanges();
99100
expect(primaryStyles.transform).toBe('scaleX(0.4)');
100101
expect(getBufferValue()).toBe(100);
101102

102103
progressComponent.value = 35;
103104
progressComponent.bufferValue = 55;
105+
fixture.detectChanges();
104106
expect(primaryStyles.transform).toBe('scaleX(0.35)');
105107
expect(getBufferValue()).toBe(100);
106108

107109
progressComponent.mode = 'buffer';
110+
fixture.detectChanges();
108111
expect(primaryStyles.transform).toBe('scaleX(0.35)');
109112
expect(getBufferValue()).toEqual(55);
110113

111114
progressComponent.value = 60;
112115
progressComponent.bufferValue = 60;
116+
fixture.detectChanges();
113117
expect(primaryStyles.transform).toBe('scaleX(0.6)');
114118
expect(getBufferValue()).toEqual(60);
115119
});
@@ -239,30 +243,31 @@ describe('MDC-based MatProgressBar', () => {
239243
expect(animationEndSpy).not.toHaveBeenCalled();
240244

241245
// On animation end, output should be emitted.
242-
dispatchFakeEvent(primaryValueBar.nativeElement, 'transitionend');
246+
dispatchFakeEvent(primaryValueBar.nativeElement, 'transitionend', true);
243247
expect(animationEndSpy).toHaveBeenCalledWith({value: 40});
244248
});
245249
});
246250

247251
describe('animation trigger on buffer setting', () => {
248252
let fixture: ComponentFixture<BufferProgressBar>;
253+
let progressElement: DebugElement;
249254
let progressComponent: MatProgressBar;
250255
let primaryValueBar: DebugElement;
251256

252257
beforeEach(() => {
253258
fixture = createComponent(BufferProgressBar);
254259

255-
const progressElement = fixture.debugElement.query(By.css('mat-progress-bar'))!;
260+
progressElement = fixture.debugElement.query(By.css('mat-progress-bar'))!;
256261
progressComponent = progressElement.componentInstance;
257262
primaryValueBar = progressElement.query(By.css('.mdc-linear-progress__primary-bar'))!;
258263
});
259264

260-
it('should bind on transitionend eventListener on primaryBarValue', () => {
261-
spyOn(primaryValueBar.nativeElement, 'addEventListener');
265+
it('should bind on transitionend eventListener on the host node', () => {
266+
spyOn(progressElement.nativeElement, 'addEventListener');
262267
fixture.detectChanges();
263268

264-
expect(primaryValueBar.nativeElement.addEventListener).toHaveBeenCalled();
265-
expect(primaryValueBar.nativeElement.addEventListener.calls.mostRecent().args[0]).toBe(
269+
expect(progressElement.nativeElement.addEventListener).toHaveBeenCalled();
270+
expect(progressElement.nativeElement.addEventListener.calls.mostRecent().args[0]).toBe(
266271
'transitionend',
267272
);
268273
});
@@ -277,7 +282,7 @@ describe('MDC-based MatProgressBar', () => {
277282
expect(animationEndSpy).not.toHaveBeenCalled();
278283

279284
// On animation end, output should be emitted.
280-
dispatchFakeEvent(primaryValueBar.nativeElement, 'transitionend');
285+
dispatchFakeEvent(primaryValueBar.nativeElement, 'transitionend', true);
281286
expect(animationEndSpy).toHaveBeenCalledWith({value: 40});
282287
});
283288

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

294299
// On animation end, output should be emitted.
295-
dispatchFakeEvent(primaryValueBar.nativeElement, 'transitionend');
300+
dispatchFakeEvent(primaryValueBar.nativeElement, 'transitionend', true);
296301
expect(animationEndSpy).toHaveBeenCalledWith({value: 40});
297302
});
298303

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

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

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

320325
expect(appRef.tick).toHaveBeenCalled();
321326
expect(animationEndSpy).toHaveBeenCalledWith({value: 40});

src/material-experimental/mdc-progress-bar/progress-bar.ts

Lines changed: 36 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
EventEmitter,
2020
AfterViewInit,
2121
OnDestroy,
22+
ChangeDetectorRef,
2223
} from '@angular/core';
2324
import {CanColor, mixinColor} from '@angular/material-experimental/mdc-core';
2425
import {ANIMATION_MODULE_TYPE} from '@angular/platform-browser/animations';
@@ -27,20 +28,12 @@ import {
2728
MAT_PROGRESS_BAR_DEFAULT_OPTIONS,
2829
ProgressAnimationEnd,
2930
} from '@angular/material/progress-bar';
30-
import {
31-
MDCLinearProgressAdapter,
32-
MDCLinearProgressFoundation,
33-
WithMDCResizeObserver,
34-
} from '@material/linear-progress';
35-
import {Subscription, fromEvent, Observable} from 'rxjs';
36-
import {filter} from 'rxjs/operators';
37-
import {Directionality} from '@angular/cdk/bidi';
3831

3932
// Boilerplate for applying mixins to MatProgressBar.
4033
/** @docs-private */
4134
const _MatProgressBarBase = mixinColor(
4235
class {
43-
constructor(public _elementRef: ElementRef) {}
36+
constructor(public _elementRef: ElementRef<HTMLElement>) {}
4437
},
4538
'primary',
4639
);
@@ -57,10 +50,12 @@ export type ProgressBarMode = 'determinate' | 'indeterminate' | 'buffer' | 'quer
5750
// set tab index to -1 so screen readers will read the aria-label
5851
// Note: there is a known issue with JAWS that does not read progressbar aria labels on FireFox
5952
'tabindex': '-1',
60-
'[attr.aria-valuenow]': '(mode === "indeterminate" || mode === "query") ? null : value',
53+
'[attr.aria-valuenow]': '_isIndeterminate() ? null : value',
6154
'[attr.mode]': 'mode',
6255
'class': 'mat-mdc-progress-bar mdc-linear-progress',
6356
'[class._mat-animation-noopable]': '_isNoopAnimation',
57+
'[class.mdc-linear-progress--animation-ready]': '!_isNoopAnimation',
58+
'[class.mdc-linear-progress--indeterminate]': '_isIndeterminate()',
6459
},
6560
inputs: ['color'],
6661
templateUrl: 'progress-bar.html',
@@ -75,20 +70,14 @@ export class MatProgressBar
7570
constructor(
7671
elementRef: ElementRef<HTMLElement>,
7772
private _ngZone: NgZone,
78-
@Optional() dir?: Directionality,
73+
private _changeDetectorRef: ChangeDetectorRef,
7974
@Optional() @Inject(ANIMATION_MODULE_TYPE) public _animationMode?: string,
8075
@Optional()
8176
@Inject(MAT_PROGRESS_BAR_DEFAULT_OPTIONS)
8277
defaults?: MatProgressBarDefaultOptions,
8378
) {
8479
super(elementRef);
8580
this._isNoopAnimation = _animationMode === 'NoopAnimations';
86-
if (dir) {
87-
this._dirChangeSubscription = dir.change.subscribe(() => {
88-
this._syncFoundation();
89-
this._foundation?.restartAnimation();
90-
});
91-
}
9281

9382
if (defaults) {
9483
if (defaults.color) {
@@ -99,56 +88,6 @@ export class MatProgressBar
9988
}
10089
}
10190

102-
/** Implements all of the logic of the MDC progress bar. */
103-
private _foundation: MDCLinearProgressFoundation | undefined;
104-
105-
/** Adapter used by MDC to interact with the DOM. */
106-
private _adapter: MDCLinearProgressAdapter = {
107-
addClass: (className: string) => this._elementRef.nativeElement.classList.add(className),
108-
forceLayout: () => this._elementRef.nativeElement.offsetWidth,
109-
removeAttribute: (name: string) => this._elementRef.nativeElement.removeAttribute(name),
110-
setAttribute: (name: string, value: string) => {
111-
if (name !== 'aria-valuenow') {
112-
this._elementRef.nativeElement.setAttribute(name, value);
113-
}
114-
},
115-
hasClass: (className: string) => this._elementRef.nativeElement.classList.contains(className),
116-
removeClass: (className: string) => this._elementRef.nativeElement.classList.remove(className),
117-
setPrimaryBarStyle: (styleProperty: string, value: string) => {
118-
(this._primaryBar.style as any)[styleProperty] = value;
119-
},
120-
setBufferBarStyle: (styleProperty: string, value: string) => {
121-
(this._bufferBar.style as any)[styleProperty] = value;
122-
},
123-
setStyle: (styleProperty: string, value: string) => {
124-
(this._elementRef.nativeElement.style as any)[styleProperty] = value;
125-
},
126-
getWidth: () => this._elementRef.nativeElement.offsetWidth,
127-
attachResizeObserver: callback => {
128-
const resizeObserverConstructor =
129-
typeof window !== 'undefined' &&
130-
(window as unknown as WithMDCResizeObserver).ResizeObserver;
131-
132-
if (resizeObserverConstructor) {
133-
return this._ngZone.runOutsideAngular(() => {
134-
const observer = new resizeObserverConstructor(callback);
135-
136-
// Internal client users found production errors where `observe` was not a function
137-
// on the constructed `observer`. This should not happen, but adding this check for this
138-
// edge case.
139-
if (typeof observer.observe === 'function') {
140-
observer.observe(this._elementRef.nativeElement);
141-
return observer;
142-
}
143-
144-
return null;
145-
});
146-
}
147-
148-
return null;
149-
},
150-
};
151-
15291
/** Flag that indicates whether NoopAnimations mode is set to true. */
15392
_isNoopAnimation = false;
15493

@@ -159,7 +98,7 @@ export class MatProgressBar
15998
}
16099
set value(v: number) {
161100
this._value = clamp(v || 0);
162-
this._syncFoundation();
101+
this._changeDetectorRef.markForCheck();
163102
}
164103
private _value = 0;
165104

@@ -170,26 +109,17 @@ export class MatProgressBar
170109
}
171110
set bufferValue(v: number) {
172111
this._bufferValue = clamp(v || 0);
173-
this._syncFoundation();
112+
this._changeDetectorRef.markForCheck();
174113
}
175114
private _bufferValue = 0;
176115

177-
private _primaryBar: HTMLElement;
178-
private _bufferBar: HTMLElement;
179-
180116
/**
181117
* Event emitted when animation of the primary progress bar completes. This event will not
182118
* be emitted when animations are disabled, nor will it be emitted for modes with continuous
183119
* animations (indeterminate and query).
184120
*/
185121
@Output() readonly animationEnd = new EventEmitter<ProgressAnimationEnd>();
186122

187-
/** Reference to animation end subscription to be unsubscribed on destroy. */
188-
private _animationEndSubscription = Subscription.EMPTY;
189-
190-
/** Subscription to when the layout direction changes. */
191-
private _dirChangeSubscription = Subscription.EMPTY;
192-
193123
/**
194124
* Mode of the progress bar.
195125
*
@@ -205,63 +135,51 @@ export class MatProgressBar
205135
// Note that we don't technically need a getter and a setter here,
206136
// but we use it to match the behavior of the existing mat-progress-bar.
207137
this._mode = value;
208-
this._syncFoundation();
138+
this._changeDetectorRef.markForCheck();
209139
}
210140
private _mode: ProgressBarMode = 'determinate';
211141

212142
ngAfterViewInit() {
213-
const element = this._elementRef.nativeElement;
214-
215-
this._primaryBar = element.querySelector('.mdc-linear-progress__primary-bar') as HTMLElement;
216-
this._bufferBar = element.querySelector('.mdc-linear-progress__buffer-bar') as HTMLElement;
217-
218-
this._foundation = new MDCLinearProgressFoundation(this._adapter);
219-
this._foundation.init();
220-
this._syncFoundation();
221-
222143
// Run outside angular so change detection didn't get triggered on every transition end
223144
// instead only on the animation that we care about (primary value bar's transitionend)
224145
this._ngZone.runOutsideAngular(() => {
225-
this._animationEndSubscription = (
226-
fromEvent(this._primaryBar, 'transitionend') as Observable<TransitionEvent>
227-
)
228-
.pipe(filter((e: TransitionEvent) => e.target === this._primaryBar))
229-
.subscribe(() => {
230-
if (this.animationEnd.observers.length === 0) {
231-
return;
232-
}
233-
234-
if (this.mode === 'determinate' || this.mode === 'buffer') {
235-
this._ngZone.run(() => this.animationEnd.next({value: this.value}));
236-
}
237-
});
146+
this._elementRef.nativeElement.addEventListener('transitionend', this._transitionendHandler);
238147
});
239148
}
240149

241150
ngOnDestroy() {
242-
if (this._foundation) {
243-
this._foundation.destroy();
244-
}
245-
this._animationEndSubscription.unsubscribe();
246-
this._dirChangeSubscription.unsubscribe();
151+
this._elementRef.nativeElement.removeEventListener('transitionend', this._transitionendHandler);
247152
}
248153

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

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

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

260-
if (mode === 'buffer') {
261-
foundation.setBuffer(this.bufferValue / 100);
262-
}
169+
/** Event handler for `transitionend` events. */
170+
private _transitionendHandler = (event: TransitionEvent) => {
171+
if (
172+
this.animationEnd.observers.length === 0 ||
173+
!event.target ||
174+
!(event.target as HTMLElement).classList.contains('mdc-linear-progress__primary-bar')
175+
) {
176+
return;
263177
}
264-
}
178+
179+
if (this.mode === 'determinate' || this.mode === 'buffer') {
180+
this._ngZone.run(() => this.animationEnd.next({value: this.value}));
181+
}
182+
};
265183
}
266184

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

0 commit comments

Comments
 (0)