Progress and Stepper #33
@@ -4,6 +4,8 @@ import { useState } from "react";
|
|||||||
import Tooltip from "../components/Tooltip";
|
import Tooltip from "../components/Tooltip";
|
||||||
import Alert from "../components/Alert";
|
import Alert from "../components/Alert";
|
||||||
import Button from "../components/Button";
|
import Button from "../components/Button";
|
||||||
|
import Stepper from "../components/Stepper";
|
||||||
|
import Progress from "../components/Progress";
|
||||||
|
|
||||||
export default function ComponentsPreview() {
|
export default function ComponentsPreview() {
|
||||||
const [alertVisible, setAlertVisible] = useState({
|
const [alertVisible, setAlertVisible] = useState({
|
||||||
@@ -178,6 +180,132 @@ export default function ComponentsPreview() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{/* Stepper Section */}
|
||||||
|
<section className="space-y-[var(--spacing-scale-024)]">
|
||||||
|
<h2 className="font-bricolage-grotesque text-[32px] leading-[40px] font-bold text-[var(--color-content-default-primary)]">
|
||||||
|
Stepper Component
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div className="bg-[var(--color-surface-default-secondary)] rounded-[var(--radius-300,12px)] p-[var(--spacing-scale-032)] space-y-[var(--spacing-scale-024)]">
|
||||||
|
<div className="space-y-[var(--spacing-scale-016)]">
|
||||||
|
<div>
|
||||||
|
<p className="font-inter text-[14px] leading-[20px] text-[var(--color-content-default-secondary)] mb-[var(--spacing-scale-008)]">
|
||||||
|
Step 1 of 5
|
||||||
|
</p>
|
||||||
|
<Stepper active={1} totalSteps={5} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="font-inter text-[14px] leading-[20px] text-[var(--color-content-default-secondary)] mb-[var(--spacing-scale-008)]">
|
||||||
|
Step 2 of 5
|
||||||
|
</p>
|
||||||
|
<Stepper active={2} totalSteps={5} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="font-inter text-[14px] leading-[20px] text-[var(--color-content-default-secondary)] mb-[var(--spacing-scale-008)]">
|
||||||
|
Step 3 of 5
|
||||||
|
</p>
|
||||||
|
<Stepper active={3} totalSteps={5} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="font-inter text-[14px] leading-[20px] text-[var(--color-content-default-secondary)] mb-[var(--spacing-scale-008)]">
|
||||||
|
Step 4 of 5
|
||||||
|
</p>
|
||||||
|
<Stepper active={4} totalSteps={5} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="font-inter text-[14px] leading-[20px] text-[var(--color-content-default-secondary)] mb-[var(--spacing-scale-008)]">
|
||||||
|
Step 5 of 5
|
||||||
|
</p>
|
||||||
|
<Stepper active={5} totalSteps={5} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Progress Section */}
|
||||||
|
<section className="space-y-[var(--spacing-scale-024)]">
|
||||||
|
<h2 className="font-bricolage-grotesque text-[32px] leading-[40px] font-bold text-[var(--color-content-default-primary)]">
|
||||||
|
Progress Component
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div className="bg-white rounded-[var(--radius-300,12px)] p-[var(--spacing-scale-032)] space-y-[var(--spacing-scale-024)]">
|
||||||
|
<div className="space-y-[var(--spacing-scale-016)]">
|
||||||
|
<div>
|
||||||
|
<p className="font-inter text-[14px] leading-[20px] text-[var(--color-content-default-secondary)] mb-[var(--spacing-scale-008)]">
|
||||||
|
Progress: 1-0
|
||||||
|
</p>
|
||||||
|
<Progress progress="1-0" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="font-inter text-[14px] leading-[20px] text-[var(--color-content-default-secondary)] mb-[var(--spacing-scale-008)]">
|
||||||
|
Progress: 1-1
|
||||||
|
</p>
|
||||||
|
<Progress progress="1-1" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="font-inter text-[14px] leading-[20px] text-[var(--color-content-default-secondary)] mb-[var(--spacing-scale-008)]">
|
||||||
|
Progress: 1-2
|
||||||
|
</p>
|
||||||
|
<Progress progress="1-2" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="font-inter text-[14px] leading-[20px] text-[var(--color-content-default-secondary)] mb-[var(--spacing-scale-008)]">
|
||||||
|
Progress: 1-3
|
||||||
|
</p>
|
||||||
|
<Progress progress="1-3" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="font-inter text-[14px] leading-[20px] text-[var(--color-content-default-secondary)] mb-[var(--spacing-scale-008)]">
|
||||||
|
Progress: 1-4
|
||||||
|
</p>
|
||||||
|
<Progress progress="1-4" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="font-inter text-[14px] leading-[20px] text-[var(--color-content-default-secondary)] mb-[var(--spacing-scale-008)]">
|
||||||
|
Progress: 1-5
|
||||||
|
</p>
|
||||||
|
<Progress progress="1-5" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="font-inter text-[14px] leading-[20px] text-[var(--color-content-default-secondary)] mb-[var(--spacing-scale-008)]">
|
||||||
|
Progress: 2-0
|
||||||
|
</p>
|
||||||
|
<Progress progress="2-0" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="font-inter text-[14px] leading-[20px] text-[var(--color-content-default-secondary)] mb-[var(--spacing-scale-008)]">
|
||||||
|
Progress: 2-1
|
||||||
|
</p>
|
||||||
|
<Progress progress="2-1" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="font-inter text-[14px] leading-[20px] text-[var(--color-content-default-secondary)] mb-[var(--spacing-scale-008)]">
|
||||||
|
Progress: 2-2
|
||||||
|
</p>
|
||||||
|
<Progress progress="2-2" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="font-inter text-[14px] leading-[20px] text-[var(--color-content-default-secondary)] mb-[var(--spacing-scale-008)]">
|
||||||
|
Progress: 3-0
|
||||||
|
</p>
|
||||||
|
<Progress progress="3-0" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="font-inter text-[14px] leading-[20px] text-[var(--color-content-default-secondary)] mb-[var(--spacing-scale-008)]">
|
||||||
|
Progress: 3-1
|
||||||
|
</p>
|
||||||
|
<Progress progress="3-1" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="font-inter text-[14px] leading-[20px] text-[var(--color-content-default-secondary)] mb-[var(--spacing-scale-008)]">
|
||||||
|
Progress: 3-2
|
||||||
|
</p>
|
||||||
|
<Progress progress="3-2" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { memo } from "react";
|
||||||
|
import { ProgressView } from "./Progress.view";
|
||||||
|
import type { ProgressProps } from "./Progress.types";
|
||||||
|
|
||||||
|
const ProgressContainer = memo<ProgressProps>(
|
||||||
|
({ progress = "3-2", className = "" }) => {
|
||||||
|
const barClasses = `h-[8px] relative w-full`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ProgressView
|
||||||
|
progress={progress}
|
||||||
|
className={className}
|
||||||
|
barClasses={barClasses}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
ProgressContainer.displayName = "Progress";
|
||||||
|
|
||||||
|
export default ProgressContainer;
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
export type ProgressBarState =
|
||||||
|
| "1-0"
|
||||||
|
| "1-1"
|
||||||
|
| "1-2"
|
||||||
|
| "1-3"
|
||||||
|
| "1-4"
|
||||||
|
| "1-5"
|
||||||
|
| "2-0"
|
||||||
|
| "2-1"
|
||||||
|
| "2-2"
|
||||||
|
| "3-0"
|
||||||
|
| "3-1"
|
||||||
|
| "3-2";
|
||||||
|
|
||||||
|
export interface ProgressProps {
|
||||||
|
progress?: ProgressBarState;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProgressViewProps {
|
||||||
|
progress: ProgressBarState;
|
||||||
|
className: string;
|
||||||
|
barClasses: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import type { ProgressViewProps } from "./Progress.types";
|
||||||
|
|
||||||
|
export function ProgressView({
|
||||||
|
progress,
|
||||||
|
className,
|
||||||
|
barClasses,
|
||||||
|
}: ProgressViewProps) {
|
||||||
|
// Progress bar type
|
||||||
|
const [fullSegments, partialSegment] = progress.split("-").map(Number);
|
||||||
|
// Calculate total progress:
|
||||||
|
// - For 1-X: first section is (X+1)/6 filled
|
||||||
|
// - For 2-X: first section full, second section X/3 filled
|
||||||
|
// - For 3-X: first two sections full, third section X/3 filled
|
||||||
|
// Max is 3 full segments = 9 units
|
||||||
|
let totalProgress = 0;
|
||||||
|
if (fullSegments === 1) {
|
||||||
|
totalProgress = (partialSegment + 1) / 6; // 1/6 to 6/6 of first section
|
||||||
|
} else if (fullSegments === 2) {
|
||||||
|
totalProgress = 1 + partialSegment / 3; // 1 full + 0/3 to 2/3 of second
|
||||||
|
} else if (fullSegments === 3) {
|
||||||
|
totalProgress = 2 + partialSegment / 3; // 2 full + 0/3 to 2/3 of third
|
||||||
|
}
|
||||||
|
const maxProgress = 3;
|
||||||
|
const progressPercentage = Math.round((totalProgress / maxProgress) * 100);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`${barClasses} ${className}`}
|
||||||
|
role="progressbar"
|
||||||
|
aria-valuenow={totalProgress}
|
||||||
|
aria-valuemin={0}
|
||||||
|
aria-valuemax={3}
|
||||||
|
aria-label={`Progress: ${progressPercentage}%`}
|
||||||
|
>
|
||||||
|
{/* Background layer - 3 segments */}
|
||||||
|
<div className="absolute inset-0 flex gap-[var(--spacing-scale-008)] px-[4px]">
|
||||||
|
<div className="flex-1 h-full bg-[var(--color-surface-default-secondary)] rounded-l-[var(--radius-full)]" />
|
||||||
|
<div className="flex-1 h-full bg-[var(--color-surface-default-secondary)]" />
|
||||||
|
<div className="flex-1 h-full bg-[var(--color-surface-default-secondary)] rounded-r-[var(--radius-full)]" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Fill layer - always show 3 sections, fill amount varies */}
|
||||||
|
<div className="absolute inset-0 flex gap-[var(--spacing-scale-008)] px-[4px] overflow-hidden">
|
||||||
|
{/* First section - for 1-X: (X+1)/6 filled, for 2-X and 3-X: fully filled */}
|
||||||
|
<div className="flex-1 h-full relative">
|
||||||
|
{fullSegments === 1 ? (
|
||||||
|
<div
|
||||||
|
className="absolute inset-y-0 left-0 bg-[var(--color-content-default-brand-primary)] rounded-l-[var(--radius-full)]"
|
||||||
|
style={{ width: `${((partialSegment + 1) / 6) * 100}%` }}
|
||||||
|
/>
|
||||||
|
) : fullSegments >= 2 ? (
|
||||||
|
<div className="absolute inset-0 bg-[var(--color-content-default-brand-primary)] rounded-l-[var(--radius-full)]" />
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
{/* Second section - for 2-X: X/3 filled, for 3-X: fully filled, otherwise empty */}
|
||||||
|
<div className="flex-1 h-full relative">
|
||||||
|
{fullSegments === 2 ? (
|
||||||
|
partialSegment > 0 ? (
|
||||||
|
<div
|
||||||
|
className="absolute inset-y-0 left-0 bg-[var(--color-content-default-brand-primary)]"
|
||||||
|
style={{ width: `${(partialSegment / 3) * 100}%` }}
|
||||||
|
/>
|
||||||
|
) : null
|
||||||
|
) : fullSegments >= 3 ? (
|
||||||
|
<div className="absolute inset-0 bg-[var(--color-content-default-brand-primary)]" />
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
{/* Third section - for 3-X: X/3 filled, otherwise empty */}
|
||||||
|
<div className="flex-1 h-full relative">
|
||||||
|
{fullSegments === 3 && partialSegment > 0 ? (
|
||||||
|
<div
|
||||||
|
className="absolute inset-y-0 left-0 bg-[var(--color-content-default-brand-primary)]"
|
||||||
|
style={{ width: `${(partialSegment / 3) * 100}%` }}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { default } from "./Progress.container";
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { memo } from "react";
|
||||||
|
import { StepperView } from "./Stepper.view";
|
||||||
|
import type { StepperProps } from "./Stepper.types";
|
||||||
|
|
||||||
|
const StepperContainer = memo<StepperProps>(
|
||||||
|
({ active = 1, totalSteps = 5, className = "" }) => {
|
||||||
|
const stepperClasses = `flex gap-[var(--spacing-scale-012)] items-center relative`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StepperView
|
||||||
|
active={active}
|
||||||
|
totalSteps={totalSteps}
|
||||||
|
className={className}
|
||||||
|
stepperClasses={stepperClasses}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
StepperContainer.displayName = "Stepper";
|
||||||
|
|
||||||
|
export default StepperContainer;
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
export type StepperActive = 1 | 2 | 3 | 4 | 5;
|
||||||
|
|
||||||
|
export interface StepperProps {
|
||||||
|
active?: StepperActive;
|
||||||
|
totalSteps?: number;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StepperViewProps {
|
||||||
|
active: StepperActive;
|
||||||
|
totalSteps: number;
|
||||||
|
className: string;
|
||||||
|
stepperClasses: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import type { StepperViewProps } from "./Stepper.types";
|
||||||
|
|
||||||
|
export function StepperView({
|
||||||
|
active,
|
||||||
|
totalSteps,
|
||||||
|
className,
|
||||||
|
stepperClasses,
|
||||||
|
}: StepperViewProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`${stepperClasses} ${className}`}
|
||||||
|
role="progressbar"
|
||||||
|
aria-valuenow={active}
|
||||||
|
aria-valuemin={1}
|
||||||
|
aria-valuemax={totalSteps}
|
||||||
|
aria-label={`Step ${active} of ${totalSteps}`}
|
||||||
|
>
|
||||||
|
{Array.from({ length: totalSteps }, (_, index) => {
|
||||||
|
const stepNumber = index + 1;
|
||||||
|
const isActive = stepNumber === active;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={stepNumber}
|
||||||
|
className="shrink-0 w-[12px] h-[12px] flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="12"
|
||||||
|
height="12"
|
||||||
|
viewBox="0 0 12 12"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
aria-hidden="true"
|
||||||
|
className={isActive ? "opacity-100" : "opacity-30"}
|
||||||
|
>
|
||||||
|
<circle
|
||||||
|
cx="6"
|
||||||
|
cy="6"
|
||||||
|
r="5.5"
|
||||||
|
fill="var(--color-surface-inverse-secondary)"
|
||||||
|
stroke="var(--color-surface-inverse-secondary)"
|
||||||
|
strokeWidth="1"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { default } from "./Stepper.container";
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
import Progress from "../app/components/Progress";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: "Components/Progress",
|
||||||
|
component: Progress,
|
||||||
|
parameters: {
|
||||||
|
layout: "centered",
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
component:
|
||||||
|
"Progress bar component for showing completion percentage. Displays a 3-segment progress bar with support for partial fills.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
argTypes: {
|
||||||
|
progress: {
|
||||||
|
control: { type: "select" },
|
||||||
|
options: [
|
||||||
|
"1-0",
|
||||||
|
"1-1",
|
||||||
|
"1-2",
|
||||||
|
"1-3",
|
||||||
|
"1-4",
|
||||||
|
"1-5",
|
||||||
|
"2-0",
|
||||||
|
"2-1",
|
||||||
|
"2-2",
|
||||||
|
"3-0",
|
||||||
|
"3-1",
|
||||||
|
"3-2",
|
||||||
|
],
|
||||||
|
description: "Progress state (format: segments-partial)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tags: ["autodocs"],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Default = {
|
||||||
|
args: {
|
||||||
|
progress: "3-2",
|
||||||
|
},
|
||||||
|
render: (args) => <Progress {...args} />,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AllStates = {
|
||||||
|
args: {},
|
||||||
|
render: (_args) => (
|
||||||
|
<div className="space-y-4 w-full max-w-[600px]">
|
||||||
|
<div>
|
||||||
|
<p className="text-white mb-2">1-0</p>
|
||||||
|
<Progress {..._args} progress="1-0" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-white mb-2">1-1</p>
|
||||||
|
<Progress {..._args} progress="1-1" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-white mb-2">1-2</p>
|
||||||
|
<Progress {..._args} progress="1-2" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-white mb-2">1-3</p>
|
||||||
|
<Progress {..._args} progress="1-3" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-white mb-2">1-4</p>
|
||||||
|
<Progress {..._args} progress="1-4" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-white mb-2">1-5</p>
|
||||||
|
<Progress {..._args} progress="1-5" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-white mb-2">2-0</p>
|
||||||
|
<Progress {..._args} progress="2-0" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-white mb-2">2-1</p>
|
||||||
|
<Progress {..._args} progress="2-1" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-white mb-2">2-2</p>
|
||||||
|
<Progress {..._args} progress="2-2" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-white mb-2">3-0</p>
|
||||||
|
<Progress {..._args} progress="3-0" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-white mb-2">3-1</p>
|
||||||
|
<Progress {..._args} progress="3-1" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-white mb-2">3-2</p>
|
||||||
|
<Progress {..._args} progress="3-2" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
parameters: {
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
story: "Different progress states of the progress bar component.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
import Stepper from "../app/components/Stepper";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: "Components/Stepper",
|
||||||
|
component: Stepper,
|
||||||
|
parameters: {
|
||||||
|
layout: "centered",
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
component:
|
||||||
|
"Stepper component for showing progress through multi-step processes. Displays a series of steps with active steps highlighted.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
argTypes: {
|
||||||
|
active: {
|
||||||
|
control: { type: "number", min: 1, max: 5 },
|
||||||
|
description: "The active step number",
|
||||||
|
},
|
||||||
|
totalSteps: {
|
||||||
|
control: { type: "number", min: 1, max: 10 },
|
||||||
|
description: "Total number of steps",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tags: ["autodocs"],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Default = {
|
||||||
|
args: {
|
||||||
|
active: 1,
|
||||||
|
totalSteps: 5,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AllStates = {
|
||||||
|
args: {
|
||||||
|
totalSteps: 5,
|
||||||
|
},
|
||||||
|
render: (_args) => (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<p className="text-white mb-2">Step 1 of 5</p>
|
||||||
|
<Stepper {..._args} active={1} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-white mb-2">Step 2 of 5</p>
|
||||||
|
<Stepper {..._args} active={2} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-white mb-2">Step 3 of 5</p>
|
||||||
|
<Stepper {..._args} active={3} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-white mb-2">Step 4 of 5</p>
|
||||||
|
<Stepper {..._args} active={4} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-white mb-2">Step 5 of 5</p>
|
||||||
|
<Stepper {..._args} active={5} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
parameters: {
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
story: "Different active states of the stepper component.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DifferentStepCounts = {
|
||||||
|
args: {},
|
||||||
|
render: () => (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<p className="text-white mb-2">3 Steps - Step 2</p>
|
||||||
|
<Stepper active={2} totalSteps={3} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-white mb-2">5 Steps - Step 3</p>
|
||||||
|
<Stepper active={3} totalSteps={5} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-white mb-2">7 Steps - Step 4</p>
|
||||||
|
<Stepper active={4} totalSteps={7} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
parameters: {
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
story: "Stepper with different total step counts.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import { render, screen } from "@testing-library/react";
|
||||||
|
import "@testing-library/jest-dom/vitest";
|
||||||
|
import Progress from "../../app/components/Progress";
|
||||||
|
import {
|
||||||
|
componentTestSuite,
|
||||||
|
ComponentTestSuiteConfig,
|
||||||
|
} from "../utils/componentTestSuite";
|
||||||
|
|
||||||
|
type ProgressProps = React.ComponentProps<typeof Progress>;
|
||||||
|
|
||||||
|
const baseProps: ProgressProps = {
|
||||||
|
progress: "2-1",
|
||||||
|
};
|
||||||
|
|
||||||
|
const config: ComponentTestSuiteConfig<ProgressProps> = {
|
||||||
|
component: Progress,
|
||||||
|
name: "Progress",
|
||||||
|
props: baseProps,
|
||||||
|
requiredProps: [],
|
||||||
|
optionalProps: {
|
||||||
|
progress: "3-2",
|
||||||
|
className: "custom-class",
|
||||||
|
},
|
||||||
|
primaryRole: "progressbar",
|
||||||
|
testCases: {
|
||||||
|
renders: true,
|
||||||
|
accessibility: true,
|
||||||
|
keyboardNavigation: false, // Progress is not keyboard navigable
|
||||||
|
disabledState: false, // Progress doesn't have disabled state
|
||||||
|
errorState: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
componentTestSuite<ProgressProps>(config);
|
||||||
|
|
||||||
|
describe("Progress (behavioral tests)", () => {
|
||||||
|
it("renders progress bar with correct progress value", () => {
|
||||||
|
render(<Progress progress="2-1" />);
|
||||||
|
const progressbar = screen.getByRole("progressbar");
|
||||||
|
// 2-1: First section full (1) + second section 1/3 filled = 1 + 1/3 ≈ 1.333
|
||||||
|
expect(progressbar).toHaveAttribute("aria-valuenow", "1.3333333333333333");
|
||||||
|
expect(progressbar).toHaveAttribute("aria-valuemin", "0");
|
||||||
|
expect(progressbar).toHaveAttribute("aria-valuemax", "3");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("applies custom className", () => {
|
||||||
|
const { container } = render(<Progress className="custom-class" />);
|
||||||
|
expect(container.firstChild).toHaveClass("custom-class");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("defaults to progress 3-2 when progress is not specified", () => {
|
||||||
|
render(<Progress />);
|
||||||
|
const progressbar = screen.getByRole("progressbar");
|
||||||
|
// 3-2: First two sections full (2) + third section 2/3 filled = 2 + 2/3 ≈ 2.667
|
||||||
|
expect(progressbar).toHaveAttribute("aria-valuenow", "2.6666666666666665");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles all progress states correctly", () => {
|
||||||
|
const testCases = [
|
||||||
|
{ progress: "1-0" as const, expected: 1 / 6 }, // First section 1/6 filled
|
||||||
|
{ progress: "1-5" as const, expected: 1 }, // First section 6/6 filled (fully filled)
|
||||||
|
{ progress: "2-0" as const, expected: 1 }, // First section full, second empty
|
||||||
|
{ progress: "2-2" as const, expected: 1 + 2 / 3 }, // First section full, second section 2/3 filled
|
||||||
|
{ progress: "3-0" as const, expected: 2 }, // First two sections full, third empty
|
||||||
|
{ progress: "3-2" as const, expected: 2 + 2 / 3 }, // First two sections full, third section 2/3 filled
|
||||||
|
];
|
||||||
|
|
||||||
|
testCases.forEach(({ progress, expected }) => {
|
||||||
|
const { unmount } = render(<Progress progress={progress} />);
|
||||||
|
const progressbar = screen.getByRole("progressbar");
|
||||||
|
expect(progressbar).toHaveAttribute("aria-valuenow", String(expected));
|
||||||
|
unmount();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import { render, screen } from "@testing-library/react";
|
||||||
|
import "@testing-library/jest-dom/vitest";
|
||||||
|
import Stepper from "../../app/components/Stepper";
|
||||||
|
import {
|
||||||
|
componentTestSuite,
|
||||||
|
ComponentTestSuiteConfig,
|
||||||
|
} from "../utils/componentTestSuite";
|
||||||
|
|
||||||
|
type StepperProps = React.ComponentProps<typeof Stepper>;
|
||||||
|
|
||||||
|
const baseProps: StepperProps = {
|
||||||
|
active: 1,
|
||||||
|
totalSteps: 5,
|
||||||
|
};
|
||||||
|
|
||||||
|
const config: ComponentTestSuiteConfig<StepperProps> = {
|
||||||
|
component: Stepper,
|
||||||
|
name: "Stepper",
|
||||||
|
props: baseProps,
|
||||||
|
requiredProps: [],
|
||||||
|
optionalProps: {
|
||||||
|
active: 3,
|
||||||
|
totalSteps: 3,
|
||||||
|
className: "custom-class",
|
||||||
|
},
|
||||||
|
primaryRole: "progressbar",
|
||||||
|
testCases: {
|
||||||
|
renders: true,
|
||||||
|
accessibility: true,
|
||||||
|
keyboardNavigation: false, // Stepper is not keyboard navigable
|
||||||
|
disabledState: false, // Stepper doesn't have disabled state
|
||||||
|
errorState: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
componentTestSuite<StepperProps>(config);
|
||||||
|
|
||||||
|
describe("Stepper (behavioral tests)", () => {
|
||||||
|
it("renders with correct number of steps", () => {
|
||||||
|
render(<Stepper active={3} totalSteps={5} />);
|
||||||
|
const progressbar = screen.getByRole("progressbar");
|
||||||
|
expect(progressbar).toHaveAttribute("aria-valuenow", "3");
|
||||||
|
expect(progressbar).toHaveAttribute("aria-valuemin", "1");
|
||||||
|
expect(progressbar).toHaveAttribute("aria-valuemax", "5");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows active steps correctly", () => {
|
||||||
|
const { container } = render(<Stepper active={2} totalSteps={5} />);
|
||||||
|
// Should have 1 active (filled) circle (step 2) and 4 inactive (outline) circles
|
||||||
|
const svgs = container.querySelectorAll("svg");
|
||||||
|
expect(svgs.length).toBe(5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("applies custom className", () => {
|
||||||
|
const { container } = render(<Stepper className="custom-class" />);
|
||||||
|
expect(container.firstChild).toHaveClass("custom-class");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("defaults to active 1 when active is not specified", () => {
|
||||||
|
render(<Stepper totalSteps={5} />);
|
||||||
|
const progressbar = screen.getByRole("progressbar");
|
||||||
|
expect(progressbar).toHaveAttribute("aria-valuenow", "1");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("defaults to totalSteps 5 when totalSteps is not specified", () => {
|
||||||
|
render(<Stepper active={1} />);
|
||||||
|
const progressbar = screen.getByRole("progressbar");
|
||||||
|
expect(progressbar).toHaveAttribute("aria-valuemax", "5");
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user