diff --git a/playroom/snippets.tsx b/playroom/snippets.tsx index d52c9e17cc..305682953d 100644 --- a/playroom/snippets.tsx +++ b/playroom/snippets.tsx @@ -2634,6 +2634,11 @@ export default [ name: 'ProgressBar', code: '', }, + { + group: 'Progress', + name: 'ProgressBarStepped', + code: '', + }, { group: 'NavigationBreadcrumbs', name: 'NavigationBreadcrumbs', diff --git a/src/__screenshot_tests__/__image_snapshots__/progress-bar-screenshot-test-tsx-progress-bar-color-error-1-snap.png b/src/__screenshot_tests__/__image_snapshots__/progress-bar-screenshot-test-tsx-progress-bar-color-error-1-snap.png new file mode 100644 index 0000000000..8582978747 Binary files /dev/null and b/src/__screenshot_tests__/__image_snapshots__/progress-bar-screenshot-test-tsx-progress-bar-color-error-1-snap.png differ diff --git a/src/__screenshot_tests__/__image_snapshots__/progress-bar-screenshot-test-tsx-progress-bar-color-red-1-snap.png b/src/__screenshot_tests__/__image_snapshots__/progress-bar-screenshot-test-tsx-progress-bar-color-red-1-snap.png deleted file mode 100644 index 56e906c1ef..0000000000 Binary files a/src/__screenshot_tests__/__image_snapshots__/progress-bar-screenshot-test-tsx-progress-bar-color-red-1-snap.png and /dev/null differ diff --git a/src/__screenshot_tests__/__image_snapshots__/progress-bar-screenshot-test-tsx-progress-bar-stepped-color-default-1-snap.png b/src/__screenshot_tests__/__image_snapshots__/progress-bar-screenshot-test-tsx-progress-bar-stepped-color-default-1-snap.png new file mode 100644 index 0000000000..a493a3893c Binary files /dev/null and b/src/__screenshot_tests__/__image_snapshots__/progress-bar-screenshot-test-tsx-progress-bar-stepped-color-default-1-snap.png differ diff --git a/src/__screenshot_tests__/__image_snapshots__/progress-bar-screenshot-test-tsx-progress-bar-stepped-color-error-1-snap.png b/src/__screenshot_tests__/__image_snapshots__/progress-bar-screenshot-test-tsx-progress-bar-stepped-color-error-1-snap.png new file mode 100644 index 0000000000..1cc9baaffa Binary files /dev/null and b/src/__screenshot_tests__/__image_snapshots__/progress-bar-screenshot-test-tsx-progress-bar-stepped-color-error-1-snap.png differ diff --git a/src/__screenshot_tests__/progress-bar-screenshot-test.tsx b/src/__screenshot_tests__/progress-bar-screenshot-test.tsx index 8649a31bcd..241a602c88 100644 --- a/src/__screenshot_tests__/progress-bar-screenshot-test.tsx +++ b/src/__screenshot_tests__/progress-bar-screenshot-test.tsx @@ -1,9 +1,9 @@ import {openStoryPage, screen} from '../test-utils'; -const COLORS = ['default', 'red']; +const COLORS = ['default', 'error']; test.each(COLORS)('ProgressBar - color={%s}', async (color) => { - await openStoryPage({id: 'components-progressbar--default', args: {color}}); + await openStoryPage({id: 'components-progress-bar--progress-bar-story', args: {color}}); const stepper = await screen.findByTestId('progress-bar'); @@ -11,3 +11,13 @@ test.each(COLORS)('ProgressBar - color={%s}', async (color) => { expect(image).toMatchImageSnapshot(); }); + +test.each(COLORS)('ProgressBarStepped - color={%s}', async (color) => { + await openStoryPage({id: 'components-progress-bar--progress-bar-stepped-story', args: {color}}); + + const stepper = await screen.findByTestId('progress-bar-stepped'); + + const image = await stepper.screenshot(); + + expect(image).toMatchImageSnapshot(); +}); diff --git a/src/__stories__/progress-bar-story.tsx b/src/__stories__/progress-bar-story.tsx index 93a4f95e86..488f6ae2ba 100644 --- a/src/__stories__/progress-bar-story.tsx +++ b/src/__stories__/progress-bar-story.tsx @@ -1,35 +1,66 @@ import * as React from 'react'; -import {ProgressBar} from '..'; +import {ProgressBar, ProgressBarStepped} from '..'; +import {vars} from '../skins/skin-contract.css'; export default { - title: 'Components/ProgressBar', + title: 'Components/Progress bar', argTypes: { color: { - options: ['default', 'red'], + options: ['default', 'error'], control: {type: 'select'}, }, }, }; -type Args = { +type ProgressBarStoryArgs = { reverse: boolean; progressPercent: number; - color: 'default' | 'red'; + color: 'default' | 'error'; }; -export const Default: StoryComponent = ({reverse, progressPercent, color}) => ( -
- -
+export const ProgressBarStory: StoryComponent = ({reverse, progressPercent, color}) => ( + ); -Default.storyName = 'ProgressBar'; -Default.args = { +ProgressBarStory.storyName = 'ProgressBar'; +ProgressBarStory.args = { reverse: false, progressPercent: 30, color: 'default', }; + +type ProgressBarSteppedStoryArgs = { + steps: number; + currentStep: number; + color: 'default' | 'error'; +}; + +export const ProgressBarSteppedStory: StoryComponent = ({ + steps, + currentStep, + color, +}) => ( + +); + +ProgressBarSteppedStory.storyName = 'ProgressBarStepped'; +ProgressBarSteppedStory.args = { + steps: 4, + currentStep: 3, + color: 'default', +}; +ProgressBarSteppedStory.argTypes = { + steps: { + control: {type: 'range', min: 1, max: 6, step: 1}, + }, +}; diff --git a/src/community/blocks.tsx b/src/community/blocks.tsx index ad3197d1ba..14d96647f0 100644 --- a/src/community/blocks.tsx +++ b/src/community/blocks.tsx @@ -5,7 +5,7 @@ import {Text2, Text3, Text5, Text8} from '../text'; import {vars} from '../skins/skin-contract.css'; import Inline from '../inline'; import Box from '../box'; -import ProgressBar from '../progress-bar'; +import {ProgressBar} from '../progress-bar'; import classNames from 'classnames'; import type StackingGroup from '../stacking-group'; diff --git a/src/index.tsx b/src/index.tsx index cfcd974751..eb6dda0c75 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -61,7 +61,7 @@ export {default as Inline} from './inline'; export {default as HorizontalScroll} from './horizontal-scroll'; export {default as HighlightedCard} from './highlighted-card'; export {default as Stepper} from './stepper'; -export {default as ProgressBar} from './progress-bar'; +export {ProgressBar, ProgressBarStepped} from './progress-bar'; export { MediaCard, DataCard, diff --git a/src/progress-bar.css.ts b/src/progress-bar.css.ts index 330f48428c..dd871a8240 100644 --- a/src/progress-bar.css.ts +++ b/src/progress-bar.css.ts @@ -27,16 +27,29 @@ const barKeyFramesInverte = keyframes({ }, }); -export const normal = style([ - { - transition: `max-width ${transition}`, - animation: `${barKeyFrames} ${transition}`, +export const normal = style({ + transition: `max-width ${transition}`, + animation: `${barKeyFrames} ${transition}`, + '@media': { + ['(prefers-reduced-motion)']: { + transition: 'none', + animation: 'none', + }, }, -]); +}); -export const inverse = style([ - { - transition: `max-width ${transition}`, - animation: `${barKeyFramesInverte} ${transition}`, +export const inverse = style({ + transition: `max-width ${transition}`, + animation: `${barKeyFramesInverte} ${transition}`, + '@media': { + ['(prefers-reduced-motion)']: { + transition: 'none', + animation: 'none', + }, }, -]); +}); + +export const progressBarSteppedContainer = style({ + display: 'inline-block', + width: '100%', +}); diff --git a/src/progress-bar.tsx b/src/progress-bar.tsx index 87bc6f60f7..379582a100 100644 --- a/src/progress-bar.tsx +++ b/src/progress-bar.tsx @@ -4,10 +4,11 @@ import {vars} from './skins/skin-contract.css'; import * as styles from './progress-bar.css'; import {getPrefixedDataAttributes} from './utils/dom'; import classNames from 'classnames'; +import Inline from './inline'; import type {DataAttributes} from './utils/types'; -type Props = { +type ProgressBarProps = { progressPercent: number; color?: string; children?: void; @@ -17,7 +18,7 @@ type Props = { reverse?: boolean; }; -const ProgressBar: React.FC = ({ +export const ProgressBar: React.FC = ({ progressPercent, color, 'aria-label': ariaLabel, @@ -26,14 +27,20 @@ const ProgressBar: React.FC = ({ reverse = false, }) => { const {texts} = useTheme(); - const defaultLabel = texts.loading; - const label = ariaLabelledBy ? undefined : ariaLabel || defaultLabel; + const progressValue = Math.max(0, Math.min(100, progressPercent)); + + const getFormattedLabel = () => { + return `${ariaLabel || texts.loading}, ${progressValue}% ${texts.progressBarCompletedLabel}`; + }; + + const label = ariaLabelledBy ? undefined : getFormattedLabel(); + return (
= ({
@@ -50,4 +57,78 @@ const ProgressBar: React.FC = ({ ); }; -export default ProgressBar; +type ProgressBarSteppedProps = { + steps: number; + currentStep?: number; + color?: string; + dataAttributes?: DataAttributes; + 'aria-label'?: string; + 'aria-labelledby'?: string; +}; + +export const ProgressBarStepped: React.FC = ({ + steps, + currentStep = 0, + color, + dataAttributes, + 'aria-label': ariaLabel, + 'aria-labelledby': ariaLabelledBy, +}) => { + const {texts} = useTheme(); + + const [step, setStep] = React.useState(Math.ceil(currentStep)); + const [isBack, setIsBack] = React.useState(false); + + React.useEffect(() => { + const newStep = Math.ceil(currentStep); + if (step !== newStep) { + setIsBack(newStep < step); + setStep(newStep); + } + }, [currentStep, steps, step]); + + const getFormattedLabel = () => { + const label = texts.progressBarStepLabel.replace('1$s', String(step)).replace('2$s', String(steps)); + return ariaLabel ? `${ariaLabel}, ${label.toLowerCase()}` : label; + }; + + const label = ariaLabelledBy ? undefined : getFormattedLabel(); + + return ( +
+ + {Array.from({length: steps}, (_, index) => { + const isCurrent = index === step; + const isCompleted = index < step; + const hasAnimation = index === step - 1; + + return ( + + ); +}; diff --git a/src/stepper.tsx b/src/stepper.tsx index 51e9599d32..72670fa3d3 100644 --- a/src/stepper.tsx +++ b/src/stepper.tsx @@ -29,12 +29,17 @@ const Stepper: React.FC = ({ const {isDesktopOrBigger} = useScreenSize(); const {height, ref} = useElementDimensions(); const textContainerHeight = height; - const previousIndexRef = React.useRef(currentIndex); - const isBack = previousIndexRef.current > currentIndex; - if (currentIndex !== previousIndexRef.current) { - previousIndexRef.current = currentIndex; - } + const [step, setStep] = React.useState(Math.ceil(currentIndex)); + const [isBack, setIsBack] = React.useState(false); + + React.useEffect(() => { + const newStep = Math.ceil(currentIndex); + if (step !== newStep) { + setIsBack(newStep < step); + setStep(newStep); + } + }, [currentIndex, steps, step]); return (
= ({ {...getPrefixedDataAttributes(dataAttributes, 'Stepper')} > {steps.map((text, index) => { - const isCurrent = index === currentIndex; + const isCurrent = index === step; const isLastStep = index === steps.length - 1; - const isCompleted = index < currentIndex; - const hasAnimation = index === currentIndex - 1; + const isCompleted = index < step; + const hasAnimation = index === step - 1; return ( diff --git a/src/theme.tsx b/src/theme.tsx index 134dd37a78..cbf307d38c 100644 --- a/src/theme.tsx +++ b/src/theme.tsx @@ -41,6 +41,8 @@ const TEXTS_ES = { playIconButtonLabel: 'Reproducir', pauseIconButtonLabel: 'Pausar', sheetConfirmButton: 'Continuar', + progressBarCompletedLabel: 'completo', + progressBarStepLabel: 'Paso 1$s de 2$s', pinFieldInputLabel: 'Dígito 1$s de 2$s', }; @@ -78,6 +80,8 @@ const TEXTS_EN: ThemeTexts = { playIconButtonLabel: 'Play', pauseIconButtonLabel: 'Pause', sheetConfirmButton: 'Continue', + progressBarCompletedLabel: 'completed', + progressBarStepLabel: 'Step 1$s of 2$s', pinFieldInputLabel: 'Digit 1$s of 2$s', }; @@ -115,6 +119,8 @@ const TEXTS_DE: ThemeTexts = { playIconButtonLabel: 'Abspielen', pauseIconButtonLabel: 'Pausieren', sheetConfirmButton: 'Fortfahren', + progressBarCompletedLabel: 'vollendet', + progressBarStepLabel: 'Schritt 1$s von 2$s', pinFieldInputLabel: 'Ziffer 1$s von 2$s', }; @@ -152,6 +158,8 @@ const TEXTS_PT: ThemeTexts = { playIconButtonLabel: 'Reproduzir', pauseIconButtonLabel: 'Pausar', sheetConfirmButton: 'Continuar', + progressBarCompletedLabel: 'concluído', + progressBarStepLabel: 'Etapa 1$s de 2$s', pinFieldInputLabel: 'Dígito 1$s de 2$s', };