Skip to content

Commit

Permalink
feat: add stepper and step components
Browse files Browse the repository at this point in the history
  • Loading branch information
anusha-c18 authored Oct 3, 2024
1 parent b03c168 commit 85c995d
Show file tree
Hide file tree
Showing 11 changed files with 868 additions and 0 deletions.
113 changes: 113 additions & 0 deletions src/components/Stepper/Step/Step.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
@import "vanilla-framework";

.step-number {
border: 0.08rem solid black;
border-radius: 1rem;
height: 1.4rem;
line-height: 1.3;
margin-left: $sph--small;
margin-right: 0.1rem;
margin-top: 0.1rem;
text-align: center;
width: 1.4rem;
}

.step-number-disabled {
border: 0.08rem solid #757575;
color: #757575;
}

.step-content {
display: flex;
flex: 1;
flex-direction: column;
margin-left: $sph--small;
}

.step-enabled:hover {
cursor: pointer;
text-decoration: underline;
}

.step-disabled {
color: #757575;
pointer-events: none;
}

.step-status-icon {
height: 1.6rem;
margin-left: 0.4rem;
width: 1.6rem;
}

.step-selected {
background-color: var(--vf-color-background-alt);
}

.step-optional-content {
font-size: 12px;
max-width: 10rem;
}

.stepper-horizontal {
display: flex;

.p-inline-list__item {
margin: 0;
}

.step {
border-top: 0.2rem solid var(--vf-color-border-default);
display: flex;
height: 100%;
padding: 0.4rem $spv--medium;
width: fit-content;
}

.step-status-icon {
margin-left: 0;
}

.step-number {
margin-left: 0;
}

.step-content {
max-width: 10rem;
}

.progress-line {
border-top: 0.2rem solid black;
}

:first-child .step {
padding-left: 0;
}
}

.stepper-vertical {
.p-list__item {
padding-bottom: 0;
padding-top: 0;
}

.step {
border-left: 0.2rem solid var(--vf-color-border-default);
display: flex;
padding: $spv--medium 0;
padding-right: 0.5rem;
width: fit-content;
}

.progress-line {
border-left: 0.2rem solid black;
}

:first-child .step {
padding-top: 0;
}

:last-child .step {
padding-bottom: 0;
}
}
29 changes: 29 additions & 0 deletions src/components/Stepper/Step/Step.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import React from "react";
import { Meta, StoryObj } from "@storybook/react";
import Step from "./Step";
import Stepper from "../Stepper";

const meta: Meta<typeof Step> = {
component: Step,
render: (args) => (
<Stepper variant="horizontal" steps={[<Step key="step" {...args} />]} />
),
tags: ["autodocs"],
};

export default meta;

type Story = StoryObj<typeof Step>;

export const Default: Story = {
name: "Default",

args: {
title: "Step 1",
index: 1,
enabled: false,
hasProgressLine: false,
iconName: "number",
handleClick: () => {},
},
};
66 changes: 66 additions & 0 deletions src/components/Stepper/Step/Step.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import Step from "./Step";
import type { Props } from "./Step";

describe("Step component", () => {
const props: Props = {
hasProgressLine: true,
index: 1,
title: "Title",
enabled: true,
iconName: "number",
handleClick: jest.fn(),
};

it("renders the step with the required props", () => {
render(<Step {...props} />);
expect(screen.getByText("Title")).toBeInTheDocument();
expect(screen.getByText("1")).toBeInTheDocument();
expect(document.querySelector(".progress-line")).toBeInTheDocument();
});

it("can display an icon", () => {
render(<Step {...props} iconName="success" />);
expect(document.querySelector(".p-icon--success")).toBeInTheDocument();
});

it("can remove the progress line", () => {
render(<Step {...props} hasProgressLine={false} />);
expect(document.querySelector(".progress-line")).toBeNull();
});

it("can disable the step", () => {
render(<Step {...props} enabled={false} />);
expect(screen.getByText("Title")).toHaveClass("step-disabled");
});

it("can select the step", () => {
render(<Step {...props} selected={true} />);
expect(document.querySelector(".step-selected")).toBeInTheDocument();
});

it("can call handleClick when clicked", async () => {
render(<Step {...props} />);
await userEvent.click(screen.getByText("Title"));
expect(props.handleClick).toHaveBeenCalled();
});

it("can display optional label", () => {
render(<Step {...props} label="Optional label" />);

expect(screen.getByText("Optional label")).toBeInTheDocument();
});

it("can display optional link", () => {
const linkProps = {
href: "/test-link",
children: "Link",
};
render(<Step {...props} linkProps={linkProps} />);
const linkElement = screen.getByRole("link", { name: "Link" });
expect(linkElement).toBeInTheDocument();
expect(linkElement).toHaveAttribute("href", "/test-link");
});
});
118 changes: 118 additions & 0 deletions src/components/Stepper/Step/Step.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import classNames from "classnames";
import React from "react";
import Icon from "components/Icon";
import Link, { LinkProps } from "components/Link";
import { ClassName } from "types";
import "./Step.scss";

export type Props = {
/**
* Whether the step has a darkened progress line.
*/
hasProgressLine: boolean;
/**
* Index of the step.
*/
index: number;
/**
* Title of the step.
*/
title: string;
/**
* Optional label for the step.
*/
label?: string;
/**
* Optional props to configure the `Link` component.
*/
linkProps?: LinkProps;
/**
* Whether the step is clickable. If set to false, the step is not clickable and the text is muted with a light-dark colour.
*/
enabled: boolean;
/**
* Optional value to highlight the selected step.
*/
selected?: boolean;
/**
* Icon to display in the step. Specify "number" if the index should be displayed.
*/
iconName: string;
/**
* Optional class(es) to pass to the Icon component.
*/
iconClassName?: ClassName;
/**
* Function that is called when the step is clicked.
*/
handleClick: () => void;
};

const Step = ({
hasProgressLine,
index,
title,
label,
linkProps,
enabled,
selected = false,
iconName,
iconClassName,
handleClick,
...props
}: Props): JSX.Element => {
const stepStatusClass = enabled ? "step-enabled" : "step-disabled";

return (
<div
className={classNames("step", {
"progress-line": hasProgressLine,
"step-selected": selected,
})}
{...props}
>
{iconName === "number" ? (
<span
className={classNames("step-number", {
"step-number-disabled": !enabled,
})}
>
{index}
</span>
) : (
<Icon
name={iconName}
className={classNames("step-status-icon", iconClassName)}
/>
)}
<div className="step-content">
<span className={classNames(stepStatusClass)} onClick={handleClick}>
{title}
</span>
{label && (
<span
className={classNames(
"step-optional-content",
"u-no-margin--bottom",
{
"step-disabled": !enabled,
},
)}
>
{label}
</span>
)}
{linkProps && (
<Link
className="p-text--small u-no-margin--bottom step-optional-content"
{...linkProps}
>
{linkProps.children}
</Link>
)}
</div>
</div>
);
};

export default Step;
2 changes: 2 additions & 0 deletions src/components/Stepper/Step/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default } from "./Step";
export type { Props as StepProps } from "./Step";
Loading

0 comments on commit 85c995d

Please sign in to comment.