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) {