Skip to content

Commit

Permalink
Merge pull request #986 from glints-dev/feature/steps-component
Browse files Browse the repository at this point in the history
Steps: add new component
  • Loading branch information
michac789 authored Nov 6, 2023
2 parents 15c6d2f + b531917 commit d4aea05
Show file tree
Hide file tree
Showing 12 changed files with 408 additions and 0 deletions.
81 changes: 81 additions & 0 deletions src/@next/Steps/Step.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import React from 'react';
import { Typography } from '../Typography';
import {
CircleDiv,
StepItemContainer,
StepItemWrapper,
VerticalLine,
VerticalLineWrapper,
} from './StepStyle';
import { Icon } from '../Icon';
import { Neutral } from '../utilities/colors';

export interface StepProps {
/** Styles of step, predetermined from parent component, or you can overwrite this */
variant?: 'pending' | 'completed' | 'processing' | 'error';
/** Label given to the step component */
label?: string;
/** Step number to be shown, by default it's 1,2,3,... from the parent component, or you can overwrite this */
index?: number;
type?: 'normal' | 'dot';
}

export const Step = React.forwardRef<HTMLDivElement, StepProps>(function Step(
{ variant = 'pending', label = '', index = 0, type = 'normal' }: StepProps,
ref
) {
return (
<StepItemContainer className="step-item-container">
<StepItemWrapper
ref={ref}
data-dot={type === 'dot'}
className="step-item-wrapper"
>
<CircleDiv data-variant={variant} data-dot={type === 'dot'}>
{variant === 'completed' && (
<Icon name="ri-check" className="circle-icon" />
)}
{variant === 'error' && (
<Icon name="ri-close" className="circle-icon" />
)}
{variant === 'processing' && (
<Typography
as="span"
variant="caption"
color={Neutral.B100}
className="circle-icon"
>
{index}
</Typography>
)}
{variant === 'pending' && (
<Typography
as="span"
variant="caption"
color={Neutral.B40}
className="circle-icon"
>
{index}
</Typography>
)}
</CircleDiv>
<Typography
as="div"
variant={
variant === 'processing' || variant === 'error' ? 'body2' : 'body1'
}
color={
variant === 'pending' || variant === 'completed'
? Neutral.B40
: Neutral.B18
}
>
{label}
</Typography>
</StepItemWrapper>
<VerticalLineWrapper data-dot={type === 'dot'}>
<VerticalLine data-variant={variant} data-dot={type === 'dot'} />
</VerticalLineWrapper>
</StepItemContainer>
);
});
108 changes: 108 additions & 0 deletions src/@next/Steps/StepStyle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import styled from 'styled-components';
import { Breakpoints } from '..';
import { space4, space12, space16 } from '../utilities/spacing';
import { Blue, Neutral, Red } from '../utilities/colors';
import { borderRadiusHalf } from '../utilities/borderRadius';

export const StepItemContainer = styled.div`
&:last-child > div:last-child {
display: none;
}
`;

export const StepItemWrapper = styled.div`
display: flex;
align-items: center;
gap: ${space12};
cursor: default;
&[data-dot='true'] {
gap: ${space16};
}
`;

export const CircleDiv = styled.div`
display: flex;
align-items: center;
justify-content: center;
height: 28px;
width: 28px;
border-radius: ${borderRadiusHalf};
svg {
height: 16px;
width: 16px;
}
&[data-variant='pending'] {
background-color: ${Neutral.B95};
}
&[data-variant='completed'] {
background-color: ${Blue.S08};
svg {
fill: ${Blue.S99};
}
}
&[data-variant='processing'] {
background-color: ${Blue.S99};
}
&[data-variant='error'] {
background-color: ${Red.B93};
svg {
fill: ${Neutral.B100};
}
}
&[data-dot='true'] {
height: 10px;
width: 10px;
> .circle-icon {
display: none;
}
&[data-variant='completed'] {
background-color: ${Blue.S99};
}
}
@media (max-width: ${Breakpoints.large}) {
height: 24px;
width: 24px;
svg {
height: 14px;
width: 14px;
}
}
`;

export const VerticalLineWrapper = styled.div`
display: flex;
align-items: center;
justify-content: center;
padding: ${space4} 0;
height: 64px;
width: 28px;
&[data-dot='true'] {
width: 10px;
}
@media (max-width: ${Breakpoints.large}) {
width: 24px;
}
`;

export const VerticalLine = styled.div`
width: 2px;
height: 100%;
background-color: ${Neutral.B85};
&[data-variant='completed'] {
background-color: ${Blue.S99};
}
&[data-dot='true'] {
width: 1.5px;
}
`;
77 changes: 77 additions & 0 deletions src/@next/Steps/Steps.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import React from 'react';
import { Meta, Story } from '@storybook/react';
import { ButtonGroup } from '../ButtonGroup';
import { Button } from '../Button';

import { BaseContainer } from '../../Layout/GlintsContainer/GlintsContainer';
import { withGlintsPortalContainer } from '../../helpers/storybook/Decorators';
import { Steps, StepsProps } from './Steps';

export default {
title: '@next/Steps',
component: Steps,
decorators: [
Story => <BaseContainer>{Story()}</BaseContainer>,
withGlintsPortalContainer,
],
} as Meta;

const Template: Story<StepsProps> = args => {
const [currentStep, setCurrentStep] = React.useState<number>(1);
const [errorSteps, setErrorSteps] = React.useState<number[]>([]);

const handlePrevClick = () => {
if (currentStep > 1) setCurrentStep(currentStep - 1);
};

const handleNextClick = () => {
if (currentStep < 6) setCurrentStep(currentStep + 1);
};

const handleSetError = (index: number) => {
setErrorSteps(prevErrorSteps => {
if (prevErrorSteps.includes(index)) {
return prevErrorSteps.filter(item => item !== index);
} else {
return [...prevErrorSteps, index];
}
});
};

return (
<>
<Steps {...args} currentStep={currentStep} errorSteps={errorSteps}>
<Steps.Step label="Label 1" />
<Steps.Step label="Label 2" />
<Steps.Step label="Label 3" />
<Steps.Step label="Label 4" />
<Steps.Step label="Label 5" />
</Steps>
<div style={{ margin: '16px 0 8px' }}>
Current Step: <b>{currentStep}</b>
</div>
<div style={{ margin: '8px 0' }}>
Error Steps: <b>[{errorSteps.join(', ')}]</b>
</div>
<ButtonGroup>
<Button onClick={handlePrevClick} data-testid="prev-button">
Prev
</Button>
<Button onClick={handleNextClick} data-testid="next-button">
Next
</Button>
<Button
onClick={() => handleSetError(currentStep)}
data-testid="error-button"
>
Toggle Error
</Button>
</ButtonGroup>
</>
);
};

export const Interactive = Template.bind({});
Interactive.args = {
type: 'normal',
};
57 changes: 57 additions & 0 deletions src/@next/Steps/Steps.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import React from 'react';
import { Step, StepProps } from './Step';

export interface StepsProps extends React.HTMLAttributes<HTMLDivElement> {
/** Step index (1-indexed) marked as processing */
currentStep: number;
/** List of indexes (1-indexed) to be marked as error */
errorSteps?: number[];
/** Step components as child are required (1-indexed) */
children?: React.ReactElement<StepProps>[];
/** If dot type, display dot only; default is normal; automatically passed to all children */
type?: 'normal' | 'dot';
}

export const StepsComponent = React.forwardRef<HTMLDivElement, StepsProps>(
function Collapse(
{
currentStep = 0,
errorSteps = [],
children,
type = 'normal',
...props
}: StepsProps,
ref
) {
return (
<div ref={ref} {...props}>
{React.Children.map(children, (child, index) => {
const variant: StepProps['variant'] = errorSteps.includes(index + 1)
? 'error'
: index + 1 === currentStep
? 'processing'
: index + 1 < currentStep
? 'completed'
: 'pending';
const childVariant =
(child.props as Pick<StepProps, 'variant'>)?.variant || variant;
const childIndex =
(child.props as Pick<StepProps, 'index'>)?.index || index + 1;

return (
<child.type
{...child.props}
variant={childVariant}
index={childIndex}
type={type}
/>
);
})}
</div>
);
}
);

export const Steps = Object.assign(StepsComponent, {
Step: Step,
});
68 changes: 68 additions & 0 deletions test/e2e/steps/steps.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { test, expect } from '@playwright/test';
import { StepsPage } from './stepsPage';

test('Steps - default', async ({ page }) => {
const stepsPage = new StepsPage(page);
await stepsPage.goto();

await expect(stepsPage.container).toHaveScreenshot('steps-default.png');
});

test('Steps - progress', async ({ page }) => {
const stepsPage = new StepsPage(page);
await stepsPage.goto();

await stepsPage.nextButton.click();
await stepsPage.nextButton.click();

await expect(stepsPage.container).toHaveScreenshot('steps-progress.png');
});

test('Steps - error', async ({ page }) => {
const stepsPage = new StepsPage(page);
await stepsPage.goto();

await stepsPage.nextButton.click();
await stepsPage.nextButton.click();
await stepsPage.errorButton.click();

await expect(stepsPage.container).toHaveScreenshot('steps-error.png');
});

test('Steps - completed', async ({ page }) => {
const stepsPage = new StepsPage(page);
await stepsPage.goto();

await stepsPage.nextButton.click();
await stepsPage.nextButton.click();
await stepsPage.nextButton.click();
await stepsPage.nextButton.click();
await stepsPage.nextButton.click();

await expect(stepsPage.container).toHaveScreenshot('steps-completed.png');
});

test('Steps - small screen', async ({ page }) => {
page.setViewportSize({ width: 768, height: 600 });
const stepsPage = new StepsPage(page);
await stepsPage.goto();

await stepsPage.nextButton.click();
await stepsPage.nextButton.click();
await stepsPage.errorButton.click();
await stepsPage.nextButton.click();

await expect(stepsPage.container).toHaveScreenshot('steps-small-screen.png');
});

test('Steps - dot style', async ({ page }) => {
const stepsPage = new StepsPage(page);
await stepsPage.goto('args=type:dot');

await stepsPage.nextButton.click();
await stepsPage.nextButton.click();
await stepsPage.errorButton.click();
await stepsPage.nextButton.click();

await expect(stepsPage.container).toHaveScreenshot('steps-dot.png');
});
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.
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.
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.
Loading

0 comments on commit d4aea05

Please sign in to comment.