diff --git a/packages/@adobe/react-spectrum/src/progress/ProgressBarBase.tsx b/packages/@adobe/react-spectrum/src/progress/ProgressBarBase.tsx
index 6a5bd6f8a69..1059ac8a5fc 100644
--- a/packages/@adobe/react-spectrum/src/progress/ProgressBarBase.tsx
+++ b/packages/@adobe/react-spectrum/src/progress/ProgressBarBase.tsx
@@ -86,7 +86,8 @@ export const ProgressBarBase = React.forwardRef(function ProgressBarBase(
let barStyle: CSSProperties = {};
if (!isIndeterminate) {
- let percentage = (value - minValue) / (maxValue - minValue);
+ let range = maxValue - minValue;
+ let percentage = range === 0 ? 0 : (value - minValue) / range;
barStyle.width = `${Math.round(percentage * 100)}%`;
}
diff --git a/packages/@adobe/react-spectrum/src/progress/ProgressCircle.tsx b/packages/@adobe/react-spectrum/src/progress/ProgressCircle.tsx
index 9b0c82c1146..a78692cfcb1 100644
--- a/packages/@adobe/react-spectrum/src/progress/ProgressCircle.tsx
+++ b/packages/@adobe/react-spectrum/src/progress/ProgressCircle.tsx
@@ -92,7 +92,8 @@ export const ProgressCircle = React.forwardRef(function ProgressCircle(
let subMask1Style: CSSProperties = {};
let subMask2Style: CSSProperties = {};
if (!isIndeterminate) {
- let percentage = ((value - minValue) / (maxValue - minValue)) * 100;
+ let range = maxValue - minValue;
+ let percentage = range === 0 ? 0 : ((value - minValue) / range) * 100;
let angle;
if (percentage > 0 && percentage <= 50) {
angle = -180 + (percentage / 50) * 180;
diff --git a/packages/react-aria-components/src/Meter.tsx b/packages/react-aria-components/src/Meter.tsx
index e3f27032dec..d2060e40f95 100644
--- a/packages/react-aria-components/src/Meter.tsx
+++ b/packages/react-aria-components/src/Meter.tsx
@@ -11,7 +11,6 @@
*/
import {AriaMeterProps, useMeter} from 'react-aria/useMeter';
-
import {clamp} from 'react-stately/private/utils/number';
import {
ClassNameOrFunction,
@@ -69,12 +68,13 @@ export const Meter = /*#__PURE__*/ (forwardRef as forwardRefType)(function Meter
[props, ref] = useContextProps(props, ref, MeterContext);
let {value = 0, minValue = 0, maxValue = 100} = props;
value = clamp(value, minValue, maxValue);
+ let range = maxValue - minValue;
let [labelRef, label] = useSlot(!props['aria-label'] && !props['aria-labelledby']);
let {meterProps, labelProps} = useMeter({...props, label});
// Calculate the width of the progress bar as a percentage
- let percentage = ((value - minValue) / (maxValue - minValue)) * 100;
+ let percentage = range === 0 ? 0 : ((value - minValue) / range) * 100;
let renderProps = useRenderProps({
...props,
diff --git a/packages/react-aria-components/src/ProgressBar.tsx b/packages/react-aria-components/src/ProgressBar.tsx
index 3d546f92174..c57ee4fbbc1 100644
--- a/packages/react-aria-components/src/ProgressBar.tsx
+++ b/packages/react-aria-components/src/ProgressBar.tsx
@@ -11,7 +11,6 @@
*/
import {AriaProgressBarProps, useProgressBar} from 'react-aria/useProgressBar';
-
import {clamp} from 'react-stately/private/utils/number';
import {
ClassNameOrFunction,
@@ -81,8 +80,16 @@ export const ProgressBar = forwardRef(function ProgressBar(
let [labelRef, label] = useSlot(!props['aria-label'] && !props['aria-labelledby']);
let {progressBarProps, labelProps} = useProgressBar({...props, label});
+ let range = maxValue - minValue;
// Calculate the width of the progress bar as a percentage
- let percentage = isIndeterminate ? undefined : ((value - minValue) / (maxValue - minValue)) * 100;
+ let percentage: number | undefined = undefined;
+ if (!isIndeterminate) {
+ if (range === 0) {
+ percentage = 0;
+ } else {
+ percentage = ((value - minValue) / range) * 100;
+ }
+ }
let renderProps = useRenderProps({
...props,
diff --git a/packages/react-aria-components/test/Meter.test.js b/packages/react-aria-components/test/Meter.test.js
index aa11b3f80d0..1053af69ae8 100644
--- a/packages/react-aria-components/test/Meter.test.js
+++ b/packages/react-aria-components/test/Meter.test.js
@@ -22,6 +22,7 @@ let TestMeter = props => (
<>
{valueText}
+ {percentage}
>
)}
@@ -48,6 +49,45 @@ describe('Meter', () => {
expect(bar).toHaveStyle('width: 25%');
});
+ it('supports a custom range', () => {
+ let {getByRole} = render();
+
+ let meter = getByRole('meter');
+ expect(meter).toHaveAttribute('aria-valuenow', '3');
+ expect(meter).toHaveAttribute('aria-valuemin', '0');
+ expect(meter).toHaveAttribute('aria-valuemax', '6');
+ expect(meter).toHaveAttribute('aria-valuetext', '50%');
+
+ let value = meter.querySelector('.value');
+ expect(value).toHaveTextContent('50%');
+
+ let percentage = meter.querySelector('.percentage');
+ expect(percentage).toHaveTextContent('50');
+
+ let bar = meter.querySelector('.bar');
+ expect(bar).toHaveStyle('width: 50%');
+ });
+
+ it('renders 0 percent for an empty range', () => {
+ let {getByRole} = render();
+
+ let meter = getByRole('meter');
+ expect(meter).toHaveAttribute('aria-valuenow', '0');
+ expect(meter).toHaveAttribute('aria-valuemin', '0');
+ expect(meter).toHaveAttribute('aria-valuemax', '0');
+ expect(meter).toHaveAttribute('aria-valuetext', '0%');
+ expect(meter).not.toHaveAttribute('aria-valuetext', 'NaN%');
+
+ let value = meter.querySelector('.value');
+ expect(value).toHaveTextContent('0%');
+
+ let percentage = meter.querySelector('.percentage');
+ expect(percentage).toHaveTextContent('0');
+
+ let bar = meter.querySelector('.bar');
+ expect(bar).toHaveStyle('width: 0%');
+ });
+
it('should support slot', () => {
let {getByRole} = render(
diff --git a/packages/react-aria-components/test/ProgressBar.test.js b/packages/react-aria-components/test/ProgressBar.test.js
index bbee1b2f8ab..15924de3553 100644
--- a/packages/react-aria-components/test/ProgressBar.test.js
+++ b/packages/react-aria-components/test/ProgressBar.test.js
@@ -22,6 +22,7 @@ let TestProgressBar = props => (
<>
{valueText}
+ {percentage}
>
)}
@@ -48,23 +49,83 @@ describe('ProgressBar', () => {
expect(bar).toHaveStyle('width: 25%');
});
+ it('supports a custom range', () => {
+ let {getByRole} = render();
+
+ let progressbar = getByRole('progressbar');
+ expect(progressbar).toHaveAttribute('aria-valuenow', '3');
+ expect(progressbar).toHaveAttribute('aria-valuemin', '0');
+ expect(progressbar).toHaveAttribute('aria-valuemax', '6');
+ expect(progressbar).toHaveAttribute('aria-valuetext', '50%');
+
+ let value = progressbar.querySelector('.value');
+ expect(value).toHaveTextContent('50%');
+
+ let percentage = progressbar.querySelector('.percentage');
+ expect(percentage).toHaveTextContent('50');
+
+ let bar = progressbar.querySelector('.bar');
+ expect(bar).toHaveStyle('width: 50%');
+ });
+
+ it('renders 0 percent for an empty range', () => {
+ let {getByRole} = render();
+
+ let progressbar = getByRole('progressbar');
+ expect(progressbar).toHaveAttribute('aria-valuenow', '0');
+ expect(progressbar).toHaveAttribute('aria-valuemin', '0');
+ expect(progressbar).toHaveAttribute('aria-valuemax', '0');
+ expect(progressbar).toHaveAttribute('aria-valuetext', '0%');
+ expect(progressbar).not.toHaveAttribute('aria-valuetext', 'NaN%');
+
+ let value = progressbar.querySelector('.value');
+ expect(value).toHaveTextContent('0%');
+
+ let percentage = progressbar.querySelector('.percentage');
+ expect(percentage).toHaveTextContent('0');
+
+ let bar = progressbar.querySelector('.bar');
+ expect(bar).toHaveStyle('width: 0%');
+ });
+
+ it('renders 0 percent for an empty range with a non-zero bound', () => {
+ let {getByRole} = render();
+
+ let progressbar = getByRole('progressbar');
+ expect(progressbar).toHaveAttribute('aria-valuenow', '5');
+ expect(progressbar).toHaveAttribute('aria-valuemin', '5');
+ expect(progressbar).toHaveAttribute('aria-valuemax', '5');
+ expect(progressbar).toHaveAttribute('aria-valuetext', '0%');
+
+ let percentage = progressbar.querySelector('.percentage');
+ expect(percentage).toHaveTextContent('0');
+
+ let bar = progressbar.querySelector('.bar');
+ expect(bar).toHaveStyle('width: 0%');
+ });
+
it('supports indeterminate state', () => {
+ let renderedPercentage;
let {getByRole} = render(
`progressbar ${isIndeterminate ? 'indeterminate' : ''}`}>
- {({percentage, valueText}) => (
- <>
-
-
- >
- )}
+ {({percentage}) => {
+ renderedPercentage = percentage;
+ return (
+ <>
+
+
+ >
+ );
+ }}
);
let progressbar = getByRole('progressbar');
expect(progressbar).toHaveAttribute('class', 'progressbar indeterminate');
expect(progressbar).not.toHaveAttribute('aria-valuenow');
+ expect(renderedPercentage).toBeUndefined();
let bar = progressbar.querySelector('.bar');
expect(bar.style.width).toBe('');
diff --git a/packages/react-aria/src/progress/useProgressBar.ts b/packages/react-aria/src/progress/useProgressBar.ts
index 5b27625af9e..2ad3899b0f4 100644
--- a/packages/react-aria/src/progress/useProgressBar.ts
+++ b/packages/react-aria/src/progress/useProgressBar.ts
@@ -94,7 +94,8 @@ export function useProgressBar(props: AriaProgressBarProps): ProgressBarAria {
});
value = clamp(value, minValue, maxValue);
- let percentage = (value - minValue) / (maxValue - minValue);
+ let range = maxValue - minValue;
+ let percentage = range === 0 ? 0 : (value - minValue) / range;
let formatter = useNumberFormatter(formatOptions);
if (!isIndeterminate && !valueLabel) {