Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ProgressBarStepped): create component #905

Merged
merged 8 commits into from
Oct 16, 2023
Merged
Show file tree
Hide file tree
Changes from 7 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
5 changes: 5 additions & 0 deletions playroom/snippets.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2484,6 +2484,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.
marcoskolodny marked this conversation as resolved.
Show resolved Hide resolved
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();
});
63 changes: 48 additions & 15 deletions src/__stories__/progress-bar-story.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,68 @@
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 = {
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}
/>
);

ProgressBarStory.storyName = 'ProgressBar';
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd move this up, near ProgressBarStory definition

ProgressBarSteppedStory.storyName = 'ProgressBarStepped';

ProgressBarStory.args = {
Copy link
Contributor

Choose a reason for hiding this comment

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

same for this

reverse: false,
progressPercent: 30,
color: 'default',
};

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 @@ -60,7 +60,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',
};

const TEXTS_EN: ThemeTexts = {
Expand Down Expand Up @@ -77,6 +79,8 @@ const TEXTS_EN: ThemeTexts = {
playIconButtonLabel: 'Play',
pauseIconButtonLabel: 'Pause',
sheetConfirmButton: 'Continue',
progressBarCompletedLabel: 'completed',
progressBarStepLabel: 'Step 1$s of 2$s',
};

const TEXTS_DE: ThemeTexts = {
Expand Down Expand Up @@ -113,6 +117,8 @@ const TEXTS_DE: ThemeTexts = {
playIconButtonLabel: 'Abspielen',
pauseIconButtonLabel: 'Pausieren',
sheetConfirmButton: 'Fortfahren',
progressBarCompletedLabel: 'vollendet',
progressBarStepLabel: 'Schritt 1$s von 2$s',
};

const TEXTS_PT: ThemeTexts = {
Expand Down Expand Up @@ -149,6 +155,8 @@ const TEXTS_PT: ThemeTexts = {
playIconButtonLabel: 'Reproduzir',
pauseIconButtonLabel: 'Pausar',
sheetConfirmButton: 'Continuar',
progressBarCompletedLabel: 'concluído',
progressBarStepLabel: 'Etapa 1$s de 2$s',
};

export const getTexts = (locale: Locale): typeof TEXTS_ES => {
Expand Down