From f22db94bf2910db6dcb74796c3bb13c02d9f6e88 Mon Sep 17 00:00:00 2001 From: Prabhu Murthy Date: Fri, 24 Jun 2022 14:45:27 +0530 Subject: [PATCH] Revamped wizard stepper UX (#14) --- .eslintrc.js | 15 ++- package.json | 1 + pnpm-lock.yaml | 34 +++++++ src/App.tsx | 90 ++++++++--------- .../form-field/form-field-input.tsx | 30 +++--- src/components/form-field/form-field.tsx | 20 ++-- src/components/page/page.tsx | 9 +- src/components/theme-default.tsx | 8 +- src/components/wizard-context.ts | 11 +++ .../__tests__/wizard-footer.test.tsx | 5 +- .../wizard-footer/wizard-footer.tsx | 14 +-- .../wizard-header/wizard-header-tab.tsx | 89 +++++++++++++++++ .../wizard-header/wizard-header.model.ts | 1 + .../wizard-header/wizard-header.module.scss | 96 ++++++++++++------- .../wizard-header/wizard-header.tsx | 94 ++++-------------- src/components/wizard.model.ts | 4 + src/components/wizard.tsx | 39 ++++---- 17 files changed, 342 insertions(+), 218 deletions(-) create mode 100644 src/components/wizard-context.ts create mode 100644 src/components/wizard-header/wizard-header-tab.tsx diff --git a/.eslintrc.js b/.eslintrc.js index 934a04b..b7954f1 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -17,6 +17,17 @@ module.exports = { ecmaVersion: "latest", sourceType: "module", }, - plugins: ["react", "@typescript-eslint"], - rules: {}, + plugins: ["react", "@typescript-eslint", "sort-keys-fix"], + rules: { + "react/jsx-sort-props": 1, + "sort-keys-fix/sort-keys-fix": "warn", + "sort-keys": [ + "error", + "asc", + { + caseSensitive: true, + natural: true, + }, + ], + }, }; diff --git a/package.json b/package.json index 1c110c9..c1d9a58 100644 --- a/package.json +++ b/package.json @@ -119,6 +119,7 @@ "eslint-plugin-promise": "^6.0.0", "eslint-plugin-react": "^7.30.0", "eslint-plugin-security": "^1.5.0", + "eslint-plugin-sort-keys-fix": "^1.1.2", "husky": "^8.0.1", "jsdom": "^20.0.0", "mini-css-extract-plugin": "^2.6.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cf9c389..0466b7b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -30,6 +30,7 @@ specifiers: eslint-plugin-promise: ^6.0.0 eslint-plugin-react: ^7.30.0 eslint-plugin-security: ^1.5.0 + eslint-plugin-sort-keys-fix: ^1.1.2 husky: ^8.0.1 jsdom: ^20.0.0 mini-css-extract-plugin: ^2.6.1 @@ -89,6 +90,7 @@ devDependencies: eslint-plugin-promise: 6.0.0_eslint@8.18.0 eslint-plugin-react: 7.30.0_eslint@8.18.0 eslint-plugin-security: 1.5.0 + eslint-plugin-sort-keys-fix: 1.1.2 husky: 8.0.1 jsdom: 20.0.0 mini-css-extract-plugin: 2.6.1_webpack@5.73.0 @@ -2338,6 +2340,14 @@ packages: acorn: 8.7.1 dev: true + /acorn-jsx/5.3.2_acorn@7.4.1: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + dependencies: + acorn: 7.4.1 + dev: true + /acorn-jsx/5.3.2_acorn@8.7.1: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -4444,6 +4454,16 @@ packages: safe-regex: 2.1.1 dev: true + /eslint-plugin-sort-keys-fix/1.1.2: + resolution: {integrity: sha512-DNPHFGCA0/hZIsfODbeLZqaGY/+q3vgtshF85r+YWDNCQ2apd9PNs/zL6ttKm0nD1IFwvxyg3YOTI7FHl4unrw==} + engines: {node: '>=0.10.0'} + dependencies: + espree: 6.2.1 + esutils: 2.0.3 + natural-compare: 1.4.0 + requireindex: 1.2.0 + dev: true + /eslint-scope/5.1.1: resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} engines: {node: '>=8.0.0'} @@ -4536,6 +4556,15 @@ packages: - supports-color dev: true + /espree/6.2.1: + resolution: {integrity: sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw==} + engines: {node: '>=6.0.0'} + dependencies: + acorn: 7.4.1 + acorn-jsx: 5.3.2_acorn@7.4.1 + eslint-visitor-keys: 1.3.0 + dev: true + /espree/9.3.2: resolution: {integrity: sha512-D211tC7ZwouTIuY5x9XnS0E9sWNChB7IYKX/Xp5eQj3nFXhqmiUDB9q27y76oFl8jTg3pXcQx/bpxMfs3CIZbA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -8046,6 +8075,11 @@ packages: engines: {node: '>=0.10.0'} dev: true + /requireindex/1.2.0: + resolution: {integrity: sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww==} + engines: {node: '>=0.10.5'} + dev: true + /resolve-cwd/3.0.0: resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} engines: {node: '>=8'} diff --git a/src/App.tsx b/src/App.tsx index 16472b5..1e40022 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,50 +1,27 @@ import "./App.css"; import { Wizard } from "./components/wizard"; -import { Box, Dollar, Twitter, User } from "./example-assets"; function App() { return (
, - , - , - , - ]} bodyHeight={750} highlightFieldsOnValidation - validationDelay={100} + noPageTitle strict={false} - // RTL onFinish={(val) => console.log(val)} pages={[ { - title: "Introduction", fields: [ { + isRequired: true, label: "First Name", name: "firstName", - type: "text", - isRequired: true, placeholder: "Enter your first name", + type: "text", validationMessage: "You cannot leave this field empty", }, { @@ -53,26 +30,26 @@ function App() { type: "text", }, { + label: "Date of Birth", name: "dateOfBirth", type: "date", - label: "Date of Birth", }, { + isRequired: true, label: "Email", name: "email", type: "email", - isRequired: true, validationMessage: "Incorrect email id. example: xxxx@yyy.com", }, { - name: "Phone number", label: "Phone", + name: "Phone number", type: "phone", }, ], + title: "Introduction", }, { - title: "Employment", fields: [ { label: "Company Name", @@ -90,42 +67,42 @@ function App() { type: "textarea", }, { + isRequired: true, label: "Are you currently working?", name: "employed", - type: "radio", - isRequired: true, options: [ { name: "yes", value: true }, { name: "no", value: false }, ], + type: "radio", }, { + isRequired: true, label: "Choose your skills", name: "skills", - type: "checkbox", - isRequired: true, options: [ { name: "HTML", value: "html" }, { name: "CSS", value: "css" }, { name: "JavaScript", value: "javascript" }, { name: "React", value: "react" }, ], + type: "checkbox", }, ], + title: "Employment", }, { - title: "CV & Social Links", fields: [ { - name: "CV", label: "Upload your CV", + name: "CV", type: "file", }, { + isRequired: true, label: "Portfolio URL", name: "Portfolio", type: "url", - isRequired: true, }, { label: "Linkedin URL", @@ -138,25 +115,25 @@ function App() { type: "url", }, { - name: "twitter", label: "Twitter URL", + name: "twitter", type: "url", }, ], + title: "CV & Social Links", }, { - title: "salary", fields: [ { + isRequired: true, label: "Select your current salary range", name: "salaryRange", - type: "select", - isRequired: true, - options: [ { name: "10,000$ - 50,000$", value: "10-50k" }, { name: "50,000$ - 100,000$", value: "50-100k" }, ], + + type: "select", }, { label: "Expected Salary", @@ -164,8 +141,35 @@ function App() { type: "text", }, ], + title: "salary", }, ]} + // noPageTitle + // icons={[ + // , + // , + // , + // , + // ]} + showStepperTitles + silent + // silent + // strict={false} + // RTL + theme={{ + background: "#000", + fail: "#cf352e", + formFieldBackground: "#282828", + formFieldBorder: "#000", + inputBackground: "#464646", + inputTextColor: "#fff", + primary: "#007fff", + success: "#519259", + tabColor: "#7d7d7d", + tabLineColor: "#464646", + textColor: "#fff", + }} + validationDelay={100} />
); diff --git a/src/components/form-field/form-field-input.tsx b/src/components/form-field/form-field-input.tsx index 6b29a8d..5e64093 100644 --- a/src/components/form-field/form-field-input.tsx +++ b/src/components/form-field/form-field-input.tsx @@ -1,7 +1,7 @@ import classNames from "classnames"; import { FunctionComponent, useContext, useMemo } from "react"; import Asterisk from "../../icons/asterisk"; -import { WizardContext } from "../wizard"; +import { WizardContext } from "../wizard-context"; import { FormChangeEvent, FormFieldProps } from "./form-field.model"; import styles from "./form-field.module.scss"; @@ -39,21 +39,21 @@ const FormFieldInput: FunctionComponent = ({ if (isTextField) { return ( ); } else if (type === "select") { return ( ); } else if (type === "radio" || type === "checkbox") { @@ -78,13 +78,13 @@ const FormFieldInput: FunctionComponent = ({ {options.map(({ name: optionName, id, value }) => ( @@ -99,9 +99,9 @@ const FormFieldInput: FunctionComponent = ({ {getInputType} {isRequired && ( diff --git a/src/components/form-field/form-field.tsx b/src/components/form-field/form-field.tsx index d93ddec..419ee48 100644 --- a/src/components/form-field/form-field.tsx +++ b/src/components/form-field/form-field.tsx @@ -10,7 +10,7 @@ import { } from "react"; import CheckIcon from "../../icons/check"; import Exclamation from "../../icons/exclamation"; -import { WizardContext } from "./../wizard"; +import { WizardContext } from "../wizard-context"; import { FormFieldInput } from "./form-field-input"; import { FormFieldMessage } from "./form-field-message"; import { FormChangeEvent, FormFieldProps } from "./form-field.model"; @@ -75,7 +75,7 @@ const FormField: FunctionComponent = ({ isValid ? styles.is_valid : isValid !== null ? styles.is_not_valid : "", highlight ? styles.highlight : "", RTL ? styles.RTL : "", - silent ? styles.no_border : '' + silent ? styles.no_border : "" ), [isValid, highlight, silent] ); @@ -119,11 +119,11 @@ const FormField: FunctionComponent = ({ return (
{canShowCheckIcon ? ( - + ) : !isValid && isValid !== null ? ( - + ) : null} @@ -132,21 +132,21 @@ const FormField: FunctionComponent = ({
diff --git a/src/components/page/page.tsx b/src/components/page/page.tsx index c01e502..117619a 100644 --- a/src/components/page/page.tsx +++ b/src/components/page/page.tsx @@ -7,13 +7,13 @@ import { useImperativeHandle, useMemo, useRef, - useState, + useState } from "react"; import { getValidationMessage, validator } from "../../utils"; import { FormField } from "../form-field/form-field"; import { FormFieldProps } from "../form-field/form-field.model"; import { PageModelProps } from "../page/page.model"; -import { WizardContext } from "../wizard"; +import { WizardContext } from './../wizard-context'; import styles from "./page.module.scss"; const Page = forwardRef<{ height: number; id: string }, PageModelProps>( @@ -109,7 +109,7 @@ const Page = forwardRef<{ height: number; id: string }, PageModelProps>( }, [JSON.stringify(_fields)]); return ( -
+
{!noPageTitle && (
{title} @@ -122,8 +122,8 @@ const Page = forwardRef<{ height: number; id: string }, PageModelProps>( ))}
@@ -135,3 +135,4 @@ const Page = forwardRef<{ height: number; id: string }, PageModelProps>( Page.displayName = "Page"; export { Page }; + diff --git a/src/components/theme-default.tsx b/src/components/theme-default.tsx index fe3c50e..cff820a 100644 --- a/src/components/theme-default.tsx +++ b/src/components/theme-default.tsx @@ -1,16 +1,16 @@ import { Theme } from "./wizard.model"; export const ThemeDefaults: Theme = { - primary: "#007fff", background: "#f8f8f8", - formFieldBackground: "#fff", - success: "#1db954", fail: "#de1738", - textColor: "#000", + formFieldBackground: "#fff", formFieldBorder: "#dcdcdc", inputBackground: "#e8e8e8", inputTextColor: "#000", + primary: "#007fff", + success: "#1db954", tabColor: "#f8f8f8", tabLineColor: "#ccc", + textColor: "#000", warning: "#ffae42", }; diff --git a/src/components/wizard-context.ts b/src/components/wizard-context.ts new file mode 100644 index 0000000..3c017f7 --- /dev/null +++ b/src/components/wizard-context.ts @@ -0,0 +1,11 @@ +import { createContext } from "react"; +import { contextType } from "./wizard.model"; + +export const WizardContext = createContext({ + RTL: false, + highlightFieldsOnValidation: false, + noPageTitle: false, + silent: false, + strict: true, + validationDelay: 250, +}); diff --git a/src/components/wizard-footer/__tests__/wizard-footer.test.tsx b/src/components/wizard-footer/__tests__/wizard-footer.test.tsx index d00acdc..42ffb9a 100644 --- a/src/components/wizard-footer/__tests__/wizard-footer.test.tsx +++ b/src/components/wizard-footer/__tests__/wizard-footer.test.tsx @@ -1,5 +1,4 @@ import { fireEvent, render } from "@testing-library/react"; -import React from "react"; import { describe, expect, it, vi } from "vitest"; import { PageModelProps } from "../../page/page.model"; import { WizardFooter } from "../wizard-footer"; @@ -82,12 +81,12 @@ describe.concurrent("WizardFooter", () => { ); expect(getByRole("alert")).toHaveTextContent( - "Please correct the errors in the form." + "Please correct errors in the form." ); }); }); diff --git a/src/components/wizard-footer/wizard-footer.tsx b/src/components/wizard-footer/wizard-footer.tsx index 44ccc41..f587daf 100644 --- a/src/components/wizard-footer/wizard-footer.tsx +++ b/src/components/wizard-footer/wizard-footer.tsx @@ -2,8 +2,8 @@ import classNames from "classnames"; import { FunctionComponent, useContext, useMemo } from "react"; import ChevronLeft from "../../icons/chevron-left"; import ChevronRight from "../../icons/chevron-right"; -import { WizardContext } from "../wizard"; import { WizardFooterProps } from "../wizard-footer/wizard-footer.model"; +import { WizardContext } from "./../wizard-context"; import styles from "./wizard-footer.module.scss"; const WizardFooter: FunctionComponent = ({ @@ -86,13 +86,13 @@ const WizardFooter: FunctionComponent = ({
{!hideBack && (