Skip to content

Commit

Permalink
feat(ProgressBarStepped): create component (#905)
Browse files Browse the repository at this point in the history
* first POC

* update hack

* update component logic, story, accesibility and tests

* use skin color for progress bar stories

* fix margin in container

* update logic for ProgressBarStepped and Stepper
  • Loading branch information
marcoskolodny authored Oct 16, 2023
1 parent 484dbc7 commit 25ed4eb
Show file tree
Hide file tree
Showing 13 changed files with 197 additions and 44 deletions.
5 changes: 5 additions & 0 deletions playroom/snippets.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2634,6 +2634,11 @@ export default [
name: 'ProgressBar',
code: '<ProgressBar progressPercent={35} />',
},
{
group: 'Progress',
name: 'ProgressBarStepped',
code: '<ProgressBarStepped steps={6} currentStep={3} />',
},
{
group: 'NavigationBreadcrumbs',
name: 'NavigationBreadcrumbs',
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 12 additions & 2 deletions src/__screenshot_tests__/progress-bar-screenshot-test.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,23 @@
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');

const image = await stepper.screenshot();

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();
});
61 changes: 46 additions & 15 deletions src/__stories__/progress-bar-story.tsx
Original file line number Diff line number Diff line change
@@ -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<Args> = ({reverse, progressPercent, color}) => (
<div data-testid="progress-bar">
<ProgressBar
progressPercent={progressPercent}
reverse={reverse}
color={color === 'default' ? undefined : color}
/>
</div>
export const ProgressBarStory: StoryComponent<ProgressBarStoryArgs> = ({reverse, progressPercent, color}) => (
<ProgressBar
dataAttributes={{testid: 'progress-bar'}}
progressPercent={progressPercent}
reverse={reverse}
color={color === 'error' ? vars.colors.error : undefined}
/>
);

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<ProgressBarSteppedStoryArgs> = ({
steps,
currentStep,
color,
}) => (
<ProgressBarStepped
steps={steps}
currentStep={currentStep}
dataAttributes={{testid: 'progress-bar-stepped'}}
color={color === 'error' ? vars.colors.error : undefined}
/>
);

ProgressBarSteppedStory.storyName = 'ProgressBarStepped';
ProgressBarSteppedStory.args = {
steps: 4,
currentStep: 3,
color: 'default',
};
ProgressBarSteppedStory.argTypes = {
steps: {
control: {type: 'range', min: 1, max: 6, step: 1},
},
};
2 changes: 1 addition & 1 deletion src/community/blocks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
2 changes: 1 addition & 1 deletion src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
33 changes: 23 additions & 10 deletions src/progress-bar.css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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%',
});
95 changes: 88 additions & 7 deletions src/progress-bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -17,7 +18,7 @@ type Props = {
reverse?: boolean;
};

const ProgressBar: React.FC<Props> = ({
export const ProgressBar: React.FC<ProgressBarProps> = ({
progressPercent,
color,
'aria-label': ariaLabel,
Expand All @@ -26,14 +27,20 @@ const ProgressBar: React.FC<Props> = ({
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 (
<div
{...getPrefixedDataAttributes(dataAttributes, 'ProgressBar')}
className={styles.barBackground}
role="progressbar"
aria-valuenow={progressPercent}
aria-valuenow={progressValue}
aria-valuemin={0}
aria-valuemax={100}
aria-label={label}
Expand All @@ -42,12 +49,86 @@ const ProgressBar: React.FC<Props> = ({
<div
className={classNames(styles.bar, reverse ? styles.inverse : styles.normal)}
style={{
maxWidth: `${progressPercent}%`,
maxWidth: `${progressValue}%`,
backgroundColor: color ?? vars.colors.controlActivated,
}}
/>
</div>
);
};

export default ProgressBar;
type ProgressBarSteppedProps = {
steps: number;
currentStep?: number;
color?: string;
dataAttributes?: DataAttributes;
'aria-label'?: string;
'aria-labelledby'?: string;
};

export const ProgressBarStepped: React.FC<ProgressBarSteppedProps> = ({
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 (
<div
{...getPrefixedDataAttributes(dataAttributes, 'ProgressBarStepped')}
role="progressbar"
aria-valuenow={step}
aria-valuemin={0}
aria-valuemax={steps}
aria-label={label}
aria-labelledby={ariaLabelledBy}
className={styles.progressBarSteppedContainer}
>
<Inline space={8} fullWidth>
{Array.from({length: steps}, (_, index) => {
const isCurrent = index === step;
const isCompleted = index < step;
const hasAnimation = index === step - 1;

return (
<div key={index} className={styles.barBackground} aria-hidden="true">
{(isCompleted || isCurrent) && (
<div
className={classNames(styles.bar, {
[styles.normal]: hasAnimation && !isBack,
[styles.inverse]: isCurrent && isBack,
})}
style={{
backgroundColor: color ?? vars.colors.controlActivated,
maxWidth: isCompleted || (hasAnimation && !isBack) ? '100%' : '0',
}}
/>
)}
</div>
);
})}
</Inline>
</div>
);
};
21 changes: 13 additions & 8 deletions src/stepper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,17 @@ const Stepper: React.FC<StepperProps> = ({
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 (
<div
Expand All @@ -45,10 +50,10 @@ const Stepper: React.FC<StepperProps> = ({
{...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 (
<React.Fragment key={index}>
Expand Down
8 changes: 8 additions & 0 deletions src/theme.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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',
};

Expand Down Expand Up @@ -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',
};

Expand Down Expand Up @@ -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',
};

Expand Down Expand Up @@ -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',
};

Expand Down

1 comment on commit 25ed4eb

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deploy preview for mistica-web ready!

✅ Preview
https://mistica-2zn0ek1q4-tuentisre.vercel.app

Built with commit 25ed4eb.
This pull request is being automatically deployed with vercel-action

Please sign in to comment.