diff --git a/example/src/components/StepperDemo.tsx b/example/src/components/StepperDemo.tsx
new file mode 100644
index 00000000..374d48b1
--- /dev/null
+++ b/example/src/components/StepperDemo.tsx
@@ -0,0 +1,410 @@
+import React, { useState } from 'react';
+import {
+ Step,
+ Stepper,
+ StepHeader,
+ StepContent,
+} from '@capgeminiuk/dcx-react-library';
+import './stepper.scss';
+
+const StepperDemo = () => {
+ const [activeStepHorizontal, setActiveStepHorizontal] = useState(0);
+ const [activeStepVertical, setActiveStepVertical] = useState(0);
+ const [activeStepCustomSeparator, setActiveStepCustomSeparator] = useState(0);
+ const [activeStepItems, setActiveStepItems] = useState(0);
+
+ const steps = [
+ {
+ header: 'Campaign Settings',
+ content: (
+
+
+ Campaign Name:
+
+
+
+ Budget:
+
+
+ Schedule:
+
+
+
+ ),
+ },
+ {
+ header: 'Target Audience',
+ content: (
+
+
+ Age Range:
+
+ 18-24
+ 25-34
+ 35-44
+ 45-54
+ 55-64
+ 65+
+
+
+
+ Location:
+
+
+
+ Interests:
+
+
+
+ ),
+ },
+ {
+ header: 'Ad Design',
+ content: (
+
+
+ Ad Title:
+
+
+
+ Ad Description:
+
+
+
+ Call to Action:
+
+ Buy Now
+ Learn More
+ Sign Up
+
+
+
+ ),
+ },
+ {
+ header: 'Review & Submit',
+ content: (
+
+
+ Please review your campaign settings, target audience, and ad design
+ before submitting.
+
+
+ Submit Campaign
+
+
+ ),
+ },
+ ];
+
+ const items = [
+ {
+ header: 'Order Summary',
+ content: (
+
+
+ Product Name:
+
+
+
+ Quantity:
+
+
+
+ Price:
+
+
+
+ ),
+ },
+ {
+ header: 'Shipping Information',
+ content: (
+
+
+ Shipping Method:
+
+ Standard
+ Express
+
+
+
+ Address:
+
+
+
+ ),
+ },
+ {
+ header: 'Payment Information',
+ content: (
+
+
+ Credit Card Number:
+
+
+
+ Expiration Date:
+
+
+
+ CVV:
+
+
+
+ ),
+ },
+ {
+ header: 'Billing Information',
+ content: (
+
+
+ Street Address:
+
+
+
+ City:
+
+
+
+ County:
+
+
+
+ Post Code:
+
+
+
+ ),
+ },
+ {
+ header: 'Review and Confirm',
+ content: (
+
+
+ Order Notes:
+
+
+
+ Agree to Terms:
+
+
+
+ ),
+ },
+ {
+ header: 'Place Order',
+ content: (
+
+
+ Submit Order
+
+
+ ),
+ },
+ ];
+
+ const renderStepper = (
+ activeStep: number,
+ setActiveStep: React.Dispatch>,
+ steps: { header: string; content: React.ReactNode }[],
+ orientation: 'horizontal' | 'vertical',
+ customSeparator: JSX.Element | undefined = undefined
+ ) => (
+ }
+ >
+ {steps.map((step, index) => (
+
+ setActiveStep(index)} style={{ cursor: 'pointer', display: 'flex', alignItems: 'center' }}>
+ = index ? '#1976d2' : '#D1D0CE',
+ color: 'white',
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ marginRight: '10px',
+ fontSize: '14px',
+ fontWeight: 'bold'
+ }}>
+ {activeStep > index ? '✓' : index + 1}
+
+ {step.header}
+
+
+ {step.content}
+
+ {index > 0 && (
+ setActiveStep(index - 1)}
+ aria-label="Previous Step"
+ style={{
+ padding: '10px 20px',
+ fontSize: '14px',
+ backgroundColor: '#1976d2',
+ color: 'white',
+ border: 'none',
+ borderRadius: '4px',
+ cursor: 'pointer',
+ margin: '0 5px',
+ boxShadow: '0 2px 4px rgba(0, 0, 0, 0.1)',
+ }}
+ >
+ Prev
+
+ )}
+ {index < steps.length - 1 && (
+ setActiveStep(index + 1)}
+ aria-label="Next Step"
+ style={{
+ padding: '10px 20px',
+ fontSize: '14px',
+ backgroundColor: '#1976d2',
+ color: 'white',
+ border: 'none',
+ borderRadius: '4px',
+ cursor: 'pointer',
+ margin: '0 5px',
+ boxShadow: '0 2px 4px rgba(0, 0, 0, 0.1)',
+ }}
+ >
+ Next
+
+ )}
+ {index === steps.length - 1 && (
+ alert('Form Submitted')}
+ aria-label="Submit Form"
+ style={{
+ padding: '10px 20px',
+ fontSize: '14px',
+ backgroundColor: '#28a745',
+ color: 'white',
+ border: 'none',
+ borderRadius: '4px',
+ cursor: 'pointer',
+ margin: '0 5px',
+ boxShadow: '0 2px 4px rgba(0, 0, 0, 0.1)',
+ }}
+ >
+ Submit
+
+ )}
+
+
+
+ ))}
+
+ );
+
+ return (
+
+
Horizontal Stepper
+ {renderStepper(activeStepHorizontal, setActiveStepHorizontal, steps, 'horizontal')}
+
+ Vertical Stepper
+ {renderStepper(activeStepVertical, setActiveStepVertical, steps, 'vertical')}
+
+ Stepper with Custom Separator
+ {renderStepper(
+ activeStepCustomSeparator,
+ setActiveStepCustomSeparator,
+ steps,
+ 'horizontal',
+ |
+ )}
+
+ Order Process Stepper
+ {renderStepper(
+ activeStepItems,
+ setActiveStepItems,
+ items,
+ 'horizontal'
+ )}
+
+ );
+};
+
+export default StepperDemo;
\ No newline at end of file
diff --git a/example/src/components/index.ts b/example/src/components/index.ts
index 8937358f..c1a48537 100644
--- a/example/src/components/index.ts
+++ b/example/src/components/index.ts
@@ -29,3 +29,4 @@ export { ButtonGroupDemo } from './ButtonGroupDemo';
export { CardDemo } from './CardDemo';
export { SkeletonDemo } from './SkeletonDemo';
export { LoadingSpinnerDemo } from './LoadingSpinnerDemo';
+export { default as StepperDemo } from './StepperDemo';
diff --git a/example/src/components/stepper.scss b/example/src/components/stepper.scss
new file mode 100644
index 00000000..e1395dbf
--- /dev/null
+++ b/example/src/components/stepper.scss
@@ -0,0 +1,121 @@
+.dcx-stepper {
+ width: 100%;
+ margin-bottom: 40px;
+}
+
+.dcx-horizontal-stepper .dcx-header-container {
+ display: flex;
+ flex-direction: row;
+ align-items: flex-start;
+ justify-content: space-between;
+ flex-wrap: nowrap;
+}
+
+.dcx-vertical-stepper .dcx-header-container {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+}
+
+.dcx-header-wrapper {
+ display: flex;
+ align-items: center;
+ margin-bottom: 10px;
+}
+
+.dcx-step-header {
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ font-weight: bold;
+ font-size: 16px;
+ color: #333;
+}
+
+.dcx-step-header .step-number {
+ width: 30px;
+ height: 30px;
+ border-radius: 50%;
+ background-color: #d1d0ce;
+ color: white;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin-right: 10px;
+ font-size: 14px;
+ font-weight: bold;
+}
+
+.dcx-step-header .step-number.active {
+ background-color: #1976d2;
+}
+
+.dcx-step-content {
+ display: none;
+}
+
+.dcx-step-content.dcx-visible-content {
+ display: block;
+}
+
+.dcx-content-container {
+ margin-top: 20px;
+}
+
+.step-button {
+ padding: 10px 20px;
+ font-size: 14px;
+ background-color: #1976d2;
+ color: white;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+ margin: 0 5px;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+ transition: background-color 0.3s ease;
+}
+
+.step-button:hover {
+ background-color: #0056b3;
+}
+
+.step-button.submit {
+ background-color: #28a745;
+}
+
+.step-button.submit:hover {
+ background-color: #218838;
+}
+
+.custom-separator {
+ margin: 0 10px;
+ border: 0;
+ height: 1px;
+ background: #ccc;
+}
+
+.form-label {
+ display: flex;
+ flex-direction: column;
+ font-weight: bold;
+}
+
+.form-input,
+.form-select,
+.form-textarea {
+ padding: 8px;
+ margin-top: 5px;
+ border: 1px solid #ccc;
+ border-radius: 4px;
+}
+
+.form-checkbox {
+ margin-top: 10px;
+}
+
+.custom-separator {
+ width: 100%;
+ border: 0;
+ border-top: 1px solid #ccc;
+ margin: 10px 0;
+}
\ No newline at end of file
diff --git a/example/src/index.tsx b/example/src/index.tsx
index 5d9fe603..ca8331c6 100644
--- a/example/src/index.tsx
+++ b/example/src/index.tsx
@@ -27,7 +27,8 @@ import {
TableDemo,
ToggleDemo,
TooltipDemo,
- LoadingSpinnerDemo
+ LoadingSpinnerDemo,
+ StepperDemo,
} from './components';
import { BrowserRouter, Route, Routes } from 'react-router-dom';
@@ -83,6 +84,7 @@ const App = () => (
} />
} />
} />
+ } />
diff --git a/src/index.ts b/src/index.ts
index f65386f8..70afc10b 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -40,3 +40,4 @@ export * from './card';
export * from './paginator';
export * from './skeleton';
export * from './spinner';
+export * from './stepper';
diff --git a/src/stepper/Step.tsx b/src/stepper/Step.tsx
new file mode 100644
index 00000000..36ceae92
--- /dev/null
+++ b/src/stepper/Step.tsx
@@ -0,0 +1,14 @@
+import React from 'react';
+
+type StepProps = React.HTMLAttributes & {
+ /**
+ * The children prop is an array of JSX elements that represent the content of the step.
+ */
+ children: JSX.Element[];
+};
+
+export const Step = ({ children, ...rest }: StepProps) => (
+
+ {children}
+
+);
\ No newline at end of file
diff --git a/src/stepper/StepContent.tsx b/src/stepper/StepContent.tsx
new file mode 100644
index 00000000..e8409f17
--- /dev/null
+++ b/src/stepper/StepContent.tsx
@@ -0,0 +1,33 @@
+import React from 'react';
+import { classNames } from '../common';
+
+export type StepContentProps = {
+ /**
+ * it will allow to specify the content you prefer
+ */
+ children?: any;
+ /**
+ * will allow to style the content of each step
+ */
+ className?: string;
+ /**
+ * allow to show hide the content
+ */
+ visible?: boolean;
+};
+
+export const StepContent = ({
+ children,
+ className,
+ visible,
+}: StepContentProps) => (
+
+ {children}
+
+);
\ No newline at end of file
diff --git a/src/stepper/StepHeader.tsx b/src/stepper/StepHeader.tsx
new file mode 100644
index 00000000..c84103c3
--- /dev/null
+++ b/src/stepper/StepHeader.tsx
@@ -0,0 +1,50 @@
+import React from 'react';
+import { useStepper } from './UseStepper';
+import { classNames } from '../common';
+export type StepHeaderProps = {
+ /**
+ * this will allow to pass a custom content to the header like a span or other.
+ * The container will be a button
+ */
+ children?: JSX.Element;
+ /**
+ *
+ */
+ headerClassName?: string;
+ /**
+ * you can define a custom separator between each steps
+ */
+ separator?: JSX.Element;
+ /**
+ * internal usage to determine the content that need to be displayed
+ **/
+ _index?: number;
+};
+
+
+export const StepHeader = ({
+ _index,
+ separator,
+ children,
+ headerClassName,
+ ...props
+}: any) => {
+ const { changeActiveStep } = useStepper();
+ const headerClassNames = classNames([
+ 'dcx-stepper-header-content',
+ headerClassName,
+ ]);
+ return (
+ <>
+ changeActiveStep(_index)}
+ {...props}
+ >
+ {children}
+
+ <>{separator}>
+ >
+ );
+};
\ No newline at end of file
diff --git a/src/stepper/Stepper.tsx b/src/stepper/Stepper.tsx
new file mode 100644
index 00000000..8f9f8b0d
--- /dev/null
+++ b/src/stepper/Stepper.tsx
@@ -0,0 +1,164 @@
+import React, {
+ useState,
+ useEffect,
+ Children,
+ cloneElement,
+ memo,
+} from 'react';
+import { StepperContext } from './UseStepper';
+import { classNames } from '../common';
+
+export type StepperProps = {
+ /**
+ * An array of JSX elements representing the steps.
+ */
+ children: JSX.Element[];
+
+ /**
+ * An optional JSX element to be used as a separator between steps.
+ */
+ separator?: JSX.Element;
+
+ /**
+ * The index of the initially selected step. Defaults to 0.
+ */
+ selectedStep?: number;
+
+ /**
+ * The class name to be applied to the active step.
+ */
+ activeStepClassName?: string;
+
+ /**
+ * The class name to be applied to the stepper container.
+ */
+ stepperClassName?: string;
+
+ /**
+ * The class name to be applied to the header of each step.
+ */
+ headerClassName?: string;
+
+ /**
+ * The class name to be applied to the content of each step.
+ */
+ contentClassName?: string;
+
+ /**
+ * The orientation of the stepper, either 'horizontal' or 'vertical'.
+ */
+ orientation?: 'horizontal' | 'vertical';
+ /**
+ * This allows the Stepper component to accept any valid HTML attributes for a div element.
+ */
+ props?: React.HTMLAttributes;
+};
+
+export const Stepper = memo(
+ ({
+ children,
+ separator,
+ selectedStep = 0,
+ activeStepClassName,
+ stepperClassName,
+ headerClassName,
+ contentClassName,
+ orientation = 'horizontal',
+ ...props
+ }: StepperProps) => {
+ const [activeStep, setActiveStep] = useState(selectedStep);
+
+ useEffect(() => {
+ setActiveStep(selectedStep);
+ }, [selectedStep]);
+
+ const onClickHandler = (index: number) => setActiveStep(index);
+
+ const headers: JSX.Element[] = [];
+ const contents: JSX.Element[] = [];
+
+ Children.forEach(children, (child, index) => {
+ if (child.type.name === 'Step') {
+ let stepHeader: JSX.Element | null = null;
+ let stepContent: JSX.Element | null = null;
+
+ Children.forEach(child.props.children, (child) => {
+ if (child.type.name === 'StepHeader') {
+ const headerClasses = classNames([
+ 'dcx-step-header',
+ { 'dcx-active-step': index === activeStep },
+ { [`${activeStepClassName}`]: index === activeStep },
+ headerClassName,
+ ]);
+
+ stepHeader = cloneElement(child, {
+ key: `header-${index}`,
+ _index: index,
+ className: headerClasses,
+ 'aria-selected': index === activeStep ? 'true' : 'false',
+ 'aria-posinset': index + 1,
+ 'aria-setsize': Children.count(children),
+ tabIndex: index === activeStep ? '0' : '-1',
+ onClick: () => onClickHandler(index),
+ });
+ } else if (child.type.name === 'StepContent') {
+ const contentClasses = classNames([
+ 'dcx-step-content',
+ contentClassName,
+ { 'dcx-visible-content': index === activeStep },
+ ]);
+
+ stepContent = cloneElement(child, {
+ key: `content-${index}`,
+ className: contentClasses,
+ visible: index === activeStep,
+ });
+ }
+ });
+
+ if (stepHeader) {
+ headers.push(
+
+ {stepHeader}
+
+ );
+ if (separator && index < children.length - 1) {
+ headers.push(
+ cloneElement(separator, {
+ key: `separator-${index}`,
+ className: 'dcx-separator',
+ })
+ );
+ }
+ }
+
+ if (stepContent) {
+ contents.push(
+
+ {stepContent}
+
+ );
+ }
+ }
+ });
+
+ const containerClasses = classNames([
+ 'dcx-stepper',
+ orientation === 'horizontal'
+ ? 'dcx-horizontal-stepper'
+ : 'dcx-vertical-stepper',
+ stepperClassName,
+ ]);
+
+ return (
+
+
+
{headers}
+
{contents}
+
+
+ );
+ }
+);
diff --git a/src/stepper/UseStepper.tsx b/src/stepper/UseStepper.tsx
new file mode 100644
index 00000000..78f418cb
--- /dev/null
+++ b/src/stepper/UseStepper.tsx
@@ -0,0 +1,23 @@
+import { createContext, useContext } from 'react';
+
+export type StepperContextProps = {
+ /**
+ * it will provide the current step
+ */
+ activeStep: number;
+ /**
+ * it will allow to set the current step
+ */
+ changeActiveStep: (step: number) => void;
+};
+
+export const StepperContext =
+ createContext(undefined);
+
+export const useStepper = () => {
+ const context = useContext(StepperContext);
+ if (context === undefined) {
+ throw new Error('Step must be used within a Stepper');
+ }
+ return context;
+};
\ No newline at end of file
diff --git a/src/stepper/__test__/Step.test.tsx b/src/stepper/__test__/Step.test.tsx
new file mode 100644
index 00000000..87fd7051
--- /dev/null
+++ b/src/stepper/__test__/Step.test.tsx
@@ -0,0 +1,36 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import '@testing-library/jest-dom';
+import { Step } from '../Step';
+
+describe('Step', () => {
+ it('should render the Step component with children', () => {
+ const { getByText } = render(
+
+ {[Step Content
]}
+
+ );
+ expect(getByText('Step Content')).toBeInTheDocument();
+ });
+
+ it('should render the Step component with multiple children', () => {
+ const { getByText } = render(
+
+ {[Step 1
, Step 2
]}
+
+ );
+ expect(getByText('Step 1')).toBeInTheDocument();
+ expect(getByText('Step 2')).toBeInTheDocument();
+ });
+
+ it('applies additional props correctly', () => {
+ render(
+
+ {[Step 1
]}
+
+ );
+
+ const stepElement = screen.getByTestId('step');
+ expect(stepElement).toHaveClass('custom-step');
+ });
+});
\ No newline at end of file
diff --git a/src/stepper/__test__/StepContent.test.tsx b/src/stepper/__test__/StepContent.test.tsx
new file mode 100644
index 00000000..bbb3c662
--- /dev/null
+++ b/src/stepper/__test__/StepContent.test.tsx
@@ -0,0 +1,35 @@
+import React from 'react';
+import { render } from '@testing-library/react';
+import '@testing-library/jest-dom';
+import { StepContent } from '../StepContent';
+
+describe('StepContent Component', () => {
+ test('renders without crashing', () => {
+ const { container } = render( );
+ expect(container).toBeInTheDocument();
+ });
+
+ test('renders multiple children correctly', () => {
+ const { getByText } = render(
+
+ Child 1
+ Child 2
+
+ );
+ expect(getByText('Child 1')).toBeInTheDocument();
+ expect(getByText('Child 2')).toBeInTheDocument();
+ });
+
+ test('applies className prop correctly', () => {
+ const { container } = render( );
+ expect(container.firstChild).toHaveClass('custom-class');
+ });
+
+ test('visible prop controls display style correctly', () => {
+ const { container, rerender } = render( );
+ expect(container.firstChild).toHaveStyle('display: none');
+
+ rerender( );
+ expect(container.firstChild).toHaveStyle('display: inherit');
+ });
+});
\ No newline at end of file
diff --git a/src/stepper/__test__/StepHeader.test.tsx b/src/stepper/__test__/StepHeader.test.tsx
new file mode 100644
index 00000000..d0c53c90
--- /dev/null
+++ b/src/stepper/__test__/StepHeader.test.tsx
@@ -0,0 +1,53 @@
+import React from 'react';
+import { render, screen, fireEvent } from '@testing-library/react';
+import '@testing-library/jest-dom';
+import { StepHeader } from '../StepHeader';
+import { useStepper } from '../UseStepper';
+
+jest.mock('../UseStepper');
+
+describe('StepHeader', () => {
+ const mockChangeActiveStep = jest.fn();
+
+ beforeEach(() => {
+ (useStepper as jest.Mock).mockReturnValue({
+ changeActiveStep: mockChangeActiveStep,
+ });
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ test('renders correctly with default props', () => {
+ render( );
+ const button = screen.getByRole('tab');
+ expect(button).toBeInTheDocument();
+ expect(button).toHaveClass('dcx-stepper-header-content');
+ });
+
+ test('renders children correctly', () => {
+ render(Step 1 );
+ const child = screen.getByText('Step 1');
+ expect(child).toBeInTheDocument();
+ });
+
+ test('applies custom class names', () => {
+ render( );
+ const button = screen.getByRole('tab');
+ expect(button).toHaveClass('dcx-stepper-header-content custom-class');
+ });
+
+ test('calls changeActiveStep with correct index when clicked', () => {
+ render( );
+ const button = screen.getByRole('tab');
+ fireEvent.click(button);
+ expect(mockChangeActiveStep).toHaveBeenCalledWith(2);
+ });
+
+ test('renders the separator correctly', () => {
+ render(Separator} />);
+ const separator = screen.getByText('Separator');
+ expect(separator).toBeInTheDocument();
+ });
+});
\ No newline at end of file
diff --git a/src/stepper/__test__/Stepper.test.tsx b/src/stepper/__test__/Stepper.test.tsx
new file mode 100644
index 00000000..6290e72e
--- /dev/null
+++ b/src/stepper/__test__/Stepper.test.tsx
@@ -0,0 +1,321 @@
+import React from 'react';
+import { render, screen, fireEvent } from '@testing-library/react';
+import '@testing-library/jest-dom';
+import { Stepper } from '../Stepper';
+import { Step } from '../Step';
+import { StepHeader } from '../StepHeader';
+import { StepContent } from '../StepContent';
+
+describe('Stepper Component', () => {
+
+ it('renders Stepper component with default props', () => {
+ render(
+
+
+ Step 1
+ Content 1
+
+
+ Step 2
+ Content 2
+
+
+ );
+
+ expect(screen.getByText('Step 1')).toBeInTheDocument();
+ expect(screen.getByText('Content 1')).toBeInTheDocument();
+ expect(screen.getByText('Step 2')).toBeInTheDocument();
+ expect(screen.getByText('Content 2')).toBeInTheDocument();
+ });
+
+ it('throws an error if Step is used outside of Stepper', () => {
+ expect(() => {
+ render(
+
+ Step 1
+ Content 1
+
+ );
+ }).toThrow('Step must be used within a Stepper');
+ });
+
+ it('sets the active step based on selectedStep prop', () => {
+ render(
+
+
+ Step 1
+ Content 1
+
+
+ Step 2
+ Content 2
+
+
+ );
+
+ expect(screen.getByText('Step 2').parentElement).toHaveClass(
+ 'dcx-header-wrapper'
+ );
+ });
+
+ it('changes active step on header click', () => {
+ render(
+
+
+ Step 1
+ Content 1
+
+
+ Step 2
+ Content 2
+
+
+ );
+
+ fireEvent.click(screen.getByText('Step 2'));
+ expect(screen.getByText('Step 2').parentElement).toHaveClass(
+ 'dcx-header-wrapper'
+ );
+ });
+
+ it('applies custom class names', () => {
+ render(
+
+
+ Step 1
+ Content 1
+
+
+ Step 2
+ Content 2
+
+
+ );
+
+ expect(screen.getByText('Step 1').closest('.dcx-stepper')).toHaveClass(
+ 'custom-stepper'
+ );
+ expect(screen.getByText('Content 1').closest('.dcx-stepper')).toHaveClass(
+ 'custom-stepper'
+ );
+ });
+
+ it('renders custom separator', () => {
+ render(
+ |}>
+
+ Step 1
+ Content 1
+
+
+ Step 2
+ Content 2
+
+
+ );
+
+ const separators = document.querySelectorAll('span');
+ expect(separators.length).toBeGreaterThan(0);
+ expect(separators[0]).toHaveTextContent('|');
+ });
+
+ it('updates active step when selectedStep prop changes', () => {
+ const { rerender } = render(
+
+
+ Step 1
+ Content 1
+
+
+ Step 2
+ Content 2
+
+
+ );
+
+ expect(screen.getByText('Step 1').parentElement).toHaveClass('dcx-header-wrapper');
+
+ rerender(
+
+
+ Step 1
+ Content 1
+
+
+ Step 2
+ Content 2
+
+
+ );
+
+ expect(screen.getByText('Step 2').parentElement).toHaveClass(
+ 'dcx-header-wrapper'
+ );
+ });
+
+ it('renders correctly with no steps', () => {
+ render( );
+ expect(screen.queryByText('Step 1')).not.toBeInTheDocument();
+ expect(screen.queryByText('Step 2')).not.toBeInTheDocument();
+ });
+
+ it('renders correctly with one step', () => {
+ render(
+
+ {[
+
+ Step 1
+ Content 1
+ ,
+ ]}
+
+ );
+
+ expect(screen.getByText('Step 1')).toBeInTheDocument();
+ expect(screen.getByText('Content 1')).toBeInTheDocument();
+ });
+
+ it('handles Step with no StepHeader or StepContent', () => {
+ render(
+
+ {[
+
+ {/* No StepHeader or StepContent */}
+ <>>
+ <>>
+ ,
+ ]}
+
+ );
+
+ expect(screen.queryByText('Step 1')).not.toBeInTheDocument();
+ expect(screen.queryByText('Content 2')).not.toBeInTheDocument();
+ });
+
+ it('Stepper ignores non-Step child component', () => {
+ render(
+
+ Hello
+ Non-Step Component
+
+ );
+
+ expect(screen.queryByText('Hello')).not.toBeInTheDocument();
+ expect(screen.queryByText('Non-Step Component')).not.toBeInTheDocument();
+ });
+
+ it('handles out of bounds selectedStep prop', () => {
+ render(
+
+
+ Step 1
+ Content 1
+
+
+ Step 2
+ Content 2
+
+
+ );
+
+ expect(screen.getByText('Step 2').parentElement).toHaveClass(
+ 'dcx-header-wrapper'
+ );
+ });
+
+ it('updates context when step header is clicked', () => {
+ render(
+
+
+ Step 1
+ Content 1
+
+
+ Step 2
+ Content 2
+
+
+ );
+
+ fireEvent.click(screen.getByText('Step 2'));
+ expect(screen.getByText('Content 2').parentElement).toHaveClass(
+ 'dcx-content-wrapper'
+ );
+ });
+
+ it('applies activeStepClassName to the active step', () => {
+ render(
+
+
+ Step 1
+ Content 1
+
+
+ Step 2
+ Content 2
+
+
+ );
+
+ expect(screen.getByText('Step 2').parentElement).toHaveClass(
+ 'dcx-header-wrapper'
+ );
+ });
+
+ it('applies headerClassName and contentClassName to StepHeader and StepContent', () => {
+ render(
+
+
+ Step 1
+ Content 1
+
+
+ Step 2
+ Content 2
+
+
+ );
+
+ expect(screen.getByText('Step 1')).toHaveClass('custom-header');
+ expect(screen.getByText('Content 1')).toHaveClass('custom-content');
+ });
+
+ it('applies orientation class based on orientation prop', () => {
+ const { rerender } = render(
+
+
+ Step 1
+ Content 1
+
+
+ Step 2
+ Content 2
+
+
+ );
+
+ expect(screen.getByText('Step 1').closest('.dcx-stepper')).toHaveClass(
+ 'dcx-horizontal-stepper'
+ );
+
+ rerender(
+
+
+ Step 1
+ Content 1
+
+
+ Step 2
+ Content 2
+
+
+ );
+
+ expect(screen.getByText('Step 1').closest('.dcx-stepper')).toHaveClass(
+ 'dcx-vertical-stepper'
+ );
+ });
+});
diff --git a/src/stepper/__test__/UseStepper.test.tsx b/src/stepper/__test__/UseStepper.test.tsx
new file mode 100644
index 00000000..455f7901
--- /dev/null
+++ b/src/stepper/__test__/UseStepper.test.tsx
@@ -0,0 +1,61 @@
+import React from 'react';
+import { render } from '@testing-library/react';
+import { StepperContext, StepperContextProps, useStepper } from '../UseStepper';
+import '@testing-library/jest-dom';
+
+const TestComponent: React.FC = () => {
+ const { activeStep, changeActiveStep } = useStepper();
+
+ return (
+
+ {activeStep}
+ changeActiveStep(2)}>Change Step
+
+ );
+};
+
+describe('useStepper', () => {
+ it('provides activeStep as a number', () => {
+ const mockContextValue: StepperContextProps = {
+ activeStep: 0,
+ changeActiveStep: jest.fn(),
+ };
+
+ const { getByTestId } = render(
+
+
+
+ );
+
+ expect(getByTestId('active-step')).toHaveTextContent('0');
+ });
+
+ it('calls changeActiveStep when the button is clicked', () => {
+ const mockChangeActiveStep = jest.fn();
+ const mockContextValue: StepperContextProps = {
+ activeStep: 0,
+ changeActiveStep: mockChangeActiveStep,
+ };
+
+ const { getByText } = render(
+
+
+
+ );
+
+ const button = getByText('Change Step');
+ button.click();
+
+ expect(mockChangeActiveStep).toHaveBeenCalledWith(2);
+ });
+
+ it('throws an error if used outside of StepperContext', () => {
+ const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
+
+ expect(() => {
+ render( );
+ }).toThrow('Step must be used within a Stepper');
+
+ consoleErrorSpy.mockRestore();
+ });
+});
\ No newline at end of file
diff --git a/src/stepper/index.ts b/src/stepper/index.ts
new file mode 100644
index 00000000..cdd83982
--- /dev/null
+++ b/src/stepper/index.ts
@@ -0,0 +1,4 @@
+export { Stepper } from './Stepper';
+export { Step } from './Step';
+export { StepHeader } from './StepHeader';
+export { StepContent } from './StepContent';
\ No newline at end of file
diff --git a/stories/Stepper/ClassBased.stories.js b/stories/Stepper/ClassBased.stories.js
new file mode 100644
index 00000000..c8de171b
--- /dev/null
+++ b/stories/Stepper/ClassBased.stories.js
@@ -0,0 +1,521 @@
+/* eslint-disable @typescript-eslint/no-unused-vars */
+import React, { useState } from 'react';
+import {
+ Stepper,
+ Step,
+ StepHeader,
+ StepContent,
+} from '../../src/stepper';
+
+/**
+ * In this section, we are using the Stepper component styled with custom style. Feel free to use your own CSS and style the Stepper component as you prefer.
+ */
+export default {
+ title: 'DCXLibrary/Layout/Stepper/Class based',
+ component: Stepper,
+ parameters: {
+ options: {
+ showPanel: true,
+ },
+ },
+ tags: ['autodocs'],
+};
+
+/**
+ * By default, the Stepper is designed to have only one step active at a time.
+ */
+
+export const BasicStepper = {
+ name: 'Horizontal Stepper',
+ render: function (args) {
+ const [activeStep, setActiveStep] = useState(0);
+
+ const handleStepChange = (step) => {
+ setActiveStep(step);
+ };
+
+ const steps = [
+ {
+ header: 'Introduction',
+ content: 'This is the content for the Introduction step.',
+ },
+ {
+ header: 'Details',
+ content: 'This is the content for the Details step.',
+ },
+ {
+ header: 'Confirmation',
+ content: 'This is the content for the Confirmation step.',
+ },
+ ];
+
+ return (
+
+
+ {steps.map((step, index) => (
+
handleStepChange(index)}
+ aria-label={`Step ${index + 1}`}
+ style={{
+ display: 'flex',
+ flexDirection: 'column',
+ alignItems: 'center',
+ textAlign: 'center',
+ cursor: 'pointer',
+ flex: 1,
+ }}
+ >
+
+
= index ? '#1976d2' : '#D1D0CE',
+ color: 'white',
+ fontSize: '16px',
+ fontWeight: 'bold',
+ marginBottom: '8px',
+ position: 'relative',
+ }}>
+ {activeStep > index ? (
+ ✓
+ ) : (
+ index + 1
+ )}
+
+
+ {step.header}
+
+
+
+ ))}
+
+
+
+ {steps.map((step, index) => (
+ activeStep === index && (
+
+ {step.content}
+
+ )
+ ))}
+
+
+
+ handleStepChange(activeStep - 1)}
+ aria-label="Previous Step"
+ disabled={activeStep === 0}
+ >
+ Prev
+
+ {activeStep < steps.length - 1 && (
+ handleStepChange(activeStep + 1)}
+ aria-label="Next Step"
+ >
+ Next
+
+ )}
+ {activeStep === steps.length - 1 && (
+ alert('Form Submitted')}
+ aria-label="Submit Form"
+ >
+ Submit
+
+ )}
+
+
+ );
+ },
+ args: {
+ activeStep: 0,
+ },
+};
+/**
+ * This component renders a vertical stepper with custom class names.
+ */
+export const VerticalStepper = {
+ name: 'Vertical Stepper',
+ render: function (args) {
+ const [activeStep, setActiveStep] = useState(0);
+
+ const handleStepChange = (step) => {
+ if (step >= 0 && step < args.steps.length) {
+ setActiveStep(step);
+ }
+ };
+
+ const isLastStep = activeStep === args.steps.length - 1;
+
+ return (
+
+
+ {args.steps.map((step, index) => (
+
+
+
= index ? '#1976d2' : '#D1D0CE',
+ color: 'white',
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ marginRight: '16px',
+ fontSize: '16px',
+ fontWeight: 'bold',
+ position: 'relative',
+ }}
+ >
+ {activeStep > index ? (
+ ✓
+ ) : (
+ index + 1
+ )}
+
+
+ {step.header}
+
+
+ {activeStep === index && (
+
+
{step.content}
+
+ handleStepChange(index - 1)}
+ style={{
+ padding: '10px 20px',
+ fontSize: '14px',
+ backgroundColor: '#1976d2',
+ color: 'white',
+ border: 'none',
+ borderRadius: '4px',
+ cursor: 'pointer',
+ marginRight: '8px',
+ }}
+ disabled={index === 0}
+ >
+ Back
+
+ handleStepChange(index + 1)}
+ style={{
+ padding: '10px 20px',
+ fontSize: '14px',
+ backgroundColor: isLastStep ? '#28a745' : '#1976d2',
+ color: 'white',
+ border: 'none',
+ borderRadius: '4px',
+ cursor: 'pointer',
+ }}
+ >
+ {isLastStep ? 'Submit' : 'Continue'}
+
+
+
+ )}
+
+ ))}
+
+
+ );
+ },
+ args: {
+ activeStep: 0,
+ steps: [
+ {
+ header: 'Campaign Settings',
+ content: 'Configure your campaign settings including name, budget, and schedule.',
+ },
+ {
+ header: 'Target Audience',
+ content: 'Define your target audience by specifying age range, location, and interests.',
+ },
+ {
+ header: 'Ad Design',
+ content: 'Design your ad by providing a title, description, and call to action.',
+ },
+ {
+ header: 'Review and Submit',
+ content: 'Review all your settings and submit your campaign for approval.',
+ },
+ ],
+ onSubmit: () => alert('Campaign Submitted!'),
+ },
+};
+
+
+
+/**
+ * This is a demo component for the Stepper with multiple form sections.
+ */
+export const StepperDemo = {
+ name: 'Stepper Demo',
+ render: function (args) {
+ const [activeStep, setActiveStep] = useState(0);
+
+ const handleStepChange = (step) => {
+ setActiveStep(step);
+ };
+
+ const steps = [
+ {
+ header: 'Personal Information',
+ content: (
+
+ ),
+ },
+ {
+ header: 'Address Details',
+ content: (
+
+ ),
+ },
+ {
+ header: 'Payment Information',
+ content: (
+
+ ),
+ },
+ {
+ header: 'Shipping Details',
+ content: (
+
+
+ Shipping Method:
+
+ Standard
+ Express
+
+
+
+ Shipping Address:
+
+
+
+ ),
+ },
+ {
+ header: 'Review and Submit',
+ content: (
+
+
+ Comments:
+
+
+
+ Agree to Terms:
+
+
+
+ ),
+ },
+ ];
+
+ return (
+
+
+ {steps.map((step, index) => (
+
handleStepChange(index)}
+ aria-label={`Step ${index + 1}`}
+ style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', cursor: 'pointer', flex: 1 }}
+ >
+
+
= index ? '#1976d2' : '#D1D0CE', color: 'white', fontSize: '16px', fontWeight: 'bold', marginBottom: '8px', position: 'relative' }}>
+ {activeStep > index ? (
+ ✓
+ ) : (
+ index + 1
+ )}
+
+
+ {step.header}
+
+
+
+ ))}
+
+
+
+ {steps.map((step, index) => (
+ activeStep === index && (
+
+ {step.content}
+
+ )
+ ))}
+
+
+
+ handleStepChange(activeStep - 1)}
+ aria-label="Previous Step"
+ disabled={activeStep === 0}
+ >
+ Prev
+
+ {activeStep < steps.length - 1 && (
+ handleStepChange(activeStep + 1)}
+ aria-label="Next Step"
+ >
+ Next
+
+ )}
+ {activeStep === steps.length - 1 && (
+ alert('Form Submitted')}
+ aria-label="Submit Form"
+ >
+ Submit
+
+ )}
+
+
+ );
+ },
+ args: {
+ activeStep: 0,
+ },
+};
\ No newline at end of file
diff --git a/stories/Stepper/Documentation.mdx b/stories/Stepper/Documentation.mdx
new file mode 100644
index 00000000..7a13c85e
--- /dev/null
+++ b/stories/Stepper/Documentation.mdx
@@ -0,0 +1,57 @@
+import { Meta, Story, Canvas, ArgTypes } from '@storybook/addon-docs';
+import * as StepperStories from './UnStyled.stories';
+
+
+
+# Stepper
+
+The Stepper component is designed to facilitate the creation of multi-step workflows in your application. It provides a structured way to guide users through a series of steps, ensuring a smooth and intuitive process.
+The `Stepper` component handles the active step state and offers navigation controls to move between steps seamlessly.
+
+Each `Step` represents a step in the process. It has a `StepHeader` for the title and a `StepContent` for the details of the step.
+
+This documentation provides an overview of the Stepper component, how to use it, and the available properties.
+In order to use the Stepper component in your project, you should import all the following necessary components to use the Stepper in your project:
+
+```js
+ import { Stepper, Step, StepHeader, StepContent } from '@capgeminiuk/dcx-react-library';
+```
+
+
+When you import the Stepper component without providing any className or style associated, it will look as follows:
+
+
+
+An example with all the available properties is:
+
+```js
+
+
+ Step 1
+
+ This is the content for step 1. Here you can provide detailed instructions or information.
+
+
+
+ Step 2
+
+ This is the content for step 2. Continue providing information or instructions here.
+
+
+
+ Step 3
+
+ This is the content for step 3. Finalize your instructions or information here.
+
+
+
+
+```
+
+
\ No newline at end of file
diff --git a/stories/Stepper/Live.stories.js b/stories/Stepper/Live.stories.js
new file mode 100644
index 00000000..e87b681f
--- /dev/null
+++ b/stories/Stepper/Live.stories.js
@@ -0,0 +1,23 @@
+import { Stepper } from '../../src/stepper';
+import StepperLive from '../liveEdit/StepperLive';
+
+export default {
+ title: 'DCXLibrary/Layout/Stepper/Live',
+ component: Stepper,
+
+ parameters: {
+ options: {
+ showPanel: false,
+ },
+ viewMode: 'docs',
+ previewTabs: {
+ canvas: {
+ hidden: true,
+ },
+ },
+ },
+};
+
+export const Live = {
+ render: () => ,
+};
\ No newline at end of file
diff --git a/stories/Stepper/UnStyled.stories.js b/stories/Stepper/UnStyled.stories.js
new file mode 100644
index 00000000..8c1b4687
--- /dev/null
+++ b/stories/Stepper/UnStyled.stories.js
@@ -0,0 +1,52 @@
+import React from 'react';
+import { Stepper, Step, StepHeader, StepContent } from '../../src/stepper';
+
+export default {
+ title: 'DCXLibrary/Layout/Stepper/Without style',
+ component: Stepper,
+ parameters: {
+ options: {
+ showPanel: true,
+ },
+ },
+};
+
+export const Unstyled = {
+ render: function (args) {
+ return (
+
+
+ Step 1
+
+
+ This is the content for step 1. Here you can provide detailed instructions or information.
+
+
+
+
+ Step 2
+
+
+ This is the content for step 2. Continue providing information or instructions here.
+
+
+
+
+ Step 3
+
+
+ This is the content for step 3. Finalize your instructions or information here.
+
+
+
+
+ );
+ },
+ args: {
+ selectedStep: 0,
+ activeStepClass: 'active-step',
+ stepperClassName: 'custom-stepper',
+ headerClassName: 'custom-header',
+ contentClassName: 'custom-content',
+ },
+};
\ No newline at end of file
diff --git a/stories/liveEdit/StepperLive.tsx b/stories/liveEdit/StepperLive.tsx
new file mode 100644
index 00000000..2ed9fcd3
--- /dev/null
+++ b/stories/liveEdit/StepperLive.tsx
@@ -0,0 +1,167 @@
+import React from 'react';
+import { LiveProvider, LiveEditor, LiveError, LivePreview } from 'react-live';
+import { Stepper, Step, StepHeader, StepContent } from '../../src/stepper';
+
+const StepperDemo = `
+function StepperDemo() {
+ return (
+
+
+
+
+ 1
+
+ Step 1
+
+
+ This is the content for step 1. Here you can provide detailed instructions or information.
+
+
+
+
+
+ 2
+
+ Step 2
+
+
+ This is the content for step 2. Continue providing information or instructions here.
+
+
+
+
+
+ 3
+
+ Step 3
+
+
+ This is the content for step 3. Finalize your instructions or information here.
+
+
+
+ );
+}
+`;
+
+const StepperLive = () => {
+ const scope = { Stepper, Step, StepHeader, StepContent };
+ return (
+
+
+
+
+
+
+
+ );
+};
+
+export default StepperLive;
\ No newline at end of file