Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions packages/react-aria-components/src/Meter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -59,6 +60,8 @@ export interface MeterRenderProps {

export const MeterContext = createContext<ContextValue<MeterProps, HTMLDivElement>>(null);

const DEFAULT_FORMAT_OPTIONS: Intl.NumberFormatOptions = {style: 'percent'};

/**
* A meter represents a quantity within a known range, or a fractional value.
*/
Expand All @@ -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,
Expand Down
15 changes: 13 additions & 2 deletions packages/react-aria-components/src/ProgressBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -66,6 +67,8 @@ export interface ProgressBarRenderProps {
export const ProgressBarContext =
createContext<ContextValue<ProgressBarProps, HTMLDivElement>>(null);

const DEFAULT_FORMAT_OPTIONS: Intl.NumberFormatOptions = {style: 'percent'};

/**
* Progress bars show either determinate or indeterminate progress of an operation
* over time.
Expand All @@ -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,
Expand Down
40 changes: 40 additions & 0 deletions packages/react-aria-components/test/Meter.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ let TestMeter = props => (
<>
<Label>Storage space</Label>
<span className="value">{valueText}</span>
<span className="percentage">{percentage}</span>
<div className="bar" style={{width: percentage + '%'}} />
</>
)}
Expand All @@ -48,6 +49,45 @@ describe('Meter', () => {
expect(bar).toHaveStyle('width: 25%');
});

it('supports a custom range', () => {
let {getByRole} = render(<TestMeter value={3} minValue={0} maxValue={6} />);

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(<TestMeter value={0} minValue={0} maxValue={0} />);

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(
<MeterContext.Provider value={{slots: {test: {'aria-label': 'test'}}}}>
Expand Down
73 changes: 67 additions & 6 deletions packages/react-aria-components/test/ProgressBar.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ let TestProgressBar = props => (
<>
<Label>Loading…</Label>
<span className="value">{valueText}</span>
<span className="percentage">{percentage}</span>
<div className="bar" style={{width: percentage + '%'}} />
</>
)}
Expand All @@ -48,23 +49,83 @@ describe('ProgressBar', () => {
expect(bar).toHaveStyle('width: 25%');
});

it('supports a custom range', () => {
let {getByRole} = render(<TestProgressBar value={3} minValue={0} maxValue={6} />);

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(<TestProgressBar value={0} minValue={0} maxValue={0} />);

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(<TestProgressBar value={5} minValue={5} maxValue={5} />);

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
className={({isIndeterminate}) => `progressbar ${isIndeterminate ? 'indeterminate' : ''}`}>
{({percentage, valueText}) => (
<>
<Label>Loading…</Label>
<div className="bar" style={{width: percentage + '%'}} />
</>
)}
{({percentage}) => {
renderedPercentage = percentage;
return (
<>
<Label>Loading…</Label>
<div className="bar" style={{width: percentage + '%'}} />
</>
);
}}
</ProgressBar>
);

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('');
Expand Down