Skip to content

Commit

Permalink
leverage discriminating union and move down LiveRegion
Browse files Browse the repository at this point in the history
  • Loading branch information
Alex Kessaris committed Jul 26, 2023
1 parent 46619ea commit 2b32385
Show file tree
Hide file tree
Showing 4 changed files with 96 additions and 49 deletions.
1 change: 1 addition & 0 deletions pages/progress-bar/permutations-utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ const permutations = createPermutations<ProgressBarProps>([
resultButtonText: [undefined, 'Result button text'],
label: [undefined, 'Label'],
description: [undefined, 'description'],
type: ['percentage'],
additionalInfo: [undefined, 'additional info'],
},
{
Expand Down
37 changes: 36 additions & 1 deletion src/progress-bar/__tests__/progress-bar.test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import * as React from 'react';
import { render } from '@testing-library/react';
import { render, within } from '@testing-library/react';
import ProgressBarWrapper from '../../../lib/components/test-utils/dom/progress-bar';
import createWrapper from '../../../lib/components/test-utils/dom';
import ProgressBar, { ProgressBarProps } from '../../../lib/components/progress-bar';
Expand Down Expand Up @@ -213,3 +213,38 @@ describe('Progress updates', () => {
expect(wrapper.find(`.${liveRegionStyles.root}`)?.getElement().textContent).toBe(`${label}: 2%`);
});
});

describe('ARIA value text', () => {
const setup = (props: Partial<ProgressBarProps>) => {
const wrapper = renderProgressBar(props);
return within(wrapper.getElement()).getByRole('progressbar');
};

describe('percentage', () => {
test('default', () => {
const progressBar = setup({ type: 'percentage', value: 1 });
expect(progressBar.getAttribute('aria-valuetext')).toEqual('1%');
expect(progressBar.getAttribute('aria-valuenow')).toEqual('1');
});
});

describe('ratio', () => {
test('default', () => {
const progressBar = setup({ type: 'ratio', value: 1 });
expect(progressBar.getAttribute('aria-valuetext')).toEqual('1/100');
expect(progressBar.getAttribute('aria-valuenow')).toEqual('1');
});

test('maxValue provided', () => {
const progressBar = setup({ type: 'ratio', value: 1, maxValue: 10 });
expect(progressBar.getAttribute('aria-valuetext')).toEqual('1/10');
expect(progressBar.getAttribute('aria-valuenow')).toEqual('1');
});

test('ariaValueText provided', () => {
const progressBar = setup({ type: 'ratio', value: 1, maxValue: 10, ariaValueText: '1 of 10 tasks' });
expect(progressBar.getAttribute('aria-valuetext')).toEqual('1 of 10 tasks');
expect(progressBar.getAttribute('aria-valuenow')).toEqual('1');
});
});
});
29 changes: 5 additions & 24 deletions src/progress-bar/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import React, { useEffect, useMemo, useState } from 'react';
import React from 'react';
import clsx from 'clsx';

import styles from './styles.css.js';
Expand All @@ -12,18 +12,11 @@ import { useUniqueId } from '../internal/hooks/use-unique-id';
import { Progress, ResultState, SmallText } from './internal';
import { applyDisplayName } from '../internal/utils/apply-display-name';
import useBaseComponent from '../internal/hooks/use-base-component';
import { throttle } from '../internal/utils/throttle';
import LiveRegion from '../internal/components/live-region';

const ASSERTION_FREQUENCY = 5000; // interval in ms between progress announcements

export { ProgressBarProps };

export default function ProgressBar({
value = 0,
type = 'percentage',
maxValue = 100,
ariaValueText,
status = 'in-progress',
variant = 'standalone',
resultButtonText,
Expand All @@ -42,18 +35,6 @@ export default function ProgressBar({
const isInFlash = variant === 'flash';
const isInProgressState = status === 'in-progress';

const [assertion, setAssertion] = useState('');
const throttledAssertion = useMemo(() => {
return throttle((value: ProgressBarProps['value']) => {
const announcement = type === 'ratio' ? `${value} of ${maxValue}}` : `${value}%`;
setAssertion(`${label ?? ''}: ${announcement}`);
}, ASSERTION_FREQUENCY);
}, [label, maxValue, type]);

useEffect(() => {
throttledAssertion(value);
}, [throttledAssertion, value]);

if (isInFlash && resultButtonText) {
warnOnce(
'ProgressBar',
Expand All @@ -76,14 +57,14 @@ export default function ProgressBar({
{isInProgressState ? (
<>
<Progress
label={label}
type={rest.type || 'percentage'}
value={value}
maxValue={maxValue}
type={type}
maxValue={(rest.type === 'ratio' && rest.maxValue) || 100}
ariaValueText={rest.type === 'ratio' ? rest.ariaValueText : undefined}
labelId={labelId}
isInFlash={isInFlash}
ariaValueText={ariaValueText}
/>
<LiveRegion delay={0}>{assertion}</LiveRegion>
</>
) : (
<ResultState
Expand Down
78 changes: 54 additions & 24 deletions src/progress-bar/internal.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import React from 'react';
import React, { ReactNode, useEffect, useMemo, useState } from 'react';
import clsx from 'clsx';
import { BoxProps } from '../box/interfaces';
import InternalBox from '../box/internal';
Expand All @@ -11,47 +11,77 @@ import InternalStatusIndicator from '../status-indicator/internal';
import { ProgressBarProps } from './interfaces';
import styles from './styles.css.js';

import { throttle } from '../internal/utils/throttle';
import LiveRegion from '../internal/components/live-region';

const MAX_VALUE = 100;
const ASSERTION_FREQUENCY = 5000; // interval in ms between progress announcements

const clamp = (value: number, lowerLimit: number, upperLimit: number) => {
return Math.max(Math.min(value, upperLimit), lowerLimit);
};

interface ProgressProps {
label: ReactNode;
type: 'percentage' | 'ratio';
value: number;
maxValue: number;
isInFlash: boolean;
labelId: string;
ariaValueText?: string;
}
export const Progress = ({ value, maxValue = MAX_VALUE, type, isInFlash, labelId, ariaValueText }: ProgressProps) => {
export const Progress = ({
label,
value,
maxValue = MAX_VALUE,
type,
isInFlash,
labelId,
ariaValueText,
}: ProgressProps) => {
const roundedValue = Math.round(value);
const progressValue = clamp(roundedValue, 0, maxValue);
const valueText = ariaValueText || type === 'ratio' ? `${progressValue}/${maxValue}` : `${progressValue}%`;
const isRatio = type === 'ratio';
const percentage = isRatio && maxValue !== 100 ? Math.round(((progressValue * 1.0) / maxValue) * 100) : progressValue;
const valueText = isRatio ? ariaValueText || `${progressValue}/${maxValue}` : `${percentage}%`;

const [assertion, setAssertion] = useState('');
const throttledAssertion = useMemo(() => {
return throttle((value: ProgressBarProps['value']) => {
const announcement = type === 'ratio' ? `${value} of ${maxValue}}` : `${value}%`;
setAssertion(`${label ?? ''}: ${announcement}`);
}, ASSERTION_FREQUENCY);
}, [label, type, maxValue]);

useEffect(() => {
throttledAssertion(value);
}, [throttledAssertion, value]);

return (
<div className={styles['progress-container']}>
<progress
className={clsx(
styles.progress,
progressValue >= maxValue && styles.complete,
isInFlash && styles['progress-in-flash']
)}
max={maxValue}
value={progressValue}
aria-valuemin={0}
aria-valuemax={maxValue}
aria-labelledby={labelId}
aria-valuenow={progressValue}
aria-valuetext={valueText}
/>
<span aria-hidden="true" className={styles['percentage-container']}>
<InternalBox className={styles.percentage} variant="small" color={isInFlash ? 'inherit' : undefined}>
{valueText}
</InternalBox>
</span>
</div>
<>
<div className={styles['progress-container']}>
<progress
className={clsx(
styles.progress,
progressValue >= maxValue && styles.complete,
isInFlash && styles['progress-in-flash']
)}
max={maxValue}
value={progressValue}
aria-valuemin={0}
aria-valuemax={maxValue}
aria-labelledby={labelId}
aria-valuenow={isRatio ? progressValue : percentage}
aria-valuetext={valueText}
/>
<span aria-hidden="true" className={styles['percentage-container']}>
<InternalBox className={styles.percentage} variant="small" color={isInFlash ? 'inherit' : undefined}>
{valueText}
</InternalBox>
</span>
</div>
<LiveRegion delay={0}>{assertion}</LiveRegion>
</>
);
};

Expand Down

0 comments on commit 2b32385

Please sign in to comment.