From a3c523db4a007f43a5007058180cc6a13d5758c3 Mon Sep 17 00:00:00 2001 From: ReshmaJoshy <81366074+ReshmaJoshy@users.noreply.github.com> Date: Tue, 28 Nov 2023 10:00:06 +0530 Subject: [PATCH] Feat:Updated component to support props orientation: horizontal/vertical and label position: top/bottom/left/right wrt step node --- .gitignore | 1 + CODE_OF_CONDUCT.md | 2 +- LICENSE | 2 +- PULL_REQUEST_TEMPLATE.md | 76 ++++++ README.md | 181 ++++++++------ STYLE_GUIDELINES.md | 23 ++ package-lock.json | 19 +- package.json | 15 +- src/assets/horizontal-stepper-example.png | Bin 0 -> 7285 bytes src/assets/vertical-stepper-example.png | Bin 17534 -> 7515 bytes src/bubble/bubble.tsx | 88 ------- src/bubble/index.ts | 3 - src/bubble/types.d.ts | 14 -- src/constants.ts | 36 +-- src/index.tsx | 2 +- src/node/index.ts | 3 + src/node/node.tsx | 52 ++++ src/{bubble => node}/styles.module.scss | 17 +- src/node/types.d.ts | 13 + src/stepper-component/index.ts | 3 - src/stepper-component/stepperComponent.tsx | 58 ----- src/stepper-component/styles.module.scss | 32 --- src/stepper-component/types.d.ts | 20 -- src/stepper/index.ts | 3 + src/stepper/step.tsx | 124 ++++++++++ src/stepper/stepContent.tsx | 80 ++++++ src/stepper/stepInfo.tsx | 126 ++++++++++ src/stepper/stepperComponent.tsx | 33 +++ src/stepper/styles.scss | 205 +++++++++++++++ src/stepper/types.d.ts | 59 +++++ src/stories/StepperComponent.stories.tsx | 151 +++++++---- src/tests/stepperComponent.test.tsx | 275 ++++++++++++++++----- src/utils/getLabelStyle.ts | 12 + src/utils/getStyles.ts | 13 + tsconfig.json | 1 + 35 files changed, 1305 insertions(+), 437 deletions(-) create mode 100644 PULL_REQUEST_TEMPLATE.md create mode 100644 STYLE_GUIDELINES.md create mode 100644 src/assets/horizontal-stepper-example.png delete mode 100644 src/bubble/bubble.tsx delete mode 100644 src/bubble/index.ts delete mode 100644 src/bubble/types.d.ts create mode 100644 src/node/index.ts create mode 100644 src/node/node.tsx rename src/{bubble => node}/styles.module.scss (84%) create mode 100644 src/node/types.d.ts delete mode 100644 src/stepper-component/index.ts delete mode 100644 src/stepper-component/stepperComponent.tsx delete mode 100644 src/stepper-component/styles.module.scss delete mode 100644 src/stepper-component/types.d.ts create mode 100644 src/stepper/index.ts create mode 100644 src/stepper/step.tsx create mode 100644 src/stepper/stepContent.tsx create mode 100644 src/stepper/stepInfo.tsx create mode 100644 src/stepper/stepperComponent.tsx create mode 100644 src/stepper/styles.scss create mode 100644 src/stepper/types.d.ts create mode 100644 src/utils/getLabelStyle.ts create mode 100644 src/utils/getStyles.ts diff --git a/.gitignore b/.gitignore index 9b69fe1..ef6d887 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ /build /dist .rpt2_cache +coverage # misc .DS_Store diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index b8ed5cf..08bfd22 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -55,7 +55,7 @@ further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported by contacting the project team at opensource@hodgef.com. All +reported by contacting the project team at it.team@keyvalue.systems. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. diff --git a/LICENSE b/LICENSE index da5eede..7508013 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2019 Francisco Hodge +Copyright (c) 2023 Keyvalue Software Systems Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..764822e --- /dev/null +++ b/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,76 @@ +<!-- Thank you for contributing to @keyvaluesystems/react-stepper! --> +<!-- Before submitting a pull request, please review our contributing guidelines. --> + + + +## Pull Request Checklist + + + +- [ ] **Read the contributing guidelines.** +- [ ] **Linked to an issue:** Fixes # (replace with the issue number, if applicable) +- [ ] **Branch is up-to-date with the base branch:** `main` +- [ ] **Changes pass tests locally:** `npm test` or `yarn test` +- [ ] **Documentation has been updated, if necessary** +- [ ] **Code follows the style guide of the project** + + +## Description + + + +<!-- Provide a brief description of your changes. --> + + + +## Screenshots (if applicable) + + + +<!-- Add screenshots or GIFs to help explain your changes. --> + + + +## Additional Notes + + + +<!-- Any additional information you want to provide that is not covered by the checklist or description. --> + + + +## Related Issues or PRs + + + +<!-- If your pull request is related to any issue(s) or other pull request(s), mention them here. --> + + + +## Reviewer Guidelines + + + +<!-- Suggest specific areas of the codebase that you would like the reviewer to focus on. --> + + + +## Testing Instructions + + + +<!-- Provide step-by-step instructions on how to test your changes. --> + + + +## Checklist for Reviewers + + + +- [ ] Code follows project conventions and style +- [ ] Changes do not introduce new warnings or errors +- [ ] Unit tests cover the changes +- [ ] Documentation is updated + + +## By submitting this pull request, I confirm that my contribution is made under the terms of the MIT License. diff --git a/README.md b/README.md index 9e8e35c..7d496b9 100644 --- a/README.md +++ b/README.md @@ -1,75 +1,72 @@ +# React Stepper -# React Vertical Stepper -<a href="https://www.npmjs.com/package/@keyvaluesystems/react-vertical-stepper"><img src="https://badgen.net/npm/v/@keyvaluesystems/react-vertical-stepper?color=blue" alt="npm version"></a> <a href="https://www.npmjs.com/package/@keyvaluesystems/react-vertical-stepper" ><img src="https://img.shields.io/npm/dw/@keyvaluesystems/react-vertical-stepper?label=Downloads" /></a> <a href="https://github.com/KeyValueSoftwareSystems/react-vertical-stepper"><img src="https://github.com/KeyValueSoftwareSystems/react-vertical-stepper/actions/workflows/update-and-publish.yml/badge.svg" alt="" /></a> +<a href="https://www.npmjs.com/package/@keyvaluesystems/react-stepper"><img src="https://badgen.net/npm/v/@keyvaluesystems/react-stepper?color=blue" alt="npm version"></a> <a href="https://www.npmjs.com/package/@keyvaluesystems/react-stepper" ><img src="https://img.shields.io/npm/dw/@keyvaluesystems/react-stepper?label=Downloads" /></a> <a href="https://github.com/KeyValueSoftwareSystems/react-stepper"><img src="https://github.com/KeyValueSoftwareSystems/react-stepper/actions/workflows/update-and-publish.yml/badge.svg" alt="" /></a> -<div align="center"> -<img src="./src/assets/vertical-stepper-example.png" alt="" width="269" height="416"/> +<div style="display: flex; align-items: center;"> +<div style="padding-left: 30px"> +<img src="./src/assets/vertical-stepper-example.png" alt="" width="173" height="281"/> </div> - -A fully customizable ready to use vertical stepper UI package for React. -Try tweaking a vertical stepper using this codesandbox link <a href="https://codesandbox.io/s/vertical-stepper-demo-x24q7u" >here</a> +<div style="padding-left: 30px"> +<img src="./src/assets/horizontal-stepper-example.png" alt="" width="576" height="132"/> +</div> +</div> +A fully customizable ready to use stepper UI package for React. +Try tweaking a stepper using this codesandbox link <a href="https://codesandbox.io/p/sandbox/react-stepper-zp2jrs?file=%2Fsrc%2FApp.js" >here</a> ## Installation +The easiest way to use react-stepper-ui-component is to install it from npm and build it into your app with Webpack. + ```bash -npm install react-vertical-stepper +npm install @keyvaluesystems/react-stepper ``` You’ll need to install React separately since it isn't included in the package. ## Usage -React Vertical Stepper can run in a very basic mode by just providing the `steps` and `currentStepIndex` props like this: +React Stepper can run in a very basic mode by just providing the `steps` and `currentStepIndex` props like this: ```jsx -import React, { useState } from 'react'; -import Stepper from 'react-vertical-stepper'; - -function App() { - const [currentStepIndex, setCurrentStepIndex] = useState(0); - - stepsArray = [{ - label: 'Step 1', - description: 'This is Step 1', - status: 'completed' - },{ - label: 'Step 2', - description: 'This is Step 2', - status: 'visited' - },{ - label: 'Step 3', - description: 'This is Step 3', - status: 'unvisited' - }]; - - return ( - <Stepper - steps={stepsArray} - currentStepIndex={currentStepIndex} - /> - ); -} - -export default App; +<Stepper + steps={[ + { + label: "Step 1", + description: "This is Step 1", + completed: true, + }, + { + label: "Step 2", + description: "This is Step 2", + completed: false, + }, + { + label: "Step 3", + description: "This is Step 3", + completed: false, + }, + ]} + currentStepIndex={1} +/> ``` -The `steps` array is an array of objects with basic keys like -- `label` - a string that can be shown as step label title to your step indicator -- `description` - a string that can be show as step description below the step label -- `status` - can be provided with any of `visited`, `unvisited`, `completed`. Will be required if you are using default styles. +The `steps` array is an array of objects with following keys: ->Note: You can also add any other keys to the step object and other statuses like `skipped` for different customizations as per requirements +- `label` - A mandatory string representing the label/title of the step. +- `description` - Optional extra information or description for the step. +- `completed` - Boolean flag for indicating step completion status. -You can customize the step indicator bubble with your own DOM element using the `renderBubble` prop +You can customize each step node with your own DOM element using the `renderNode` prop ```jsx <Stepper steps={stepsArray} currentStepIndex={currentStepIndex} - renderBubble={(step, stepIndex) => (<div key={stepIndex}>{step.label}</div>)} + renderNode={(step, stepIndex) => <div key={stepIndex}>{step.label}</div>} /> ``` -The `step` param provided by the `renderBubble` callback is the same object you pass as array item in `steps` prop. + +The `step` param provided by the `renderNode` callback is the same object you pass as array item in `steps` prop. ## Props @@ -90,31 +87,52 @@ Props that can be passed to the component are listed below: <td><code>undefined</code></td> </tr> <tr> - <td><code><b>currentIndex:</b> number</code></td> + <td><code><b>currentStepIndex:</b> number</code></td> <td>The index of current active step.</td> <td><code>0</code></td> </tr> <tr> <td><code><b>onStepClick?:</b> (step: object, stepIndex: number): void</code></td> <td> - A step click handler that fires each time you click on a step, its label or its description. + A step click handler that fires each time you click on a step. </td> <td><code>undefined</code></td> </tr> <tr> - <td><code><b>renderBubble?:</b> (step: object, stepIndex: number): ReactElement</code></td> + <td><code><b>renderNode?:</b> (step: object, stepIndex: number): ReactElement</code></td> <td> - A render function to customize your step indicator with your own element. + A render function to customize each step node with your own element. </td> <td><code>undefined</code></td> </tr> <tr> - <td><code><b>labelPosition?:</b> 'left' | 'right'</code></td> + <td><code><b>orientation?:</b> 'horizontal' | 'vertical'</code></td> + <td> + Determines the layout of the stepper. + </td> + <td><code>vertical</code></td> + </tr> + <tr> + <td><code><b>labelPosition?:</b> 'left' | 'right' | 'top' | 'bottom'</code></td> <td> - Allows you to align step label and description to either <code>left</code> or <code>right</code> of step indicator + Allows you to align step label and description with respect to its node </td> <td><code>right</code></td> </tr> + <tr> + <td><code><b>showDescriptionsForAllSteps</b> boolean</code></td> + <td> + A boolean prop specifying whether to show descriptions for all steps within the stepper. + </td> + <td><code>false</code></td> + </tr> + <tr> + <td><code><b>stepContent</b>(step: object, stepIndex: number): ReactElement</code></td> + <td> + Prop that allows for dynamic content display when the step is active + </td> + <td><code>undefined</code></td> + </tr> <tr> <td><code><b>styles?:</b> object</code></td> <td> @@ -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 ( - <Stepper + 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 }), + Node: (step, stepIndex) => ({ ...styles }), + ActiveNode: (step, stepIndex) => ({ ...styles }), + InActiveNode: (step, stepIndex) => ({ ...styles }), + }; + return ( + <Stepper steps={stepsArray} currentStepIndex={currentStepIndex} styles={stylesOverride} - /> - ); + /> + ); } 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 0000000000000000000000000000000000000000..43ee223b241684d1cc46c45013955cbe0a45ea3d GIT binary patch literal 7285 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 literal 0 HcmV?d00001 diff --git a/src/assets/vertical-stepper-example.png b/src/assets/vertical-stepper-example.png index 509793cd2c907cd4426f19ef7bdb4d1b5b2575fd..cd49c336e1fd3151c2cac1247c0a66cf358d4d38 100644 GIT binary patch literal 7515 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 Zf4z|+r`*A+>HX^i#W$+5#nSIS{|oLx|5X40 literal 17534 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 H{*V6$>jvbo 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<IBubbleProps> = (props) => { - const { - step, - renderAdornment, - index, - currentStepIndex, - handleStepClick = null, - showCursor, - getStyles, - labelPosition - } = props; - - return ( - <div - className={`${styles.eachBubble} - ${showCursor && styles.cursorPointer} - ${index === currentStepIndex && styles.activeStepBubble} - ${step.status === STEP_STATUSES.UNVISITED && currentStepIndex !== index && styles.inactiveStepBubble} - `} - style={{ - ...((getStyles(Elements.Bubble)) || {}), - ...((index === currentStepIndex && getStyles(Elements.ActiveBubble)) || {}), - ...((step.status === STEP_STATUSES.UNVISITED && currentStepIndex !== index - && getStyles(Elements.InActiveBubble)) || {}) - }} - onClick={(): void | null => handleStepClick && handleStepClick()} - role="presentation" - id="stepper-bubble" - > - {(renderAdornment && renderAdornment(step, index)) - || ( - <> - {step?.status === STEP_STATUSES.COMPLETED && ( - <img - src={whiteTick} - className={styles.whiteTickImg} - alt="" - />) - || index + 1} - </> - )} - <div className={`${styles.labelContainer} ${styles[`labelContainer__${labelPosition || LABEL_POSITION.RIGHT}`]}`}> - {step?.label && ( - <span - className={`${styles.labelTitle} - ${showCursor && styles.cursorPointer} - ${index === currentStepIndex && styles.activeLabelTitle}`} - style={{ - ...((getStyles(Elements.LabelTitle)) || {}), - ...((index === currentStepIndex && getStyles(Elements.ActiveLabelTitle)) || {}) - }} - onClick={(): void | null => handleStepClick && handleStepClick()} - role="presentation" - id={`stepper-label-${index}`} - > - {step.label} - </span> - )} - {step?.description && ( - <span - className={`${styles.labelDescription} - ${handleStepClick && styles.cursorPointer} - ${index === currentStepIndex && styles.activeLabelDescription}`} - style={{ - ...((getStyles(Elements.LabelDescription)) || {}), - ...((index === currentStepIndex && - getStyles(Elements.ActiveLabelDescription)) || {}) - }} - onClick={(): void | null => handleStepClick && handleStepClick()} - role="presentation" - id={`stepper-desc-${index}`} - > - {step.description} - </span> - )} - </div> - </div> - ); -}; - -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<INodeProps> = (props) => { + const { + step, + renderNode, + index, + currentStepIndex, + handleStepClick, + showCursor, + getStyles + } = props; + + return ( + <div + className={`${styles.eachNode} + ${showCursor && styles.cursorPointer} + ${index === currentStepIndex && styles.activeStepNode} + ${!step.completed && currentStepIndex !== index && styles.inactiveStepNode} + ${step.completed && currentStepIndex !== index && styles.completedStepNode} + `} + style={{ + ...((getStyles(Elements.Node)) || {}), + ...((index === currentStepIndex && getStyles(Elements.ActiveNode)) || {}), + ...((!step.completed && currentStepIndex !== index + && getStyles(Elements.InActiveNode)) || {}) + }} + onClick={(): void | null => handleStepClick && handleStepClick()} + role="presentation" + id="stepper-node" + > + {(renderNode && renderNode(step, index)) + || ( + <> + {step?.completed && ( + <img + src={whiteTick} + className={styles.whiteTickImg} + alt="" + />) + || index + 1} + </> + )} + </div> + ); +}; + +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<IStepperProps> = (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 ( - <div className={classes.stepperContainer}> - {steps?.map((step: IStep, stepIndex: number): ReactElement => ( - <div key={stepIndex} className={classes.eachStep} id="stepper-steps"> - <div className={classes.bubbleLineWrapper}> - <Bubble - step={step} - index={stepIndex} - currentStepIndex= {currentStepIndex} - handleStepClick={(): void => onStepClick && onStepClick(step, stepIndex)} - showCursor={!!onStepClick} - renderAdornment={renderBubble} - getStyles={(element: Elements): object => getStyles(element, step, stepIndex)} - labelPosition={labelPosition} - /> - {stepIndex < steps?.length - 1 && ( - <div - className={`${classes.lineSeparator} - ${stepIndex > currentStepIndex - 1 && classes.inactiveStepLineSeparator}`} - style={{ - ...((getStyles(Elements.LineSeparator, step, stepIndex)) || {}), - ...((stepIndex > currentStepIndex - 1 - && getStyles(Elements.InactiveLineSeparator, step, stepIndex)) || {}) - }} - /> - )} - </div> - </div> - ))} - </div> - ); -}; - -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<HTMLDivElement | null>(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 ? ( + <StepInfo + orientation={orientation} + labelPosition={labelPosition} + isVertical={isVertical} + isInlineLabelsAndSteps={isInlineLabelsAndSteps} + index={index} + currentStepIndex={currentStepIndex} + step={step} + showDescriptionsForAllSteps={showDescriptionsForAllSteps} + onStepClick={onStepClick} + renderNode={renderNode} + styles={styles} + nodeRef={nodeRef} + prevConnectorClassName={prevConnectorClassName} + nextConnectorClassName={nextConnectorClassName} + /> + ) : ( + <div + className={ + orientation === ORIENTATION.VERTICAL && + labelPosition === LABEL_POSITION.LEFT + ? "verticalTextLeftContainer" + : "" + } + > + <StepInfo + orientation={orientation} + labelPosition={labelPosition} + isVertical={isVertical} + isInlineLabelsAndSteps={isInlineLabelsAndSteps} + index={index} + currentStepIndex={currentStepIndex} + step={step} + showDescriptionsForAllSteps={showDescriptionsForAllSteps} + onStepClick={onStepClick} + renderNode={renderNode} + styles={styles} + nodeRef={nodeRef} + prevConnectorClassName={prevConnectorClassName} + nextConnectorClassName={nextConnectorClassName} + /> + <StepContent + labelPosition={labelPosition} + isVertical={isVertical} + currentStepIndex={currentStepIndex} + index={index} + styles={styles} + step={step} + showDescriptionsForAllSteps={showDescriptionsForAllSteps} + middleConnectorClassName={middleConnectorClassName} + stepContent={stepContent} + nodeWidth={nodeWidth} + /> + </div> + ); +}; + +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) => ( + <div + className={`descriptionContainer ${ + labelPosition === "left" ? "labelLeft leftDescription" : "" + }`} + > + {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. */ + <div + className={ + labelPosition === LABEL_POSITION.LEFT + ? "leftContentMiddleConnectorWrapper" + : "middleConnectorWrapper" + } + style={{ + width: nodeWidth / 2 + 1 + }} + > + <div + className={middleConnectorClassName} + style={{ + ...(currentStepIndex > index + ? getStyles(styles, Elements.LineSeparator, step, index) || {} + : getStyles( + styles, + Elements.InactiveLineSeparator, + step, + index + ) || {}) + }} + /> + </div> + )} + <div className={isVertical ? "verticalContentWrapper" : ""}> + {(showDescriptionsForAllSteps || index === currentStepIndex) && ( + <div + className="description" + id={`step-description-${index}`} + style={{ + ...(currentStepIndex === index + ? getStyles( + styles, + Elements.ActiveLabelDescription, + step, + index + ) || {} + : getStyles(styles, Elements.LabelDescription, step, index) || + {}) + }} + > + {step.stepDescription} + </div> + )} + {isVertical && + index === currentStepIndex && + stepContent && + stepContent(step, index)} + </div> + </div> +); + +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) => ( + <div + id="stepper-step" + className={ + isVertical + ? `verticalStepperWrapper ${ + labelPosition === LABEL_POSITION.LEFT ? "labelLeft" : "" + }` + : "horizontalStepperWrapper" + } + > + {!isInlineLabelsAndSteps && ( + <div className={getLabelStyle(orientation, labelPosition)}> + <div + className="label" + id={`step-label-${index}`} + style={{ + ...(getStyles(styles, Elements.LabelTitle, step, index) || {}), + ...(index === currentStepIndex && + (getStyles(styles, Elements.ActiveLabelTitle, step, index) || {})) + }} + > + {step.stepLabel} + </div> + {(showDescriptionsForAllSteps || index === currentStepIndex) && + orientation === ORIENTATION.HORIZONTAL && + labelPosition === LABEL_POSITION.TOP && ( + <div + className="description" + id={`step-horizontal-top-description-${index}`} + style={{ + ...(currentStepIndex === index + ? getStyles(styles, Elements.ActiveLabelDescription, step, index) || + {} + : getStyles(styles, Elements.LabelDescription, step, index) || {}) + }} + > + {step.stepDescription} + </div> + )} + </div> + )} + <div className="stepContainer" id={`${index}-node`} ref={nodeRef}> + <div + className={prevConnectorClassName} + style={{ + ...(currentStepIndex >= index + ? getStyles(styles, Elements.LineSeparator, step, index) || {} + : getStyles(styles, Elements.InactiveLineSeparator, step, index) || {}) + }} + /> + <div + className={`node ${ + [LABEL_POSITION.TOP, LABEL_POSITION.LEFT].includes(labelPosition) + ? "reversedNode" + : "" + }`} + > + <Node + step={step} + index={index} + currentStepIndex={currentStepIndex} + handleStepClick={(): void => + onStepClick && onStepClick(step, index) + } + showCursor={!!onStepClick} + renderNode={renderNode} + getStyles={(element: Elements): object => + getStyles(styles, element, step, index) + } + /> + </div> + {isInlineLabelsAndSteps && ( + <div + className={`labelContainer ${ + [LABEL_POSITION.TOP, LABEL_POSITION.LEFT].includes(labelPosition) + ? "reversedLabelContainer" + : "" + }`} + > + <div className={`label ${isVertical && "verticalStepperInlineLabel"}`} id={`step-inline-label-${index}`} + style={{ + ...(getStyles(styles, Elements.LabelTitle, step, index) || {}), + ...(index === currentStepIndex && + (getStyles(styles, Elements.ActiveLabelTitle, step, index) || {})) + }}> + {step.stepLabel} + </div> + </div> + )} + <div + className={nextConnectorClassName} + style={{ + ...(currentStepIndex > index + ? getStyles(styles, Elements.LineSeparator, step, index) || {} + : getStyles(styles, Elements.InactiveLineSeparator, step, index) || {}) + }} + /> + </div> + </div> +); + +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 ( + <> + <ul + className={`stepper ${ + isVertical ? "verticalStepper" : "horizontalStepper" + }`} + > + {steps.map((step, index) => Step({ stepperProps: props, step, index }))} + </ul> + {!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<HTMLDivElement> | 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<typeof Stepper>; - - - const Template: ComponentStory<typeof Stepper> = (args) => <Stepper {...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<typeof Stepper>; + +const Template: ComponentStory<typeof Stepper> = (props) => ( + <Stepper {...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 (<div style={{width: "100%", height: "400px", backgroundColor: "gray", display: "flex", justifyContent: "center", alignItems: "center"}}>Test</div>) + } +}; + +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(<Stepper steps={steps} />) - 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(<Stepper steps={steps} />); + 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(<Stepper steps={steps} />) - 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(<Stepper steps={steps} />); + 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(<Stepper steps={steps} />); - 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(<Stepper steps={steps} />); + 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( - <Stepper - steps={steps} - onStepClick={onClick} - /> - ) - 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(<Stepper steps={steps} onStepClick={onClick} />); + 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(<Stepper steps={steps} renderNode={renderNode} />); + 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( + <Stepper + steps={steps} + renderNode={renderNode} + currentStepIndex={1} + styles={styles} + /> + ); + 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( + <Stepper + steps={steps} + orientation={ORIENTATION.HORIZONTAL} + labelPosition={LABEL_POSITION.TOP} + /> + ); + 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( + <Stepper + steps={steps} + orientation={ORIENTATION.HORIZONTAL} + labelPosition={LABEL_POSITION.BOTTOM} + /> + ); + 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( + <Stepper + steps={steps} + orientation={ORIENTATION.HORIZONTAL} + labelPosition={LABEL_POSITION.LEFT} + /> + ); + 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",