From 462488ddcefb8ccf0cdea84947bfb01bd00e38d6 Mon Sep 17 00:00:00 2001 From: adilallo <39313955+adilallo@users.noreply.github.com> Date: Mon, 2 Feb 2026 11:24:47 -0700 Subject: [PATCH 1/3] Implement stepper and progress bar --- app/components-preview/page.tsx | 128 ++++++++++++++++++ .../Progress/Progress.container.tsx | 23 ++++ app/components/Progress/Progress.types.ts | 24 ++++ app/components/Progress/Progress.view.tsx | 80 +++++++++++ app/components/Progress/index.tsx | 1 + app/components/Stepper/Stepper.container.tsx | 24 ++++ app/components/Stepper/Stepper.types.ts | 14 ++ app/components/Stepper/Stepper.view.tsx | 50 +++++++ app/components/Stepper/index.tsx | 1 + stories/Progress.stories.js | 120 ++++++++++++++++ stories/Stepper.stories.js | 97 +++++++++++++ tests/components/Progress.test.tsx | 77 +++++++++++ tests/components/Stepper.test.tsx | 72 ++++++++++ 13 files changed, 711 insertions(+) create mode 100644 app/components/Progress/Progress.container.tsx create mode 100644 app/components/Progress/Progress.types.ts create mode 100644 app/components/Progress/Progress.view.tsx create mode 100644 app/components/Progress/index.tsx create mode 100644 app/components/Stepper/Stepper.container.tsx create mode 100644 app/components/Stepper/Stepper.types.ts create mode 100644 app/components/Stepper/Stepper.view.tsx create mode 100644 app/components/Stepper/index.tsx create mode 100644 stories/Progress.stories.js create mode 100644 stories/Stepper.stories.js create mode 100644 tests/components/Progress.test.tsx create mode 100644 tests/components/Stepper.test.tsx diff --git a/app/components-preview/page.tsx b/app/components-preview/page.tsx index c0d241e..35c3ba0 100644 --- a/app/components-preview/page.tsx +++ b/app/components-preview/page.tsx @@ -4,6 +4,8 @@ import { useState } from "react"; import Tooltip from "../components/Tooltip"; import Alert from "../components/Alert"; import Button from "../components/Button"; +import Stepper from "../components/Stepper"; +import Progress from "../components/Progress"; export default function ComponentsPreview() { const [alertVisible, setAlertVisible] = useState({ @@ -178,6 +180,132 @@ export default function ComponentsPreview() { + + {/* Stepper Section */} +
+

+ Stepper Component +

+ +
+
+
+

+ Step 1 of 5 +

+ +
+
+

+ Step 2 of 5 +

+ +
+
+

+ Step 3 of 5 +

+ +
+
+

+ Step 4 of 5 +

+ +
+
+

+ Step 5 of 5 +

+ +
+
+
+
+ + {/* Progress Section */} +
+

+ Progress Component +

+ +
+
+
+

+ Progress: 1-0 +

+ +
+
+

+ Progress: 1-1 +

+ +
+
+

+ Progress: 1-2 +

+ +
+
+

+ Progress: 1-3 +

+ +
+
+

+ Progress: 1-4 +

+ +
+
+

+ Progress: 1-5 +

+ +
+
+

+ Progress: 2-0 +

+ +
+
+

+ Progress: 2-1 +

+ +
+
+

+ Progress: 2-2 +

+ +
+
+

+ Progress: 3-0 +

+ +
+
+

+ Progress: 3-1 +

+ +
+
+

+ Progress: 3-2 +

+ +
+
+
+
); diff --git a/app/components/Progress/Progress.container.tsx b/app/components/Progress/Progress.container.tsx new file mode 100644 index 0000000..b74b660 --- /dev/null +++ b/app/components/Progress/Progress.container.tsx @@ -0,0 +1,23 @@ +"use client"; + +import { memo } from "react"; +import { ProgressView } from "./Progress.view"; +import type { ProgressProps } from "./Progress.types"; + +const ProgressContainer = memo( + ({ progress = "3-2", className = "" }) => { + const barClasses = `h-[8px] relative w-full`; + + return ( + + ); + }, +); + +ProgressContainer.displayName = "Progress"; + +export default ProgressContainer; diff --git a/app/components/Progress/Progress.types.ts b/app/components/Progress/Progress.types.ts new file mode 100644 index 0000000..c64d9f2 --- /dev/null +++ b/app/components/Progress/Progress.types.ts @@ -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; +} diff --git a/app/components/Progress/Progress.view.tsx b/app/components/Progress/Progress.view.tsx new file mode 100644 index 0000000..e41ce33 --- /dev/null +++ b/app/components/Progress/Progress.view.tsx @@ -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 ( +
+ {/* Background layer - 3 segments */} +
+
+
+
+
+ + {/* Fill layer - always show 3 sections, fill amount varies */} +
+ {/* First section - for 1-X: (X+1)/6 filled, for 2-X and 3-X: fully filled */} +
+ {fullSegments === 1 ? ( +
+ ) : fullSegments >= 2 ? ( +
+ ) : null} +
+ {/* Second section - for 2-X: X/3 filled, for 3-X: fully filled, otherwise empty */} +
+ {fullSegments === 2 ? ( + partialSegment > 0 ? ( +
+ ) : null + ) : fullSegments >= 3 ? ( +
+ ) : null} +
+ {/* Third section - for 3-X: X/3 filled, otherwise empty */} +
+ {fullSegments === 3 && partialSegment > 0 ? ( +
+ ) : null} +
+
+
+ ); +} diff --git a/app/components/Progress/index.tsx b/app/components/Progress/index.tsx new file mode 100644 index 0000000..178ca49 --- /dev/null +++ b/app/components/Progress/index.tsx @@ -0,0 +1 @@ +export { default } from "./Progress.container"; diff --git a/app/components/Stepper/Stepper.container.tsx b/app/components/Stepper/Stepper.container.tsx new file mode 100644 index 0000000..25f0f66 --- /dev/null +++ b/app/components/Stepper/Stepper.container.tsx @@ -0,0 +1,24 @@ +"use client"; + +import { memo } from "react"; +import { StepperView } from "./Stepper.view"; +import type { StepperProps } from "./Stepper.types"; + +const StepperContainer = memo( + ({ active = 1, totalSteps = 5, className = "" }) => { + const stepperClasses = `flex gap-[var(--spacing-scale-012)] items-center relative`; + + return ( + + ); + }, +); + +StepperContainer.displayName = "Stepper"; + +export default StepperContainer; diff --git a/app/components/Stepper/Stepper.types.ts b/app/components/Stepper/Stepper.types.ts new file mode 100644 index 0000000..7c635b4 --- /dev/null +++ b/app/components/Stepper/Stepper.types.ts @@ -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; +} diff --git a/app/components/Stepper/Stepper.view.tsx b/app/components/Stepper/Stepper.view.tsx new file mode 100644 index 0000000..3ffda2e --- /dev/null +++ b/app/components/Stepper/Stepper.view.tsx @@ -0,0 +1,50 @@ +import type { StepperViewProps } from "./Stepper.types"; + +export function StepperView({ + active, + totalSteps, + className, + stepperClasses, +}: StepperViewProps) { + return ( +
+ {Array.from({ length: totalSteps }, (_, index) => { + const stepNumber = index + 1; + const isActive = stepNumber === active; + + return ( +
+ +
+ ); + })} +
+ ); +} diff --git a/app/components/Stepper/index.tsx b/app/components/Stepper/index.tsx new file mode 100644 index 0000000..5d41a67 --- /dev/null +++ b/app/components/Stepper/index.tsx @@ -0,0 +1 @@ +export { default } from "./Stepper.container"; diff --git a/stories/Progress.stories.js b/stories/Progress.stories.js new file mode 100644 index 0000000..1af6b69 --- /dev/null +++ b/stories/Progress.stories.js @@ -0,0 +1,120 @@ +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", + }, +}; + +export const AllStates = { + args: {}, + render: (_args) => ( +
+
+

Progress: 1-0 (1 segment)

+ +
+
+

Progress: 1-1 (1 segment + partial)

+ +
+
+

Progress: 1-5 (1 segment + full partial)

+ +
+
+

Progress: 2-0 (2 segments)

+ +
+
+

Progress: 2-1 (2 segments + partial)

+ +
+
+

Progress: 2-2 (2 segments + partial)

+ +
+
+

Progress: 3-0 (3 segments - complete)

+ +
+
+

Progress: 3-1 (3 segments - complete)

+ +
+
+

Progress: 3-2 (3 segments - complete)

+ +
+
+ ), + parameters: { + docs: { + description: { + story: "Different progress states of the progress bar component.", + }, + }, + }, +}; + +export const ProgressExamples = { + args: {}, + render: () => ( +
+
+

Early (1-0)

+ +
+
+

Middle (2-1)

+ +
+
+

Complete (3-2)

+ +
+
+ ), + parameters: { + docs: { + description: { + story: "Common progress bar examples.", + }, + }, + }, +}; diff --git a/stories/Stepper.stories.js b/stories/Stepper.stories.js new file mode 100644 index 0000000..71281a8 --- /dev/null +++ b/stories/Stepper.stories.js @@ -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) => ( +
+
+

Step 1 of 5

+ +
+
+

Step 2 of 5

+ +
+
+

Step 3 of 5

+ +
+
+

Step 4 of 5

+ +
+
+

Step 5 of 5

+ +
+
+ ), + parameters: { + docs: { + description: { + story: "Different active states of the stepper component.", + }, + }, + }, +}; + +export const DifferentStepCounts = { + args: {}, + render: () => ( +
+
+

3 Steps - Step 2

+ +
+
+

5 Steps - Step 3

+ +
+
+

7 Steps - Step 4

+ +
+
+ ), + parameters: { + docs: { + description: { + story: "Stepper with different total step counts.", + }, + }, + }, +}; diff --git a/tests/components/Progress.test.tsx b/tests/components/Progress.test.tsx new file mode 100644 index 0000000..23edb33 --- /dev/null +++ b/tests/components/Progress.test.tsx @@ -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; + +const baseProps: ProgressProps = { + progress: "2-1", +}; + +const config: ComponentTestSuiteConfig = { + 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(config); + +describe("Progress (behavioral tests)", () => { + it("renders progress bar with correct progress value", () => { + render(); + 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(); + expect(container.firstChild).toHaveClass("custom-class"); + }); + + it("defaults to progress 3-2 when progress is not specified", () => { + render(); + 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(); + const progressbar = screen.getByRole("progressbar"); + expect(progressbar).toHaveAttribute("aria-valuenow", String(expected)); + unmount(); + }); + }); +}); diff --git a/tests/components/Stepper.test.tsx b/tests/components/Stepper.test.tsx new file mode 100644 index 0000000..98fad2a --- /dev/null +++ b/tests/components/Stepper.test.tsx @@ -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; + +const baseProps: StepperProps = { + active: 1, + totalSteps: 5, +}; + +const config: ComponentTestSuiteConfig = { + 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(config); + +describe("Stepper (behavioral tests)", () => { + it("renders with correct number of steps", () => { + render(); + 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(); + // 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(); + expect(container.firstChild).toHaveClass("custom-class"); + }); + + it("defaults to active 1 when active is not specified", () => { + render(); + const progressbar = screen.getByRole("progressbar"); + expect(progressbar).toHaveAttribute("aria-valuenow", "1"); + }); + + it("defaults to totalSteps 5 when totalSteps is not specified", () => { + render(); + const progressbar = screen.getByRole("progressbar"); + expect(progressbar).toHaveAttribute("aria-valuemax", "5"); + }); +}); From 9d4a5f4238a8d3886eba93a56efe019e0c4aa63a Mon Sep 17 00:00:00 2001 From: adilallo <39313955+adilallo@users.noreply.github.com> Date: Mon, 2 Feb 2026 11:32:06 -0700 Subject: [PATCH 2/3] Fixes on progress component to pass tests --- stories/Progress.stories.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/stories/Progress.stories.js b/stories/Progress.stories.js index 1af6b69..8b8d8e2 100644 --- a/stories/Progress.stories.js +++ b/stories/Progress.stories.js @@ -54,7 +54,9 @@ export const AllStates = {
-

Progress: 1-5 (1 segment + full partial)

+

+ Progress: 1-5 (1 segment + full partial) +

From 6223ac5475e7d8432b423babc1c4fb08fc85338a Mon Sep 17 00:00:00 2001 From: adilallo <39313955+adilallo@users.noreply.github.com> Date: Mon, 2 Feb 2026 11:41:49 -0700 Subject: [PATCH 3/3] Adjust progress stories --- stories/Progress.stories.js | 60 ++++++++++++++----------------------- 1 file changed, 22 insertions(+), 38 deletions(-) diff --git a/stories/Progress.stories.js b/stories/Progress.stories.js index 8b8d8e2..2e3be3c 100644 --- a/stories/Progress.stories.js +++ b/stories/Progress.stories.js @@ -39,6 +39,7 @@ export const Default = { args: { progress: "3-2", }, + render: (args) => , }; export const AllStates = { @@ -46,41 +47,51 @@ export const AllStates = { render: (_args) => (
-

Progress: 1-0 (1 segment)

+

1-0

-

Progress: 1-1 (1 segment + partial)

+

1-1

-

- Progress: 1-5 (1 segment + full partial) -

+

1-2

+ +
+
+

1-3

+ +
+
+

1-4

+ +
+
+

1-5

-

Progress: 2-0 (2 segments)

+

2-0

-

Progress: 2-1 (2 segments + partial)

+

2-1

-

Progress: 2-2 (2 segments + partial)

+

2-2

-

Progress: 3-0 (3 segments - complete)

+

3-0

-

Progress: 3-1 (3 segments - complete)

+

3-1

-

Progress: 3-2 (3 segments - complete)

+

3-2

@@ -93,30 +104,3 @@ export const AllStates = { }, }, }; - -export const ProgressExamples = { - args: {}, - render: () => ( -
-
-

Early (1-0)

- -
-
-

Middle (2-1)

- -
-
-

Complete (3-2)

- -
-
- ), - parameters: { - docs: { - description: { - story: "Common progress bar examples.", - }, - }, - }, -};