From 00f73635529f423b0227057c063e6744b5df4bcb Mon Sep 17 00:00:00 2001 From: Berhane Yohannes Date: Thu, 8 Aug 2024 19:57:55 +0100 Subject: [PATCH 01/15] Stepper component with example demo --- example/src/components/StepperDemo.tsx | 167 +++++++++++++ example/src/components/index.ts | 1 + example/src/components/stepper.scss | 46 ++++ example/src/index.tsx | 4 +- src/index.ts | 1 + src/stepper/Step.tsx | 10 + src/stepper/StepContent.tsx | 33 +++ src/stepper/StepHeader.tsx | 50 ++++ src/stepper/Stepper.tsx | 132 +++++++++++ src/stepper/UseStepper.tsx | 23 ++ src/stepper/__test__/Step.test.tsx | 25 ++ src/stepper/__test__/StepContent.test.tsx | 35 +++ src/stepper/__test__/StepHeader.test.tsx | 53 +++++ src/stepper/__test__/Stepper.test.tsx | 273 ++++++++++++++++++++++ src/stepper/__test__/UseStepper.test.tsx | 61 +++++ src/stepper/index.ts | 4 + 16 files changed, 917 insertions(+), 1 deletion(-) create mode 100644 example/src/components/StepperDemo.tsx create mode 100644 example/src/components/stepper.scss create mode 100644 src/stepper/Step.tsx create mode 100644 src/stepper/StepContent.tsx create mode 100644 src/stepper/StepHeader.tsx create mode 100644 src/stepper/Stepper.tsx create mode 100644 src/stepper/UseStepper.tsx create mode 100644 src/stepper/__test__/Step.test.tsx create mode 100644 src/stepper/__test__/StepContent.test.tsx create mode 100644 src/stepper/__test__/StepHeader.test.tsx create mode 100644 src/stepper/__test__/Stepper.test.tsx create mode 100644 src/stepper/__test__/UseStepper.test.tsx create mode 100644 src/stepper/index.ts diff --git a/example/src/components/StepperDemo.tsx b/example/src/components/StepperDemo.tsx new file mode 100644 index 00000000..7143533f --- /dev/null +++ b/example/src/components/StepperDemo.tsx @@ -0,0 +1,167 @@ +import React from 'react'; +import { + Step, + Stepper, + StepHeader, + StepContent, +} from '@capgeminiuk/dcx-react-library'; +import './stepper.scss'; + +export const StepperDemo = () => { + const [activeStepHorizontal, setActiveStepHorizontal] = React.useState(0); + const [activeStepVertical, setActiveStepVertical] = React.useState(0); + const [activeStepCustomSeparator, setActiveStepCustomSeparator] = React.useState(0); + + const moveNextHorizontal = () => { + setActiveStepHorizontal(activeStepHorizontal + 1); + }; + + const movePrevHorizontal = () => { + setActiveStepHorizontal(activeStepHorizontal - 1); + }; + + const moveNextVertical = () => { + setActiveStepVertical(activeStepVertical + 1); + }; + + const movePrevVertical = () => { + setActiveStepVertical(activeStepVertical - 1); + }; + + const moveNextCustomSeparator = () => { + setActiveStepCustomSeparator(activeStepCustomSeparator + 1); + }; + + const movePrevCustomSeparator = () => { + setActiveStepCustomSeparator(activeStepCustomSeparator - 1); + }; + + return ( +
+

Horizontal Stepper

+ }> + + +
1
+ Select campaign settings +
+ +
Configure the basic settings for your campaign, such as name, budget, and duration.
+
+ +
+
+
+ + +
2
+ Create an ad group +
+ +
Define the target audience and bidding strategy for your ad group.
+
+ + +
+
+
+ + +
3
+ Create an ad +
+ +
Design your ad content, including images, text, and call-to-action.
+
+ +
+
+
+
+ +

Vertical Stepper

+ }> + + +
1
+ Select campaign settings +
+ +
Configure the basic settings for your campaign, such as name, budget, and duration.
+
+ +
+
+
+ + +
2
+ Create an ad group +
+ +
Define the target audience and bidding strategy for your ad group.
+
+ + +
+
+
+ + +
3
+ Create an ad +
+ +
Design your ad content, including images, text, and call-to-action.
+
+ +
+
+
+
+ +

Stepper with Custom Separator

+ |
}> + + +
1
+ Select campaign settings +
+ +
Configure the basic settings for your campaign, such as name, budget, and duration.
+
+ +
+
+
+ + +
2
+ Create an ad group +
+ +
Define the target audience and bidding strategy for your ad group.
+
+ + +
+
+
+ + +
3
+ Create an ad +
+ +
Design your ad content, including images, text, and call-to-action.
+
+ +
+
+
+ + + ); +}; + +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..b8fde7ad --- /dev/null +++ b/example/src/components/stepper.scss @@ -0,0 +1,46 @@ +.dcx-horizontal-stepper { + display: flex; + flex-direction: row; + padding: 10px; +} + +.dcx-step { + display: flex; + flex-direction: column; + align-items: center; + margin-right: 20px; + position: relative; + z-index: 1; +} + +.dcx-stepper-header-button { + color: #a7a8ab; + margin-bottom: 10px; +} + +.dcx-stepper-header-button > .stepNumber { + background-color: #a7a8ab; +} + +.dcx-active-step { + color: black; +} + +.dcx-active-step > .stepNumber { + background-color: #1876d2; +} + +.stepNumber { + width: 20px; + height: 20px; + color: white; + border-radius: 10px; + margin-bottom: 5px; + background-color: rgba(0, 0, 0, 0.54); +} + +.separator { + border-color: #a7a8ab; + width: 120px; + margin: 20px 8px; +} \ 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..495290ab --- /dev/null +++ b/src/stepper/Step.tsx @@ -0,0 +1,10 @@ +import React from 'react'; + +export type StepProps = { + /** + * The children prop is an array of JSX elements that represent the content of the step. + */ + children: JSX.Element[]; +}; + +export const Step = ({ children }: 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..08c6536a --- /dev/null +++ b/src/stepper/Stepper.tsx @@ -0,0 +1,132 @@ +import React, { useState, useEffect, Children, cloneElement, memo } from 'react'; +import { StepperContext } from './UseStepper'; +import { classNames } from '../common'; + +export type StepperProps = { + /** + * Specifies the content of the stepper. The allowed elements are Step, StepHeader, StepContent. + */ + children: JSX.Element[]; + /** + * Specifies a custom separator. + */ + separator?: JSX.Element; + /** + * Programmatically set the active step (starts from 0). + */ + selectedStep?: number; + /** + * Specifies a specific class for the selected step. + */ + activeStepClass?: string; + /** + * Defines the className of the entire stepper. + */ + stepperClassName?: string; + /** + * Defines the className of the header container. + */ + headerContainerClassNames?: string; + /** + * Defines the style of all StepHeader elements from the parent. + * To style them independently, use className on the StepHeader element. + */ + headerClassName?: string; + /** + * Defines the className of the content container. + */ + contentContainerClassNames?: string; + /** + * Defines the style of all StepContent elements from the parent. + * To style them independently, use className on the StepContent element. + */ + contentClassName?: string; + /** + * Defines the orientation of the stepper. + */ + orientation?: 'horizontal' | 'vertical'; +}; + +export const Stepper = memo(({ + children, + separator, + selectedStep = 0, + activeStepClass, + stepperClassName, + headerClassName, + contentClassName, + orientation = 'horizontal', +}: StepperProps) => { + const [activeStep, setActiveStep] = useState(selectedStep); + + useEffect(() => { + setActiveStep(selectedStep); + }, [selectedStep]); + + const onClickHandler = (index: number) => setActiveStep(index); + + const steps: 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-active-step': index === activeStep }, + { [`${activeStepClass}`]: index === activeStep }, + headerClassName, + ]); + + stepHeader = cloneElement(child, { + key: `header-${index}`, + _index: index, + headerClassName: 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') { + stepContent = cloneElement(child, { + key: `content-${index}`, + className: contentClassName, + visible: index === activeStep, + }); + } + }); + + if (stepHeader && stepContent) { + steps.push( +
+ {stepHeader} + {stepContent} +
+ ); + + if (separator && index < children.length - 1) { + steps.push( + cloneElement(separator, { key: `separator-${index}`, className: 'dcx-separator' }) + ); + } + } + } + }); + + const containerClasses = classNames([ + 'dcx-stepper', + orientation === 'horizontal' ? 'dcx-horizontal-stepper' : 'dcx-vertical-stepper', + stepperClassName, + ]); + + return ( + +
+ {steps} +
+
+ ); +}); \ No newline at end of file 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..ebcbba8b --- /dev/null +++ b/src/stepper/__test__/Step.test.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import { render } 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(); + }); +}); \ 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..fc070af8 --- /dev/null +++ b/src/stepper/__test__/Stepper.test.tsx @@ -0,0 +1,273 @@ +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-step'); + }); + + 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-step'); + }); + + it('applies custom class names', () => { + render( + + + Step 1 + Content 1 + + + Step 2 + Content 2 + + + ); + + expect(screen.getByText('Step 1').parentElement?.parentElement).toHaveClass('dcx-stepper custom-stepper'); + expect(screen.getByText('Content 1').parentElement?.parentElement).toHaveClass('dcx-stepper 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-step'); + + rerender( + + + Step 1 + Content 1 + + + Step 2 + Content 2 + + + ); + + expect(screen.getByText('Step 2').parentElement).toHaveClass('dcx-step'); + }); + + 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 out of bounds selectedStep prop', () => { + render( + + + Step 1 + Content 1 + + + Step 2 + Content 2 + + + ); + + expect(screen.getByText('Step 2').parentElement).toHaveClass('dcx-step'); + }); + + 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-step'); + }); + + it('applies activeStepClass to the active step', () => { + render( + + + Step 1 + Content 1 + + + Step 2 + Content 2 + + + ); + + expect(screen.getByText('Step 2').parentElement).toHaveClass('dcx-step'); + }); + + 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').parentElement?.parentElement).toHaveClass('dcx-horizontal-stepper'); + + rerender( + + + Step 1 + Content 1 + + + Step 2 + Content 2 + + + ); + + expect(screen.getByText('Step 1').parentElement?.parentElement).toHaveClass('dcx-vertical-stepper'); + }); +}); \ No newline at end of file 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 From 610b3fde70b38336bddce6e27c4fc8aebe48b77e Mon Sep 17 00:00:00 2001 From: Berhane Yohannes Date: Wed, 21 Aug 2024 17:32:19 +0100 Subject: [PATCH 02/15] feat: Add stepper component and demos --- example/src/components/StepperDemo.tsx | 483 +++++++++++++++++-------- example/src/components/stepper.scss | 107 ++++-- src/stepper/Step.tsx | 8 +- src/stepper/Stepper.tsx | 8 +- src/stepper/__test__/Stepper.test.tsx | 4 +- 5 files changed, 437 insertions(+), 173 deletions(-) diff --git a/example/src/components/StepperDemo.tsx b/example/src/components/StepperDemo.tsx index 7143533f..c500d416 100644 --- a/example/src/components/StepperDemo.tsx +++ b/example/src/components/StepperDemo.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useState } from 'react'; import { Step, Stepper, @@ -7,161 +7,362 @@ import { } from '@capgeminiuk/dcx-react-library'; import './stepper.scss'; -export const StepperDemo = () => { - const [activeStepHorizontal, setActiveStepHorizontal] = React.useState(0); - const [activeStepVertical, setActiveStepVertical] = React.useState(0); - const [activeStepCustomSeparator, setActiveStepCustomSeparator] = React.useState(0); +const StepperDemo: React.FC = () => { + const [activeStepHorizontal, setActiveStepHorizontal] = useState(0); + const [activeStepVertical, setActiveStepVertical] = useState(0); + const [activeStepCustomSeparator, setActiveStepCustomSeparator] = useState(0); + const [activeStepItems, setActiveStepItems] = useState(0); - const moveNextHorizontal = () => { - setActiveStepHorizontal(activeStepHorizontal + 1); + const handleStepChange = ( + setter: React.Dispatch>, + step: number + ) => { + setter(step); }; - const movePrevHorizontal = () => { - setActiveStepHorizontal(activeStepHorizontal - 1); - }; - - const moveNextVertical = () => { - setActiveStepVertical(activeStepVertical + 1); - }; - - const movePrevVertical = () => { - setActiveStepVertical(activeStepVertical - 1); - }; - - const moveNextCustomSeparator = () => { - setActiveStepCustomSeparator(activeStepCustomSeparator + 1); - }; + 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 movePrevCustomSeparator = () => { - setActiveStepCustomSeparator(activeStepCustomSeparator - 1); - }; + 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: ( +
+ +
+ ), + }, + ]; - return ( -
-

Horizontal Stepper

- }> - - -
1
- Select campaign settings -
- -
Configure the basic settings for your campaign, such as name, budget, and duration.
-
- + const renderStepper = ( + activeStep: number, + setter: React.Dispatch>, + items: any[], + orientation: 'horizontal' | 'vertical', + customSeparator: JSX.Element | undefined = undefined + ) => ( + } + > + {items.map((item, index) => ( + + +
+ {activeStep > index ? '✔️' : index + 1}
- -
- - -
2
- Create an ad group -
- -
Define the target audience and bidding strategy for your ad group.
-
- - -
-
-
- - -
3
- Create an ad + {item.header}
- -
Design your ad content, including images, text, and call-to-action.
-
- + +
{item.content}
+
+ {index > 0 && ( + + )} + {index < items.length - 1 && ( + + )}
- + ))} + + ); + + return ( +
+

Horizontal Stepper

+ {renderStepper( + activeStepHorizontal, + setActiveStepHorizontal, + steps, + 'horizontal' + )}

Vertical Stepper

- }> - - -
1
- Select campaign settings -
- -
Configure the basic settings for your campaign, such as name, budget, and duration.
-
- -
-
-
- - -
2
- Create an ad group -
- -
Define the target audience and bidding strategy for your ad group.
-
- - -
-
-
- - -
3
- Create an ad -
- -
Design your ad content, including images, text, and call-to-action.
-
- -
-
-
-
+ {renderStepper( + activeStepVertical, + setActiveStepVertical, + steps, + 'vertical' + )}

Stepper with Custom Separator

- |
}> - - -
1
- Select campaign settings -
- -
Configure the basic settings for your campaign, such as name, budget, and duration.
-
- -
-
-
- - -
2
- Create an ad group -
- -
Define the target audience and bidding strategy for your ad group.
-
- - -
-
-
- - -
3
- Create an ad -
- -
Design your ad content, including images, text, and call-to-action.
-
- -
-
-
- + {renderStepper( + activeStepCustomSeparator, + setActiveStepCustomSeparator, + steps, + 'horizontal', + | + )} + +

Order Process Stepper

+ {renderStepper(activeStepItems, setActiveStepItems, items, 'horizontal')}
); }; -export default StepperDemo; \ No newline at end of file +export default StepperDemo; diff --git a/example/src/components/stepper.scss b/example/src/components/stepper.scss index b8fde7ad..b88c832d 100644 --- a/example/src/components/stepper.scss +++ b/example/src/components/stepper.scss @@ -1,46 +1,105 @@ +.stepper-demo { + padding: 20px; +} + .dcx-horizontal-stepper { display: flex; flex-direction: row; - padding: 10px; + align-items: center; + justify-content: space-between; + flex-wrap: nowrap; } -.dcx-step { +.dcx-vertical-stepper { display: flex; flex-direction: column; - align-items: center; - margin-right: 20px; - position: relative; - z-index: 1; + align-items: flex-start; +} + +.step { + display: flex; + flex-direction: column; + align-items: flex-start; + padding: 10px; + margin-right: 20px; /* Add spacing between horizontal steps */ } -.dcx-stepper-header-button { - color: #a7a8ab; +.step-header { + display: flex; + align-items: center; + font-weight: bold; margin-bottom: 10px; } -.dcx-stepper-header-button > .stepNumber { - background-color: #a7a8ab; +.step-number { + width: 25px; + height: 25px; + color: white; + border-radius: 50%; + background-color: rgba(0, 0, 0, 0.54); + display: flex; + align-items: center; + justify-content: center; + margin-right: 10px; +} + +.step-content { + display: flex; + flex-direction: column; + gap: 10px; } -.dcx-active-step { - color: black; +.separator, +.custom-separator { + margin: 0 10px; + border: 0; + height: 1px; + background: #ccc; } -.dcx-active-step > .stepNumber { - background-color: #1876d2; +.button-container { + display: flex; + justify-content: space-between; /* Adjusted to align buttons better */ + margin-top: 15px; + width: 100%; } -.stepNumber { - width: 20px; - height: 20px; +.button-container button { + padding: 8px 16px; + background-color: #007bff; color: white; - border-radius: 10px; - margin-bottom: 5px; - background-color: rgba(0, 0, 0, 0.54); + border: none; + border-radius: 4px; + cursor: pointer; +} + +.button-container button:hover { + background-color: #0056b3; +} + +.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; } -.separator { - border-color: #a7a8ab; - width: 120px; - margin: 20px 8px; +.step-separator { + border: 0; + height: 1px; + background: #ccc; + margin: 20px 0; + width: 100%; } \ No newline at end of file diff --git a/src/stepper/Step.tsx b/src/stepper/Step.tsx index 495290ab..36ceae92 100644 --- a/src/stepper/Step.tsx +++ b/src/stepper/Step.tsx @@ -1,10 +1,14 @@ import React from 'react'; -export type StepProps = { +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 }: StepProps) => <>{children}; \ No newline at end of file +export const Step = ({ children, ...rest }: StepProps) => ( +
+ {children} +
+); \ No newline at end of file diff --git a/src/stepper/Stepper.tsx b/src/stepper/Stepper.tsx index 08c6536a..b6a03ce1 100644 --- a/src/stepper/Stepper.tsx +++ b/src/stepper/Stepper.tsx @@ -16,9 +16,9 @@ export type StepperProps = { */ selectedStep?: number; /** - * Specifies a specific class for the selected step. + * Specifies a specific className for the selected step. */ - activeStepClass?: string; + activeStepClassName?: string; /** * Defines the className of the entire stepper. */ @@ -51,7 +51,7 @@ export const Stepper = memo(({ children, separator, selectedStep = 0, - activeStepClass, + activeStepClassName, stepperClassName, headerClassName, contentClassName, @@ -76,7 +76,7 @@ export const Stepper = memo(({ if (child.type.name === 'StepHeader') { const headerClasses = classNames([ { 'dcx-active-step': index === activeStep }, - { [`${activeStepClass}`]: index === activeStep }, + { [`${activeStepClassName}`]: index === activeStep }, headerClassName, ]); diff --git a/src/stepper/__test__/Stepper.test.tsx b/src/stepper/__test__/Stepper.test.tsx index fc070af8..1166d208 100644 --- a/src/stepper/__test__/Stepper.test.tsx +++ b/src/stepper/__test__/Stepper.test.tsx @@ -204,9 +204,9 @@ describe('Stepper Component', () => { expect(screen.getByText('Content 2').parentElement).toHaveClass('dcx-step'); }); - it('applies activeStepClass to the active step', () => { + it('applies activeStepClassName to the active step', () => { render( - + Step 1 Content 1 From 7ec8a27d82f92552159cccff106a4f7dbe05628f Mon Sep 17 00:00:00 2001 From: Berhane Yohannes Date: Wed, 21 Aug 2024 17:33:21 +0100 Subject: [PATCH 03/15] feat: Add Stepper documentation and live demos with out class --- stories/Stepper/ClassBased.stories.js | 160 ++++++++++++++++++++++++++ stories/Stepper/Documentation.mdx | 57 +++++++++ stories/Stepper/Live.stories.js | 23 ++++ stories/Stepper/UnStyled.stories.js | 52 +++++++++ stories/liveEdit/StepperLive.tsx | 51 ++++++++ 5 files changed, 343 insertions(+) create mode 100644 stories/Stepper/ClassBased.stories.js create mode 100644 stories/Stepper/Documentation.mdx create mode 100644 stories/Stepper/Live.stories.js create mode 100644 stories/Stepper/UnStyled.stories.js create mode 100644 stories/liveEdit/StepperLive.tsx diff --git a/stories/Stepper/ClassBased.stories.js b/stories/Stepper/ClassBased.stories.js new file mode 100644 index 00000000..288c7a79 --- /dev/null +++ b/stories/Stepper/ClassBased.stories.js @@ -0,0 +1,160 @@ +import { + Stepper, + Step, + StepHeader, + StepContent, +} from '../../src/stepper'; + +/** + * In this section, we are using the Stepper component styled with the GovUk style by passing the relative className. 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: 'Basic', + render: function (args) { + return ( + + + + Step 1: Introduction + + +

+ This is the content for Step 1: Introduction. +

+
+
+ + + Step 2: Details + + +

+ This is the content for Step 2: Details. +

+
+
+ + + Step 3: Confirmation + + +

+ This is the content for Step 3: Confirmation. +

+
+
+
+ ); + }, + args: { + activeStep: 0, + }, +}; + +/** + * Passing the property *activeStep* we can set the initial active step upon initialization. + */ +export const defaultActiveStep = { + name: 'Default Active Step', + render: function (args) { + return ( + + + + Step 1: Introduction + + +

+ This is the content for Step 1: Introduction. +

+
+
+ + + Step 2: Details + + +

+ This is the content for Step 2: Details. +

+
+
+ + + Step 3: Confirmation + + +

+ This is the content for Step 3: Confirmation. +

+
+
+
+ ); + }, + args: { + activeStep: 1, + }, +}; + +/** + * Passing the property *stepClassName* and *contentClassName* as props at the root level will allow for custom styling of the step and content sections of the stepper. + */ +export const definedStepAndContentClassNames = { + name: 'Global classNames', + render: function (args) { + return ( + + + + Step 1: Introduction + + +

+ This is the content for Step 1: Introduction. +

+
+
+ + + Step 2: Details + + +

+ This is the content for Step 2: Details. +

+
+
+ + + Step 3: Confirmation + + +

+ This is the content for Step 3: Confirmation. +

+
+
+
+ ); + }, + args: { + activeStep: 0, + stepClassName: 'govuk-stepper__step-header', + contentClassName: 'govuk-stepper__step-content', + }, +}; \ 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..45df148d --- /dev/null +++ b/stories/liveEdit/StepperLive.tsx @@ -0,0 +1,51 @@ +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 ( + + + 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.

+
+
+
+ ); +} +`; + +const StepperLive = () => { + const scope = { Stepper, Step, StepHeader, StepContent }; + return ( + +
+ + +
+ +
+ ); +}; + +export default StepperLive; \ No newline at end of file From ac9a308f663f46b8f76bcc853521e357666fcbb5 Mon Sep 17 00:00:00 2001 From: Berhane Yohannes Date: Thu, 22 Aug 2024 15:46:52 +0100 Subject: [PATCH 04/15] feat: Refactor stepper component styles and add demos --- example/src/components/stepper.scss | 135 +++++--- stories/Stepper/ClassBased.stories.js | 476 ++++++++++++++++++++++---- stories/Stepper/style.css | 254 ++++++++++++++ 3 files changed, 744 insertions(+), 121 deletions(-) create mode 100644 stories/Stepper/style.css diff --git a/example/src/components/stepper.scss b/example/src/components/stepper.scss index b88c832d..37fe0956 100644 --- a/example/src/components/stepper.scss +++ b/example/src/components/stepper.scss @@ -1,85 +1,127 @@ -.stepper-demo { +.stepper-container { + max-width: 800px; + margin: 0 auto; padding: 20px; + text-align: center; } -.dcx-horizontal-stepper { +.stepper-header { display: flex; - flex-direction: row; - align-items: center; - justify-content: space-between; + justify-content: center; + margin-bottom: 20px; flex-wrap: nowrap; } -.dcx-vertical-stepper { - display: flex; - flex-direction: column; - align-items: flex-start; -} - -.step { - display: flex; - flex-direction: column; - align-items: flex-start; +.step-header-item { + flex: 1; + text-align: center; + cursor: pointer; padding: 10px; - margin-right: 20px; /* Add spacing between horizontal steps */ + position: relative; } -.step-header { - display: flex; - align-items: center; +.step-header-item.active { + color: #007bff; font-weight: bold; - margin-bottom: 10px; } -.step-number { - width: 25px; - height: 25px; +.step-header-item.inactive { + color: #666; +} + +.step-header-item.completed { + color: #28a745; +} + +.step-number-container { + width: 30px; + height: 30px; color: white; border-radius: 50%; background-color: rgba(0, 0, 0, 0.54); display: flex; align-items: center; justify-content: center; - margin-right: 10px; + margin: 0 auto; } -.step-content { +.step-header-item.completed .step-number-container { + background-color: #28a745; +} + +.step-header-item.active .step-number-container { + background-color: #007bff; +} + +.step-content-wrapper { display: flex; flex-direction: column; - gap: 10px; + align-items: center; } -.separator, -.custom-separator { - margin: 0 10px; - border: 0; - height: 1px; - background: #ccc; +.step-content { + padding: 20px; + border: 1px solid #ccc; + border-radius: 4px; + width: 100%; + max-width: 600px; + margin-bottom: 15px; } .button-container { display: flex; - justify-content: space-between; /* Adjusted to align buttons better */ - margin-top: 15px; - width: 100%; + justify-content: center; + gap: 10px; } -.button-container button { +.nav-button { padding: 8px 16px; background-color: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; + font-size: 18px; } -.button-container button:hover { +.nav-button:hover { background-color: #0056b3; } -.form-label { +.nav-button.prev { + background-color: #6c757d; +} + +.nav-button.prev:hover { + background-color: #5a6268; +} + +.nav-button.next { + background-color: #28a745; +} + +.nav-button.next:hover { + background-color: #218838; +} + +.nav-button.submit { + background-color: #007bff; +} + +.nav-button.submit:hover { + background-color: #0056b3; +} + +.form-row { display: flex; - flex-direction: column; + align-items: center; + margin-bottom: 10px; +} + +.form-label { + width: 200px; + text-align: right; + padding-right: 10px; font-weight: bold; } @@ -87,19 +129,16 @@ .form-select, .form-textarea { padding: 8px; - margin-top: 5px; border: 1px solid #ccc; border-radius: 4px; + flex: 1; +} + +.form-textarea { + resize: vertical; + height: 80px; } .form-checkbox { margin-top: 10px; } - -.step-separator { - border: 0; - height: 1px; - background: #ccc; - margin: 20px 0; - width: 100%; -} \ No newline at end of file diff --git a/stories/Stepper/ClassBased.stories.js b/stories/Stepper/ClassBased.stories.js index 288c7a79..5e0e3929 100644 --- a/stories/Stepper/ClassBased.stories.js +++ b/stories/Stepper/ClassBased.stories.js @@ -1,12 +1,14 @@ +import React, { useState } from 'react'; import { Stepper, Step, StepHeader, StepContent, } from '../../src/stepper'; +import './style.css'; /** - * In this section, we are using the Stepper component styled with the GovUk style by passing the relative className. Feel free to use your own CSS and style the Stepper component as you prefer. + * 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', @@ -66,95 +68,423 @@ export const BasicStepper = { }; /** - * Passing the property *activeStep* we can set the initial active step upon initialization. + * This component renders a vertical stepper with custom class names. */ -export const defaultActiveStep = { - name: 'Default Active Step', +export const VerticalStepper = { + name: 'Vertical Stepper', render: function (args) { + const [activeStep, setActiveStep] = useState(0); + + const handleStepChange = (step) => { + setActiveStep(step); + }; + return ( - - - - Step 1: Introduction - - -

- This is the content for Step 1: Introduction. -

-
-
- - - Step 2: Details - - -

- This is the content for Step 2: Details. -

-
-
- - - Step 3: Confirmation - - -

- This is the content for Step 3: Confirmation. -

-
-
+ } + > + {args.steps.map((step, index) => ( + + +
+ {activeStep > index ? '✔️' : index + 1} +
+ {step.header} +
+ +
{step.content}
+
+ {index > 0 && ( + + )} + {index < args.steps.length - 1 && ( + + )} +
+
+
+ ))}
); }, args: { - activeStep: 1, + activeStep: 0, + 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. +

+ +
+ ), + }, + ], }, }; + /** - * Passing the property *stepClassName* and *contentClassName* as props at the root level will allow for custom styling of the step and content sections of the stepper. + * This component renders a horizontal stepper with a custom separator and custom class names. */ -export const definedStepAndContentClassNames = { - name: 'Global classNames', +export const CustomSeparatorStepper = { + name: 'Stepper with Custom Separator', 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 ( - - - - Step 1: Introduction - - -

- This is the content for Step 1: Introduction. -

-
-
- - - Step 2: Details - - -

- This is the content for Step 2: Details. -

-
-
- - - Step 3: Confirmation - - -

- This is the content for Step 3: Confirmation. -

-
-
-
+
+ |} + className="stepper-horizontal" + > + {args.steps.map((step, index) => ( + + +
+ {activeStep > index ? '✔️' : index + 1} +
+ {step.header} +
+ + {step.content} +
+ + {!isLastStep && ( + + )} + {isLastStep && ( + + )} +
+
+
+ ))} +
+
+ ); + }, + 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 & 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) => ( +
index ? 'completed' : activeStep === index ? 'active' : 'inactive'}`} + onClick={() => handleStepChange(index)} + aria-label={`Step ${index + 1}`} + > +
+ {activeStep > index ? '✔️' : index + 1} +
+
+ {step.header} +
+
+ ))} +
+ +
+
+ {steps[activeStep].content} +
+ +
+ {activeStep > 0 && ( + + )} + {activeStep < steps.length - 1 && ( + + )} + {activeStep === steps.length - 1 && ( + + )} +
+
+
); }, args: { activeStep: 0, - stepClassName: 'govuk-stepper__step-header', - contentClassName: 'govuk-stepper__step-content', }, }; \ No newline at end of file diff --git a/stories/Stepper/style.css b/stories/Stepper/style.css new file mode 100644 index 00000000..4ef1dbeb --- /dev/null +++ b/stories/Stepper/style.css @@ -0,0 +1,254 @@ +/* Container */ +.stepper-container { + max-width: 800px; + margin: 0 auto; + padding: 20px; + text-align: center; +} + +/* Header */ +.stepper-header { + display: flex; + justify-content: center; + margin-bottom: 20px; + flex-wrap: nowrap; +} + +.step-header-item { + flex: 1; + text-align: center; + cursor: pointer; + padding: 10px; + position: relative; +} + +.step-header-item.active { + color: #007bff; + font-weight: bold; +} + +.step-header-item.inactive { + color: #666; +} + +.step-header-item.completed { + color: #28a745; +} + +/* Step Number Container */ +.step-number-container { + width: 30px; + height: 30px; + color: white; + border-radius: 50%; + background-color: rgba(0, 0, 0, 0.54); + display: flex; + align-items: center; + justify-content: center; + margin: 0 auto; +} + +.step-header-item.completed .step-number-container { + background-color: #28a745; +} + +.step-header-item.active .step-number-container { + background-color: #007bff; +} + +/* Content */ +.step-content-wrapper { + display: flex; + flex-direction: column; + align-items: center; +} + +.step-content { + padding: 20px; + border-radius: 4px; + width: 100%; + max-width: 600px; + margin-bottom: 15px; +} + +/* Buttons */ +.button-container { + display: flex; + justify-content: center; + gap: 10px; +} + +.nav-button { + padding: 8px 16px; + background-color: #007bff; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 18px; +} + +.nav-button:hover { + background-color: #0056b3; +} + +.nav-button.prev { + background-color: #6c757d; +} + +.nav-button.prev:hover { + background-color: #5a6268; +} + +.nav-button.next { + background-color: #28a745; +} + +.nav-button.next:hover { + background-color: #218838; +} + +.nav-button.submit { + background-color: #007bff; +} + +.nav-button.submit:hover { + background-color: #0056b3; +} + +/* Forms */ +.form-row { + display: flex; + align-items: center; + margin-bottom: 10px; +} + +.form-label { + width: 200px; + text-align: right; + padding-right: 10px; + font-weight: bold; +} + +.form-input, +.form-select, +.form-textarea { + padding: 8px; + border: 1px solid #ccc; + border-radius: 4px; + flex: 1; +} + +.form-textarea { + resize: vertical; + height: 80px; +} + +.form-checkbox { + margin-top: 10px; +} + +/* Separator */ +.separator { + width: 100%; + border: 1px solid #ccc; + margin: 20px 0; +} + +.custom-separator { + font-size: 24px; + font-weight: bold; + color: #007bff; +} + + +/* custom separtator stepper */ + +.stepper-wrapper { + display: flex; + flex-direction: column; + align-items: center; + padding: 20px; +} + +.stepper-horizontal { + display: flex; + flex-direction: row; + align-items: center; + flex-wrap: nowrap; /* Prevent wrapping */ +} + +.step-item { + display: flex; + flex-direction: column; + align-items: center; + margin: 0 20px; /* Adjust spacing between steps */ + width: 150px; /* Set a fixed width for steps to ensure horizontal alignment */ +} + +.step-header-wrapper { + display: flex; + flex-direction: column; + align-items: center; + font-weight: bold; + text-align: center; + margin-bottom: 10px; /* Space between header and content */ +} + +.step-number-badge { + font-size: 24px; + background-color: #007bff; + color: white; + width: 40px; + height: 40px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 5px; +} + +.step-item.active .step-number-badge { + background-color: #28a745; /* Green for active step */ +} + +.step-content-wrapper { + display: flex; + flex-direction: column; + align-items: center; + margin-top: 10px; + text-align: center; /* Center content text */ +} + +.navigation-buttons { + display: flex; + justify-content: center; + gap: 10px; /* Adjust spacing between buttons */ + margin-top: 20px; /* More space above buttons */ +} + +.navigation-button { + padding: 8px 16px; + background-color: #007bff; + color: white; + border: none; + border-radius: 5px; + cursor: pointer; + transition: background-color 0.3s; + font-size: 16px; /* Adjust button text size */ +} + +.navigation-button:hover { + background-color: #0056b3; /* Darker blue on hover */ +} + +.navigation-button:disabled { + background-color: #cccccc; /* Grey when disabled */ + cursor: not-allowed; +} + +.separator-custom { + margin: 0 20px; + font-size: 24px; + color: #cccccc; +} From 66010db36e3c0ee8a5b5e5d6d42d275c2e711a80 Mon Sep 17 00:00:00 2001 From: Berhane Yohannes Date: Thu, 22 Aug 2024 18:22:57 +0100 Subject: [PATCH 05/15] Refactor stepper component styles and add demos --- example/src/components/StepperDemo.tsx | 38 +-- example/src/components/stepper.scss | 135 ++++------ src/stepper/__test__/Step.test.tsx | 13 +- stories/Stepper/BasicStepper.css | 36 +++ stories/Stepper/ClassBased.stories.js | 342 ++++++++----------------- stories/Stepper/StepperDemo.css | 139 ++++++++++ stories/Stepper/VerticalStepper.css | 91 +++++++ stories/Stepper/style.css | 254 ------------------ 8 files changed, 453 insertions(+), 595 deletions(-) create mode 100644 stories/Stepper/BasicStepper.css create mode 100644 stories/Stepper/StepperDemo.css create mode 100644 stories/Stepper/VerticalStepper.css delete mode 100644 stories/Stepper/style.css diff --git a/example/src/components/StepperDemo.tsx b/example/src/components/StepperDemo.tsx index c500d416..ef40888c 100644 --- a/example/src/components/StepperDemo.tsx +++ b/example/src/components/StepperDemo.tsx @@ -26,14 +26,14 @@ const StepperDemo: React.FC = () => { content: (
@@ -55,11 +55,11 @@ const StepperDemo: React.FC = () => {
@@ -70,11 +70,11 @@ const StepperDemo: React.FC = () => { content: (