Skip to content

Commit

Permalink
feat: [M3-7058] - Add AGLB Create Page - Stepper component (#9520)
Browse files Browse the repository at this point in the history
* feat: stepper component POC

* VerticalLinearStepper unit tests

* Stepper component story

* Code cleanup

* Added changeset: AGLB - Stepper component

* Update packages/manager/src/components/VerticalLinearStepper/VerticalLinearStepper.styles.ts

Co-authored-by: Connie Liu <139280159+coliu-akamai@users.noreply.github.com>

* Update packages/manager/src/components/VerticalLinearStepper/VerticalLinearStepper.styles.ts

Co-authored-by: Connie Liu <139280159+coliu-akamai@users.noreply.github.com>

* Update packages/manager/src/components/VerticalLinearStepper/VerticalLinearStepper.styles.ts

Co-authored-by: Connie Liu <139280159+coliu-akamai@users.noreply.github.com>

* Fix console warnings in storybook

* Update VerticalLinearStepper.test.tsx

* Update unit tests

* Update VerticalLinearStepper.test.tsx

* Update pr-9520-upcoming-features-1692378386106.md

---------

Co-authored-by: Connie Liu <139280159+coliu-akamai@users.noreply.github.com>
  • Loading branch information
cpathipa and coliu-akamai authored Aug 28, 2023
1 parent 8e07891 commit ddb5d4b
Show file tree
Hide file tree
Showing 5 changed files with 329 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Upcoming Features
---

Add AGLB Create Page - Stepper component ([#9520](https://github.com/linode/manager/pull/9520))
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { Meta, StoryObj } from '@storybook/react';
import React from 'react';

import { Typography } from '../Typography';
import { VerticalLinearStepper } from './VerticalLinearStepper';

const meta: Meta<typeof VerticalLinearStepper> = {
component: VerticalLinearStepper,
title: 'Components/VerticalLinearStepper',
};

type Story = StoryObj<typeof VerticalLinearStepper>;

export const Default: Story = {
args: {
steps: [
{
content: (
<Typography>
This is some test copy which acts as content for this Stepper
component step 1.{' '}
</Typography>
),
label: 'Step1',
},
{
content: (
<Typography>
This is some test copy which acts as content for this Stepper
component step 2.{' '}
</Typography>
),
label: 'Step2',
},
{
content: (
<Typography>
This is some test copy which acts as content for this Stepper
component step 3.{' '}
</Typography>
),
label: 'Step3',
},
],
},
render: (args) => {
const VerticalLinearStepperExample = () => {
return <VerticalLinearStepper {...args} />;
};
return <VerticalLinearStepperExample />;
},
};

export default meta;
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { StepConnector, StepIcon } from '@mui/material';
import { styled } from '@mui/material/styles';

import { isPropValid } from 'src/utilities/isPropValid';

type StyledCircleIconProps = {
activeStep: number;
index: number;
};

export const StyledCircleIcon = styled('div', {
label: 'StyledCircleIcon',
shouldForwardProp: (prop) => isPropValid(['activeStep', 'index'], prop),
})<StyledCircleIconProps>(({ theme, ...props }) => ({
alignItems: 'center',
backgroundColor:
props.index === props.activeStep
? theme.palette.primary.main
: props.index < props.activeStep
? '#ADD8E6' // TODO: need UX confirmation on color code
: theme.bg.bgPaper, // Adjust colors as needed
border:
props.index < props.activeStep || props.index === props.activeStep
? `2px solid ${theme.palette.primary.main}`
: `2px solid ${theme.borderColors.borderTable}`, // Adjust border styles as needed
borderRadius: '50%',
display: 'flex',
height: theme.spacing(3),
justifyContent: 'center',
width: theme.spacing(3),
}));

export const CustomStepIcon = styled(StepIcon, { label: 'StyledCircleIcon' })(
() => ({
active: {},
completed: {},
root: {
'&$completed': {
display: 'none', // Hide the checkmark icon on completed steps
},
},
})
);

export const StyledColorlibConnector = styled(StepConnector, {
label: 'StyledColorlibConnector',
})(() => ({
'& .MuiStepConnector-line': {
borderColor: '#eaeaf0',
borderLeftWidth: '3px',
minHeight: '28px',
},
}));
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import '@testing-library/jest-dom/extend-expect';
import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';

import { renderWithTheme } from 'src/utilities/testHelpers';

import { VerticalLinearStepper } from './VerticalLinearStepper';

const steps = [
{
content: <div>Step 1 Content</div>,
handler: jest.fn(), // Mock function for testing
label: 'Details',
},
{
content: <div>Step 2 Content</div>,
handler: jest.fn(),
label: 'Summary',
},
{
content: <div>Step 3 Content</div>,
handler: jest.fn(),
label: 'Next',
},
];

describe('VerticalLinearStepper', () => {
test('Should render initial step content', () => {
renderWithTheme(<VerticalLinearStepper steps={steps} />);
expect(screen.getByText('Step 1 Content')).toBeInTheDocument();
expect(screen.queryByText('Step 2 Content')).toBeNull();
expect(screen.queryByText('Step 3 Content')).toBeNull();
expect(screen.getByText('Next: Summary')).toBeInTheDocument();
expect(screen.queryByText('Previous: Details')).toBeNull();
});
test('Should navigate to second step conent', () => {
renderWithTheme(<VerticalLinearStepper steps={steps} />);
userEvent.click(screen.getByTestId('summary'));
expect(steps[0].handler).toHaveBeenCalledTimes(1);
expect(screen.getByText('Step 2 Content')).toBeInTheDocument();
expect(screen.queryByText('Step 1 Content')).toBeNull();
expect(screen.queryByText('Step 3 Content')).toBeNull();
expect(screen.getByText('Next: Next')).toBeInTheDocument();
expect(screen.queryByText('Previous: Summary')).toBeNull();
});
test('Should navigate to final step conent', () => {
renderWithTheme(<VerticalLinearStepper steps={steps} />);
userEvent.click(screen.getByTestId('summary'));
userEvent.click(screen.getByTestId('next'));
expect(steps[1].handler).toHaveBeenCalledTimes(1);
expect(screen.queryByText('Step 1 Content')).toBeNull();
expect(screen.queryByText('Step 2 Content')).toBeNull();
expect(screen.getByText('Step 3 Content')).toBeInTheDocument();
expect(screen.queryByText('Next: Summary')).toBeNull();
expect(screen.getByText('Previous: Summary')).toBeInTheDocument();
});
test('Should be able to go previous step', () => {
renderWithTheme(<VerticalLinearStepper steps={steps} />);
userEvent.click(screen.getByTestId('summary'));
userEvent.click(screen.getByText('Previous: Details'));
expect(steps[1].handler).toHaveBeenCalledTimes(1);
expect(screen.getByText('Step 1 Content')).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import {
Step,
StepConnector,
StepContent,
StepLabel,
Stepper,
} from '@mui/material';
import Box from '@mui/material/Box';
import { Theme } from '@mui/material/styles';
import React, { useState } from 'react';

import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel';

import {
CustomStepIcon,
StyledCircleIcon,
StyledColorlibConnector,
} from './VerticalLinearStepper.styles';

type VerticalLinearStep = {
content: JSX.Element;
handler?: () => void;
label: string;
};

interface VerticalLinearStepperProps {
steps: VerticalLinearStep[];
}

export const VerticalLinearStepper = ({
steps,
}: VerticalLinearStepperProps) => {
const [activeStep, setActiveStep] = useState(0);

const handleNext = () => {
setActiveStep((prevActiveStep) => prevActiveStep + 1);
};

const handleBack = () => {
setActiveStep((prevActiveStep) => prevActiveStep - 1);
};

return (
<Box
sx={(theme: Theme) => ({
backgroundColor: theme.bg.bgPaper,
display: 'flex',
margin: 'auto',
maxWidth: 800,
p: `${theme.spacing(2)}`,
})}
>
{/* Left Column - Vertical Steps */}
<Box>
<Stepper
activeStep={activeStep}
connector={<StyledColorlibConnector />}
orientation="vertical"
>
{steps.map((step: VerticalLinearStep, index: number) => (
<Step key={step.label}>
<StepLabel
icon={
<CustomStepIcon
icon={
<StyledCircleIcon activeStep={activeStep} index={index} />
}
/>
}
sx={{
'& .MuiStepIcon-text': {
display: 'none',
},
p: 0,
}}
>
{step.label}
</StepLabel>
</Step>
))}
</Stepper>
</Box>
{/* Right Column - Stepper Content */}
<Box sx={{ flex: 2 }}>
<Stepper
connector={
<StepConnector
sx={{
'& .MuiStepConnector-line': {
display: 'none',
},
'& .MuiStepConnector-vertical': {
display: 'none',
},
}}
/>
}
activeStep={activeStep}
orientation="vertical"
>
{steps.map(({ content, handler, label }, index) => (
<Step key={label}>
{index === activeStep ? (
<StepContent sx={{ border: 'none' }}>
<Box
sx={(theme) => ({
bgcolor: theme.bg.app,
p: theme.spacing(2),
})}
>
{content}
</Box>

<Box sx={{ mb: 2 }}>
<ActionsPanel
primaryButtonProps={
index !== 2
? {
'data-testid': steps[
index + 1
]?.label.toLocaleLowerCase(),
label: `Next: ${steps[index + 1]?.label}`,
onClick: () => {
handleNext();
handler?.();
},
sx: { mr: 1, mt: 1 },
}
: undefined
}
secondaryButtonProps={
index !== 0
? {
buttonType: 'outlined',
label: `Previous: ${steps[index - 1]?.label}`,
onClick: handleBack,
sx: { mr: 1, mt: 1 },
}
: undefined
}
style={{ justifyContent: 'flex-start' }}
/>
</Box>
</StepContent>
) : null}
</Step>
))}
</Stepper>
</Box>
</Box>
);
};

0 comments on commit ddb5d4b

Please sign in to comment.