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: ( +
+ + + +
+ ), + }, + { + header: 'Target Audience', + content: ( +
+ + + +
+ ), + }, + { + header: 'Ad Design', + content: ( +
+ + + +
+ ), + }, + { + header: 'Review & Submit', + content: ( +
+

+ Please review your campaign settings, target audience, and ad design + before submitting. +

+ +
+ ), + }, + ]; + + const items = [ + { + header: 'Order Summary', + content: ( +
+ + + +
+ ), + }, + { + header: 'Shipping Information', + content: ( +
+ + +
+ ), + }, + { + header: 'Payment Information', + content: ( +
+ + + +
+ ), + }, + { + header: 'Billing Information', + content: ( +
+ + + + +
+ ), + }, + { + header: 'Review and Confirm', + content: ( +
+ + +
+ ), + }, + { + header: 'Place Order', + content: ( +
+ +
+ ), + }, + ]; + + 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 && ( + + )} + {index < steps.length - 1 && ( + + )} + {index === steps.length - 1 && ( + + )} +
+
+
+ ))} +
+ ); + + 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 ( + <> + + <>{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} + +
+ ); +}; + +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} +
+ ) + ))} +
+ +
+ + {activeStep < steps.length - 1 && ( + + )} + {activeStep === steps.length - 1 && ( + + )} +
+
+ ); + }, + 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}

+
+ + +
+
+ )} +
+ ))} +
+
+ ); + }, + 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: ( +
+
+ + +
+
+ + +
+
+ ), + }, + { + header: 'Review and Submit', + content: ( +
+
+ + +
+
+ + +
+
+ ), + }, + ]; + + 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} +
+ ) + ))} +
+ +
+ + {activeStep < steps.length - 1 && ( + + )} + {activeStep === steps.length - 1 && ( + + )} +
+
+ ); + }, + 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