styles?: object |
@@ -127,44 +145,43 @@ Props that can be passed to the component are listed below:
## Style Customizations
-All the default styles provided by this package are overridable using the `style` prop
-the below code shows all the overridable styles:
+All the default styles provided by this package can be overridden using the `style` prop
+the below code shows all the styles that can be overridden:
```jsx
-import React from 'react';
-import Stepper from 'react-vertical-stepper';
+import React from "react";
+import Stepper from "react-stepper";
function App() {
-
- const stylesOverride = {
- LabelTitle: (step, stepIndex) => ({...styles}),
- ActiveLabelTitle: (step, stepIndex) => ({...styles}),
- LabelDescription: (step, stepIndex) => ({...styles}),
- ActiveLabelDescription: (step, stepIndex) => ({...styles}),
- LineSeparator: (step, stepIndex) => ({...styles}),
- InactiveLineSeparator: (step, stepIndex) => ({...styles}),
- Bubble: (step, stepIndex) => ({...styles}),
- ActiveBubble: (step, stepIndex) => ({...styles}),
- InActiveBubble: (step, stepIndex) => ({...styles}),
- };
- return (
- ({ ...styles }),
+ ActiveLabelTitle: (step, stepIndex) => ({ ...styles }),
+ LabelDescription: (step, stepIndex) => ({ ...styles }),
+ ActiveLabelDescription: (step, stepIndex) => ({ ...styles }),
+ LineSeparator: (step, stepIndex) => ({ ...styles }),
+ InactiveLineSeparator: (step, stepIndex) => ({ ...styles }),
+ Node: (step, stepIndex) => ({ ...styles }),
+ ActiveNode: (step, stepIndex) => ({ ...styles }),
+ InActiveNode: (step, stepIndex) => ({ ...styles }),
+ };
+ return (
+
- );
+ />
+ );
}
export default App;
```
-
-- `LabelTitle` - overrides the step label style
-- `ActiveLabelTitle` - overrides the step label style of current active step
-- `LabelDescription` - overrides the step description style
-- `ActiveLabelDescription` - overrides the step description style of current active step
-- `LineSeparator` - overrides default step connector line styles
-- `InactiveLineSeparator` - overrides styles of step connector line after current active step
-- `Bubble` - overrides default styles of step indicator
-- `ActiveBubble` - overrides default styles of step indicator of current active step
-- `InActiveBubble` - overrides default styles of step indicator that has `unvisited` step status
\ No newline at end of file
+
+- `LabelTitle` - overrides the step label style
+- `ActiveLabelTitle` - overrides the step label style of current active step
+- `LabelDescription` - overrides the step description style
+- `ActiveLabelDescription` - overrides the step description style of current active step
+- `LineSeparator` - overrides default step connector line styles
+- `InactiveLineSeparator` - overrides styles of step connector line after current active step
+- `Node` - overrides default styles of step indicator
+- `ActiveNode` - overrides default styles of step indicator of current active step
+- `InActiveNode` - overrides default styles of step indicator that is not completed and not active
diff --git a/STYLE_GUIDELINES.md b/STYLE_GUIDELINES.md
new file mode 100644
index 0000000..a95b384
--- /dev/null
+++ b/STYLE_GUIDELINES.md
@@ -0,0 +1,23 @@
+## SCSS Style Guidelines for @keyvaluesystems/react-stepper
+
+**Introduction**
+
+As an open-source project utilizing SCSS, @keyvaluesystems/react-stepper strives to maintain a consistent and well-structured codebase. These SCSS style guidelines serve as a reference for contributors, ensuring that their SCSS code adheres to established conventions and best practices.
+
+**SCSS Coding Conventions**
+
+- Organize SCSS files into a logical structure.
+- Use meaningful and descriptive names for variables, mixins, and classes.
+- Use SCSS nesting judiciously to organize complex styles.
+- Include comments to explain non-obvious logic and complex styles.
+- Utilize SCSS variables to define reusable values.
+- Employ a SCSS linting tool.
+- Should support devices with all resolutions
+- Follow CamelCase conventions for class names that concisely convey their purpose, enhancing code organization and readability
+- Adhere to the practice of reusing style classes to improve code organization and maintainability.
+
+**Documentation Practices**
+
+- Provide clear documentation for exported mixins and variables.
+- Include a README file within the SCSS directory if necessary.
+- Add comments to SCSS files.
diff --git a/package-lock.json b/package-lock.json
index 4f04c9d..bbd6e41 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,5 +1,5 @@
{
- "name": "@keyvaluesystems/react-vertical-stepper",
+ "name": "@keyvaluesystems/react-stepper",
"version": "0.1.6",
"lockfileVersion": 1,
"requires": true,
@@ -23918,6 +23918,23 @@
"dev": true,
"requires": {
"loose-envify": "^1.1.0"
+ },
+ "dependencies": {
+ "js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "dev": true
+ },
+ "loose-envify": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+ "dev": true,
+ "requires": {
+ "js-tokens": "^3.0.0 || ^4.0.0"
+ }
+ }
}
},
"react-app-polyfill": {
diff --git a/package.json b/package.json
index 9f3adca..8ae120e 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
- "name": "@keyvaluesystems/react-vertical-stepper",
+ "name": "@keyvaluesystems/react-stepper",
"version": "0.1.6",
- "description": "A fully customizable vertical stepper component",
+ "description": "A fully customizable stepper component",
"main": "build/index.js",
"source": "src/index.tsx",
"types": "build/types/index.d.ts",
@@ -20,11 +20,11 @@
},
"repository": {
"type": "github",
- "url": "git+https://github.com/KeyValueSoftwareSystems/react-vertical-stepper.git"
+ "url": "git+https://github.com/KeyValueSoftwareSystems/react-stepper.git"
},
"author": "Keyvalue",
- "license": "ISC",
- "homepage": "https://github.com/KeyValueSoftwareSystems/react-vertical-stepper",
+ "license": "MIT",
+ "homepage": "https://github.com/KeyValueSoftwareSystems/react-stepper",
"keywords": [
"library",
"starter",
@@ -35,7 +35,8 @@
"steps",
"stepper",
"vertical-stepper",
- " steps-ui",
+ "horizontal stepper",
+ "steps-ui",
"workflow-stepper",
"progress-ui"
],
@@ -114,7 +115,7 @@
]
},
"bugs": {
- "url": "https://github.com/KeyValueSoftwareSystems/react-vertical-stepper/issues"
+ "url": "https://github.com/KeyValueSoftwareSystems/react-stepper/issues"
},
"dependencies": {}
}
diff --git a/src/assets/horizontal-stepper-example.png b/src/assets/horizontal-stepper-example.png
new file mode 100644
index 0000000..43ee223
Binary files /dev/null and b/src/assets/horizontal-stepper-example.png differ
diff --git a/src/assets/vertical-stepper-example.png b/src/assets/vertical-stepper-example.png
index 509793c..cd49c33 100644
Binary files a/src/assets/vertical-stepper-example.png and b/src/assets/vertical-stepper-example.png differ
diff --git a/src/bubble/bubble.tsx b/src/bubble/bubble.tsx
deleted file mode 100644
index a712d82..0000000
--- a/src/bubble/bubble.tsx
+++ /dev/null
@@ -1,88 +0,0 @@
-import React, { FC } from "react";
-import type { IBubbleProps } from "./types";
-import { Elements } from "../constants";
-import whiteTick from '../assets/white-tick.svg';
-import { STEP_STATUSES, LABEL_POSITION } from '../constants';
-import styles from './styles.module.scss';
-
-const Bubble: FC = (props) => {
- const {
- step,
- renderAdornment,
- index,
- currentStepIndex,
- handleStepClick = null,
- showCursor,
- getStyles,
- labelPosition
- } = props;
-
- return (
- handleStepClick && handleStepClick()}
- role="presentation"
- id="stepper-bubble"
- >
- {(renderAdornment && renderAdornment(step, index))
- || (
- <>
- {step?.status === STEP_STATUSES.COMPLETED && (
- )
- || index + 1}
- >
- )}
-
- {step?.label && (
- handleStepClick && handleStepClick()}
- role="presentation"
- id={`stepper-label-${index}`}
- >
- {step.label}
-
- )}
- {step?.description && (
- handleStepClick && handleStepClick()}
- role="presentation"
- id={`stepper-desc-${index}`}
- >
- {step.description}
-
- )}
-
-
- );
-};
-
-export default Bubble;
\ No newline at end of file
diff --git a/src/bubble/index.ts b/src/bubble/index.ts
deleted file mode 100644
index 9d927cf..0000000
--- a/src/bubble/index.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-import Bubble from "./bubble";
-
-export default Bubble;
\ No newline at end of file
diff --git a/src/bubble/types.d.ts b/src/bubble/types.d.ts
deleted file mode 100644
index 9fd9c0d..0000000
--- a/src/bubble/types.d.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import { ReactElement } from "react";
-import { IStep } from "../stepper-component/types";
-import { Elements } from "../constants";
-
-export type IBubbleProps = {
- step: IStep,
- renderAdornment?(step: IStep, index: number): ReactElement,
- index: number,
- currentStepIndex?: number,
- handleStepClick(): void,
- showCursor: boolean,
- getStyles(element: Elements): object,
- labelPosition: 'left' | 'right'
-}
\ No newline at end of file
diff --git a/src/constants.ts b/src/constants.ts
index feb7ab8..eee7a96 100644
--- a/src/constants.ts
+++ b/src/constants.ts
@@ -1,22 +1,24 @@
-export enum STEP_STATUSES {
- VISITED = 'visited',
- UNVISITED = 'unvisited',
- COMPLETED = 'completed'
-}
export enum LABEL_POSITION {
- LEFT = 'left',
- RIGHT = 'right'
+ LEFT = "left",
+ RIGHT = "right",
+ TOP = "top",
+ BOTTOM = "bottom",
+}
+
+export enum ORIENTATION {
+ HORIZONTAL = "horizontal",
+ VERTICAL = "vertical",
}
export enum Elements {
- LabelDescription = "LabelDescription",
- LabelTitle = "LabelTitle",
- ActiveLabelTitle = "ActiveLabelTitle",
- ActiveLabelDescription = "ActiveLabelDescription",
- LineSeparator = "LineSeparator",
- InactiveLineSeparator = "InactiveLineSeparator",
- Bubble = "Bubble",
- ActiveBubble = "ActiveBubble",
- InActiveBubble = "InActiveBubble"
- }
\ No newline at end of file
+ LabelDescription = "LabelDescription",
+ LabelTitle = "LabelTitle",
+ ActiveLabelTitle = "ActiveLabelTitle",
+ ActiveLabelDescription = "ActiveLabelDescription",
+ LineSeparator = "LineSeparator",
+ InactiveLineSeparator = "InactiveLineSeparator",
+ Node = "Node",
+ ActiveNode = "ActiveNode",
+ InActiveNode = "InActiveNode",
+}
diff --git a/src/index.tsx b/src/index.tsx
index a42f26e..d2a09f4 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -1,3 +1,3 @@
-import Stepper from "./stepper-component";
+import Stepper from "./stepper";
export default Stepper;
diff --git a/src/node/index.ts b/src/node/index.ts
new file mode 100644
index 0000000..94710a4
--- /dev/null
+++ b/src/node/index.ts
@@ -0,0 +1,3 @@
+import Node from "./node";
+
+export default Node;
diff --git a/src/node/node.tsx b/src/node/node.tsx
new file mode 100644
index 0000000..a9b70ae
--- /dev/null
+++ b/src/node/node.tsx
@@ -0,0 +1,52 @@
+import React, { FC } from "react";
+import type { INodeProps } from "./types";
+import { Elements } from "../constants";
+import whiteTick from "../assets/white-tick.svg";
+import styles from "./styles.module.scss";
+
+const Node: FC = (props) => {
+ const {
+ step,
+ renderNode,
+ index,
+ currentStepIndex,
+ handleStepClick,
+ showCursor,
+ getStyles
+ } = props;
+
+ return (
+ handleStepClick && handleStepClick()}
+ role="presentation"
+ id="stepper-node"
+ >
+ {(renderNode && renderNode(step, index))
+ || (
+ <>
+ {step?.completed && (
+ )
+ || index + 1}
+ >
+ )}
+
+ );
+};
+
+export default Node;
diff --git a/src/bubble/styles.module.scss b/src/node/styles.module.scss
similarity index 84%
rename from src/bubble/styles.module.scss
rename to src/node/styles.module.scss
index 9e18a5b..09da43e 100644
--- a/src/bubble/styles.module.scss
+++ b/src/node/styles.module.scss
@@ -1,8 +1,8 @@
-.eachBubble {
+.eachNode {
border-radius: 50%;
height: 24px;
width: 24px;
- background: #312ec0;
+ background: #7b7b84;
color: white;
display: flex;
align-items: center;
@@ -11,16 +11,19 @@
font-weight: 400;
font-size: 12px;
line-height: 16px;
- margin: 7px;
+ margin-top: 7px;
+ margin-bottom: 7px;
position: relative;
}
- .activeStepBubble {
- border: 7px solid #CBCBEF;
- margin: 0;
+ .activeStepNode {
+ background: #312ec0;
}
- .inactiveStepBubble {
+ .inactiveStepNode {
opacity: 0.4;
}
+ .completedStepNode {
+ background: #312ec0;
+ }
.whiteTickImg {
object-fit: cover;
width: 10px;
diff --git a/src/node/types.d.ts b/src/node/types.d.ts
new file mode 100644
index 0000000..9ac608b
--- /dev/null
+++ b/src/node/types.d.ts
@@ -0,0 +1,13 @@
+import { ReactElement } from "react";
+import { IStep } from "../stepper/types";
+import { Elements } from "../constants";
+
+export type INodeProps = {
+ step: IStep;
+ renderNode?(step: IStep, index: number): ReactElement;
+ index: number;
+ currentStepIndex?: number;
+ handleStepClick(): void;
+ showCursor: boolean;
+ getStyles(element: Elements): object;
+};
diff --git a/src/stepper-component/index.ts b/src/stepper-component/index.ts
deleted file mode 100644
index 39ac37d..0000000
--- a/src/stepper-component/index.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-import Stepper from './stepperComponent';
-
-export default Stepper;
diff --git a/src/stepper-component/stepperComponent.tsx b/src/stepper-component/stepperComponent.tsx
deleted file mode 100644
index 4f067ee..0000000
--- a/src/stepper-component/stepperComponent.tsx
+++ /dev/null
@@ -1,58 +0,0 @@
-import React, { ReactElement, FC } from 'react';
-import classes from './styles.module.scss';
-import type { IStep, IStepperProps } from './types';
-import Bubble from '../bubble';
-import { LABEL_POSITION, Elements } from '../constants';
-
-const Stepper: FC = (props) => {
- const {
- steps,
- currentStepIndex = 0,
- onStepClick,
- renderBubble,
- styles = {},
- labelPosition = LABEL_POSITION.RIGHT
- } = props;
-
- const getStyles = (element: Elements, step: IStep, index: number): object => {
- const getElementStyle = styles[element];
- if (getElementStyle) {
- return getElementStyle(step, index);
- }
- return {};
- };
-
- return (
-
- {steps?.map((step: IStep, stepIndex: number): ReactElement => (
-
-
- onStepClick && onStepClick(step, stepIndex)}
- showCursor={!!onStepClick}
- renderAdornment={renderBubble}
- getStyles={(element: Elements): object => getStyles(element, step, stepIndex)}
- labelPosition={labelPosition}
- />
- {stepIndex < steps?.length - 1 && (
- currentStepIndex - 1 && classes.inactiveStepLineSeparator}`}
- style={{
- ...((getStyles(Elements.LineSeparator, step, stepIndex)) || {}),
- ...((stepIndex > currentStepIndex - 1
- && getStyles(Elements.InactiveLineSeparator, step, stepIndex)) || {})
- }}
- />
- )}
-
-
- ))}
-
- );
-};
-
-export default Stepper;
\ No newline at end of file
diff --git a/src/stepper-component/styles.module.scss b/src/stepper-component/styles.module.scss
deleted file mode 100644
index 89d7a96..0000000
--- a/src/stepper-component/styles.module.scss
+++ /dev/null
@@ -1,32 +0,0 @@
-.stepperContainer {
- display: flex;
- width: 100%;
- height: 100%;
- flex-direction: column;
- margin-left: 10px;
- align-items: center;
- .eachStep {
- display: flex;
- flex-direction: column;
- align-items: center;
- position: relative;
- .bubbleLineWrapper {
- display: flex;
- flex-direction: column;
- align-items: center;
- width: fit-content;
- .lineSeparator {
- height: 22px;
- width: 1px;
- border-right: 2px solid #dfdff2;
- margin: 4px 0;
- }
- .inactiveStepLineSeparator {
- border-right: 2px dashed #dfdff2;
- }
- }
- }
-}
-.cursorPointer {
- cursor: pointer;
-}
diff --git a/src/stepper-component/types.d.ts b/src/stepper-component/types.d.ts
deleted file mode 100644
index 2790645..0000000
--- a/src/stepper-component/types.d.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import { ReactElement } from "react"
-import { LABEL_POSITION } from "../constants"
-import { Elements } from "../constants"
-
-export type IStep = {
- label: string,
- description?: string,
- status: string
-}
-
-export type IStepperProps = {
- steps: IStep[],
- currentStepIndex?: number,
- onStepClick?(step: IStep, stepIndex: number): void,
- renderBubble?(step: IStep, stepIndex: number): ReactElement,
- styles?: { [key in Elements]: IStyleFunction },
- labelPosition?: LABEL_POSITION.LEFT | LABEL_POSITION.RIGHT
-}
-
-export type IStyleFunction = (step: IStep, stepIndex: number) => object
diff --git a/src/stepper/index.ts b/src/stepper/index.ts
new file mode 100644
index 0000000..808a9e7
--- /dev/null
+++ b/src/stepper/index.ts
@@ -0,0 +1,3 @@
+import Stepper from "./stepperComponent";
+
+export default Stepper;
diff --git a/src/stepper/step.tsx b/src/stepper/step.tsx
new file mode 100644
index 0000000..78f06e6
--- /dev/null
+++ b/src/stepper/step.tsx
@@ -0,0 +1,124 @@
+import React, { useRef, useEffect, useState } from "react";
+import "./styles.scss";
+import type { IStepProps } from "../stepper/types";
+import { LABEL_POSITION, ORIENTATION } from "../constants";
+import StepContent from "./stepContent";
+import StepInfo from "./stepInfo";
+
+// Each step consists of a node, a label, and connectors to the previous and next steps.
+const Step: (props: IStepProps) => JSX.Element = ({
+ stepperProps,
+ step,
+ index
+}: IStepProps) => {
+ const {
+ steps,
+ currentStepIndex = 0,
+ styles = {},
+ labelPosition = LABEL_POSITION.RIGHT,
+ orientation = ORIENTATION.VERTICAL,
+ showDescriptionsForAllSteps = false,
+ stepContent,
+ onStepClick,
+ renderNode
+ } = stepperProps;
+ const [nodeWidth, setNodeWidth] = useState(0);
+
+ const isVertical = orientation === ORIENTATION.VERTICAL;
+
+ /* isInlineLabelsAndSteps = true means label and steps are in the same axis (eg: Horizontal stepper with label direction left/right and
+ vertical stepper with label direction top/bottom) */
+ const isInlineLabelsAndSteps =
+ (isVertical &&
+ [LABEL_POSITION.TOP, LABEL_POSITION.BOTTOM].includes(labelPosition)) ||
+ (!isVertical &&
+ [LABEL_POSITION.LEFT, LABEL_POSITION.RIGHT].includes(labelPosition));
+
+ const nodeRef = useRef (null);
+
+ useEffect(() => {
+ const node = nodeRef.current;
+ if (node) {
+ const width = node.getBoundingClientRect().width;
+ setNodeWidth(width);
+ }
+ }, [steps, nodeRef]);
+
+ // prevConnector represents the connector line from the current step's node (nth node) to the preceding step's node (n-1 th node).
+ const prevConnectorClassName = `stepConnector leftConnector ${
+ currentStepIndex >= index ? "activeConnector" : ""
+ } ${index === 0 ? "hiddenConnector" : ""}`;
+
+ // nextConnector represents the connector line from the current step's node (nth node) to the preceding step's node (n-1 th node).
+
+ const nextConnectorClassName = `stepConnector rightConnector ${
+ currentStepIndex > index ? "activeConnector" : ""
+ } ${index === steps.length - 1 ? "hiddenConnector" : ""}`;
+
+ /* middleConnector connects the current step nextConnector to (n+1th) step prevConnector,
+ allowing the display of descriptions or content between the two steps when necessary. */
+
+ const middleConnectorClassName = `middleStepConnector ${
+ currentStepIndex > index ? "activeConnector" : ""
+ } ${index === steps.length - 1 ? "hiddenConnector" : ""}`;
+
+ return orientation === ORIENTATION.HORIZONTAL &&
+ labelPosition === LABEL_POSITION.TOP ? (
+
+ ) : (
+
+
+
+
+ );
+};
+
+export default Step;
diff --git a/src/stepper/stepContent.tsx b/src/stepper/stepContent.tsx
new file mode 100644
index 0000000..0cf958f
--- /dev/null
+++ b/src/stepper/stepContent.tsx
@@ -0,0 +1,80 @@
+import React from "react";
+import "./styles.scss";
+import { LABEL_POSITION, Elements } from "../constants";
+import getStyles from "../utils/getStyles";
+import { IStepContentProps } from "./types";
+
+const StepContent: (props: IStepContentProps) => JSX.Element = ({
+ labelPosition,
+ isVertical,
+ currentStepIndex,
+ index,
+ styles,
+ step,
+ showDescriptionsForAllSteps,
+ middleConnectorClassName,
+ stepContent,
+ nodeWidth
+}: IStepContentProps) => (
+
+ {isVertical && (
+ /* In a vertical stepper, utilize an extra middle connector to dynamically adjust the length based on the height of step descriptions.
+ This ensures a visually balanced layout by accommodating varying content heights. */
+
+ index
+ ? getStyles(styles, Elements.LineSeparator, step, index) || {}
+ : getStyles(
+ styles,
+ Elements.InactiveLineSeparator,
+ step,
+ index
+ ) || {})
+ }}
+ />
+
+ )}
+
+ {(showDescriptionsForAllSteps || index === currentStepIndex) && (
+
+ {step.stepDescription}
+
+ )}
+ {isVertical &&
+ index === currentStepIndex &&
+ stepContent &&
+ stepContent(step, index)}
+
+
+);
+
+export default StepContent;
diff --git a/src/stepper/stepInfo.tsx b/src/stepper/stepInfo.tsx
new file mode 100644
index 0000000..ad296bc
--- /dev/null
+++ b/src/stepper/stepInfo.tsx
@@ -0,0 +1,126 @@
+import React from "react";
+import "./styles.scss";
+import type { IStepInfoProps } from "./types";
+import Node from "../node";
+import { LABEL_POSITION, Elements, ORIENTATION } from "../constants";
+import getStyles from "../utils/getStyles";
+import getLabelStyle from "../utils/getLabelStyle";
+
+const StepInfo: (props: IStepInfoProps) => JSX.Element = ({
+ orientation,
+ labelPosition,
+ isVertical,
+ isInlineLabelsAndSteps,
+ index,
+ currentStepIndex,
+ step,
+ showDescriptionsForAllSteps,
+ onStepClick,
+ renderNode,
+ styles,
+ nodeRef,
+ prevConnectorClassName,
+ nextConnectorClassName
+}: IStepInfoProps) => (
+
+ {!isInlineLabelsAndSteps && (
+
+
+ {step.stepLabel}
+
+ {(showDescriptionsForAllSteps || index === currentStepIndex) &&
+ orientation === ORIENTATION.HORIZONTAL &&
+ labelPosition === LABEL_POSITION.TOP && (
+
+ {step.stepDescription}
+
+ )}
+
+ )}
+
+ = index
+ ? getStyles(styles, Elements.LineSeparator, step, index) || {}
+ : getStyles(styles, Elements.InactiveLineSeparator, step, index) || {})
+ }}
+ />
+
+
+ onStepClick && onStepClick(step, index)
+ }
+ showCursor={!!onStepClick}
+ renderNode={renderNode}
+ getStyles={(element: Elements): object =>
+ getStyles(styles, element, step, index)
+ }
+ />
+
+ {isInlineLabelsAndSteps && (
+
+ )}
+ index
+ ? getStyles(styles, Elements.LineSeparator, step, index) || {}
+ : getStyles(styles, Elements.InactiveLineSeparator, step, index) || {})
+ }}
+ />
+
+
+);
+
+export default StepInfo;
diff --git a/src/stepper/stepperComponent.tsx b/src/stepper/stepperComponent.tsx
new file mode 100644
index 0000000..b6a89a3
--- /dev/null
+++ b/src/stepper/stepperComponent.tsx
@@ -0,0 +1,33 @@
+import React from "react";
+import "./styles.scss";
+import type { IStepperProps } from "./types";
+import { ORIENTATION } from "../constants";
+import Step from "./step";
+
+const Stepper = (props: IStepperProps): JSX.Element => {
+ const {
+ steps,
+ currentStepIndex = 0,
+ orientation = ORIENTATION.VERTICAL,
+ stepContent
+ } = props;
+
+ const isVertical = orientation === ORIENTATION.VERTICAL;
+
+ return (
+ <>
+
+ {steps.map((step, index) => Step({ stepperProps: props, step, index }))}
+
+ {!isVertical && // For horizontal stepper, the content is displayed below the stepper with full width
+ stepContent &&
+ stepContent(steps[currentStepIndex], currentStepIndex)}
+ >
+ );
+};
+
+export default Stepper;
diff --git a/src/stepper/styles.scss b/src/stepper/styles.scss
new file mode 100644
index 0000000..990fd73
--- /dev/null
+++ b/src/stepper/styles.scss
@@ -0,0 +1,205 @@
+$grey-color: #e1e1e1;
+$active-color: #312ec0;
+$completed-color: #47aed6;
+
+.stepper {
+ margin: 0;
+ padding: 1em;
+ display: flex;
+ font-family: inherit;
+ list-style: none;
+}
+
+.horizontalStepperWrapper {
+ display: flex;
+ flex-direction: column;
+}
+
+.verticalStepperWrapper {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+}
+
+.labelLeft {
+ justify-content: flex-end;
+}
+
+.horizontalStepper {
+ flex-flow: row nowrap;
+ width: 100%;
+ height: fit-content;
+ justify-content: center;
+ .stepContainer {
+ display: flex;
+ flex-direction: row;
+ height: auto;
+ width: 100%;
+ flex-wrap: wrap;
+ align-items: center;
+ justify-content: center;
+ }
+ .stepConnector {
+ margin: 0;
+ display: flex;
+ flex: 1;
+ background-color: $grey-color;
+ overflow: hidden;
+ width: 100px;
+ min-width: 0;
+ height: 2px;
+ }
+ .activeConnector {
+ background-color: #312ec0;
+ }
+ .descriptionContainer {
+ display: flex;
+ justify-content: center;
+ }
+
+}
+
+.verticalStepper {
+ flex-flow: column nowrap;
+ width: fit-content;
+ height: 100%;
+ .stepContainer {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ width: auto;
+ flex-wrap: wrap;
+ align-items: center;
+ justify-content: center;
+ }
+ .stepConnector {
+ margin: 0;
+ display: flex;
+ flex: 1;
+ background-color: $grey-color;
+ overflow: hidden;
+ height: auto;
+ min-height: 10px;
+ width: 2px;
+ }
+ .middleStepConnector {
+ margin: 0;
+ display: flex;
+ background-color: $grey-color;
+ overflow: hidden;
+ height: auto;
+ min-height: 10px;
+ width: 2px;
+ }
+ .activeConnector {
+ background-color: #312ec0;
+ }
+ .descriptionContainer {
+ display: flex;
+ }
+
+}
+
+
+.hiddenConnector {
+ visibility: hidden;
+}
+
+.node {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ order: 2;
+ padding: 3px;
+}
+.leftConnector {
+ order: 1;
+}
+.rightConnector {
+ order: 4;
+}
+
+.labelContainer {
+ display: flex;
+ word-wrap: break-word;
+ justify-content: center;
+ order: 3;
+}
+
+.label {
+ font-size: 0.9em;
+ white-space: pre-wrap;
+ word-wrap: break-word;
+ padding: 3px;
+ text-align: center;
+ font-weight: bold;
+ display: flex;
+ max-width: 400px;
+}
+
+.verticalStepperInlineLabel {
+ display: flex;
+ width: 100px;
+ justify-content: center;
+}
+
+.reversedLabelContainer {
+ order: 2;
+}
+
+.reversedNode {
+ order: 3;
+}
+
+.leftDescription {
+ flex-direction: row-reverse;
+}
+.verticalTextLeftContainer {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-end;
+ width: 100%;
+}
+
+.horizontalLabelTop {
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-end;
+ align-items: center;
+ flex: 2 100%;
+}
+
+.horizontalLabelBottom {
+ display: flex;
+ justify-content: center;
+ order: 1;
+}
+
+.verticalLabelRight {
+ order: 1;
+}
+
+.description {
+ color: #4e4b4b;
+ padding-bottom: 5px;
+}
+
+.middleConnectorWrapper {
+ display: flex;
+ justify-content: flex-end;;
+}
+
+.leftContentMiddleConnectorWrapper {
+ display: flex;
+ justify-content: flex-start;
+}
+
+.verticalContentWrapper {
+ padding-left: 10px;
+ padding-right: 10px;
+}
+
+.horizontalStepperDescription {
+ display: flex;
+ justify-content: center;;
+}
diff --git a/src/stepper/types.d.ts b/src/stepper/types.d.ts
new file mode 100644
index 0000000..c78ee74
--- /dev/null
+++ b/src/stepper/types.d.ts
@@ -0,0 +1,59 @@
+import { LegacyRef, ReactElement } from "react";
+import { LABEL_POSITION, ORIENTATION } from "../constants";
+import { Elements } from "../constants";
+
+export type IStep = {
+ stepLabel: string;
+ stepDescription?: string;
+ completed?: boolean;
+};
+
+export type IStepperProps = {
+ steps: IStep[];
+ currentStepIndex?: number;
+ orientation?: ORIENTATION.HORIZONTAL | ORIENTATION.VERTICAL;
+ styles?: { [key in Elements]: IStyleFunction };
+ labelPosition?: LABEL_POSITION.LEFT | LABEL_POSITION.RIGHT | LABEL_POSITION.TOP | LABEL_POSITION.BOTTOM;
+ showDescriptionsForAllSteps?: boolean;
+ stepContent?(step: IStep, stepIndex: number): ReactElement;
+ onStepClick?(step: IStep, stepIndex: number): void;
+ renderNode?(step: IStep, stepIndex: number): ReactElement;
+};
+
+export type IStyleFunction = (step: IStep, stepIndex: number) => object;
+
+export type IStepProps = {
+ stepperProps: IStepperProps;
+ step: IStep;
+ index: number;
+}
+
+export type IStepInfoProps = {
+ orientation: ORIENTATION.HORIZONTAL | ORIENTATION.VERTICAL;
+ labelPosition:LABEL_POSITION.LEFT | LABEL_POSITION.RIGHT | LABEL_POSITION.TOP | LABEL_POSITION.BOTTOM;
+ isVertical: boolean;
+ isInlineLabelsAndSteps: boolean;
+ index: number;
+ currentStepIndex: number;
+ step: IStep;
+ showDescriptionsForAllSteps: boolean;
+ onStepClick?(step: IStep, stepIndex: number): void;
+ renderNode?(step: IStep, stepIndex: number): ReactElement;
+ styles: { [key in Elements]?: IStyleFunction };
+ nodeRef: LegacyRef | undefined
+ prevConnectorClassName: string;
+ nextConnectorClassName: string;
+}
+
+export type IStepContentProps = {
+ labelPosition: LABEL_POSITION.LEFT | LABEL_POSITION.RIGHT | LABEL_POSITION.TOP | LABEL_POSITION.BOTTOM;
+ isVertical: boolean;
+ currentStepIndex: number;
+ index: number;
+ styles: { [key in Elements]?: IStyleFunction };
+ step: IStep
+ showDescriptionsForAllSteps: boolean;
+ middleConnectorClassName: string;
+ stepContent?(step: IStep, stepIndex: number): ReactElement;
+ nodeWidth: number;
+}
diff --git a/src/stories/StepperComponent.stories.tsx b/src/stories/StepperComponent.stories.tsx
index c4e816b..c834956 100644
--- a/src/stories/StepperComponent.stories.tsx
+++ b/src/stories/StepperComponent.stories.tsx
@@ -1,51 +1,116 @@
-import React from 'react';
-import { ComponentStory, ComponentMeta } from '@storybook/react';
-import Stepper from '../stepper-component';
-import { IStep } from '../stepper-component/types';
+import React from "react";
+import {
+ ComponentStory,
+ ComponentMeta,
+} from "@storybook/react";
+import Stepper from "../stepper";
export default {
- title: 'Example/Stepper',
- component: Stepper,
- parameters: {
- // More on Story layout: https://storybook.js.org/docs/react/configure/story-layout
- layout: 'fullscreen',
- },
- } as ComponentMeta;
-
-
- const Template: ComponentStory = (args) => ;
-
-export const VerticalStepper = Template.bind({});
-VerticalStepper.args = {
- steps: [{
- label: 'Step 1',
- description: 'The quick brown fox jumps over the lazy dog'
+ title: "Example/Stepper",
+ component: Stepper,
+ parameters: {
+ layout: "fullscreen",
+ },
+} as ComponentMeta;
+
+const Template: ComponentStory = (props) => (
+
+);
+
+const steps = [
+ {
+ stepLabel: "Step 1",
+ stepDescription: "The quick brown fox jumps over the lazy dog",
+ completed: true,
},
{
- label: 'Step 2',
- description: 'The quick brown fox jumps over the lazy dog'
+ stepLabel: "Step 2",
+ stepDescription: "The quick brown fox jumps over the lazy dog",
+ completed: true,
},
{
- label: 'Step 3',
- description: 'The quick brown fox jumps over the lazy dog'
+ stepLabel: "Step 3",
+ stepDescription: "The quick brown fox jumps over the lazy dog",
+ completed: false,
},
{
- label: 'Step 4',
- description: 'The quick brown fox jumps over the lazy dog'
- }],
- currentStepIndex: 2,
- // onStepClick: (stepIndex: number) => console.log("🚀 ~ file: StepperComponent.stories.tsx:37 ~ stepIndex", stepIndex)
- // renderBubble: (step, index) => (<>>),
- // labelPosition: 'right',
- // styles: {
- // Bubble: () => ({ background: 'yellow'}),
- // LineSeparator: (step: IStep, index: number) => (index === 2 ? { borderRight: '1px solid red' } : {}),
- // InactiveLineSeparator: (step: IStep, index: number) => (index === 2 ? { borderRight: '1px dashed red' } : {}),
- // LabelTitle: () => ({ background: 'red'}),
- // ActiveLabelTitle: () => ({ background: 'green'}),
- // LabelDescription: () => ({ background: 'red'}),
- // ActiveLabelDescription: () => ({ background: 'green'}),
- // ActiveBubble: () => ({ background: 'orange'}),
- // InActiveBubble: () => ({ background: 'grey'})
- // }
-};
\ No newline at end of file
+ stepLabel: "Step 4",
+ stepDescription: "The quick brown fox jumps over the lazy dog",
+ completed: false,
+ },
+];
+
+export const HorizontalStepperWithLabelOnLeft = Template.bind({});
+HorizontalStepperWithLabelOnLeft.args = {
+ orientation: "horizontal",
+ labelPosition: "left",
+ currentStepIndex: 2,
+ steps,
+ showDescriptionsForAllSteps: false,
+};
+
+export const HorizontalStepperWithLabelOnRight = Template.bind({});
+HorizontalStepperWithLabelOnRight.args = {
+ orientation: "horizontal",
+ labelPosition: "right",
+ currentStepIndex: 2,
+ steps,
+ showDescriptionsForAllSteps: false,
+};
+
+export const HorizontalStepperWithLabelOnTop = Template.bind({});
+HorizontalStepperWithLabelOnTop.args = {
+ orientation: "horizontal",
+ labelPosition: "top",
+ currentStepIndex: 2,
+ steps,
+ showDescriptionsForAllSteps: false,
+ stepContent: () => {
+ return (Test )
+ }
+};
+
+export const HorizontalStepperWithLabelOnBottom = Template.bind({});
+HorizontalStepperWithLabelOnBottom.args = {
+ orientation: "horizontal",
+ labelPosition: "bottom",
+ currentStepIndex: 2,
+ steps,
+ showDescriptionsForAllSteps: false,
+};
+
+export const VerticalStepperWithLabelOnLeft = Template.bind({});
+VerticalStepperWithLabelOnLeft.args = {
+ orientation: "vertical",
+ labelPosition: "left",
+ currentStepIndex: 2,
+ steps,
+ showDescriptionsForAllSteps: false,
+};
+
+export const VerticalStepperWithLabelOnRight = Template.bind({});
+VerticalStepperWithLabelOnRight.args = {
+ orientation: "vertical",
+ labelPosition: "right",
+ currentStepIndex: 2,
+ steps,
+ showDescriptionsForAllSteps: false,
+};
+
+export const VerticalStepperWithLabelOnTop = Template.bind({});
+VerticalStepperWithLabelOnTop.args = {
+ orientation: "vertical",
+ labelPosition: "top",
+ currentStepIndex: 2,
+ steps,
+ showDescriptionsForAllSteps: false,
+};
+
+export const VerticalStepperWithLabelOnBottom = Template.bind({});
+VerticalStepperWithLabelOnBottom.args = {
+ orientation: "vertical",
+ labelPosition: "bottom",
+ currentStepIndex: 2,
+ steps,
+ showDescriptionsForAllSteps: false,
+};
diff --git a/src/tests/stepperComponent.test.tsx b/src/tests/stepperComponent.test.tsx
index 8750fb3..b878b47 100644
--- a/src/tests/stepperComponent.test.tsx
+++ b/src/tests/stepperComponent.test.tsx
@@ -1,70 +1,227 @@
-import React from 'react';
+import React from "react";
import {
- render,
- fireEvent,
- queryByAttribute,
- queryAllByAttribute
+ render,
+ fireEvent,
+ queryByAttribute,
+ queryAllByAttribute,
} from "@testing-library/react";
-import { IStep } from '../stepper-component/types';
-import Stepper from "../stepper-component/stepperComponent";
+import { IStep } from "../stepper/types";
+import Stepper from "../stepper/stepperComponent";
+import { LABEL_POSITION, ORIENTATION } from "../constants";
+
+const getById = queryByAttribute.bind(null, "id");
+const getAllById = queryAllByAttribute.bind(null, "id");
-const getById = queryByAttribute.bind(null, 'id');
-const getAllById = queryAllByAttribute.bind(null, 'id');
test("Stepper Component - Label and description", async () => {
- const steps: IStep[] = [{
- label: 'Step 1',
- description: 'Demo description',
- status: 'completed'
- }]
- const dom = render()
- const label = await getById(dom.container, "stepper-label-0");
- expect(label.innerHTML).toBe('Step 1');
- const description = await getById(dom.container, "stepper-desc-0");
- expect(description.innerHTML).toBe('Demo description');
+ const steps: IStep[] = [
+ {
+ stepLabel: "Step 1",
+ stepDescription: "Demo description",
+ completed: true,
+ },
+ ];
+ const dom = render();
+ const label = await getById(dom.container, "step-label-0");
+ expect(label.innerHTML).toBe("Step 1");
+ const description = await getById(dom.container, "step-description-0");
+ expect(description.innerHTML).toBe("Demo description");
});
test("Stepper Component - No description", async () => {
- const steps: IStep[] = [{
- label: 'Step 1',
- status: 'completed'
- }];
- const dom = render()
- try {
- const val = await getById(dom.container, "stepper-desc-0");
- if (val === null) throw Error();
- } catch (err){
- return;
+ const steps: IStep[] = [
+ {
+ stepLabel: "Step 1",
+ completed: true,
+ },
+ ];
+ const dom = render();
+ try {
+ const val = await getById(dom.container, "step-description-0");
+ if (val === null) {
+ throw Error("Description found");
}
- throw Error("Description found");
-})
+ } catch (err) {
+ return;
+ }
+});
-test("Stepper Component - Number of steps", async () => {
- const steps: IStep[] = [{
- label: 'Step 1',
- status: 'completed'
- },{
- label: 'Step 2',
- status: 'visited'
- }];
- const dom = render();
- const elements = await getAllById(dom.container, "stepper-steps");
- expect(elements?.length).toBe(2);
-})
+test("Stepper Component - with multiple steps and currentStepIndex passed", async () => {
+ const steps: IStep[] = [
+ {
+ stepLabel: "Step 1",
+ stepDescription: "Step 1 description",
+ completed: true,
+ },
+ {
+ stepLabel: "Step 2",
+ stepDescription: "Step 2 description",
+ completed: false,
+ },
+ {
+ stepLabel: "Step 3",
+ stepDescription: "Step 3 description",
+ completed: false,
+ },
+ ];
+ const dom = render();
+ const elements = await getAllById(dom.container, "stepper-step");
+ expect(elements?.length).toBe(3);
+});
test("Stepper Component - On Click function", async () => {
- const steps: IStep[] = [{
- label: 'Step 1',
- description: 'Demo description',
- status: 'completed'
- }];
- const onClick = jest.fn();
- const dom = render(
-
- )
- const bubble = await getById(dom.container, "stepper-bubble");
- fireEvent.click(bubble);
- expect(onClick).toBeCalled();
-})
\ No newline at end of file
+ const steps: IStep[] = [
+ {
+ stepLabel: "Step 1",
+ stepDescription: "Demo description",
+ completed: true,
+ },
+ ];
+ const onClick = jest.fn();
+ const dom = render();
+ const node = await getById(dom.container, "stepper-node");
+ fireEvent.click(node);
+ expect(onClick).toBeCalled();
+});
+
+test("Stepper Component - customized node", async () => {
+ const steps: IStep[] = [
+ {
+ stepLabel: "Step 1",
+ stepDescription: "Demo description",
+ completed: true,
+ },
+ ];
+ const renderNode = jest.fn();
+ const dom = render();
+ const node = await getById(dom.container, "stepper-node");
+ fireEvent.click(node);
+ expect(renderNode).toBeCalled();
+});
+
+test("Stepper Component - custom style", async () => {
+ const steps: IStep[] = [
+ {
+ stepLabel: "Step 1",
+ stepDescription: "Demo description 1",
+ completed: true,
+ },
+ {
+ stepLabel: "Step 2",
+ stepDescription: "Demo description 2",
+ completed: false,
+ },
+ {
+ stepLabel: "Step 1",
+ stepDescription: "Demo description 3",
+ completed: false,
+ },
+ ];
+ const styles = {
+ LineSeparator: () => ({
+ minHeight: "20px",
+ }),
+ InactiveLineSeparator: () => ({
+ backgroundColor: "black",
+ }),
+ LabelDescription: () => ({
+ color: "black",
+ }),
+ LabelTitle: () => ({
+ color: "black",
+ }),
+ ActiveLabelTitle: () => ({
+ color: "blue",
+ }),
+ ActiveLabelDescription: () => ({
+ color: "red",
+ }),
+ Node: () => ({
+ backgroundColor: "red",
+ }),
+ ActiveNode: () => ({
+ backgroundColor: "blue",
+ }),
+ InActiveNode: () => ({
+ backgroundColor: "black",
+ }),
+ };
+ const renderNode = jest.fn();
+ const dom = render(
+
+ );
+ const label1 = await getById(dom.container, "step-label-1");
+ expect(label1.innerHTML).toBe("Step 2");
+ const description1 = await getById(dom.container, "step-description-1");
+ expect(description1.innerHTML).toBe("Demo description 2");
+});
+
+test("Stepper Component - orientation:vertical and labelPosition: top", async () => {
+ const steps: IStep[] = [
+ {
+ stepLabel: "Step 1",
+ stepDescription: "Demo description",
+ completed: true,
+ },
+ ];
+ const dom = render(
+
+ );
+ const label = await getById(dom.container, "step-label-0");
+ expect(label.innerHTML).toBe("Step 1");
+ const description = await getById(
+ dom.container,
+ "step-horizontal-top-description-0"
+ );
+ expect(description.innerHTML).toBe("Demo description");
+});
+
+test("Stepper Component - orientation:vertical and labelPosition: bottom", async () => {
+ const steps: IStep[] = [
+ {
+ stepLabel: "Step 1",
+ stepDescription: "Demo description",
+ completed: true,
+ },
+ ];
+ const dom = render(
+
+ );
+ const label = await getById(dom.container, "step-label-0");
+ expect(label.innerHTML).toBe("Step 1");
+ const description = await getById(dom.container, "step-description-0");
+ expect(description.innerHTML).toBe("Demo description");
+});
+
+test("Stepper Component - orientation:vertical and labelPosition: bottom", async () => {
+ const steps: IStep[] = [
+ {
+ stepLabel: "Step 1",
+ stepDescription: "Demo description",
+ completed: true,
+ },
+ ];
+ const dom = render(
+
+ );
+ const label = await getById(dom.container, "step-inline-label-0");
+ expect(label.innerHTML).toBe("Step 1");
+ const description = await getById(dom.container, "step-description-0");
+ expect(description.innerHTML).toBe("Demo description");
+});
diff --git a/src/utils/getLabelStyle.ts b/src/utils/getLabelStyle.ts
new file mode 100644
index 0000000..0461f19
--- /dev/null
+++ b/src/utils/getLabelStyle.ts
@@ -0,0 +1,12 @@
+import { LABEL_POSITION, ORIENTATION } from "../constants";
+
+const getLabelStyle: (orientation?: string, labelPosition?: string) => string | undefined = (orientation, labelPosition) => {
+ if (orientation === ORIENTATION.HORIZONTAL) {
+ if (labelPosition === LABEL_POSITION.TOP) return "horizontalLabelTop";
+ else if (labelPosition === LABEL_POSITION.BOTTOM)
+ return "horizontalLabelBottom";
+ } else if (labelPosition === LABEL_POSITION.RIGHT)
+ return "verticalLabelRight";
+};
+
+export default getLabelStyle;
diff --git a/src/utils/getStyles.ts b/src/utils/getStyles.ts
new file mode 100644
index 0000000..a856f02
--- /dev/null
+++ b/src/utils/getStyles.ts
@@ -0,0 +1,13 @@
+import { Elements } from "../constants";
+import { IStep, IStyleFunction } from "../stepper/types";
+
+
+const getStyles = (styles: { [key in Elements]?: IStyleFunction }, element: Elements, step: IStep, index: number): object => {
+ const getElementStyle = styles[element];
+ if (getElementStyle) {
+ return getElementStyle(step, index);
+ }
+ return {};
+};
+
+export default getStyles;
diff --git a/tsconfig.json b/tsconfig.json
index 068c8b7..9b69018 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -10,6 +10,7 @@
"noEmit": false,
"declaration": true,
"suppressImplicitAnyIndexErrors": true,
+ "ignoreDeprecations": "5.0",
"allowSyntheticDefaultImports": true,
"lib": ["es2018", "dom"],
"moduleResolution": "node",
|