Skip to content

Commit

Permalink
[UI v2] feat: Adds stepper ui component (#16549)
Browse files Browse the repository at this point in the history
  • Loading branch information
devinvillarosa authored Dec 31, 2024
1 parent 34ce4fd commit 9924f8f
Show file tree
Hide file tree
Showing 8 changed files with 446 additions and 39 deletions.
2 changes: 2 additions & 0 deletions ui-v2/src/components/ui/icons/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
ChevronsLeft,
ChevronsRight,
CircleArrowOutUpRight,
CircleCheck,
Clock,
ExternalLink,
Loader2,
Expand Down Expand Up @@ -39,6 +40,7 @@ export const ICONS = {
ChevronsLeft,
ChevronsRight,
CircleArrowOutUpRight,
CircleCheck,
Clock,
ExternalLink,
Loader2,
Expand Down
1 change: 1 addition & 0 deletions ui-v2/src/components/ui/stepper/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { Stepper } from "./stepper";
19 changes: 19 additions & 0 deletions ui-v2/src/components/ui/stepper/stepper.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { Meta, StoryObj } from "@storybook/react";

import { Stepper } from "./stepper";
import { FormStepperStory } from "./stories/form-stepper-story";
import { StepperStory } from "./stories/stepper-story";

const meta: Meta<typeof Stepper> = {
title: "UI/Stepper",
component: Stepper,
};
export default meta;

export const Usage: StoryObj = {
render: () => <StepperStory />,
};

export const FormExample: StoryObj = {
render: () => <FormStepperStory />,
};
126 changes: 126 additions & 0 deletions ui-v2/src/components/ui/stepper/stepper.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { cn } from "@/lib/utils";

import { Card } from "@/components/ui/card";
import { Icon } from "@/components/ui/icons";
import { Typography } from "@/components/ui/typography";

type StepperProps = {
currentStepNum: number;
steps: Array<string> | ReadonlyArray<string>;
onClick: ({
stepName,
stepNum,
}: { stepName: string; stepNum: number }) => void;
completedSteps: Set<number>;
visitedSteps: Set<number>;
};
const Stepper = ({
currentStepNum,
onClick,
steps,
completedSteps,
visitedSteps,
}: StepperProps) => {
return (
<Card className="p-4 flex items-center justify-around">
{steps.map((step, i) => {
const isCurrentStep = currentStepNum === i;
const isStepVisited = visitedSteps.has(i);
const isStepComplete = completedSteps.has(i);

return (
<Step
key={i}
disabled={!isStepVisited}
onClick={() => onClick({ stepName: step, stepNum: i })}
number={i}
isActive={isCurrentStep}
isComplete={isStepComplete}
name={step}
/>
);
})}
</Card>
);
};

type StepProps = {
isActive?: boolean;
isComplete?: boolean;
disabled?: boolean;
number: number;
name: string;
onClick?: ({ number, name }: { number: number; name: string }) => void;
};

const Step = ({
isActive = false,
isComplete = false,
disabled = false,
onClick = () => {},
/** Assume steps are indexed =0 */
number,
name,
}: StepProps) => {
// add 1 to number assuming index 0
const adjustedNumberDisplay = number + 1;
const numberLabel =
adjustedNumberDisplay < 10
? `0${adjustedNumberDisplay}`
: String(adjustedNumberDisplay);

return (
<button
className={cn(
"flex items-center gap-3",
disabled && "cursor-not-allowed",
)}
disabled={disabled}
onClick={() => onClick({ number, name })}
>
{isComplete ? (
<Icon
id="CircleCheck"
color={isActive ? "teal" : "grey"}
className="h-12 w-12"
/>
) : (
<StepIcon isActive={isActive} label={numberLabel} />
)}
<Typography
variant="bodyLarge"
className={cn(
"text-gray-500 border-gray-500",
isActive && "text-teal-700 border-teal-700",
)}
>
{name}
</Typography>
</button>
);
};

type StepIconProps = {
label: string;
isActive?: boolean;
};
const StepIcon = ({ isActive = false, label }: StepIconProps) => (
<div
className={cn(
"flex items-center justify-center w-12 h-12 rounded-full border-4 text-gray-500 border-gray-500",
isActive && "text-teal-700 border-teal-700",
)}
>
<Typography
variant="bodyLarge"
className={cn(
"text-gray-500 border-gray-500",
isActive && "text-teal-700 border-teal-700",
)}
>
{label}
</Typography>
</div>
);

export { Step, Stepper };
209 changes: 209 additions & 0 deletions ui-v2/src/components/ui/stepper/stories/form-stepper-story.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { useStepper } from "@/hooks/use-stepper";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm, useFormContext } from "react-hook-form";
import { z } from "zod";

import { Stepper } from "@/components/ui/stepper";

const SIGNS = [
"Aries",
"Taurus",
"Gemini",
"Cancer",
"Leo",
"Vigo",
"Libra",
"Scorpio",
"Sagittarius",
"Capricon",
"Aquarius",
"Pisces",
] as const;

const formSchema = z
.object({
name: z.string().min(1),
sign: z.enum(SIGNS),
rising: z.enum(SIGNS),
})
.strict();
const EXAMPLE_STEPS = ["Name", "Sign", "Rising"] as const;
const RENDER_STEP = {
Name: () => <NameStep />,
Sign: () => <SignStep />,
Rising: () => <RisingStep />,
} as const;

export const FormStepperStory = () => {
const stepper = useStepper(EXAMPLE_STEPS.length);
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
name: "",
sign: "Aries",
rising: "Aries",
},
});

const handleSubmit = (values: z.infer<typeof formSchema>) => {
if (stepper.isFinalStep) {
console.log(values);
stepper.reset();
form.reset();
} else {
// validate step
switch (EXAMPLE_STEPS[stepper.currentStep]) {
case "Name":
void form.trigger("name");
break;
case "Sign":
void form.trigger("sign");
break;
case "Rising":
void form.trigger("rising");
break;
}
stepper.incrementStep();
}
};

return (
<div className="flex flex-col gap-4">
<Stepper
steps={EXAMPLE_STEPS}
currentStepNum={stepper.currentStep}
onClick={(step) => stepper.changeStep(step.stepNum)}
completedSteps={stepper.completedStepsSet}
visitedSteps={stepper.visitedStepsSet}
/>
<Card className="flex flex-col gap-4 p-4">
<Form {...form}>
<form
onSubmit={(e) => void form.handleSubmit(handleSubmit)(e)}
className="space-y-4"
>
{RENDER_STEP[EXAMPLE_STEPS[stepper.currentStep]]()}

<div className="flex justify-end gap-2">
<Button
variant="secondary"
disabled={stepper.isStartingStep}
onClick={stepper.decrementStep}
>
Previous
</Button>
<Button type="submit">
{stepper.isFinalStep ? "Save" : "Next"}
</Button>
</div>
</form>
</Form>
</Card>
</div>
);
};

const NameStep = () => {
const form = useFormContext();
return (
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input type="text" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
);
};

const SignStep = () => {
const form = useFormContext();
return (
<FormField
control={form.control}
name="sign"
render={({ field }) => (
<FormItem>
<FormLabel>Sign</FormLabel>
<FormControl>
<Select {...field} onValueChange={field.onChange}>
<SelectTrigger>
<SelectValue placeholder="Select a sign" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>Signs</SelectLabel>
{SIGNS.map((sign) => (
<SelectItem key={sign} value={sign}>
{sign}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
);
};

const RisingStep = () => {
const form = useFormContext();
return (
<FormField
control={form.control}
name="rising"
render={({ field }) => (
<FormItem>
<FormLabel>Sign</FormLabel>
<FormControl>
<Select {...field} onValueChange={field.onChange}>
<SelectTrigger>
<SelectValue placeholder="Select a rising sign" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>Signs</SelectLabel>
{SIGNS.map((sign) => (
<SelectItem key={sign} value={sign}>
{sign}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
);
};
Loading

0 comments on commit 9924f8f

Please sign in to comment.