diff --git a/src/@next/Steps/Step.tsx b/src/@next/Steps/Step.tsx new file mode 100644 index 000000000..542b7e186 --- /dev/null +++ b/src/@next/Steps/Step.tsx @@ -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(function Step( + { variant = 'pending', label = '', index = 0, type = 'normal' }: StepProps, + ref +) { + return ( + + + + {variant === 'completed' && ( + + )} + {variant === 'error' && ( + + )} + {variant === 'processing' && ( + + {index} + + )} + {variant === 'pending' && ( + + {index} + + )} + + + {label} + + + + + + + ); +}); diff --git a/src/@next/Steps/StepStyle.ts b/src/@next/Steps/StepStyle.ts new file mode 100644 index 000000000..1ed269e43 --- /dev/null +++ b/src/@next/Steps/StepStyle.ts @@ -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; + } +`; diff --git a/src/@next/Steps/Steps.stories.tsx b/src/@next/Steps/Steps.stories.tsx new file mode 100644 index 000000000..d9df1be5e --- /dev/null +++ b/src/@next/Steps/Steps.stories.tsx @@ -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 => {Story()}, + withGlintsPortalContainer, + ], +} as Meta; + +const Template: Story = args => { + const [currentStep, setCurrentStep] = React.useState(1); + const [errorSteps, setErrorSteps] = React.useState([]); + + 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 ( + <> + + + + + + + +
+ Current Step: {currentStep} +
+
+ Error Steps: [{errorSteps.join(', ')}] +
+ + + + + + + ); +}; + +export const Interactive = Template.bind({}); +Interactive.args = { + type: 'normal', +}; diff --git a/src/@next/Steps/Steps.tsx b/src/@next/Steps/Steps.tsx new file mode 100644 index 000000000..8bb091c7a --- /dev/null +++ b/src/@next/Steps/Steps.tsx @@ -0,0 +1,57 @@ +import React from 'react'; +import { Step, StepProps } from './Step'; + +export interface StepsProps extends React.HTMLAttributes { + /** 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[]; + /** If dot type, display dot only; default is normal; automatically passed to all children */ + type?: 'normal' | 'dot'; +} + +export const StepsComponent = React.forwardRef( + function Collapse( + { + currentStep = 0, + errorSteps = [], + children, + type = 'normal', + ...props + }: StepsProps, + ref + ) { + return ( +
+ {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)?.variant || variant; + const childIndex = + (child.props as Pick)?.index || index + 1; + + return ( + + ); + })} +
+ ); + } +); + +export const Steps = Object.assign(StepsComponent, { + Step: Step, +}); diff --git a/test/e2e/steps/steps.spec.ts b/test/e2e/steps/steps.spec.ts new file mode 100644 index 000000000..bb98e18c8 --- /dev/null +++ b/test/e2e/steps/steps.spec.ts @@ -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'); +}); diff --git a/test/e2e/steps/steps.spec.ts-snapshots/steps-completed-chromium-linux.png b/test/e2e/steps/steps.spec.ts-snapshots/steps-completed-chromium-linux.png new file mode 100644 index 000000000..317407208 Binary files /dev/null and b/test/e2e/steps/steps.spec.ts-snapshots/steps-completed-chromium-linux.png differ diff --git a/test/e2e/steps/steps.spec.ts-snapshots/steps-default-chromium-linux.png b/test/e2e/steps/steps.spec.ts-snapshots/steps-default-chromium-linux.png new file mode 100644 index 000000000..3b3cb965f Binary files /dev/null and b/test/e2e/steps/steps.spec.ts-snapshots/steps-default-chromium-linux.png differ diff --git a/test/e2e/steps/steps.spec.ts-snapshots/steps-dot-chromium-linux.png b/test/e2e/steps/steps.spec.ts-snapshots/steps-dot-chromium-linux.png new file mode 100644 index 000000000..5bc6af47d Binary files /dev/null and b/test/e2e/steps/steps.spec.ts-snapshots/steps-dot-chromium-linux.png differ diff --git a/test/e2e/steps/steps.spec.ts-snapshots/steps-error-chromium-linux.png b/test/e2e/steps/steps.spec.ts-snapshots/steps-error-chromium-linux.png new file mode 100644 index 000000000..546ec892a Binary files /dev/null and b/test/e2e/steps/steps.spec.ts-snapshots/steps-error-chromium-linux.png differ diff --git a/test/e2e/steps/steps.spec.ts-snapshots/steps-progress-chromium-linux.png b/test/e2e/steps/steps.spec.ts-snapshots/steps-progress-chromium-linux.png new file mode 100644 index 000000000..b6d7ff205 Binary files /dev/null and b/test/e2e/steps/steps.spec.ts-snapshots/steps-progress-chromium-linux.png differ diff --git a/test/e2e/steps/steps.spec.ts-snapshots/steps-small-screen-chromium-linux.png b/test/e2e/steps/steps.spec.ts-snapshots/steps-small-screen-chromium-linux.png new file mode 100644 index 000000000..f565f6802 Binary files /dev/null and b/test/e2e/steps/steps.spec.ts-snapshots/steps-small-screen-chromium-linux.png differ diff --git a/test/e2e/steps/stepsPage.ts b/test/e2e/steps/stepsPage.ts new file mode 100644 index 000000000..f3c999209 --- /dev/null +++ b/test/e2e/steps/stepsPage.ts @@ -0,0 +1,17 @@ +import { Locator, Page } from '@playwright/test'; +import { StoryBookPage } from '../storybookPage'; + +export class StepsPage extends StoryBookPage { + readonly nextButton: Locator; + readonly errorButton: Locator; + + constructor(page: Page) { + super(page, '?path=/story/next-steps--interactive'); + this.nextButton = page + .frameLocator('internal:attr=[title="storybook-preview-iframe"i]') + .locator('[data-testid="next-button"]'); + this.errorButton = page + .frameLocator('internal:attr=[title="storybook-preview-iframe"i]') + .locator('[data-testid="error-button"]'); + } +}