first commit
This commit is contained in:
35
calcom/packages/ui/components/form/step/FormStep.tsx
Normal file
35
calcom/packages/ui/components/form/step/FormStep.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import React from "react";
|
||||
|
||||
import classNames from "@calcom/lib/classNames";
|
||||
|
||||
type Props = {
|
||||
steps: number;
|
||||
currentStep: number;
|
||||
};
|
||||
|
||||
// It might be worth passing this label string from outside the component so we can translate it?
|
||||
function FormStep({ currentStep, steps }: Props) {
|
||||
return (
|
||||
<div className="w-full">
|
||||
<p className="text-muted text-xs font-medium">
|
||||
Step {currentStep} of {steps}
|
||||
</p>
|
||||
<div className="flex flex-nowrap space-x-1">
|
||||
{[...Array(steps)].map((_, j) => {
|
||||
console.log({ j, currentStep });
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
"h-1 w-full rounded-sm",
|
||||
currentStep - 1 >= j ? "bg-black" : "bg-gray-400"
|
||||
)}
|
||||
key={j}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default FormStep;
|
||||
64
calcom/packages/ui/components/form/step/Stepper.tsx
Normal file
64
calcom/packages/ui/components/form/step/Stepper.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import Link from "next/link";
|
||||
|
||||
type DefaultStep = {
|
||||
title: string;
|
||||
};
|
||||
|
||||
function Stepper<T extends DefaultStep>(props: {
|
||||
href: string;
|
||||
step: number;
|
||||
steps: T[];
|
||||
disableSteps?: boolean;
|
||||
stepLabel?: (currentStep: number, totalSteps: number) => string;
|
||||
}) {
|
||||
const {
|
||||
href,
|
||||
steps,
|
||||
stepLabel = (currentStep, totalSteps) => `Step ${currentStep} of ${totalSteps}`,
|
||||
} = props;
|
||||
const [stepperRef] = useAutoAnimate<HTMLOListElement>();
|
||||
return (
|
||||
<>
|
||||
{steps.length > 1 && (
|
||||
<nav className="flex items-center justify-center" aria-label="Progress">
|
||||
<p className="text-sm font-medium">{stepLabel(props.step, steps.length)}</p>
|
||||
<ol role="list" className="ml-8 flex items-center space-x-5" ref={stepperRef}>
|
||||
{steps.map((mapStep, index) => (
|
||||
<li key={mapStep.title}>
|
||||
<Link
|
||||
href={props.disableSteps ? "#" : `${href}?step=${index + 1}`}
|
||||
shallow
|
||||
replace
|
||||
legacyBehavior>
|
||||
{index + 1 < props.step ? (
|
||||
<a className="hover:bg-inverted block h-2.5 w-2.5 rounded-full bg-gray-600">
|
||||
<span className="sr-only">{mapStep.title}</span>
|
||||
</a>
|
||||
) : index + 1 === props.step ? (
|
||||
<a className="relative flex items-center justify-center" aria-current="step">
|
||||
<span className="absolute flex h-5 w-5 p-px" aria-hidden="true">
|
||||
<span className="bg-emphasis h-full w-full rounded-full" />
|
||||
</span>
|
||||
<span
|
||||
className="relative block h-2.5 w-2.5 rounded-full bg-gray-600"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span className="sr-only">{mapStep.title}</span>
|
||||
</a>
|
||||
) : (
|
||||
<a className="bg-emphasis block h-2.5 w-2.5 rounded-full hover:bg-gray-400">
|
||||
<span className="sr-only">{mapStep.title}</span>
|
||||
</a>
|
||||
)}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</nav>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default Stepper;
|
||||
57
calcom/packages/ui/components/form/step/Steps.tsx
Normal file
57
calcom/packages/ui/components/form/step/Steps.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import classNames from "@calcom/lib/classNames";
|
||||
|
||||
type StepWithNav = {
|
||||
maxSteps: number;
|
||||
currentStep: number;
|
||||
navigateToStep: (step: number) => void;
|
||||
disableNavigation?: false;
|
||||
stepLabel?: (currentStep: number, maxSteps: number) => string;
|
||||
};
|
||||
|
||||
type StepWithoutNav = {
|
||||
maxSteps: number;
|
||||
currentStep: number;
|
||||
navigateToStep?: undefined;
|
||||
disableNavigation: true;
|
||||
stepLabel?: (currentStep: number, maxSteps: number) => string;
|
||||
};
|
||||
|
||||
// Discriminative union on disableNavigation prop
|
||||
type StepsProps = StepWithNav | StepWithoutNav;
|
||||
|
||||
const Steps = (props: StepsProps) => {
|
||||
const {
|
||||
maxSteps,
|
||||
currentStep,
|
||||
navigateToStep,
|
||||
disableNavigation = false,
|
||||
stepLabel = (currentStep, totalSteps) => `Step ${currentStep} of ${totalSteps}`,
|
||||
} = props;
|
||||
return (
|
||||
<div className="mt-6 space-y-2">
|
||||
<p className="text-subtle text-xs font-medium">{stepLabel(currentStep, maxSteps)}</p>
|
||||
<div data-testid="step-indicator-container" className="flex w-full space-x-2 rtl:space-x-reverse">
|
||||
{new Array(maxSteps).fill(0).map((_s, index) => {
|
||||
return index <= currentStep - 1 ? (
|
||||
<div
|
||||
key={`step-${index}`}
|
||||
onClick={() => navigateToStep?.(index)}
|
||||
className={classNames(
|
||||
"bg-inverted h-1 w-full rounded-[1px]",
|
||||
index < currentStep - 1 && !disableNavigation ? "cursor-pointer" : ""
|
||||
)}
|
||||
data-testid={`step-indicator-${index}`}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
key={`step-${index}`}
|
||||
className="bg-emphasis h-1 w-full rounded-[1px] opacity-25"
|
||||
data-testid={`step-indicator-${index}`}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export { Steps };
|
||||
3
calcom/packages/ui/components/form/step/index.ts
Normal file
3
calcom/packages/ui/components/form/step/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { default as FormStep } from "./FormStep";
|
||||
export { Steps } from "./Steps";
|
||||
export { default as Stepper } from "./Stepper";
|
||||
43
calcom/packages/ui/components/form/step/steps.stories.mdx
Normal file
43
calcom/packages/ui/components/form/step/steps.stories.mdx
Normal file
@@ -0,0 +1,43 @@
|
||||
import { Canvas, Meta, Story } from "@storybook/addon-docs";
|
||||
|
||||
import { Title, VariantRow, VariantsTable, CustomArgsTable } from "@calcom/storybook/components";
|
||||
|
||||
import { Steps } from "./Steps";
|
||||
|
||||
<Meta title="UI/Form/Steps" component={Steps} />
|
||||
|
||||
<Title title="Steps" suffix="Brief" subtitle="Version 1.0 — Last Update: 15 Aug 2023" />
|
||||
|
||||
## Definition
|
||||
|
||||
Steps component is used to display the current step out of the total steps in a process.
|
||||
|
||||
## Structure
|
||||
|
||||
The `Steps` component can be used to show the steps of the total in a process.
|
||||
|
||||
<CustomArgsTable of={Steps} />
|
||||
|
||||
## Steps Story
|
||||
|
||||
<Canvas>
|
||||
<Story
|
||||
name="Steps"
|
||||
args={{ maxSteps: 4, currentStep: 2 }}
|
||||
argTypes={{ maxSteps: { control: "number" }, currentStep: { control: "number" } }}>
|
||||
{({ maxSteps, currentStep }) => (
|
||||
<VariantsTable titles={["Default"]} columnMinWidth={150}>
|
||||
<VariantRow>
|
||||
<Steps
|
||||
maxSteps={maxSteps}
|
||||
currentStep={currentStep}
|
||||
navigateToStep={(step) => {
|
||||
const newPath = `?path=/story/ui-form-steps--steps&args=currentStep:${step + 1}`;
|
||||
window.open(newPath, "_self");
|
||||
}}
|
||||
/>
|
||||
</VariantRow>
|
||||
</VariantsTable>
|
||||
)}
|
||||
</Story>
|
||||
</Canvas>
|
||||
62
calcom/packages/ui/components/form/step/steps.test.tsx
Normal file
62
calcom/packages/ui/components/form/step/steps.test.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
/* eslint-disable playwright/no-conditional-in-test */
|
||||
|
||||
/* eslint-disable playwright/missing-playwright-await */
|
||||
import { render, fireEvent } from "@testing-library/react";
|
||||
import { vi } from "vitest";
|
||||
|
||||
import { Steps } from "./Steps";
|
||||
|
||||
const MAX_STEPS = 10;
|
||||
const CURRENT_STEP = 5;
|
||||
const mockNavigateToStep = vi.fn();
|
||||
|
||||
const Props = {
|
||||
maxSteps: MAX_STEPS,
|
||||
currentStep: CURRENT_STEP,
|
||||
navigateToStep: mockNavigateToStep,
|
||||
stepLabel: (currentStep: number, totalSteps: number) => `Test Step ${currentStep} of ${totalSteps}`,
|
||||
};
|
||||
|
||||
describe("Tests for Steps Component", () => {
|
||||
test("Should render the correct number of steps", () => {
|
||||
const { queryByTestId } = render(<Steps {...Props} />);
|
||||
|
||||
const stepIndicatorDivs = queryByTestId("step-indicator-container");
|
||||
const childDivs = stepIndicatorDivs?.querySelectorAll("div");
|
||||
|
||||
const count = childDivs?.length;
|
||||
expect(stepIndicatorDivs).toBeInTheDocument();
|
||||
|
||||
expect(count).toBe(MAX_STEPS);
|
||||
|
||||
for (let i = 0; i < MAX_STEPS; i++) {
|
||||
const step = queryByTestId(`step-indicator-${i}`);
|
||||
if (i < CURRENT_STEP - 1) {
|
||||
expect(step).toHaveClass("cursor-pointer");
|
||||
} else {
|
||||
expect(step).not.toHaveClass("cursor-pointer");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("Should render correctly the label of the steps", () => {
|
||||
const { getByText } = render(<Steps {...Props} />);
|
||||
|
||||
expect(getByText(`Test Step ${CURRENT_STEP} of ${MAX_STEPS}`)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("Should navigate to the correct step when clicked", async () => {
|
||||
const { getByTestId } = render(<Steps {...Props} />);
|
||||
|
||||
for (let i = 0; i < MAX_STEPS; i++) {
|
||||
const stepIndicator = getByTestId(`step-indicator-${i}`);
|
||||
if (i < CURRENT_STEP - 1) {
|
||||
fireEvent.click(stepIndicator);
|
||||
expect(mockNavigateToStep).toHaveBeenCalledWith(i);
|
||||
mockNavigateToStep.mockClear();
|
||||
} else {
|
||||
expect(mockNavigateToStep).not.toHaveBeenCalled();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user