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

Steps: add new component #986

Merged
merged 4 commits into from
Nov 6, 2023
Merged
Show file tree
Hide file tree
Changes from 3 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
91 changes: 91 additions & 0 deletions src/@next/Steps/Step.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import React from 'react';
import { Typography } from '../Typography';
import {
CircleDiv,
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';
clickable?: boolean;
handleClick?: (index: number) => void;
}

export const Step = React.forwardRef<HTMLDivElement, StepProps>(function Step(
{
variant = 'pending',
label = '',
index = 0,
type = 'normal',
clickable = false,
handleClick,
}: StepProps,
ref
) {
return (
<>
ninariccimarie marked this conversation as resolved.
Show resolved Hide resolved
<StepItemWrapper
ref={ref}
data-dot={type === 'dot'}
data-clickable={clickable}
onClick={() => handleClick(index)}
className="step-item-wrapper"
>
<CircleDiv data-variant={variant} data-dot={type === 'dot'}>
{variant === 'completed' && (
<Icon name="ri-check" className="circle-content" />
)}
{variant === 'error' && (
<Icon name="ri-close" className="circle-content" />
)}
{variant === 'processing' && (
<Typography
as="span"
variant="caption"
color={Neutral.B100}
className="circle-content"
>
{index}
</Typography>
)}
{variant === 'pending' && (
<Typography
as="span"
variant="caption"
color={Neutral.B40}
className="circle-content"
>
{index}
</Typography>
)}
</CircleDiv>
<Typography
as="div"
chiahou marked this conversation as resolved.
Show resolved Hide resolved
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>
</>
);
});
109 changes: 109 additions & 0 deletions src/@next/Steps/StepStyle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import styled from 'styled-components';
import { Breakpoints } from '..';
import { space4, space12, space16 } from '../utilities/spacing';
import { Blue, Neutral, Red } from '../utilities/colors';

export const StepItemWrapper = styled.div`
chiahou marked this conversation as resolved.
Show resolved Hide resolved
display: flex;
align-items: center;
gap: ${space12};
cursor: default;

&[data-dot='true'] {
gap: ${space16};
}

&[data-clickable='true'] {
cursor: pointer;
}
`;

export const CircleDiv = styled.div`
display: flex;
align-items: center;
justify-content: center;
height: 28px;
width: 28px;
border-radius: 50%;
ninariccimarie marked this conversation as resolved.
Show resolved Hide resolved

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-content {
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;
chiahou marked this conversation as resolved.
Show resolved Hide resolved

&[data-dot='true'] {
width: 10px;
}

&:last-child {
display: none;
}

@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;
}
`;
89 changes: 89 additions & 0 deletions src/@next/Steps/Steps.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
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];
}
});
};

const handleStepClick = (index: number) => {
if (args.clickable) {
setCurrentStep(index);
}
};

return (
<>
<Steps
{...args}
currentStep={currentStep}
errorSteps={errorSteps}
handleClick={handleStepClick}
>
<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',
clickable: false,
};
65 changes: 65 additions & 0 deletions src/@next/Steps/Steps.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
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';
/** If true, steps are clickable; default is false; automatically passed to all children */
clickable?: boolean;
/** Callback function when step is clicked, index of the step is passed as an argument */
handleClick?: (index: number) => void;
ninariccimarie marked this conversation as resolved.
Show resolved Hide resolved
}

export const StepsComponent = React.forwardRef<HTMLDivElement, StepsProps>(
function Collapse(
{
currentStep = 0,
errorSteps = [],
children,
type = 'normal',
clickable = false,
handleClick,
...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}
clickable={clickable}
handleClick={handleClick}
/>
);
})}
</div>
);
}
);

export const Steps = Object.assign(StepsComponent, {
Step: Step,
});
Loading