Skip to content

Commit 0faa595

Browse files
authored
refactor: use css attr func to calculate progress bar width when supp… (#34674)
1 parent 436de1f commit 0faa595

File tree

7 files changed

+78
-33
lines changed

7 files changed

+78
-33
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "prerelease",
3+
"comment": "refactor: use css attr func to calculate progress indicator width",
4+
"packageName": "@fluentui/web-components",
5+
"email": "machi@microsoft.com",
6+
"dependentChangeType": "patch"
7+
}

packages/web-components/docs/web-components.api.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -707,17 +707,19 @@ export class BaseField extends FASTElement {
707707
// @public
708708
export class BaseProgressBar extends FASTElement {
709709
constructor();
710+
// (undocumented)
711+
connectedCallback(): void;
710712
// @internal
711713
elementInternals: ElementInternals;
714+
// @internal (undocumented)
715+
indicator: HTMLElement;
712716
// @internal
713717
max?: number;
714718
// @internal
715719
protected maxChanged(prev: number | undefined, next: number | undefined): void;
716720
// @internal
717721
min?: number;
718722
protected minChanged(prev: number | undefined, next: number | undefined): void;
719-
// @internal
720-
get percentComplete(): number;
721723
validationState: ProgressBarValidationState | null;
722724
validationStateChanged(prev: ProgressBarValidationState | undefined, next: ProgressBarValidationState | undefined): void;
723725
// @internal

packages/web-components/src/progress-bar/progress-bar.base.ts

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { attr, FASTElement, nullableNumberConverter, volatile } from '@microsoft/fast-element';
1+
import { attr, FASTElement, nullableNumberConverter, observable } from '@microsoft/fast-element';
22
import { swapStates } from '../utils/element-internals.js';
33
import { ProgressBarValidationState } from './progress-bar.options.js';
44

@@ -9,6 +9,10 @@ import { ProgressBarValidationState } from './progress-bar.options.js';
99
* @public
1010
*/
1111
export class BaseProgressBar extends FASTElement {
12+
/** @internal */
13+
@observable
14+
public indicator!: HTMLElement;
15+
1216
/**
1317
* The internal {@link https://developer.mozilla.org/docs/Web/API/ElementInternals | `ElementInternals`} instance for the component.
1418
*
@@ -51,6 +55,7 @@ export class BaseProgressBar extends FASTElement {
5155
*/
5256
protected valueChanged(prev: number | undefined, next: number | undefined): void {
5357
this.elementInternals.ariaValueNow = typeof next === 'number' ? `${next}` : null;
58+
this.setIndicatorWidth();
5459
}
5560

5661
/**
@@ -69,6 +74,7 @@ export class BaseProgressBar extends FASTElement {
6974
*/
7075
protected minChanged(prev: number | undefined, next: number | undefined): void {
7176
this.elementInternals.ariaValueMin = typeof next === 'number' ? `${next}` : null;
77+
this.setIndicatorWidth();
7278
}
7379

7480
/**
@@ -88,25 +94,36 @@ export class BaseProgressBar extends FASTElement {
8894
*/
8995
protected maxChanged(prev: number | undefined, next: number | undefined): void {
9096
this.elementInternals.ariaValueMax = typeof next === 'number' ? `${next}` : null;
97+
this.setIndicatorWidth();
9198
}
9299

93-
/**
94-
* Indicates progress in %
95-
* @internal
96-
*/
97-
@volatile
98-
public get percentComplete(): number {
100+
public constructor() {
101+
super();
102+
103+
this.elementInternals.role = 'progressbar';
104+
}
105+
106+
connectedCallback() {
107+
super.connectedCallback();
108+
this.setIndicatorWidth();
109+
}
110+
111+
private setIndicatorWidth() {
112+
if (!this.$fastController.isConnected || CSS.supports('width: attr(value type(<number>))')) {
113+
return;
114+
}
115+
116+
if (Number.isNaN(this.value)) {
117+
this.indicator.style.removeProperty('width');
118+
return;
119+
}
120+
99121
const min = this.min ?? 0;
100122
const max = this.max ?? 100;
101123
const value = this.value ?? 0;
102124
const range = max - min;
125+
const width = range === 0 ? 0 : Math.fround(((value - min) / range) * 100);
103126

104-
return range === 0 ? 0 : Math.fround(((value - min) / range) * 100);
105-
}
106-
107-
public constructor() {
108-
super();
109-
110-
this.elementInternals.role = 'progressbar';
127+
this.indicator.style.setProperty('width', `${width}%`);
111128
}
112129
}

packages/web-components/src/progress-bar/progress-bar.spec.ts

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ import { expect, test } from '../../test/playwright/index.js';
22
import type { ProgressBar } from './progress-bar.js';
33
import { ProgressBarShape, ProgressBarThickness, ProgressBarValidationState } from './progress-bar.options.js';
44

5+
interface BoundingBox {
6+
width: number;
7+
}
8+
59
test.describe('Progress Bar', () => {
610
test.use({
711
tagName: 'fluent-progress-bar',
@@ -37,48 +41,58 @@ test.describe('Progress Bar', () => {
3741
await expect(element).toHaveJSProperty('elementInternals.ariaValueMax', '50');
3842
});
3943

40-
test('should set the `percentComplete` property to match the `value` property as a percentage between 0 and 100 when `min` and `max` are unset', async ({
44+
test('should set indicator width to match the `value` property as a percentage between 0 and 100 when `min` and `max` are unset', async ({
4145
fastPage,
4246
}) => {
4347
const { element } = fastPage;
48+
const indicator = element.locator('.indicator');
4449

4550
await fastPage.setTemplate({ attributes: { value: '50' } });
4651

47-
await expect(element).toHaveJSProperty('percentComplete', 50);
52+
const elementBox = (await element.boundingBox()) as BoundingBox;
53+
54+
await expect(indicator).toHaveCSS('width', `${elementBox.width / 2}px`);
4855
});
4956

50-
test('should set the `percentComplete` property to match the `value` property as a percentage between `min` and `max`', async ({
57+
test('should set the indicator width to match the `value` property as a percentage between `min` and `max`', async ({
5158
fastPage,
5259
}) => {
5360
const { element } = fastPage;
61+
const indicator = element.locator('.indicator');
5462

5563
await fastPage.setTemplate({ attributes: { value: '0' } });
5664

57-
await expect(element).toHaveJSProperty('percentComplete', 0);
65+
let elementBox = (await element.boundingBox()) as BoundingBox;
66+
67+
await expect(indicator).toHaveCSS('width', '0px');
5868

5969
await element.evaluate((node: ProgressBar) => {
6070
node.value = 50;
6171
});
72+
elementBox = (await element.boundingBox()) as BoundingBox;
6273

63-
await expect(element).toHaveJSProperty('percentComplete', 50);
74+
await expect(indicator).toHaveCSS('width', `${elementBox.width / 2}px`);
6475

6576
await element.evaluate((node: ProgressBar) => {
6677
node.value = 100;
6778
});
79+
elementBox = (await element.boundingBox()) as BoundingBox;
6880

69-
await expect(element).toHaveJSProperty('percentComplete', 100);
81+
await expect(indicator).toHaveCSS('width', `${elementBox.width}px`);
7082

7183
await element.evaluate((node: ProgressBar) => {
7284
node.max = 200;
7385
});
86+
elementBox = (await element.boundingBox()) as BoundingBox;
7487

75-
await expect(element).toHaveJSProperty('percentComplete', 50);
88+
await expect(indicator).toHaveCSS('width', `${elementBox.width / 2}px`);
7689

7790
await element.evaluate((node: ProgressBar) => {
7891
node.min = 100;
7992
});
93+
elementBox = (await element.boundingBox()) as BoundingBox;
8094

81-
await expect(element).toHaveJSProperty('percentComplete', 0);
95+
await expect(indicator).toHaveCSS('width', '0px');
8296
});
8397

8498
test('should set the `thickness` property to match the `thickness` attribute', async ({ fastPage }) => {

packages/web-components/src/progress-bar/progress-bar.styles.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,13 @@ export const styles = css`
2424
background-color: ${colorNeutralBackground6};
2525
border-radius: ${borderRadiusMedium};
2626
contain: content;
27+
28+
@supports (width: attr(value type(<number>))) {
29+
--max: attr(max type(<number>), 100);
30+
--min: attr(min type(<number>), 0);
31+
--value: attr(value type(<number>), 0);
32+
--indicator-width: clamp(0%, calc((var(--value) - var(--min)) / (var(--max) - var(--min)) * 100%), 100%);
33+
}
2734
}
2835
2936
:host([thickness='large']) {
@@ -42,6 +49,10 @@ export const styles = css`
4249
4350
:host([value]) .indicator {
4451
transition: all 0.2s ease-in-out;
52+
53+
@supports (width: attr(value type(<number>))) {
54+
width: var(--indicator-width);
55+
}
4556
}
4657
4758
:host(:not([value])) .indicator {
Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,9 @@
1-
import { html } from '@microsoft/fast-element';
1+
import { html, ref } from '@microsoft/fast-element';
22
import type { ElementViewTemplate } from '@microsoft/fast-element';
33
import type { ProgressBar } from './progress-bar.js';
44

55
export function progressTemplate<T extends ProgressBar>(): ElementViewTemplate<T> {
6-
return html`
7-
<div
8-
class="indicator"
9-
part="indicator"
10-
style="${x => (typeof x.value === 'number' ? `width: ${x.percentComplete}%` : void 0)}"
11-
></div>
12-
`;
6+
return html` <div class="indicator" part="indicator" ${ref('indicator')}></div> `;
137
}
148

159
export const template: ElementViewTemplate<ProgressBar> = progressTemplate();

packages/web-components/src/progress-bar/progress-bar.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { attr } from '@microsoft/fast-element';
22
import { BaseProgressBar } from './progress-bar.base.js';
3-
import { ProgressBarShape, ProgressBarThickness } from './progress-bar.options.js';
3+
import type { ProgressBarShape, ProgressBarThickness } from './progress-bar.options.js';
44

55
/**
66
* A Progress HTML Element.

0 commit comments

Comments
 (0)