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

feat: add stepper and step components #1134

Merged
merged 1 commit into from
Oct 3, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
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;
edlerd marked this conversation as resolved.
Show resolved Hide resolved
/**
* 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