2
0

first commit

This commit is contained in:
2024-08-09 00:39:27 +02:00
commit 79688abe2e
5698 changed files with 497838 additions and 0 deletions

View 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;

View 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;

View 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 };

View File

@@ -0,0 +1,3 @@
export { default as FormStep } from "./FormStep";
export { Steps } from "./Steps";
export { default as Stepper } from "./Stepper";

View 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>

View 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();
}
}
});
});