diff --git a/packages/react-aria-components/src/Meter.tsx b/packages/react-aria-components/src/Meter.tsx index e3f27032dec..9b204ce1c0f 100644 --- a/packages/react-aria-components/src/Meter.tsx +++ b/packages/react-aria-components/src/Meter.tsx @@ -11,6 +11,7 @@ */ import {AriaMeterProps, useMeter} from 'react-aria/useMeter'; +import {useNumberFormatter} from 'react-aria/useNumberFormatter'; import {clamp} from 'react-stately/private/utils/number'; import { @@ -59,6 +60,8 @@ export interface MeterRenderProps { export const MeterContext = createContext>(null); +const DEFAULT_FORMAT_OPTIONS: Intl.NumberFormatOptions = {style: 'percent'}; + /** * A meter represents a quantity within a known range, or a fractional value. */ @@ -69,12 +72,19 @@ 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 formatOptions = props.formatOptions ?? DEFAULT_FORMAT_OPTIONS; + let formatter = useNumberFormatter(formatOptions); let [labelRef, label] = useSlot(!props['aria-label'] && !props['aria-labelledby']); - let {meterProps, labelProps} = useMeter({...props, label}); + let valueLabel = + range === 0 && !props.valueLabel && formatOptions.style === 'percent' + ? formatter.format(0) + : props.valueLabel; + let {meterProps, labelProps} = useMeter({...props, label, valueLabel}); // 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..979f5f6caf5 100644 --- a/packages/react-aria-components/src/ProgressBar.tsx +++ b/packages/react-aria-components/src/ProgressBar.tsx @@ -11,6 +11,7 @@ */ import {AriaProgressBarProps, useProgressBar} from 'react-aria/useProgressBar'; +import {useNumberFormatter} from 'react-aria/useNumberFormatter'; import {clamp} from 'react-stately/private/utils/number'; import { @@ -66,6 +67,8 @@ export interface ProgressBarRenderProps { export const ProgressBarContext = createContext>(null); +const DEFAULT_FORMAT_OPTIONS: Intl.NumberFormatOptions = {style: 'percent'}; + /** * Progress bars show either determinate or indeterminate progress of an operation * over time. @@ -77,12 +80,20 @@ export const ProgressBar = forwardRef(function ProgressBar( [props, ref] = useContextProps(props, ref, ProgressBarContext); let {value = 0, minValue = 0, maxValue = 100, isIndeterminate = false} = props; value = clamp(value, minValue, maxValue); + let range = maxValue - minValue; + let formatOptions = props.formatOptions ?? DEFAULT_FORMAT_OPTIONS; + let formatter = useNumberFormatter(formatOptions); let [labelRef, label] = useSlot(!props['aria-label'] && !props['aria-labelledby']); - let {progressBarProps, labelProps} = useProgressBar({...props, label}); + let valueLabel = + !isIndeterminate && range === 0 && !props.valueLabel && formatOptions.style === 'percent' + ? formatter.format(0) + : props.valueLabel; + let {progressBarProps, labelProps} = useProgressBar({...props, label, valueLabel}); // Calculate the width of the progress bar as a percentage - let percentage = isIndeterminate ? undefined : ((value - minValue) / (maxValue - minValue)) * 100; + let percentage = + isIndeterminate ? undefined : range === 0 ? 0 : ((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('');