Skip to content

Commit 852b0ed

Browse files
authored
refactor(progressbar): Animate the value change using CSS counters and custom CSS props (#1492)
1 parent 931f80a commit 852b0ed

19 files changed

+686
-635
lines changed

src/components/progress/base.ts

Lines changed: 64 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -5,26 +5,42 @@ import {
55
queryAssignedElements,
66
state,
77
} from 'lit/decorators.js';
8-
8+
import type { StyleInfo } from 'lit/directives/style-map.js';
99
import { watch } from '../common/decorators/watch.js';
10-
import { asPercent, clamp, formatString } from '../common/util.js';
10+
import {
11+
asPercent,
12+
clamp,
13+
formatString,
14+
isEmpty,
15+
partNameMap,
16+
} from '../common/util.js';
1117
import type { StyleVariant } from '../types.js';
1218

1319
export abstract class IgcProgressBaseComponent extends LitElement {
14-
private __internals: ElementInternals;
15-
private _ticker!: number;
20+
private readonly __internals: ElementInternals;
1621

1722
@queryAssignedElements()
18-
protected assignedElements!: Array<HTMLElement>;
23+
protected _assignedElements!: HTMLElement[];
24+
25+
@query('[part="base"]', true)
26+
protected _base!: HTMLElement;
1927

20-
@query('[part~="fill"]', true)
21-
protected progressIndicator!: Element;
28+
@state()
29+
protected _percentage = 0;
30+
31+
@state()
32+
protected _progress = 0;
2233

2334
@state()
24-
protected percentage = 0;
35+
protected _hasFraction = false;
2536

2637
@state()
27-
protected progress = 0;
38+
protected _styleInfo: StyleInfo = {
39+
'--_progress-whole': '0.00',
40+
'--_progress-integer': '0',
41+
'--_progress-fraction': '0',
42+
'--_transition-duration': '0ms',
43+
};
2844

2945
/**
3046
* Maximum value of the control.
@@ -78,49 +94,43 @@ export abstract class IgcProgressBaseComponent extends LitElement {
7894
@property({ attribute: 'label-format' })
7995
public labelFormat!: string;
8096

81-
@watch('indeterminate', { waitUntilFirstUpdate: true })
97+
@watch('indeterminate')
8298
protected indeterminateChange() {
83-
this.cancelAnimations();
84-
8599
if (!this.indeterminate) {
86-
this._setProgress();
87-
this.animateLabelTo(0, this.value);
100+
this._updateProgress();
88101
}
89102
}
90103

91-
@watch('max', { waitUntilFirstUpdate: true })
104+
@watch('max')
92105
protected maxChange() {
93106
this.max = Math.max(0, this.max);
94-
95107
if (this.value > this.max) {
96108
this.value = this.max;
97109
}
98110

99-
this._setProgress();
100-
101111
if (!this.indeterminate) {
102-
cancelAnimationFrame(this._ticker);
103-
this.animateLabelTo(this.max, this.value);
112+
this._updateProgress();
104113
}
105114
}
106115

107-
@watch('value', { waitUntilFirstUpdate: true })
108-
protected valueChange(previous: number) {
116+
@watch('value')
117+
protected valueChange() {
109118
this.value = clamp(this.value, 0, this.max);
110-
this._setProgress();
111119

112120
if (!this.indeterminate) {
113-
cancelAnimationFrame(this._ticker);
114-
this.animateLabelTo(previous, this.value);
121+
this._updateProgress();
115122
}
116123
}
117124

118125
constructor() {
119126
super();
120127
this.__internals = this.attachInternals();
121128

122-
this.__internals.role = 'progressbar';
123-
this.__internals.ariaValueMin = '0';
129+
Object.assign(this.__internals, {
130+
role: 'progressbar',
131+
ariaValueMin: '0',
132+
ariaValueNow: '0',
133+
});
124134
}
125135

126136
protected override createRenderRoot() {
@@ -134,83 +144,51 @@ export abstract class IgcProgressBaseComponent extends LitElement {
134144
}
135145

136146
private _updateARIA() {
137-
const internals = this.__internals;
138-
const text = this.labelFormat
139-
? this.renderLabelFormat()
140-
: `${this.percentage}%`;
141-
142-
internals.ariaValueMax = `${this.max}`;
143-
internals.ariaValueNow = this.indeterminate ? null : `${this.value}`;
144-
internals.ariaValueText = this.indeterminate ? null : text;
145-
}
147+
const text = this.labelFormat ? this.renderLabelFormat() : `${this.value}%`;
146148

147-
private _setProgress() {
148-
this.progress = this.value / this.max;
149+
Object.assign(this.__internals, {
150+
ariaValueMax: this.max.toString(),
151+
ariaValueNow: this.indeterminate ? null : this.value.toString(),
152+
ariaValueText: this.indeterminate ? null : text,
153+
});
149154
}
150155

151-
public override async connectedCallback() {
152-
super.connectedCallback();
156+
private _updateProgress() {
157+
const percentage = asPercent(this.value, Math.max(1, this.max));
158+
const fractionValue = Math.round((percentage % 1) * 100);
159+
this._hasFraction = fractionValue > 0;
153160

154-
await this.updateComplete;
155-
if (!this.indeterminate) {
156-
requestAnimationFrame(() => {
157-
this._setProgress();
158-
this.animateLabelTo(0, this.value);
159-
});
160-
}
161+
this._styleInfo = {
162+
'--_progress-whole': percentage.toFixed(2),
163+
'--_progress-integer': Math.floor(percentage),
164+
'--_progress-fraction': fractionValue,
165+
'--_transition-duration': `${this.animationDuration}ms`,
166+
};
161167
}
162168

163-
protected cancelAnimations() {
164-
cancelAnimationFrame(this._ticker);
165-
this.progressIndicator?.getAnimations().forEach((animation) => {
166-
if (animation instanceof CSSTransition) {
167-
animation.cancel();
168-
}
169+
protected renderLabel() {
170+
const parts = partNameMap({
171+
label: true,
172+
value: true,
173+
fraction: this._hasFraction,
169174
});
170-
}
171-
172-
protected animateLabelTo(start: number, end: number) {
173-
let t0: number;
174-
175-
const tick = (t1: number) => {
176-
t0 = t0 ?? t1;
177175

178-
const delta = Math.min(
179-
(t1 - t0) / Math.max(this.animationDuration, 1),
180-
1
181-
);
182-
183-
this.percentage = Math.floor(
184-
asPercent(delta * (end - start) + start, this.max)
185-
);
186-
187-
if (delta < 1) {
188-
this._ticker = requestAnimationFrame(tick);
189-
} else {
190-
cancelAnimationFrame(this._ticker);
191-
}
192-
};
193-
194-
requestAnimationFrame(tick);
176+
return this.labelFormat
177+
? html`<span part=${parts}>${this.renderLabelFormat()}</span>`
178+
: html`<span part="${parts} counter"></span>`;
195179
}
196180

197181
protected renderLabelFormat() {
198182
return formatString(this.labelFormat, this.value, this.max);
199183
}
200184

201185
protected renderDefaultSlot() {
202-
const hasNoLabel =
203-
this.indeterminate || this.hideLabel || this.assignedElements.length;
186+
const hideDefaultLabel =
187+
this.indeterminate || this.hideLabel || !isEmpty(this._assignedElements);
204188

205189
return html`
206190
<slot part="label"></slot>
207-
${hasNoLabel
208-
? nothing
209-
: html`<span part="label value">${this.renderLabelText()}</span>`}
191+
${hideDefaultLabel ? nothing : this.renderLabel()}
210192
`;
211193
}
212-
213-
protected renderLabelText() {
214-
return this.labelFormat ? this.renderLabelFormat() : `${this.percentage}%`;
215-
}
216194
}

0 commit comments

Comments
 (0)