Skip to content

Commit 062ade5

Browse files
authored
fix(linear-progress): Fix indeterminate animation bug (material-components#5180)
This fixes the bug where if the bar is in indeterminate state, setting it to reverse will cause the animation timers to go out of sync and make the bar look funny. BREAKING CHANGE: MDCLinearProgressAdapter adapter has new `forceLayout` method
1 parent 4d6994b commit 062ade5

File tree

5 files changed

+53
-1
lines changed

5 files changed

+53
-1
lines changed

packages/mdc-linear-progress/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ The adapter for linear progress must provide the following functions, with corre
9595
| `addClass(className: string) => void` | Adds a class to the root element. |
9696
| `removeClass(className: string) => void` | Removes a class from the root element. |
9797
| `hasClass(className: string) => boolean` | Returns boolean indicating whether the root element has a given class. |
98+
| `forceLayout() => void` | Force-trigger a layout on the root element. This is needed to restart animations correctly. |
9899
| `getPrimaryBar() => Element` | Returns the primary bar element. |
99100
| `getBuffer() => Element` | Returns the buffer element. |
100101
| `setStyle(el: Element, styleProperty: string, value: string) => void` | Sets the inline style on the given element. |

packages/mdc-linear-progress/adapter.ts

+1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
*/
3131
export interface MDCLinearProgressAdapter {
3232
addClass(className: string): void;
33+
forceLayout(): void;
3334
getBuffer(): HTMLElement | null;
3435
getPrimaryBar(): HTMLElement | null;
3536
hasClass(className: string): boolean;

packages/mdc-linear-progress/component.ts

+1
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ export class MDCLinearProgress extends MDCComponent<MDCLinearProgressFoundation>
5959
// To ensure we don't accidentally omit any methods, we need a separate, strongly typed adapter variable.
6060
const adapter: MDCLinearProgressAdapter = {
6161
addClass: (className: string) => this.root_.classList.add(className),
62+
forceLayout: () => (this.root_ as HTMLElement).offsetWidth,
6263
getBuffer: () => this.root_.querySelector(MDCLinearProgressFoundation.strings.BUFFER_SELECTOR),
6364
getPrimaryBar: () => this.root_.querySelector(MDCLinearProgressFoundation.strings.PRIMARY_BAR_SELECTOR),
6465
hasClass: (className: string) => this.root_.classList.contains(className),

packages/mdc-linear-progress/foundation.ts

+25
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ export class MDCLinearProgressFoundation extends MDCFoundation<MDCLinearProgress
3838
static get defaultAdapter(): MDCLinearProgressAdapter {
3939
return {
4040
addClass: () => undefined,
41+
forceLayout: () => undefined,
4142
getBuffer: () => null,
4243
getPrimaryBar: () => null,
4344
hasClass: () => false,
@@ -64,11 +65,23 @@ export class MDCLinearProgressFoundation extends MDCFoundation<MDCLinearProgress
6465

6566
setDeterminate(isDeterminate: boolean) {
6667
this.isDeterminate_ = isDeterminate;
68+
6769
if (this.isDeterminate_) {
6870
this.adapter_.removeClass(cssClasses.INDETERMINATE_CLASS);
6971
this.setScale_(this.adapter_.getPrimaryBar(), this.progress_);
7072
this.setScale_(this.adapter_.getBuffer(), this.buffer_);
7173
} else {
74+
if (this.isReversed_) {
75+
// Adding/removing REVERSED_CLASS starts a translate animation, while
76+
// adding INDETERMINATE_CLASS starts a scale animation. Here, we reset
77+
// the translate animation in order to keep it in sync with the new
78+
// scale animation that will start from adding INDETERMINATE_CLASS
79+
// below.
80+
this.adapter_.removeClass(cssClasses.REVERSED_CLASS);
81+
this.adapter_.forceLayout();
82+
this.adapter_.addClass(cssClasses.REVERSED_CLASS);
83+
}
84+
7285
this.adapter_.addClass(cssClasses.INDETERMINATE_CLASS);
7386
this.setScale_(this.adapter_.getPrimaryBar(), 1);
7487
this.setScale_(this.adapter_.getBuffer(), 1);
@@ -91,6 +104,18 @@ export class MDCLinearProgressFoundation extends MDCFoundation<MDCLinearProgress
91104

92105
setReverse(isReversed: boolean) {
93106
this.isReversed_ = isReversed;
107+
108+
if (!this.isDeterminate_) {
109+
// Adding INDETERMINATE_CLASS starts a scale animation, while
110+
// adding/removing REVERSED_CLASS starts a translate animation. Here, we
111+
// reset the scale animation in order to keep it in sync with the new
112+
// translate animation that will start from adding/removing REVERSED_CLASS
113+
// below.
114+
this.adapter_.removeClass(cssClasses.INDETERMINATE_CLASS);
115+
this.adapter_.forceLayout();
116+
this.adapter_.addClass(cssClasses.INDETERMINATE_CLASS);
117+
}
118+
94119
if (this.isReversed_) {
95120
this.adapter_.addClass(cssClasses.REVERSED_CLASS);
96121
} else {

test/unit/mdc-linear-progress/foundation.test.js

+25-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ test('exports cssClasses', () => {
4242

4343
test('defaultAdapter returns a complete adapter implementation', () => {
4444
verifyDefaultAdapter(MDCLinearProgressFoundation, [
45-
'addClass', 'getPrimaryBar', 'getBuffer', 'hasClass', 'removeClass', 'setStyle',
45+
'addClass', 'getPrimaryBar', 'forceLayout', 'getBuffer', 'hasClass', 'removeClass', 'setStyle',
4646
]);
4747
});
4848

@@ -70,6 +70,14 @@ test('#setDeterminate removes class', () => {
7070
td.verify(mockAdapter.removeClass(cssClasses.INDETERMINATE_CLASS));
7171
});
7272

73+
test('#setDeterminate false calls forceLayout to correctly reset animation timers when reversed', () => {
74+
const {foundation, mockAdapter} = setupTest();
75+
td.when(mockAdapter.hasClass(cssClasses.REVERSED_CLASS)).thenReturn(true);
76+
foundation.init();
77+
foundation.setDeterminate(false);
78+
td.verify(mockAdapter.forceLayout());
79+
});
80+
7381
test('#setDeterminate restores previous progress value after toggled from false to true', () => {
7482
const {foundation, mockAdapter} = setupTest();
7583
const primaryBar = {};
@@ -159,6 +167,22 @@ test('#setReverse removes class', () => {
159167
td.verify(mockAdapter.removeClass(cssClasses.REVERSED_CLASS));
160168
});
161169

170+
test('#setReverse true calls forceLayout to correctly reset animation timers when indeterminate', () => {
171+
const {foundation, mockAdapter} = setupTest();
172+
td.when(mockAdapter.hasClass(cssClasses.INDETERMINATE_CLASS)).thenReturn(true);
173+
foundation.init();
174+
foundation.setReverse(true);
175+
td.verify(mockAdapter.forceLayout());
176+
});
177+
178+
test('#setReverse false calls forceLayout to correctly reset animation timers when indeterminate', () => {
179+
const {foundation, mockAdapter} = setupTest();
180+
td.when(mockAdapter.hasClass(cssClasses.INDETERMINATE_CLASS)).thenReturn(true);
181+
foundation.init();
182+
foundation.setReverse(false);
183+
td.verify(mockAdapter.forceLayout());
184+
});
185+
162186
test('#open removes class', () => {
163187
const {foundation, mockAdapter} = setupTest();
164188
td.when(mockAdapter.hasClass(cssClasses.REVERSED_CLASS)).thenReturn(true);

0 commit comments

Comments
 (0)