Skip to content

Commit

Permalink
fix stepper (#399)
Browse files Browse the repository at this point in the history
* make nav steps sticky for horizontal stepper

* extend tests

* use render prop pattern for stepper buttons
  • Loading branch information
jay-deshmukh authored Jan 3, 2025
1 parent 35000ed commit 5e018be
Show file tree
Hide file tree
Showing 16 changed files with 314 additions and 100 deletions.
2 changes: 1 addition & 1 deletion lib/index.css

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion lib/index.css.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion lib/index.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion lib/index.js.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion lib/tyk-ui.css

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion lib/tyk-ui.css.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion lib/tyk-ui.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion lib/tyk-ui.js.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@tyk-technologies/tyk-ui",
"version": "4.4.11",
"version": "4.4.14",
"description": "Tyk UI - ui reusable components",
"main": "src/index.js",
"scripts": {
Expand Down
82 changes: 81 additions & 1 deletion src/components/Stepper/Readme.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,79 @@
## Custom buttons using Render Props method
```jsx
import React from "react";
import Button from "../Button";
import Confirm from "../Confirm";
import Stepper from "./index";

const MyStepperComponent = () => {
const handleFinish = () => {
console.log("Finished!");
};

const validateStep = (stepId) => {
return true;
};

return (
<Stepper onFinish={handleFinish} stepValidator={validateStep}>
<Stepper.Step
title="Step 1"
description="First step"
id="step1"
></Stepper.Step>

<Stepper.Step
title="Step 2"
description="Second step"
id="step2"
></Stepper.Step>

<Stepper.Buttons>
{({ goToNextStep, goToPreviousStep, isLastStep, activeStep, stepId }) => (
<div
style={{
display: "flex",
width: "100%",
justifyContent: "flex-end",
gap: "8px",
marginLeft: 0,
}}
>
{ stepId !== 'step2' && <Confirm
title="Console log"
description="Are u sure u want to console log?"
>
{(confirm) => (
<Button
onClick={confirm((event) => {
console.log("Button clicked", {event, activeStep, stepId});
})}
theme="secondary"
>
Skip (Confirm)
</Button>
)}
</Confirm>}

{activeStep > 0 && (
<Button onClick={goToPreviousStep} theme="secondary">
Back
</Button>
)}
<Button onClick={goToNextStep} theme="primary">
{isLastStep ? "Complete" : "Next"}
</Button>
</div>
)}
</Stepper.Buttons>
</Stepper>
);
};

<MyStepperComponent />;
```

## Vertical Stepper

```jsx
import React from 'react';
Expand All @@ -17,6 +93,8 @@ const ExampleStepper = () => {
onFinish={handleFinish}
stepValidator={validateStep}
stepErrMessage="Please complete all required fields before proceeding."
onChange={(step) => console.log("Step Changed : ", step)}
onSkip={(step) => console.log("SKIP", step)}
>
<Stepper.Step id="personal-info" title="Step-1">
<input type="text" placeholder="Full Name" />
Expand All @@ -40,6 +118,7 @@ const ExampleStepper = () => {
<ExampleStepper />
```

## Horizontal Stepper
```jsx
import React from "react";
import Stepper from "./index.js";
Expand All @@ -62,6 +141,7 @@ const ExampleStepper = () => {
backBtnTxt="Previous"
nextBtnTxt="Next"
finishBtnTxt="Done"
onChange={(step) => console.log("Step Changed : ", step)}
>
<Stepper.Step id="personal-info" title="Step-1">
<input type="text" placeholder="Full Name" />
Expand Down Expand Up @@ -121,4 +201,4 @@ const ExampleStepper = () => {
};

<ExampleStepper />
```
```
48 changes: 43 additions & 5 deletions src/components/Stepper/Stepper.test.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
/* eslint-disable react/prop-types */
import React from 'react';
import Stepper from './index';


function StepperComponent({
validator,
onFinish: mockOnFinish,
onSkip: mockOnSkip,
orientation = 'vertical'
}) {
const onFinish = () => {
Expand All @@ -23,6 +22,7 @@ function StepperComponent({
stepValidator={validator || stepValidator}
stepErrMessage="Validation failed"
orientation={orientation}
onSkip={mockOnSkip}
>
<Stepper.Step id="step1" title="Step 1" description="First step">
Step 1 content
Expand Down Expand Up @@ -103,8 +103,47 @@ describe('Stepper Component', () => {
});
});

describe('Vertical Orientation', () => {
describe('Skip Functionality', () => {
it('should render skip button when onSkip prop is provided', () => {
const onSkip = cy.stub().as('onSkip');
cy.mount(<StepperComponent onSkip={onSkip} />)
.get('.skip-btn button')
.should('exist')
.and('contain', 'Skip');
});

it('should not render skip button when onSkip prop is not provided', () => {
cy.mount(<StepperComponent />)
.get('.skip-btn')
.should('not.exist');
});

it('should call onSkip with current step id when skip button is clicked', () => {
const onSkip = cy.stub().as('onSkip');
cy.mount(<StepperComponent onSkip={onSkip} />)
.get('.skip-btn button')
.click()
.get('@onSkip')
.should('have.been.calledWith', 'step1');
});

it('should allow skipping multiple steps', () => {
const onSkip = cy.stub().as('onSkip');
cy.mount(<StepperComponent onSkip={onSkip} validator={() => true} />)
.get('.skip-btn button')
.click()
.get('.stepper-buttons button')
.contains('Continue')
.click()
.get('.skip-btn button')
.click()
.get('@onSkip')
.should('have.been.calledTwice')
.and('have.been.calledWith', 'step2');
});
});

describe('Vertical Orientation', () => {
it('should show green connecting line for completed steps', () => {
cy.mount(<StepperComponent orientation="vertical" validator={() => true} />)
.get('.stepper-buttons button')
Expand All @@ -119,7 +158,7 @@ describe('Stepper Component', () => {
it('should render in horizontal layout', () => {
cy.mount(<StepperComponent orientation="horizontal" />)
.get('.step-list-horizontal')
.should('exist')
.should('exist');
});

it('should show progress line below step numbers', () => {
Expand Down Expand Up @@ -150,5 +189,4 @@ describe('Stepper Component', () => {
.should('contain', 'Step 2 content');
});
});

});
65 changes: 41 additions & 24 deletions src/components/Stepper/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import React, { useState, useMemo } from "react";
import PropTypes from "prop-types";
import { StepperProvider } from "./StepperContext";
import StepList from "./js/StepList";
import StepperButtons from "./js/StepperButtons";
import { DefaultButtons } from "./js/StepperButtons";
import Buttons from "./js/Buttons";
import Step from "./js/Step";
import "./stepper.css";

const Stepper = ({
Expand All @@ -14,18 +16,34 @@ const Stepper = ({
contentHeight,
nextBtnTxt = "Continue",
finishBtnTxt = "Finish",
skipBtnTxt = "Skip",
backBtnTxt = "Back",
onChange = () => {},
onSkip = null,
}) => {
const [activeStep, setActiveStep] = useState(0);
const [errors, setErrors] = useState({});
const [validationAttempted, setValidationAttempted] = useState(false);
const isHorizontal = orientation === "horizontal";

const steps = useMemo(() => {
return React.Children.toArray(children).filter(
(child) => child.type.name === "Step"
);
}, [children]);
const { steps, buttons } = useMemo(() => {
const children_array = React.Children.toArray(children);
return {
steps: children_array.filter(
(child) => child.type.displayName === "StepperStep"
),
buttons: children_array.find(
(child) => child.type.displayName === "StepperButtons"
) || (
<DefaultButtons
nextBtnTxt={nextBtnTxt}
finishBtnTxt={finishBtnTxt}
backBtnTxt={backBtnTxt}
skipBtnTxt={skipBtnTxt}
/>
),
};
}, [children, nextBtnTxt, finishBtnTxt, backBtnTxt, skipBtnTxt]);

const contextValue = {
activeStep,
Expand All @@ -34,11 +52,13 @@ const Stepper = ({
setErrors,
steps,
onFinish,
onChange,
stepValidator,
stepErrMessage,
validationAttempted,
setValidationAttempted,
orientation,
onSkip,
};

return (
Expand All @@ -52,32 +72,25 @@ const Stepper = ({
style={
isHorizontal && contentHeight
? {
height: contentHeight,
maxHeight: contentHeight,
overflow: "scroll",
}
height: contentHeight,
maxHeight: contentHeight,
overflow: "scroll",
}
: {}
}
>
{isHorizontal && steps[activeStep]}
</div>
)}
<StepperButtons
nextBtnTxt={nextBtnTxt}
finishBtnTxt={finishBtnTxt}
backBtnTxt={backBtnTxt}
/>
{buttons}
</div>
</div>
</StepperProvider>
);
};

export const Step = ({ children }) => {
return <>{children}</>;
};

Stepper.Step = Step;
Stepper.Buttons = Buttons;

Stepper.propTypes = {
/**
Expand All @@ -94,6 +107,14 @@ Stepper.propTypes = {
/**
* Function to validate each step. Should return true if valid, false otherwise.
*/
onChange: PropTypes.func,
/**
* Callback triggered on step change.
*/
onSkip: PropTypes.func,
/**
* Callback triggered on step skip.
*/
stepValidator: PropTypes.func,
/**
* Error message to display when a step is invalid.
Expand All @@ -117,8 +138,4 @@ Stepper.defaultProps = {
orientation: "vertical",
};

Step.propTypes = {
children: PropTypes.node.isRequired,
};

export default Stepper;
export default Stepper;
Loading

0 comments on commit 5e018be

Please sign in to comment.