From 612d999a183f4b5cca692a092da5e5c36642dda8 Mon Sep 17 00:00:00 2001 From: Berhane Yohannes Date: Fri, 13 Sep 2024 14:53:43 +0100 Subject: [PATCH] Resolve PR comment and implement the stepper component to match material UI design requirement and improve demos --- example/src/components/StepperDemo.tsx | 114 +++++-- example/src/components/stepper.scss | 98 +++--- src/stepper/Stepper.tsx | 73 ++-- src/stepper/__test__/Stepper.test.tsx | 14 +- stories/Stepper/ClassBased.stories.js | 445 ++++++++++++++++--------- stories/Stepper/StepperDemo.css | 188 ----------- stories/Stepper/VerticalStepper.css | 127 ------- stories/liveEdit/StepperLive.tsx | 134 +++++++- 8 files changed, 598 insertions(+), 595 deletions(-) delete mode 100644 stories/Stepper/StepperDemo.css delete mode 100644 stories/Stepper/VerticalStepper.css diff --git a/example/src/components/StepperDemo.tsx b/example/src/components/StepperDemo.tsx index ef40888c..374d48b1 100644 --- a/example/src/components/StepperDemo.tsx +++ b/example/src/components/StepperDemo.tsx @@ -7,19 +7,12 @@ import { } from '@capgeminiuk/dcx-react-library'; import './stepper.scss'; -const StepperDemo: React.FC = () => { +const StepperDemo = () => { const [activeStepHorizontal, setActiveStepHorizontal] = useState(0); const [activeStepVertical, setActiveStepVertical] = useState(0); const [activeStepCustomSeparator, setActiveStepCustomSeparator] = useState(0); const [activeStepItems, setActiveStepItems] = useState(0); - const handleStepChange = ( - setter: React.Dispatch>, - step: number - ) => { - setter(step); - }; - const steps = [ { header: 'Campaign Settings', @@ -285,8 +278,8 @@ const StepperDemo: React.FC = () => { const renderStepper = ( activeStep: number, - setter: React.Dispatch>, - items: any[], + setActiveStep: React.Dispatch>, + steps: { header: string; content: React.ReactNode }[], orientation: 'horizontal' | 'vertical', customSeparator: JSX.Element | undefined = undefined ) => ( @@ -295,36 +288,90 @@ const StepperDemo: React.FC = () => { selectedStep={activeStep} separator={customSeparator ||
} > - {items.map((item, index) => ( + {steps.map((step, index) => ( - -
- {activeStep > index ? '✔️' : index + 1} + 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}
- {item.header} +
{step.header}
- -
{item.content}
-
+ +
{step.content}
+
{index > 0 && ( )} - {index < items.length - 1 && ( + {index < steps.length - 1 && ( )} + {index === steps.length - 1 && ( + + )}
@@ -333,22 +380,12 @@ const StepperDemo: React.FC = () => { ); return ( -
+

Horizontal Stepper

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

Vertical Stepper

- {renderStepper( - activeStepVertical, - setActiveStepVertical, - steps, - 'vertical' - )} + {renderStepper(activeStepVertical, setActiveStepVertical, steps, 'vertical')}

Stepper with Custom Separator

{renderStepper( @@ -360,9 +397,14 @@ const StepperDemo: React.FC = () => { )}

Order Process Stepper

- {renderStepper(activeStepItems, setActiveStepItems, items, 'horizontal')} + {renderStepper( + activeStepItems, + setActiveStepItems, + items, + 'horizontal' + )}
); }; -export default StepperDemo; +export default StepperDemo; \ No newline at end of file diff --git a/example/src/components/stepper.scss b/example/src/components/stepper.scss index a6066dbf..e1395dbf 100644 --- a/example/src/components/stepper.scss +++ b/example/src/components/stepper.scss @@ -1,82 +1,99 @@ -.stepper-demo { - padding: 20px; +.dcx-stepper { + width: 100%; + margin-bottom: 40px; } -.dcx-horizontal-stepper { +.dcx-horizontal-stepper .dcx-header-container { display: flex; flex-direction: row; - align-items: start; + align-items: flex-start; justify-content: space-between; flex-wrap: nowrap; } -.dcx-vertical-stepper { +.dcx-vertical-stepper .dcx-header-container { display: flex; flex-direction: column; align-items: flex-start; } -.step { +.dcx-header-wrapper { display: flex; - flex-direction: column; - align-items: flex-start; - padding: 10px; - margin-right: 20px; + align-items: center; + margin-bottom: 10px; } -.step-header { +.dcx-step-header { + cursor: pointer; display: flex; align-items: center; font-weight: bold; - margin-bottom: 10px; + font-size: 16px; + color: #333; } -.step-number { - width: 25px; - height: 25px; - color: white; +.dcx-step-header .step-number { + width: 30px; + height: 30px; border-radius: 50%; - background-color: rgba(0, 0, 0, 0.54); + background-color: #d1d0ce; + color: white; display: flex; align-items: center; justify-content: center; margin-right: 10px; + font-size: 14px; + font-weight: bold; } -.step-content { - display: flex; - flex-direction: column; - gap: 10px; +.dcx-step-header .step-number.active { + background-color: #1976d2; } -.separator, -.custom-separator { - margin: 0 10px; - border: 0; - height: 1px; - background: #ccc; +.dcx-step-content { + display: none; } -.button-container { - display: flex; - justify-content: space-between; - margin-top: 15px; - width: 100%; +.dcx-step-content.dcx-visible-content { + display: block; +} + +.dcx-content-container { + margin-top: 20px; } -.button-container button { - padding: 8px 16px; - background-color: #007bff; +.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; } -.button-container button:hover { +.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; @@ -96,10 +113,9 @@ margin-top: 10px; } -.step-separator { - border: 0; - height: 1px; - background: #ccc; - margin: 20px 0; +.custom-separator { width: 100%; + border: 0; + border-top: 1px solid #ccc; + margin: 10px 0; } \ No newline at end of file diff --git a/src/stepper/Stepper.tsx b/src/stepper/Stepper.tsx index a2066f5f..8f9f8b0d 100644 --- a/src/stepper/Stepper.tsx +++ b/src/stepper/Stepper.tsx @@ -10,39 +10,48 @@ import { classNames } from '../common'; export type StepperProps = { /** - * Specifies the content of the stepper. The allowed elements are Step, StepHeader, StepContent. + * An array of JSX elements representing the steps. */ children: JSX.Element[]; + /** - * Specifies a custom separator. + * An optional JSX element to be used as a separator between steps. */ separator?: JSX.Element; + /** - * Programmatically set the active step (starts from 0). + * The index of the initially selected step. Defaults to 0. */ selectedStep?: number; + /** - * Specifies a specific className for the selected step. + * The class name to be applied to the active step. */ activeStepClassName?: string; + /** - * Defines the className of the entire stepper. + * The class name to be applied to the stepper container. */ stepperClassName?: string; + /** - * Defines the style of all StepHeader elements from the parent. - * To style them independently, use className on the StepHeader element. + * The class name to be applied to the header of each step. */ headerClassName?: string; + /** - * Defines the style of all StepContent elements from the parent. - * To style them independently, use className on the StepContent element. + * The class name to be applied to the content of each step. */ contentClassName?: string; + /** - * Defines the orientation of the stepper. + * 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( @@ -55,6 +64,7 @@ export const Stepper = memo( headerClassName, contentClassName, orientation = 'horizontal', + ...props }: StepperProps) => { const [activeStep, setActiveStep] = useState(selectedStep); @@ -64,7 +74,8 @@ export const Stepper = memo( const onClickHandler = (index: number) => setActiveStep(index); - const steps: JSX.Element[] = []; + const headers: JSX.Element[] = []; + const contents: JSX.Element[] = []; Children.forEach(children, (child, index) => { if (child.type.name === 'Step') { @@ -74,6 +85,7 @@ export const Stepper = memo( 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, @@ -82,7 +94,7 @@ export const Stepper = memo( stepHeader = cloneElement(child, { key: `header-${index}`, _index: index, - headerClassName: headerClasses, + className: headerClasses, 'aria-selected': index === activeStep ? 'true' : 'false', 'aria-posinset': index + 1, 'aria-setsize': Children.count(children), @@ -90,28 +102,28 @@ export const Stepper = memo( 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: contentClassName, + className: contentClasses, visible: index === activeStep, }); } }); - if (stepHeader && stepContent) { - steps.push( -
-
- {stepHeader} -
-
- {stepContent} -
+ if (stepHeader) { + headers.push( +
+ {stepHeader}
); - if (separator && index < children.length - 1) { - steps.push( + headers.push( cloneElement(separator, { key: `separator-${index}`, className: 'dcx-separator', @@ -119,6 +131,14 @@ export const Stepper = memo( ); } } + + if (stepContent) { + contents.push( +
+ {stepContent} +
+ ); + } } }); @@ -134,7 +154,10 @@ export const Stepper = memo( -
{steps}
+
+
{headers}
+
{contents}
+
); } diff --git a/src/stepper/__test__/Stepper.test.tsx b/src/stepper/__test__/Stepper.test.tsx index c7cba4fe..6290e72e 100644 --- a/src/stepper/__test__/Stepper.test.tsx +++ b/src/stepper/__test__/Stepper.test.tsx @@ -54,7 +54,7 @@ describe('Stepper Component', () => { ); expect(screen.getByText('Step 2').parentElement).toHaveClass( - 'dcx-step-header' + 'dcx-header-wrapper' ); }); @@ -74,7 +74,7 @@ describe('Stepper Component', () => { fireEvent.click(screen.getByText('Step 2')); expect(screen.getByText('Step 2').parentElement).toHaveClass( - 'dcx-step-header' + 'dcx-header-wrapper' ); }); @@ -133,7 +133,7 @@ describe('Stepper Component', () => { ); - expect(screen.getByText('Step 1').parentElement).toHaveClass('dcx-step-header'); + expect(screen.getByText('Step 1').parentElement).toHaveClass('dcx-header-wrapper'); rerender( @@ -149,7 +149,7 @@ describe('Stepper Component', () => { ); expect(screen.getByText('Step 2').parentElement).toHaveClass( - 'dcx-step-header' + 'dcx-header-wrapper' ); }); @@ -219,7 +219,7 @@ describe('Stepper Component', () => { ); expect(screen.getByText('Step 2').parentElement).toHaveClass( - 'dcx-step-header' + 'dcx-header-wrapper' ); }); @@ -239,7 +239,7 @@ describe('Stepper Component', () => { fireEvent.click(screen.getByText('Step 2')); expect(screen.getByText('Content 2').parentElement).toHaveClass( - 'dcx-step-content' + 'dcx-content-wrapper' ); }); @@ -258,7 +258,7 @@ describe('Stepper Component', () => { ); expect(screen.getByText('Step 2').parentElement).toHaveClass( - 'dcx-step-header' + 'dcx-header-wrapper' ); }); diff --git a/stories/Stepper/ClassBased.stories.js b/stories/Stepper/ClassBased.stories.js index 289555f1..c8de171b 100644 --- a/stories/Stepper/ClassBased.stories.js +++ b/stories/Stepper/ClassBased.stories.js @@ -6,8 +6,6 @@ import { StepHeader, StepContent, } from '../../src/stepper'; -import './StepperDemo.css'; -import './VerticalStepper.css'; /** * 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. @@ -52,44 +50,138 @@ export const BasicStepper = { ]; 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 ? 'completed' : activeStep === index ? 'active' : 'inactive'}`}> - {activeStep > index ? '' : index + 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 && ( - {!isLastStep && ( +
+ {activeStep === index && ( +
+

{step.content}

+
- )} - {isLastStep && ( - )} +
- - + )} +
))} - +
); }, @@ -191,35 +318,19 @@ export const VerticalStepper = { steps: [ { header: 'Campaign Settings', - content: ( -
-

Configure your campaign settings including name, budget, and schedule.

-
- ), + 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.

-
- ), + 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.

-
- ), + 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.

-
- ), + content: 'Review all your settings and submit your campaign for approval.', }, ], onSubmit: () => alert('Campaign Submitted!'), @@ -244,18 +355,18 @@ export const StepperDemo = { { header: 'Personal Information', content: ( -
-
- - +
+
+ +
-
- - +
+ +
-
- - +
+ +
), @@ -263,22 +374,22 @@ export const StepperDemo = { { header: 'Address Details', content: ( -
-
- - +
+
+ +
-
- - +
+ +
-
- - +
+ +
-
- - +
+ +
), @@ -286,18 +397,18 @@ export const StepperDemo = { { header: 'Payment Information', content: ( -
-
- - +
+
+ +
-
- - +
+ +
-
- - +
+ +
), @@ -305,17 +416,17 @@ export const StepperDemo = { { header: 'Shipping Details', content: ( -
-
- -
-
- - +
+ +
), @@ -323,14 +434,14 @@ export const StepperDemo = { { header: 'Review and Submit', content: ( -
-
- - +
+
+ +
-
- - +
+ +
), @@ -338,52 +449,62 @@ export const StepperDemo = { ]; return ( -
- +
+
{steps.map((step, index) => ( - handleStepChange(index)} aria-label={`Step ${index + 1}`} + style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', cursor: 'pointer', flex: 1 }} > - -
index ? 'completed' : activeStep === index ? 'active' : 'inactive'}`}> - {activeStep > index ? '' : index + 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 > 0 && ( - - )} +
+ {activeStep < steps.length - 1 && ( )} {activeStep === steps.length - 1 && (