From e8f30eb97fb265ae9fd7762095893003658dfb69 Mon Sep 17 00:00:00 2001 From: Mateusz Lazaru Date: Thu, 9 Nov 2023 15:11:19 +0100 Subject: [PATCH 01/19] stepperPage - added demo --- src/demo/pages.tsx | 6 ++++++ .../pages/components/stepper/StepperPage.tsx | 21 +++++++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 src/demo/pages/components/stepper/StepperPage.tsx diff --git a/src/demo/pages.tsx b/src/demo/pages.tsx index ed11b84..13f469b 100644 --- a/src/demo/pages.tsx +++ b/src/demo/pages.tsx @@ -58,6 +58,7 @@ import ToastsPage from "./pages/components/toasts/ToastsPage"; import SelectPage from "./pages/forms/select/SelectPage"; import CarouselPage from "./pages/components/carousel/CarouselPage"; import VideoCarouselPage from "./pages/components/video-carousel/VideoCarouselPage"; +import StepperPage from "./pages/components/stepper/StepperPage"; //examples pages import ButtonExamples from "./pages/components/buttons/exampleList"; @@ -197,6 +198,11 @@ const componentsPages: Pages[] = [ path: "/components/video-carousel", element: , }, + { + name: "stepper", + path: "/components/stepper", + element: , + }, ]; const contentStylesPages: Pages[] = [ diff --git a/src/demo/pages/components/stepper/StepperPage.tsx b/src/demo/pages/components/stepper/StepperPage.tsx new file mode 100644 index 0000000..d2dc312 --- /dev/null +++ b/src/demo/pages/components/stepper/StepperPage.tsx @@ -0,0 +1,21 @@ +import React from "react"; +import TEStepper from "../../../../lib/components/Stepper/Stepper"; +import TEStepperStep from "../../../../lib/components/Stepper/StepperStep/StepperStep"; + +const StepperPage = () => { + return ( + + +
Step 1 content
+
+ +
Step 2 content
+
+ +
Step 3 content
+
+
+ ); +}; + +export default StepperPage; From 52284c23fb707430da6b8dabd99704b6ee30d117 Mon Sep 17 00:00:00 2001 From: Mateusz Lazaru Date: Fri, 17 Nov 2023 15:36:30 +0100 Subject: [PATCH 02/19] new component - stepper - created componend with basic funcionalities --- .../pages/components/stepper/StepperPage.tsx | 83 ++++++++++++++++--- src/lib/components/Stepper/Stepper.tsx | 69 +++++++++++++++ src/lib/components/Stepper/StepperContext.ts | 19 +++++ .../Stepper/StepperStep/StepperStep.tsx | 81 ++++++++++++++++++ .../Stepper/StepperStep/stepperStepTheme.ts | 37 +++++++++ .../components/Stepper/StepperStep/types.ts | 26 ++++++ .../Stepper/hooks/useHeadIconClasses.ts | 29 +++++++ .../hooks/useHorizontalStepperHeight.ts | 41 +++++++++ .../Stepper/hooks/useIsStepCompleted.ts | 23 +++++ src/lib/components/Stepper/stepperTheme.ts | 8 ++ src/lib/components/Stepper/types.ts | 23 +++++ src/lib/components/Stepper/utils/utils.ts | 10 +++ src/lib/hooks/useActiveValue.ts | 13 +++ 13 files changed, 450 insertions(+), 12 deletions(-) create mode 100644 src/lib/components/Stepper/Stepper.tsx create mode 100644 src/lib/components/Stepper/StepperContext.ts create mode 100644 src/lib/components/Stepper/StepperStep/StepperStep.tsx create mode 100644 src/lib/components/Stepper/StepperStep/stepperStepTheme.ts create mode 100644 src/lib/components/Stepper/StepperStep/types.ts create mode 100644 src/lib/components/Stepper/hooks/useHeadIconClasses.ts create mode 100644 src/lib/components/Stepper/hooks/useHorizontalStepperHeight.ts create mode 100644 src/lib/components/Stepper/hooks/useIsStepCompleted.ts create mode 100644 src/lib/components/Stepper/stepperTheme.ts create mode 100644 src/lib/components/Stepper/types.ts create mode 100644 src/lib/components/Stepper/utils/utils.ts create mode 100644 src/lib/hooks/useActiveValue.ts diff --git a/src/demo/pages/components/stepper/StepperPage.tsx b/src/demo/pages/components/stepper/StepperPage.tsx index d2dc312..26a0564 100644 --- a/src/demo/pages/components/stepper/StepperPage.tsx +++ b/src/demo/pages/components/stepper/StepperPage.tsx @@ -1,20 +1,79 @@ -import React from "react"; +import React, { useState } from "react"; import TEStepper from "../../../../lib/components/Stepper/Stepper"; import TEStepperStep from "../../../../lib/components/Stepper/StepperStep/StepperStep"; const StepperPage = () => { + const [activeStep, setActiveStep] = useState(1); return ( - - -
Step 1 content
-
- -
Step 2 content
-
- -
Step 3 content
-
-
+
+
+

+ Basic example +

+
+ + +
Step 1 content
+
XXXXs
+
Step 1 content
+
XXXXs
+
Step 1 content
+
XXXXs
+
Step 1 content
+
XXXXs
+
Step 1 content
+
XXXXs
+
+ +
Step 2 content
+
+ +
Step 3 content
+
+
+
+ +
+ +

+ Controlled active step +

+ +
+ { + console.log("onChange: ", stepId); + setActiveStep(stepId); + }} + > + +
Step 1 content
+
XXXXs
+
Step 1 content
+
XXXXs
+
Step 1 content
+
XXXXs
+
Step 1 content
+
XXXXs
+
Step 1 content
+
XXXXs
+
+ +
Step 2 content
+
+ +
Step 3 content
+
+
+
+
+
); }; diff --git a/src/lib/components/Stepper/Stepper.tsx b/src/lib/components/Stepper/Stepper.tsx new file mode 100644 index 0000000..aebd251 --- /dev/null +++ b/src/lib/components/Stepper/Stepper.tsx @@ -0,0 +1,69 @@ +import clsx from "clsx"; +import React, { useEffect, useRef, useState, useMemo } from "react"; +import StepperContext from "./StepperContext"; +import { StepperStepProps } from "./StepperStep/types"; +import StepperTheme from "./stepperTheme"; +import useActiveValue from "../../hooks/useActiveValue"; +import type { StepperProps } from "./types"; +// import MDBStepperMobileHead from "./StepperMobileHead/StepperMobileHead"; +// import MDBStepperMobileFooter from "./StepperMobileFooter/StepperMobileFooter"; + +const TEStepper: React.FC = ({ + theme: customTheme, + className, + defaultStep = 1, + activeStep: activeStepProp, + children, + onChange, +}) => { + const theme = { + ...StepperTheme, + ...customTheme, + }; + const classes = clsx(theme.stepper, className); + const [activeStepState, setActiveStepState] = useState(defaultStep); + const activeStep = useActiveValue(activeStepProp, activeStepState); + const stepperRef = useRef(null); + const [stepperHeight, setStepperHeight] = useState("0"); + + const childrenArray = useMemo(() => { + return React.Children.toArray( + children + ) as React.ReactElement[]; + }, [children]); + + const onChangeHandler = (id: number) => { + onChange?.(id); + setActiveStepState(id); + }; + return ( + <> + +
    + {childrenArray.map((ChildComponent, index: number) => { + return React.cloneElement(ChildComponent, { + itemId: index + 1, + activeStep, + key: "stepper-step-" + index, + onChange: onChangeHandler, + }); + })} +
+
+ + ); +}; + +export default TEStepper; diff --git a/src/lib/components/Stepper/StepperContext.ts b/src/lib/components/Stepper/StepperContext.ts new file mode 100644 index 0000000..8ebddf4 --- /dev/null +++ b/src/lib/components/Stepper/StepperContext.ts @@ -0,0 +1,19 @@ +import { createContext } from "react"; + +interface StepperContextProps { + activeStep: number; + onChange?: (id: number) => void; + stepperRef: React.RefObject | null; + stepperHeight: string; + setStepperHeight: (height: string) => void; +} + +const StepperContext = createContext({ + activeStep: 1, + onChange: () => {}, + stepperRef: null, + stepperHeight: "0", + setStepperHeight: () => {}, +}); + +export default StepperContext; diff --git a/src/lib/components/Stepper/StepperStep/StepperStep.tsx b/src/lib/components/Stepper/StepperStep/StepperStep.tsx new file mode 100644 index 0000000..a6409af --- /dev/null +++ b/src/lib/components/Stepper/StepperStep/StepperStep.tsx @@ -0,0 +1,81 @@ +import React, { useMemo, useRef, useContext } from "react"; +import StepperStepTheme from "./stepperStepTheme"; +import StepperContext from "../StepperContext"; +import { StepperStepProps } from "./types"; +import clx from "clsx"; +import useHeadIconClasses from "../hooks/useHeadIconClasses"; +import useIsStepCompleted from "../hooks/useIsStepCompleted"; +import useStepperHeight from "../hooks/useHorizontalStepperHeight"; +import { getTranslateDirection } from "../utils/utils"; + +const TEStepperStep: React.FC = ({ + theme: customTheme, + className, + itemId = 1, + headIcon = "", + headText = "", + children, +}) => { + const headRef = useRef(null); + const contentRef = useRef(null); + const { activeStep, onChange } = useContext(StepperContext); + + const animationDirection = useMemo(() => { + return getTranslateDirection(activeStep, itemId); + }, [activeStep, itemId]); + + const isActive = useMemo(() => { + return activeStep === itemId; + }, [activeStep, itemId]); + const isCompleted = useIsStepCompleted(activeStep, isActive, itemId); + const theme = { + ...StepperStepTheme, + ...customTheme, + }; + + const stepperStepClasses = clx(theme.stepperStep, className); + const headIconClasses = useHeadIconClasses( + isActive, + isCompleted, + false, + theme + ); + + useStepperHeight(isActive, headRef, contentRef, false, children); + const dynamicAnimationDirection: string = `stepperContentTranslated${animationDirection}`; + + return ( +
  • +
    onChange?.(itemId)} + ref={headRef} + > + {headIcon} + + {headText} + +
    +
    { + console.log(e); + }} + ref={contentRef} + > + {children} +
    +
  • + ); +}; + +export default TEStepperStep; diff --git a/src/lib/components/Stepper/StepperStep/stepperStepTheme.ts b/src/lib/components/Stepper/StepperStep/stepperStepTheme.ts new file mode 100644 index 0000000..0dbe14e --- /dev/null +++ b/src/lib/components/Stepper/StepperStep/stepperStepTheme.ts @@ -0,0 +1,37 @@ +interface StepperStepThemeProps { + stepperStep: string; + stepperHead: string; + stepperHeadIcon: string; + stepperHeadIconActive: string; + stepperHeadIconCompleted: string; + stepperHeadText: string; + stepperHeadTextActive: string; + stepperContentTranslatedLeft: string; + stepperContentTranslatedRight: string; + stepperContentActive: string; +} + +const StepperStepTheme = { + stepperStep: "w-[4.5rem] flex-auto", + stepperHead: + "flex cursor-pointer items-center leading-[1.3rem] no-underline before:mr-2 before:h-px before:w-full before:flex-1 before:bg-[#e0e0e0] before:content-[''] after:ml-2 after:h-px after:w-full after:flex-1 after:bg-[#e0e0e0] after:content-[''] hover:bg-[#f9f9f9] focus:outline-none dark:before:bg-neutral-600 dark:after:bg-neutral-600 dark:hover:bg-[#3b3b3b]", + stepperHeadIcon: + "my-6 mr-2 flex h-[1.938rem] w-[1.938rem] items-center justify-center rounded-full bg-[#ebedef] text-sm font-medium text-[#40464f]", + stepperHeadIconActive: + "my-6 mr-2 flex h-[1.938rem] w-[1.938rem] items-center justify-center rounded-full bg-[#ebedef] text-sm font-medium text-[#40464f] !bg-primary-100 !text-primary-700", + stepperHeadIconCompleted: + "my-6 mr-2 flex h-[1.938rem] w-[1.938rem] items-center justify-center rounded-full bg-[#ebedef] text-sm font-medium text-[#40464f] !bg-success-100 !text-success-700", + stepperHeadText: + "text-neutral-500 after:flex after:text-[0.8rem] after:content-[data-content] dark:text-neutral-300", + stepperHeadTextActive: + "font-medium after:flex after:text-[0.8rem] after:content-[data-content]", + stepperContentTranslatedLeft: + "absolute left-0 w-full p-4 transition-all duration-500 ease-in-out translate-0 -translate-x-[150%]", + stepperContentTranslatedRight: + "absolute left-0 w-full p-4 transition-all duration-500 ease-in-out translate-0 translate-x-[150%]", + stepperContentActive: + "absolute left-0 w-full p-4 transition-all duration-500 ease-in-out translate-0", +}; + +export default StepperStepTheme; +export type { StepperStepThemeProps }; diff --git a/src/lib/components/Stepper/StepperStep/types.ts b/src/lib/components/Stepper/StepperStep/types.ts new file mode 100644 index 0000000..37ccedb --- /dev/null +++ b/src/lib/components/Stepper/StepperStep/types.ts @@ -0,0 +1,26 @@ +import React from "react"; +import { BaseComponent } from "../../../types/baseComponent"; + +interface StepperThemeProps { + stepperStep: string; + stepperHead: string; + stepperHeadActive: string; + stepperHeadIcon: string; + stepperHeadText: string; + stepperHeadTextActive: string; + stepperContent: string; + stepperContentActive: string; +} + +interface StepperStepProps extends BaseComponent { + theme?: StepperThemeProps; + headClassName?: string; + contentClassName?: string; + itemId?: number; + headIcon?: React.ReactNode; + headText?: React.ReactNode; + onComplete?: (id: number) => void; + activeStep?: number; +} + +export type { StepperStepProps }; diff --git a/src/lib/components/Stepper/hooks/useHeadIconClasses.ts b/src/lib/components/Stepper/hooks/useHeadIconClasses.ts new file mode 100644 index 0000000..d941305 --- /dev/null +++ b/src/lib/components/Stepper/hooks/useHeadIconClasses.ts @@ -0,0 +1,29 @@ +const useHeadIconClasses = ( + isActive: boolean, + isCompleted: boolean, + isInvalid: boolean, + theme: any +): string => { + const { + stepperHeadIcon, + stepperHeadIconActive, + stepperHeadIconCompleted, + stepperHeadIconInvalid, + } = theme; + + if (isActive) { + return stepperHeadIconActive; + } + + if (isCompleted) { + return stepperHeadIconCompleted; + } + + if (isInvalid) { + return stepperHeadIconInvalid; + } + + return stepperHeadIcon; +}; + +export default useHeadIconClasses; diff --git a/src/lib/components/Stepper/hooks/useHorizontalStepperHeight.ts b/src/lib/components/Stepper/hooks/useHorizontalStepperHeight.ts new file mode 100644 index 0000000..3c528d0 --- /dev/null +++ b/src/lib/components/Stepper/hooks/useHorizontalStepperHeight.ts @@ -0,0 +1,41 @@ +import { useEffect, useContext, RefObject } from "react"; +import StepperContext from "../StepperContext"; + +const useStepperHeight = ( + isActive: boolean, + headRef: RefObject, + stepRef: RefObject, + vertical: boolean, + children: React.ReactNode | React.ReactNode[] +) => { + if (vertical) return; + const { setStepperHeight } = useContext(StepperContext); + + const handleResize = () => { + if (!isActive) { + return; + } + if (stepRef.current && headRef.current) { + setStepperHeight( + String(stepRef.current.scrollHeight + headRef.current.scrollHeight) + ); + } + }; + // if (vertical) { + // return setStepperHeight("unset"); + // } + + useEffect(() => { + handleResize(); + }, [isActive, children]); + + useEffect(() => { + if (!isActive) return; + window.addEventListener("resize", handleResize); + return () => { + window.removeEventListener("resize", handleResize); + }; + }, [isActive, children]); +}; + +export default useStepperHeight; diff --git a/src/lib/components/Stepper/hooks/useIsStepCompleted.ts b/src/lib/components/Stepper/hooks/useIsStepCompleted.ts new file mode 100644 index 0000000..7af5ffe --- /dev/null +++ b/src/lib/components/Stepper/hooks/useIsStepCompleted.ts @@ -0,0 +1,23 @@ +import { useState, useEffect, useRef } from "react"; + +const useIsStepCompleted = ( + activeStep: number, + isActive: boolean, + itemId: number +) => { + const [isCompleted, setIsCompleted] = useState(false); + const wasActive = useRef(isActive || false); + + useEffect(() => { + if (isActive) { + wasActive.current = isActive; + } + if (wasActive.current && activeStep >= itemId) { + setIsCompleted(true); + } + }, [isActive]); + + return isCompleted; +}; + +export default useIsStepCompleted; diff --git a/src/lib/components/Stepper/stepperTheme.ts b/src/lib/components/Stepper/stepperTheme.ts new file mode 100644 index 0000000..4a92a76 --- /dev/null +++ b/src/lib/components/Stepper/stepperTheme.ts @@ -0,0 +1,8 @@ +const StepperTheme = { + stepper: + "relative m-0 flex list-none justify-between overflow-hidden p-0 transition-[height] duration-200 ease-in-out", + stepperVertical: + "relative m-0 w-full list-none overflow-hidden p-0 transition-[height] duration-200 ease-in-out", +}; + +export default StepperTheme; diff --git a/src/lib/components/Stepper/types.ts b/src/lib/components/Stepper/types.ts new file mode 100644 index 0000000..077232e --- /dev/null +++ b/src/lib/components/Stepper/types.ts @@ -0,0 +1,23 @@ +import React from "react"; +import { BaseComponent } from "../../types/baseComponent"; +import { StepperStepProps } from "./StepperStep/types"; + +interface StepperThemeProps { + stepper: string; +} + +type StepperProps = Omit & { + stepperTheme?: StepperThemeProps; + disableHeadSteps?: boolean; + defaultStep?: number; + activeStep?: number; + linear?: boolean; + noEditable?: boolean; + type?: "horizontal" | "vertical"; + onChange?: (id: number) => void; + children: + | React.ReactElement[] + | React.ReactElement; +}; + +export type { StepperProps }; diff --git a/src/lib/components/Stepper/utils/utils.ts b/src/lib/components/Stepper/utils/utils.ts new file mode 100644 index 0000000..3ce0979 --- /dev/null +++ b/src/lib/components/Stepper/utils/utils.ts @@ -0,0 +1,10 @@ +const getTranslateDirection = (activeStep: number, step: number) => { + if (activeStep > step) { + return "Left"; + } + if (activeStep < step) { + return "Right"; + } +}; + +export { getTranslateDirection }; diff --git a/src/lib/hooks/useActiveValue.ts b/src/lib/hooks/useActiveValue.ts new file mode 100644 index 0000000..38f68f8 --- /dev/null +++ b/src/lib/hooks/useActiveValue.ts @@ -0,0 +1,13 @@ +import { useMemo } from "react"; + +const useActiveValue = (propValue: any, stateValue: any) => { + return useMemo(() => { + if (propValue !== undefined) { + return propValue; + } + + return stateValue; + }, [propValue, stateValue]); +}; + +export default useActiveValue; From 1f8a2472cc20029ec7a0c0c0d2b24fd8b998daf5 Mon Sep 17 00:00:00 2001 From: Mateusz Lazaru Date: Mon, 20 Nov 2023 22:01:43 +0100 Subject: [PATCH 03/19] stepper - added 'vertical' option, fixed invalid styles in stepperStepTheme --- .../pages/components/stepper/StepperPage.tsx | 218 ++++++++++++++++++ src/lib/components/Stepper/Stepper.tsx | 14 +- src/lib/components/Stepper/StepperContext.ts | 4 + .../Stepper/StepperStep/StepperStep.tsx | 67 ++++-- .../Stepper/StepperStep/stepperStepTheme.ts | 36 ++- .../Stepper/hooks/useHeadClasses.ts | 26 +++ .../Stepper/hooks/useHeadIconClasses.ts | 26 ++- .../hooks/useHorizontalStepperHeight.ts | 6 - .../Stepper/hooks/useVerticalStepHeight.ts | 36 +++ 9 files changed, 381 insertions(+), 52 deletions(-) create mode 100644 src/lib/components/Stepper/hooks/useHeadClasses.ts create mode 100644 src/lib/components/Stepper/hooks/useVerticalStepHeight.ts diff --git a/src/demo/pages/components/stepper/StepperPage.tsx b/src/demo/pages/components/stepper/StepperPage.tsx index 26a0564..44a1e0d 100644 --- a/src/demo/pages/components/stepper/StepperPage.tsx +++ b/src/demo/pages/components/stepper/StepperPage.tsx @@ -1,9 +1,20 @@ import React, { useState } from "react"; import TEStepper from "../../../../lib/components/Stepper/Stepper"; import TEStepperStep from "../../../../lib/components/Stepper/StepperStep/StepperStep"; +import { TECollapse } from "tw-elements-react"; const StepperPage = () => { const [activeStep, setActiveStep] = useState(1); + const [activeElement, setActiveElement] = useState(""); + + const handleClick = (value: string) => { + if (value === activeElement) { + setActiveElement(""); + } else { + setActiveElement(value); + } + }; + const [addForm, setAddForm] = useState(false); return (
    +

    Basic example @@ -72,6 +84,212 @@ const StepperPage = () => {

    + +
    + +

    + Vertical stepper +

    + +
    + + +
    Step 1 content
    +
    XXXXs
    +
    Step 1 content
    +
    XXXXs
    +
    Step 1 content
    +
    XXXXs
    +
    Step 1 content
    +
    XXXXs
    +
    Step 1 content
    +
    XXXXs
    +
    + +
    +
    Step 2 content
    + {addForm && ( + <> +
    +
    +

    + +

    + + +
    + + This is the first item's accordion body. + {" "} + Lorem ipsum dolor sit amet, consectetur adipiscing + elit. Vestibulum eu rhoncus purus, vitae tincidunt + nibh. Vivamus elementum egestas ligula in varius. + Proin ac erat pretium, ultricies leo at, cursus + ante. Pellentesque at odio euismod, mattis urna ac, + accumsan metus. Nam nisi leo, malesuada vitae + pretium et, laoreet at lorem. Curabitur non + sollicitudin neque. +
    +
    +
    +
    +
    +

    + +

    + +
    + + This is the second item's accordion body. + {" "} + Lorem ipsum dolor sit amet, consectetur adipiscing + elit. Vestibulum eu rhoncus purus, vitae tincidunt + nibh. Vivamus elementum egestas ligula in varius. + Proin ac erat pretium, ultricies leo at, cursus ante. + Pellentesque at odio euismod, mattis urna ac, accumsan + metus. Nam nisi leo, malesuada vitae pretium et, + laoreet at lorem. Curabitur non sollicitudin neque. +
    +
    +
    +
    +

    + +

    + +
    + + This is the third item's accordion body. + + Lorem ipsum dolor sit amet, consectetur adipiscing + elit. Vestibulum eu rhoncus purus, vitae tincidunt + nibh. Vivamus elementum egestas ligula in varius. + Proin ac erat pretium, ultricies leo at, cursus ante. + Pellentesque at odio euismod, mattis urna ac, accumsan + metus. Nam nisi leo, malesuada vitae pretium et, + laoreet at lorem. Curabitur non sollicitudin neque. +
    +
    +
    + + )} +
    +
    + +
    Step 3 content
    +
    +
    +
    ); diff --git a/src/lib/components/Stepper/Stepper.tsx b/src/lib/components/Stepper/Stepper.tsx index aebd251..b2386a8 100644 --- a/src/lib/components/Stepper/Stepper.tsx +++ b/src/lib/components/Stepper/Stepper.tsx @@ -1,12 +1,10 @@ import clsx from "clsx"; -import React, { useEffect, useRef, useState, useMemo } from "react"; +import React, { useRef, useState, useMemo } from "react"; import StepperContext from "./StepperContext"; import { StepperStepProps } from "./StepperStep/types"; import StepperTheme from "./stepperTheme"; import useActiveValue from "../../hooks/useActiveValue"; import type { StepperProps } from "./types"; -// import MDBStepperMobileHead from "./StepperMobileHead/StepperMobileHead"; -// import MDBStepperMobileFooter from "./StepperMobileFooter/StepperMobileFooter"; const TEStepper: React.FC = ({ theme: customTheme, @@ -15,12 +13,16 @@ const TEStepper: React.FC = ({ activeStep: activeStepProp, children, onChange, + vertical, }) => { const theme = { ...StepperTheme, ...customTheme, }; - const classes = clsx(theme.stepper, className); + const classes = clsx( + vertical ? theme.stepperVertical : theme.stepper, + className + ); const [activeStepState, setActiveStepState] = useState(defaultStep); const activeStep = useActiveValue(activeStepProp, activeStepState); const stepperRef = useRef(null); @@ -45,12 +47,14 @@ const TEStepper: React.FC = ({ stepperRef, stepperHeight, setStepperHeight, + vertical, + stepsAmount: childrenArray.length, }} >
      {childrenArray.map((ChildComponent, index: number) => { return React.cloneElement(ChildComponent, { diff --git a/src/lib/components/Stepper/StepperContext.ts b/src/lib/components/Stepper/StepperContext.ts index 8ebddf4..7d112c4 100644 --- a/src/lib/components/Stepper/StepperContext.ts +++ b/src/lib/components/Stepper/StepperContext.ts @@ -6,6 +6,8 @@ interface StepperContextProps { stepperRef: React.RefObject | null; stepperHeight: string; setStepperHeight: (height: string) => void; + vertical: boolean; + stepsAmount: number; } const StepperContext = createContext({ @@ -14,6 +16,8 @@ const StepperContext = createContext({ stepperRef: null, stepperHeight: "0", setStepperHeight: () => {}, + vertical: false, + stepsAmount: 0, }); export default StepperContext; diff --git a/src/lib/components/Stepper/StepperStep/StepperStep.tsx b/src/lib/components/Stepper/StepperStep/StepperStep.tsx index a6409af..0e4f290 100644 --- a/src/lib/components/Stepper/StepperStep/StepperStep.tsx +++ b/src/lib/components/Stepper/StepperStep/StepperStep.tsx @@ -7,6 +7,8 @@ import useHeadIconClasses from "../hooks/useHeadIconClasses"; import useIsStepCompleted from "../hooks/useIsStepCompleted"; import useStepperHeight from "../hooks/useHorizontalStepperHeight"; import { getTranslateDirection } from "../utils/utils"; +import useVerticalStepHeight from "../hooks/useVerticalStepHeight"; +import useHeadClasses from "../hooks/useHeadClasses"; const TEStepperStep: React.FC = ({ theme: customTheme, @@ -15,10 +17,13 @@ const TEStepperStep: React.FC = ({ headIcon = "", headText = "", children, + invalid, + style, }) => { const headRef = useRef(null); const contentRef = useRef(null); - const { activeStep, onChange } = useContext(StepperContext); + const { activeStep, onChange, vertical, stepsAmount } = + useContext(StepperContext); const animationDirection = useMemo(() => { return getTranslateDirection(activeStep, itemId); @@ -27,28 +32,57 @@ const TEStepperStep: React.FC = ({ const isActive = useMemo(() => { return activeStep === itemId; }, [activeStep, itemId]); + + const isLastStep = useMemo(() => { + return itemId === stepsAmount; + }, [itemId, stepsAmount]); + const isCompleted = useIsStepCompleted(activeStep, isActive, itemId); const theme = { ...StepperStepTheme, ...customTheme, }; - const stepperStepClasses = clx(theme.stepperStep, className); const headIconClasses = useHeadIconClasses( isActive, isCompleted, - false, - theme + invalid, + theme, + vertical + ); + const stepperHeadClasses = useHeadClasses(theme, itemId); + const stepperStepClasses = clx( + vertical + ? isLastStep + ? theme.stepperLastStepVertical + : theme.stepperStepVertical + : theme.stepperStep, + + className + ); + + const dynamicAnimationDirection: string = `stepperContentTranslate${animationDirection}`; + const stepperContentClasses = clx( + vertical ? theme.stepperVerticalContent : theme.stepperContent, + !vertical && theme[dynamicAnimationDirection as keyof typeof theme] ); - useStepperHeight(isActive, headRef, contentRef, false, children); - const dynamicAnimationDirection: string = `stepperContentTranslated${animationDirection}`; + const headClickHandler = () => { + itemId != activeStep && onChange?.(itemId); + }; + + useStepperHeight(isActive, headRef, contentRef, vertical, children); + const verticalStepHeight = useVerticalStepHeight( + isActive, + contentRef, + children + ); return (
    • onChange?.(itemId)} + className={stepperHeadClasses} + onClick={headClickHandler} ref={headRef} > {headIcon} @@ -61,18 +95,15 @@ const TEStepperStep: React.FC = ({
      { - console.log(e); + style={{ + height: vertical ? verticalStepHeight : "auto", + visibility: isActive ? "visible" : "hidden", }} - ref={contentRef} + className={theme.stepperContentWrapper} > - {children} +
      + {children} +
    • ); diff --git a/src/lib/components/Stepper/StepperStep/stepperStepTheme.ts b/src/lib/components/Stepper/StepperStep/stepperStepTheme.ts index 0dbe14e..c517186 100644 --- a/src/lib/components/Stepper/StepperStep/stepperStepTheme.ts +++ b/src/lib/components/Stepper/StepperStep/stepperStepTheme.ts @@ -13,24 +13,38 @@ interface StepperStepThemeProps { const StepperStepTheme = { stepperStep: "w-[4.5rem] flex-auto", + stepperStepVertical: + "relative h-fit after:absolute after:left-[2.45rem] after:top-[3.6rem] after:mt-px after:h-[calc(100%-2.45rem)] after:w-px after:bg-[#e0e0e0] dark:after:bg-neutral-600", + stepperLastStepVertical: "relative h-fit", stepperHead: - "flex cursor-pointer items-center leading-[1.3rem] no-underline before:mr-2 before:h-px before:w-full before:flex-1 before:bg-[#e0e0e0] before:content-[''] after:ml-2 after:h-px after:w-full after:flex-1 after:bg-[#e0e0e0] after:content-[''] hover:bg-[#f9f9f9] focus:outline-none dark:before:bg-neutral-600 dark:after:bg-neutral-600 dark:hover:bg-[#3b3b3b]", + "flex cursor-pointer items-center leading-[1.3rem] no-underline hover:bg-[#f9f9f9] focus:outline-none dark:hover:bg-[#3b3b3b]", + stepperHeadBeforeLineHorizontal: + "before:mr-2 before:h-px before:w-full before:flex-1 before:bg-[#e0e0e0] dark:before:bg-neutral-600", + stepperHeadAfterLineHorizontal: + "after:ml-2 after:h-px after:w-full after:flex-1 after:bg-[#e0e0e0] dark:after:bg-neutral-600", + stepperHeadAfterLineVertical: + "after:absolute after:left-[2.45rem] after:top-[3.6rem] after:mt-px after:h-[calc(100%-2.45rem)] after:w-px after:bg-[#e0e0e0] after:content-[''] dark:after:bg-neutral-600", + stepperHeadVertical: + "flex cursor-pointer items-center p-6 leading-[1.3rem] no-underline hover:bg-[#f9f9f9] focus:outline-none dark:hover:bg-[#3b3b3b]", stepperHeadIcon: "my-6 mr-2 flex h-[1.938rem] w-[1.938rem] items-center justify-center rounded-full bg-[#ebedef] text-sm font-medium text-[#40464f]", - stepperHeadIconActive: - "my-6 mr-2 flex h-[1.938rem] w-[1.938rem] items-center justify-center rounded-full bg-[#ebedef] text-sm font-medium text-[#40464f] !bg-primary-100 !text-primary-700", - stepperHeadIconCompleted: - "my-6 mr-2 flex h-[1.938rem] w-[1.938rem] items-center justify-center rounded-full bg-[#ebedef] text-sm font-medium text-[#40464f] !bg-success-100 !text-success-700", + stepperHeadIconVertical: + "mr-3 flex h-[1.938rem] w-[1.938rem] items-center justify-center rounded-full bg-[#ebedef] text-sm font-medium text-[#40464f]", + stepperHeadIconCompletedBg: "!bg-success-100 !text-success-700", + stepperHeadIconActiveBg: "!bg-primary-100 !text-primary-700", + stepperHeadIconInvalidBg: "!bg-danger-100 !text-danger-700", stepperHeadText: - "text-neutral-500 after:flex after:text-[0.8rem] after:content-[data-content] dark:text-neutral-300", + "text-neutral-500 after:flex after:text-[0.8rem] dark:text-neutral-300", stepperHeadTextActive: "font-medium after:flex after:text-[0.8rem] after:content-[data-content]", - stepperContentTranslatedLeft: - "absolute left-0 w-full p-4 transition-all duration-500 ease-in-out translate-0 -translate-x-[150%]", - stepperContentTranslatedRight: - "absolute left-0 w-full p-4 transition-all duration-500 ease-in-out translate-0 translate-x-[150%]", - stepperContentActive: + stepperContentWrapper: + "transition-all duration-500 ease-in-out overflow-hidden", + stepperContent: "absolute left-0 w-full p-4 transition-all duration-500 ease-in-out translate-0", + stepperContentTranslateLeft: "-translate-x-[150%]", + stepperContentTranslateRight: "translate-x-[150%]", + stepperVerticalContent: + "transition-[height, margin-bottom, padding-top, padding-bottom] left-0 overflow-hidden pb-6 pl-[3.75rem] pr-6 duration-300 ease-in-out", }; export default StepperStepTheme; diff --git a/src/lib/components/Stepper/hooks/useHeadClasses.ts b/src/lib/components/Stepper/hooks/useHeadClasses.ts new file mode 100644 index 0000000..b5fda02 --- /dev/null +++ b/src/lib/components/Stepper/hooks/useHeadClasses.ts @@ -0,0 +1,26 @@ +import { useContext, useMemo } from "react"; +import StepperContext from "../StepperContext"; +import clsx from "clsx"; + +export default function useHeadClasses(theme: any, itemId: number) { + const { stepsAmount, vertical } = useContext(StepperContext); + + const beforeLine = vertical ? "" : "stepperHeadBeforeLineHorizontal"; + const afterLine = vertical + ? "stepperHeadAfterLineVertical" + : "stepperHeadAfterLineHorizontal"; + + const isFirstStep = useMemo(() => itemId === 1, [itemId]); + const isLastStep = useMemo( + () => itemId === stepsAmount, + [itemId, stepsAmount] + ); + + const stepperHeadClasses = clsx( + vertical ? theme.stepperHeadVertical : theme.stepperHead, + !isFirstStep && theme[beforeLine], + !isLastStep && theme[afterLine] + ); + + return stepperHeadClasses; +} diff --git a/src/lib/components/Stepper/hooks/useHeadIconClasses.ts b/src/lib/components/Stepper/hooks/useHeadIconClasses.ts index d941305..88770a4 100644 --- a/src/lib/components/Stepper/hooks/useHeadIconClasses.ts +++ b/src/lib/components/Stepper/hooks/useHeadIconClasses.ts @@ -2,28 +2,30 @@ const useHeadIconClasses = ( isActive: boolean, isCompleted: boolean, isInvalid: boolean, - theme: any + theme: any, + vertical: boolean ): string => { const { stepperHeadIcon, - stepperHeadIconActive, - stepperHeadIconCompleted, - stepperHeadIconInvalid, + stepperHeadIconVertical, + stepperHeadIconActiveBg, + stepperHeadIconCompletedBg, + stepperHeadIconInvalidBg, } = theme; - if (isActive) { - return stepperHeadIconActive; - } + const headIconTheme = vertical ? stepperHeadIconVertical : stepperHeadIcon; - if (isCompleted) { - return stepperHeadIconCompleted; + if (isActive) { + return headIconTheme + " " + stepperHeadIconActiveBg; } - if (isInvalid) { - return stepperHeadIconInvalid; + return headIconTheme + stepperHeadIconInvalidBg; + } + if (isCompleted) { + return headIconTheme + " " + stepperHeadIconCompletedBg; } - return stepperHeadIcon; + return headIconTheme; }; export default useHeadIconClasses; diff --git a/src/lib/components/Stepper/hooks/useHorizontalStepperHeight.ts b/src/lib/components/Stepper/hooks/useHorizontalStepperHeight.ts index 3c528d0..623aec7 100644 --- a/src/lib/components/Stepper/hooks/useHorizontalStepperHeight.ts +++ b/src/lib/components/Stepper/hooks/useHorizontalStepperHeight.ts @@ -21,15 +21,9 @@ const useStepperHeight = ( ); } }; - // if (vertical) { - // return setStepperHeight("unset"); - // } useEffect(() => { handleResize(); - }, [isActive, children]); - - useEffect(() => { if (!isActive) return; window.addEventListener("resize", handleResize); return () => { diff --git a/src/lib/components/Stepper/hooks/useVerticalStepHeight.ts b/src/lib/components/Stepper/hooks/useVerticalStepHeight.ts new file mode 100644 index 0000000..89a4bc8 --- /dev/null +++ b/src/lib/components/Stepper/hooks/useVerticalStepHeight.ts @@ -0,0 +1,36 @@ +import { useState, useEffect, RefObject } from "react"; + +export default function useVerticalStepHeight( + isActive: boolean, + contentWrapperRef: RefObject, + children: React.ReactNode | React.ReactNode[] +) { + const [height, setHeight] = useState("0px"); + + useEffect(() => { + const observer = new ResizeObserver((entries) => { + if (!isActive) { + return setHeight("0px"); + } + const contentWrapperHeight = entries[0].contentRect.height; + const computed = window.getComputedStyle( + contentWrapperRef.current as Element + ); + + const offsetY = + parseFloat(computed.paddingTop) + + parseFloat(computed.paddingBottom) + + parseFloat(computed.marginBottom) + + parseFloat(computed.marginTop); + + setHeight(`${contentWrapperHeight + offsetY}px`); + }); + + observer.observe(contentWrapperRef.current as Element); + return () => { + observer.disconnect(); + }; + }, [isActive, children]); + + return height; +} From b797abc195ac3c10811341889f5c918d0f310bee Mon Sep 17 00:00:00 2001 From: Mateusz Lazaru Date: Tue, 21 Nov 2023 10:04:38 +0100 Subject: [PATCH 04/19] stepper - imported stepper in index.tsx, created separated demo examples --- .../pages/components/stepper/StepperPage.tsx | 266 +----------------- .../pages/components/stepper/exampleList.tsx | 0 .../stepper/examples/StepperBasicExample.tsx | 21 ++ .../examples/StepperControlledStep.tsx | 28 ++ .../examples/StepperVerticalExample.tsx | 21 ++ src/lib/index.tsx | 4 + 6 files changed, 81 insertions(+), 259 deletions(-) create mode 100644 src/demo/pages/components/stepper/exampleList.tsx create mode 100644 src/demo/pages/components/stepper/examples/StepperBasicExample.tsx create mode 100644 src/demo/pages/components/stepper/examples/StepperControlledStep.tsx create mode 100644 src/demo/pages/components/stepper/examples/StepperVerticalExample.tsx diff --git a/src/demo/pages/components/stepper/StepperPage.tsx b/src/demo/pages/components/stepper/StepperPage.tsx index 44a1e0d..66df066 100644 --- a/src/demo/pages/components/stepper/StepperPage.tsx +++ b/src/demo/pages/components/stepper/StepperPage.tsx @@ -1,20 +1,9 @@ -import React, { useState } from "react"; -import TEStepper from "../../../../lib/components/Stepper/Stepper"; -import TEStepperStep from "../../../../lib/components/Stepper/StepperStep/StepperStep"; -import { TECollapse } from "tw-elements-react"; +import React from "react"; +import StepperControlledStep from "./examples/StepperControlledStep"; +import StepperVerticalExample from "./examples/StepperVerticalExample"; +import StepperBasicExample from "./examples/StepperBasicExample"; const StepperPage = () => { - const [activeStep, setActiveStep] = useState(1); - const [activeElement, setActiveElement] = useState(""); - - const handleClick = (value: string) => { - if (value === activeElement) { - setActiveElement(""); - } else { - setActiveElement(value); - } - }; - const [addForm, setAddForm] = useState(false); return (
      -

      Basic example

      - - -
      Step 1 content
      -
      XXXXs
      -
      Step 1 content
      -
      XXXXs
      -
      Step 1 content
      -
      XXXXs
      -
      Step 1 content
      -
      XXXXs
      -
      Step 1 content
      -
      XXXXs
      -
      - -
      Step 2 content
      -
      - -
      Step 3 content
      -
      -
      +

      @@ -57,32 +26,7 @@ const StepperPage = () => {
      - { - console.log("onChange: ", stepId); - setActiveStep(stepId); - }} - > - -
      Step 1 content
      -
      XXXXs
      -
      Step 1 content
      -
      XXXXs
      -
      Step 1 content
      -
      XXXXs
      -
      Step 1 content
      -
      XXXXs
      -
      Step 1 content
      -
      XXXXs
      -
      - -
      Step 2 content
      -
      - -
      Step 3 content
      -
      -
      +

      @@ -92,203 +36,7 @@ const StepperPage = () => {
      - - -
      Step 1 content
      -
      XXXXs
      -
      Step 1 content
      -
      XXXXs
      -
      Step 1 content
      -
      XXXXs
      -
      Step 1 content
      -
      XXXXs
      -
      Step 1 content
      -
      XXXXs
      -
      - -
      -
      Step 2 content
      - {addForm && ( - <> -
      -
      -

      - -

      - - -
      - - This is the first item's accordion body. - {" "} - Lorem ipsum dolor sit amet, consectetur adipiscing - elit. Vestibulum eu rhoncus purus, vitae tincidunt - nibh. Vivamus elementum egestas ligula in varius. - Proin ac erat pretium, ultricies leo at, cursus - ante. Pellentesque at odio euismod, mattis urna ac, - accumsan metus. Nam nisi leo, malesuada vitae - pretium et, laoreet at lorem. Curabitur non - sollicitudin neque. -
      -
      -
      -
      -
      -

      - -

      - -
      - - This is the second item's accordion body. - {" "} - Lorem ipsum dolor sit amet, consectetur adipiscing - elit. Vestibulum eu rhoncus purus, vitae tincidunt - nibh. Vivamus elementum egestas ligula in varius. - Proin ac erat pretium, ultricies leo at, cursus ante. - Pellentesque at odio euismod, mattis urna ac, accumsan - metus. Nam nisi leo, malesuada vitae pretium et, - laoreet at lorem. Curabitur non sollicitudin neque. -
      -
      -
      -
      -

      - -

      - -
      - - This is the third item's accordion body. - - Lorem ipsum dolor sit amet, consectetur adipiscing - elit. Vestibulum eu rhoncus purus, vitae tincidunt - nibh. Vivamus elementum egestas ligula in varius. - Proin ac erat pretium, ultricies leo at, cursus ante. - Pellentesque at odio euismod, mattis urna ac, accumsan - metus. Nam nisi leo, malesuada vitae pretium et, - laoreet at lorem. Curabitur non sollicitudin neque. -
      -
      -
      - - )} -
      -
      - -
      Step 3 content
      -
      -
      +
      diff --git a/src/demo/pages/components/stepper/exampleList.tsx b/src/demo/pages/components/stepper/exampleList.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/demo/pages/components/stepper/examples/StepperBasicExample.tsx b/src/demo/pages/components/stepper/examples/StepperBasicExample.tsx new file mode 100644 index 0000000..18cd984 --- /dev/null +++ b/src/demo/pages/components/stepper/examples/StepperBasicExample.tsx @@ -0,0 +1,21 @@ +import React from "react"; +import { TEStepper, TEStepperStep } from "tw-elements-react"; + +export default function StepperBasicExample(): JSX.Element { + return ( + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod + tempor incididunt ut labore et dolore magna aliqua. + + + Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi + ut aliquip ex ea commodo consequat. + + + Duis aute irure dolor in reprehenderit in voluptate velit esse cillum + dolore eu fugiat nulla pariatur. + + + ); +} diff --git a/src/demo/pages/components/stepper/examples/StepperControlledStep.tsx b/src/demo/pages/components/stepper/examples/StepperControlledStep.tsx new file mode 100644 index 0000000..6c219dd --- /dev/null +++ b/src/demo/pages/components/stepper/examples/StepperControlledStep.tsx @@ -0,0 +1,28 @@ +import React, { useState } from "react"; +import { TEStepper, TEStepperStep } from "tw-elements-react"; + +export default function StepperControlledStep(): JSX.Element { + const [activeStep, setActiveStep] = useState(1); + + return ( + { + setActiveStep(stepId); + }} + > + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod + tempor incididunt ut labore et dolore magna aliqua. + + + Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi + ut aliquip ex ea commodo consequat. + + + Duis aute irure dolor in reprehenderit in voluptate velit esse cillum + dolore eu fugiat nulla pariatur. + + + ); +} diff --git a/src/demo/pages/components/stepper/examples/StepperVerticalExample.tsx b/src/demo/pages/components/stepper/examples/StepperVerticalExample.tsx new file mode 100644 index 0000000..c5fc0a7 --- /dev/null +++ b/src/demo/pages/components/stepper/examples/StepperVerticalExample.tsx @@ -0,0 +1,21 @@ +import React from "react"; +import { TEStepper, TEStepperStep } from "tw-elements-react"; + +export default function StepperVerticalExample(): JSX.Element { + return ( + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod + tempor incididunt ut labore et dolore magna aliqua. + + + Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi + ut aliquip ex ea commodo consequat. + + + Duis aute irure dolor in reprehenderit in voluptate velit esse cillum + dolore eu fugiat nulla pariatur. + + + ); +} diff --git a/src/lib/index.tsx b/src/lib/index.tsx index 8d575e7..01d5910 100644 --- a/src/lib/index.tsx +++ b/src/lib/index.tsx @@ -27,6 +27,8 @@ import TEToast from "./components/Toasts/Toast"; import TESelect from "./forms/Select/Select"; import TECarousel from "./components/Carousel/Carousel"; import TECarouselItem from "./components/Carousel/CarouselItem/CarouselItem"; +import TEStepper from "./components/Stepper/Stepper"; +import TEStepperStep from "./components/Stepper/StepperStep/StepperStep"; export { TECollapse, @@ -58,4 +60,6 @@ export { TESelect, TECarousel, TECarouselItem, + TEStepper, + TEStepperStep, }; From 2b54daa92749f3d038a62bc4bab0cd2ff7119273 Mon Sep 17 00:00:00 2001 From: Mateusz Lazaru Date: Tue, 21 Nov 2023 10:14:06 +0100 Subject: [PATCH 05/19] stepper - changed type 'vertical' to 'type', improved horizontal stepper height calculating --- src/lib/components/Stepper/Stepper.tsx | 6 ++- .../hooks/useHorizontalStepperHeight.ts | 41 +++++++++++-------- 2 files changed, 29 insertions(+), 18 deletions(-) diff --git a/src/lib/components/Stepper/Stepper.tsx b/src/lib/components/Stepper/Stepper.tsx index b2386a8..ee720ad 100644 --- a/src/lib/components/Stepper/Stepper.tsx +++ b/src/lib/components/Stepper/Stepper.tsx @@ -13,12 +13,14 @@ const TEStepper: React.FC = ({ activeStep: activeStepProp, children, onChange, - vertical, + type = "horizontal", + style, }) => { const theme = { ...StepperTheme, ...customTheme, }; + const vertical = type === "vertical"; const classes = clsx( vertical ? theme.stepperVertical : theme.stepper, className @@ -54,7 +56,7 @@ const TEStepper: React.FC = ({
        {childrenArray.map((ChildComponent, index: number) => { return React.cloneElement(ChildComponent, { diff --git a/src/lib/components/Stepper/hooks/useHorizontalStepperHeight.ts b/src/lib/components/Stepper/hooks/useHorizontalStepperHeight.ts index 623aec7..801c91d 100644 --- a/src/lib/components/Stepper/hooks/useHorizontalStepperHeight.ts +++ b/src/lib/components/Stepper/hooks/useHorizontalStepperHeight.ts @@ -8,26 +8,35 @@ const useStepperHeight = ( vertical: boolean, children: React.ReactNode | React.ReactNode[] ) => { - if (vertical) return; const { setStepperHeight } = useContext(StepperContext); - const handleResize = () => { - if (!isActive) { - return; - } - if (stepRef.current && headRef.current) { - setStepperHeight( - String(stepRef.current.scrollHeight + headRef.current.scrollHeight) - ); - } - }; - useEffect(() => { - handleResize(); - if (!isActive) return; - window.addEventListener("resize", handleResize); + if (vertical) return; + const headHeight = headRef.current?.offsetHeight || 0; + + const handleResize = (entries: Array) => { + if (!isActive) { + return; + } + const stepHeight = entries[0].contentRect.height; + const computed = window.getComputedStyle(stepRef.current as Element); + + const offsetY = + parseFloat(computed.paddingTop) + + parseFloat(computed.paddingBottom) + + parseFloat(computed.marginBottom) + + parseFloat(computed.marginTop); + + setStepperHeight(`${stepHeight + offsetY + headHeight}px`); + }; + + const observer = new ResizeObserver((entries) => { + handleResize(entries); + }); + + observer.observe(stepRef.current as Element); return () => { - window.removeEventListener("resize", handleResize); + observer.disconnect(); }; }, [isActive, children]); }; From 27adb7c89dc040ad66314c061ff1cd4ad400d100 Mon Sep 17 00:00:00 2001 From: Mateusz Lazaru Date: Tue, 21 Nov 2023 13:27:16 +0100 Subject: [PATCH 06/19] stepper - fixed head classes --- src/lib/components/Stepper/Stepper.tsx | 2 +- .../Stepper/StepperStep/stepperStepTheme.ts | 14 ++++++-------- .../components/Stepper/hooks/useHeadClasses.ts | 17 ++++------------- 3 files changed, 11 insertions(+), 22 deletions(-) diff --git a/src/lib/components/Stepper/Stepper.tsx b/src/lib/components/Stepper/Stepper.tsx index ee720ad..44dd9a1 100644 --- a/src/lib/components/Stepper/Stepper.tsx +++ b/src/lib/components/Stepper/Stepper.tsx @@ -28,7 +28,7 @@ const TEStepper: React.FC = ({ const [activeStepState, setActiveStepState] = useState(defaultStep); const activeStep = useActiveValue(activeStepProp, activeStepState); const stepperRef = useRef(null); - const [stepperHeight, setStepperHeight] = useState("0"); + const [stepperHeight, setStepperHeight] = useState("auto"); const childrenArray = useMemo(() => { return React.Children.toArray( diff --git a/src/lib/components/Stepper/StepperStep/stepperStepTheme.ts b/src/lib/components/Stepper/StepperStep/stepperStepTheme.ts index c517186..7f5ab86 100644 --- a/src/lib/components/Stepper/StepperStep/stepperStepTheme.ts +++ b/src/lib/components/Stepper/StepperStep/stepperStepTheme.ts @@ -16,14 +16,12 @@ const StepperStepTheme = { stepperStepVertical: "relative h-fit after:absolute after:left-[2.45rem] after:top-[3.6rem] after:mt-px after:h-[calc(100%-2.45rem)] after:w-px after:bg-[#e0e0e0] dark:after:bg-neutral-600", stepperLastStepVertical: "relative h-fit", - stepperHead: - "flex cursor-pointer items-center leading-[1.3rem] no-underline hover:bg-[#f9f9f9] focus:outline-none dark:hover:bg-[#3b3b3b]", - stepperHeadBeforeLineHorizontal: - "before:mr-2 before:h-px before:w-full before:flex-1 before:bg-[#e0e0e0] dark:before:bg-neutral-600", - stepperHeadAfterLineHorizontal: - "after:ml-2 after:h-px after:w-full after:flex-1 after:bg-[#e0e0e0] dark:after:bg-neutral-600", - stepperHeadAfterLineVertical: - "after:absolute after:left-[2.45rem] after:top-[3.6rem] after:mt-px after:h-[calc(100%-2.45rem)] after:w-px after:bg-[#e0e0e0] after:content-[''] dark:after:bg-neutral-600", + stepperHeadHorizontal: + "flex cursor-pointer items-center leading-[1.3rem] no-underline before:mr-2 before:h-px before:w-full before:flex-1 before:bg-[#e0e0e0] before:content-[''] after:ml-2 after:h-px after:w-full after:flex-1 after:bg-[#e0e0e0] after:content-[''] hover:bg-[#f9f9f9] focus:outline-none dark:before:bg-neutral-600 dark:after:bg-neutral-600 dark:hover:bg-[#3b3b3b]", + stepperFirstStepHeadHorizontal: + "flex cursor-pointer items-center pl-2 leading-[1.3rem] no-underline after:ml-2 after:h-px after:w-full after:flex-1 after:bg-[#e0e0e0] after:content-[''] hover:bg-[#f9f9f9] focus:outline-none dark:after:bg-neutral-600 dark:hover:bg-[#3b3b3b]", + stepperLastStepHeadHorizontal: + "flex cursor-pointer items-center pr-2 leading-[1.3rem] no-underline before:mr-2 before:h-px before:w-full before:flex-1 before:bg-[#e0e0e0] before:content-[''] hover:bg-[#f9f9f9] focus:outline-none dark:before:bg-neutral-600 dark:after:bg-neutral-600 dark:hover:bg-[#3b3b3b]", stepperHeadVertical: "flex cursor-pointer items-center p-6 leading-[1.3rem] no-underline hover:bg-[#f9f9f9] focus:outline-none dark:hover:bg-[#3b3b3b]", stepperHeadIcon: diff --git a/src/lib/components/Stepper/hooks/useHeadClasses.ts b/src/lib/components/Stepper/hooks/useHeadClasses.ts index b5fda02..1c2f52c 100644 --- a/src/lib/components/Stepper/hooks/useHeadClasses.ts +++ b/src/lib/components/Stepper/hooks/useHeadClasses.ts @@ -1,26 +1,17 @@ import { useContext, useMemo } from "react"; import StepperContext from "../StepperContext"; -import clsx from "clsx"; export default function useHeadClasses(theme: any, itemId: number) { const { stepsAmount, vertical } = useContext(StepperContext); - const beforeLine = vertical ? "" : "stepperHeadBeforeLineHorizontal"; - const afterLine = vertical - ? "stepperHeadAfterLineVertical" - : "stepperHeadAfterLineHorizontal"; - const isFirstStep = useMemo(() => itemId === 1, [itemId]); const isLastStep = useMemo( () => itemId === stepsAmount, [itemId, stepsAmount] ); - const stepperHeadClasses = clsx( - vertical ? theme.stepperHeadVertical : theme.stepperHead, - !isFirstStep && theme[beforeLine], - !isLastStep && theme[afterLine] - ); - - return stepperHeadClasses; + if (vertical) return theme.stepperHeadVertical; + if (isFirstStep) return theme.stepperFirstStepHeadHorizontal; + if (isLastStep) return theme.stepperLastStepHeadHorizontal; + return theme.stepperHeadHorizontal; } From 38314a18f43682051c3b23a497a99f213387b1cc Mon Sep 17 00:00:00 2001 From: Mateusz Lazaru Date: Thu, 23 Nov 2023 00:33:17 +0100 Subject: [PATCH 07/19] stepper - improved typing, removed unused props, improved code readability --- src/lib/components/Stepper/Stepper.tsx | 2 +- .../Stepper/StepperStep/StepperStep.tsx | 11 ++++---- .../Stepper/StepperStep/stepperStepTheme.ts | 27 ++++++++++++------- .../components/Stepper/StepperStep/types.ts | 15 ++--------- .../Stepper/hooks/useHeadClasses.ts | 6 ++++- .../Stepper/hooks/useHeadIconClasses.ts | 17 ++++++------ src/lib/components/Stepper/stepperTheme.ts | 2 +- src/lib/components/Stepper/types.ts | 11 +++----- 8 files changed, 44 insertions(+), 47 deletions(-) diff --git a/src/lib/components/Stepper/Stepper.tsx b/src/lib/components/Stepper/Stepper.tsx index 44dd9a1..450ea46 100644 --- a/src/lib/components/Stepper/Stepper.tsx +++ b/src/lib/components/Stepper/Stepper.tsx @@ -22,7 +22,7 @@ const TEStepper: React.FC = ({ }; const vertical = type === "vertical"; const classes = clsx( - vertical ? theme.stepperVertical : theme.stepper, + vertical ? theme.stepperVertical : theme.stepperHorizontal, className ); const [activeStepState, setActiveStepState] = useState(defaultStep); diff --git a/src/lib/components/Stepper/StepperStep/StepperStep.tsx b/src/lib/components/Stepper/StepperStep/StepperStep.tsx index 0e4f290..a2cf878 100644 --- a/src/lib/components/Stepper/StepperStep/StepperStep.tsx +++ b/src/lib/components/Stepper/StepperStep/StepperStep.tsx @@ -1,8 +1,8 @@ import React, { useMemo, useRef, useContext } from "react"; +import clx from "clsx"; import StepperStepTheme from "./stepperStepTheme"; import StepperContext from "../StepperContext"; import { StepperStepProps } from "./types"; -import clx from "clsx"; import useHeadIconClasses from "../hooks/useHeadIconClasses"; import useIsStepCompleted from "../hooks/useIsStepCompleted"; import useStepperHeight from "../hooks/useHorizontalStepperHeight"; @@ -13,11 +13,12 @@ import useHeadClasses from "../hooks/useHeadClasses"; const TEStepperStep: React.FC = ({ theme: customTheme, className, + contentClassName, + headClassName, itemId = 1, headIcon = "", headText = "", children, - invalid, style, }) => { const headRef = useRef(null); @@ -46,11 +47,10 @@ const TEStepperStep: React.FC = ({ const headIconClasses = useHeadIconClasses( isActive, isCompleted, - invalid, theme, vertical ); - const stepperHeadClasses = useHeadClasses(theme, itemId); + const stepperHeadClasses = clx(useHeadClasses(theme, itemId), headClassName); const stepperStepClasses = clx( vertical ? isLastStep @@ -64,7 +64,8 @@ const TEStepperStep: React.FC = ({ const dynamicAnimationDirection: string = `stepperContentTranslate${animationDirection}`; const stepperContentClasses = clx( vertical ? theme.stepperVerticalContent : theme.stepperContent, - !vertical && theme[dynamicAnimationDirection as keyof typeof theme] + !vertical && theme[dynamicAnimationDirection as keyof typeof theme], + contentClassName ); const headClickHandler = () => { diff --git a/src/lib/components/Stepper/StepperStep/stepperStepTheme.ts b/src/lib/components/Stepper/StepperStep/stepperStepTheme.ts index 7f5ab86..6fe927a 100644 --- a/src/lib/components/Stepper/StepperStep/stepperStepTheme.ts +++ b/src/lib/components/Stepper/StepperStep/stepperStepTheme.ts @@ -1,17 +1,25 @@ interface StepperStepThemeProps { stepperStep: string; - stepperHead: string; - stepperHeadIcon: string; - stepperHeadIconActive: string; - stepperHeadIconCompleted: string; + stepperStepVertical: string; + stepperLastStepVertical: string; + stepperHeadHorizontal: string; + stepperFirstStepHeadHorizontal: string; + stepperLastStepHeadHorizontal: string; + stepperHeadVertical: string; + stepperHeadIconHorizontal: string; + stepperHeadIconVertical: string; + stepperHeadIconActiveBg: string; + stepperHeadIconCompletedBg: string; stepperHeadText: string; stepperHeadTextActive: string; - stepperContentTranslatedLeft: string; - stepperContentTranslatedRight: string; - stepperContentActive: string; + stepperContent: string; + stepperContentTranslateLeft: string; + stepperContentTranslateRight: string; + stepperVerticalContent: string; + stepperContentWrapper: string; } -const StepperStepTheme = { +const StepperStepTheme: StepperStepThemeProps = { stepperStep: "w-[4.5rem] flex-auto", stepperStepVertical: "relative h-fit after:absolute after:left-[2.45rem] after:top-[3.6rem] after:mt-px after:h-[calc(100%-2.45rem)] after:w-px after:bg-[#e0e0e0] dark:after:bg-neutral-600", @@ -24,13 +32,12 @@ const StepperStepTheme = { "flex cursor-pointer items-center pr-2 leading-[1.3rem] no-underline before:mr-2 before:h-px before:w-full before:flex-1 before:bg-[#e0e0e0] before:content-[''] hover:bg-[#f9f9f9] focus:outline-none dark:before:bg-neutral-600 dark:after:bg-neutral-600 dark:hover:bg-[#3b3b3b]", stepperHeadVertical: "flex cursor-pointer items-center p-6 leading-[1.3rem] no-underline hover:bg-[#f9f9f9] focus:outline-none dark:hover:bg-[#3b3b3b]", - stepperHeadIcon: + stepperHeadIconHorizontal: "my-6 mr-2 flex h-[1.938rem] w-[1.938rem] items-center justify-center rounded-full bg-[#ebedef] text-sm font-medium text-[#40464f]", stepperHeadIconVertical: "mr-3 flex h-[1.938rem] w-[1.938rem] items-center justify-center rounded-full bg-[#ebedef] text-sm font-medium text-[#40464f]", stepperHeadIconCompletedBg: "!bg-success-100 !text-success-700", stepperHeadIconActiveBg: "!bg-primary-100 !text-primary-700", - stepperHeadIconInvalidBg: "!bg-danger-100 !text-danger-700", stepperHeadText: "text-neutral-500 after:flex after:text-[0.8rem] dark:text-neutral-300", stepperHeadTextActive: diff --git a/src/lib/components/Stepper/StepperStep/types.ts b/src/lib/components/Stepper/StepperStep/types.ts index 37ccedb..4934ef4 100644 --- a/src/lib/components/Stepper/StepperStep/types.ts +++ b/src/lib/components/Stepper/StepperStep/types.ts @@ -1,26 +1,15 @@ import React from "react"; import { BaseComponent } from "../../../types/baseComponent"; -interface StepperThemeProps { - stepperStep: string; - stepperHead: string; - stepperHeadActive: string; - stepperHeadIcon: string; - stepperHeadText: string; - stepperHeadTextActive: string; - stepperContent: string; - stepperContentActive: string; -} +import type { StepperStepThemeProps } from "./stepperStepTheme"; interface StepperStepProps extends BaseComponent { - theme?: StepperThemeProps; + theme?: StepperStepThemeProps; headClassName?: string; contentClassName?: string; itemId?: number; headIcon?: React.ReactNode; headText?: React.ReactNode; - onComplete?: (id: number) => void; - activeStep?: number; } export type { StepperStepProps }; diff --git a/src/lib/components/Stepper/hooks/useHeadClasses.ts b/src/lib/components/Stepper/hooks/useHeadClasses.ts index 1c2f52c..87edb5b 100644 --- a/src/lib/components/Stepper/hooks/useHeadClasses.ts +++ b/src/lib/components/Stepper/hooks/useHeadClasses.ts @@ -1,7 +1,11 @@ import { useContext, useMemo } from "react"; import StepperContext from "../StepperContext"; +import type { StepperStepThemeProps } from "../StepperStep/stepperStepTheme"; -export default function useHeadClasses(theme: any, itemId: number) { +export default function useHeadClasses( + theme: StepperStepThemeProps, + itemId: number +) { const { stepsAmount, vertical } = useContext(StepperContext); const isFirstStep = useMemo(() => itemId === 1, [itemId]); diff --git a/src/lib/components/Stepper/hooks/useHeadIconClasses.ts b/src/lib/components/Stepper/hooks/useHeadIconClasses.ts index 88770a4..8a563c5 100644 --- a/src/lib/components/Stepper/hooks/useHeadIconClasses.ts +++ b/src/lib/components/Stepper/hooks/useHeadIconClasses.ts @@ -1,28 +1,27 @@ +import clsx from "clsx"; + const useHeadIconClasses = ( isActive: boolean, isCompleted: boolean, - isInvalid: boolean, theme: any, vertical: boolean ): string => { const { - stepperHeadIcon, + stepperHeadIconHorizontal, stepperHeadIconVertical, stepperHeadIconActiveBg, stepperHeadIconCompletedBg, - stepperHeadIconInvalidBg, } = theme; - const headIconTheme = vertical ? stepperHeadIconVertical : stepperHeadIcon; + const headIconTheme = vertical + ? stepperHeadIconVertical + : stepperHeadIconHorizontal; if (isActive) { - return headIconTheme + " " + stepperHeadIconActiveBg; - } - if (isInvalid) { - return headIconTheme + stepperHeadIconInvalidBg; + return clsx(headIconTheme, stepperHeadIconActiveBg); } if (isCompleted) { - return headIconTheme + " " + stepperHeadIconCompletedBg; + return clsx(headIconTheme, stepperHeadIconCompletedBg); } return headIconTheme; diff --git a/src/lib/components/Stepper/stepperTheme.ts b/src/lib/components/Stepper/stepperTheme.ts index 4a92a76..1f5fd88 100644 --- a/src/lib/components/Stepper/stepperTheme.ts +++ b/src/lib/components/Stepper/stepperTheme.ts @@ -1,5 +1,5 @@ const StepperTheme = { - stepper: + stepperHorizontal: "relative m-0 flex list-none justify-between overflow-hidden p-0 transition-[height] duration-200 ease-in-out", stepperVertical: "relative m-0 w-full list-none overflow-hidden p-0 transition-[height] duration-200 ease-in-out", diff --git a/src/lib/components/Stepper/types.ts b/src/lib/components/Stepper/types.ts index 077232e..d6a3cc2 100644 --- a/src/lib/components/Stepper/types.ts +++ b/src/lib/components/Stepper/types.ts @@ -1,18 +1,15 @@ -import React from "react"; import { BaseComponent } from "../../types/baseComponent"; -import { StepperStepProps } from "./StepperStep/types"; +import type { StepperStepProps } from "./StepperStep/types"; interface StepperThemeProps { stepper: string; + stepperVertical: string; } type StepperProps = Omit & { - stepperTheme?: StepperThemeProps; - disableHeadSteps?: boolean; - defaultStep?: number; activeStep?: number; - linear?: boolean; - noEditable?: boolean; + defaultStep?: number; + theme?: StepperThemeProps; type?: "horizontal" | "vertical"; onChange?: (id: number) => void; children: From 7854395e5bdbdbb93ed27439e3eb87f122f66da3 Mon Sep 17 00:00:00 2001 From: Mateusz Lazaru Date: Thu, 23 Nov 2023 13:44:31 +0100 Subject: [PATCH 08/19] stepper - prepared demo and docs --- .../docs/react/components/stepper/a-ss.html | 12 + .../docs/react/components/stepper/a.html | 692 ++++++++++++++++++ .../react/components/stepper/index-ss.html | 12 + .../docs/react/components/stepper/index.html | 449 ++++++++++++ src/demo/pages.tsx | 2 + .../pages/components/stepper/exampleList.tsx | 22 + 6 files changed, 1189 insertions(+) create mode 100644 site/content/docs/react/components/stepper/a-ss.html create mode 100644 site/content/docs/react/components/stepper/a.html create mode 100644 site/content/docs/react/components/stepper/index-ss.html create mode 100644 site/content/docs/react/components/stepper/index.html diff --git a/site/content/docs/react/components/stepper/a-ss.html b/site/content/docs/react/components/stepper/a-ss.html new file mode 100644 index 0000000..083d6d1 --- /dev/null +++ b/site/content/docs/react/components/stepper/a-ss.html @@ -0,0 +1,12 @@ +--- +--- + +
      • + Import +
      • +
      • + Properties +
      • +
      • + Events +
      • \ No newline at end of file diff --git a/site/content/docs/react/components/stepper/a.html b/site/content/docs/react/components/stepper/a.html new file mode 100644 index 0000000..becbf99 --- /dev/null +++ b/site/content/docs/react/components/stepper/a.html @@ -0,0 +1,692 @@ +--- +--- + + +
        + +
        + +

        + Import +

        + + + {{< twsnippet/no-demo id="api-example41" >}} + + +
        + + +
        + + +
        + +

        + Properties +

        + +

        + TEStepper +

        + +
        +
        +
        +
        + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
        + Name + + Type + + Default + Description
        + activeStep + + Number + + - + + Controls the active step. + In most cases the value should be managed with onChange handler. +
        + defaultStep + + Number + + - + + Sets default step. Does not change the active step. +
        + theme + + object + + {} + + Allows to change the Tailwind classes used in the component. +
        +
        +
        +
        +
        + +

        + TEStepperStep +

        + +
        +
        +
        +
        + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
        + Name + + Type + + Default + Description
        + contentClassName + + String + + '' + + Adds custom classes to the content wrapper. +
        + headClassName + + String + + '' + + Adds custom classes to the step head. +
        + theme + + object + + {} + + Allows to change the Tailwind classes used in the component. +
        + headIcon + + ReactNode + + '' + + Sets the icon in the step head. +
        + headText + + ReactNode + + '' + + Sets the text in the step head. +
        +
        +
        +
        +
        +
        + + +
        + +
        + +

        + Classes +

        + +

        Custom classes can be passed via theme prop. Create an object with classes like below and pass it to the prop.

        + +

        + TEStepper +

        + +
        +
        +
        +
        + + + + + + + + + + + + + + + + + + + + +
        + Name + + Default + Description
        + stepperHorizontal + + "relative m-0 flex list-none justify-between overflow-hidden p-0 transition-[height] duration-200 ease-in-out", + + Sets styles to the TEStepper element in horizontal mode. +
        + stepperVertical + + "relative m-0 w-full list-none overflow-hidden p-0 transition-[height] duration-200 ease-in-out", + + Sets styles to the TEStepper element in vertical mode. +
        +
        +
        +
        +
        + +

        + TEStepperStep +

        + +
        +
        +
        +
        + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
        + Name + + Default + Description
        + stepperStep + + w-[4.5rem] flex-auto + + Sets styles to the stepperStep element. +
        + stepperStepVertical + + relative h-fit after:absolute after:left-[2.45rem] after:top-[3.6rem] after:mt-px after:h-[calc(100%-2.45rem)] after:w-px after:bg-[#e0e0e0] dark:after:bg-neutral-600 + + Sets styles to the stepperStep element in vertical mode. +
        + stepperLastStepVertical + + relative h-fit + + Sets styles to the last stepperStep element in vertical mode. +
        + stepperHeadHorizontal + + flex cursor-pointer items-center leading-[1.3rem] no-underline before:mr-2 before:h-px before:w-full before:flex-1 before:bg-[#e0e0e0] before:content-[''] after:ml-2 after:h-px after:w-full after:flex-1 after:bg-[#e0e0e0] after:content-[''] hover:bg-[#f9f9f9] focus:outline-none dark:before:bg-neutral-600 dark:after:bg-neutral-600 dark:hover:bg-[#3b3b3b] + + Sets styles to the stepperHead element in horizontal mode. +
        + stepperFirstStepHeadHorizontal + + flex cursor-pointer items-center pl-2 leading-[1.3rem] no-underline after:ml-2 after:h-px after:w-full after:flex-1 after:bg-[#e0e0e0] after:content-[''] hover:bg-[#f9f9f9] focus:outline-none dark:after:bg-neutral-600 dark:hover:bg-[#3b3b3b] + + Sets styles to the stepperHead element in horizontal mode for the first step. +
        + stepperLastStepHeadHorizontal + + flex cursor-pointer items-center pr-2 leading-[1.3rem] no-underline before:mr-2 before:h-px before:w-full before:flex-1 before:bg-[#e0e0e0] before:content-[''] hover:bg-[#f9f9f9] focus:outline-none dark:before:bg-neutral-600 dark:after:bg-neutral-600 dark:hover:bg-[#3b3b3b] + + Sets styles to the stepperHead element in horizontal mode for the last step. +
        + stepperHeadVertical + + flex cursor-pointer items-center p-6 leading-[1.3rem] no-underline hover:bg-[#f9f9f9] focus:outline-none dark:hover:bg-[#3b3b3b] + + Sets styles to the stepperHead element in vertical mode. +
        + stepperHeadIconHorizontal + + my-6 mr-2 flex h-[1.938rem] w-[1.938rem] items-center justify-center rounded-full bg-[#ebedef] text-sm font-medium text-[#40464f] + + Sets styles to the stepperHeadIcon element in horizontal mode. +
        + stepperHeadIconVertical + + mr-3 flex h-[1.938rem] w-[1.938rem] items-center justify-center rounded-full bg-[#ebedef] text-sm font-medium text-[#40464f] + + Sets styles to the stepperHeadIcon element in vertical mode. +
        + stepperHeadIconCompletedBg + + !bg-success-100 !text-success-700 + + Sets background color applied to the stepperHeadIcon element when step is completed. +
        + stepperHeadIconActiveBg + + !bg-primary-100 !text-primary-700 + + Sets background color applied to the stepperHeadIcon element when step is active. +
        + stepperHeadText + + text-neutral-500 after:flex after:text-[0.8rem] dark:text-neutral-300 + + Sets styles to the stepperHeadText element. +
        + stepperHeadTextActive + + font-medium after:flex after:text-[0.8rem] after:content-[data-content] + + Sets styles to the stepperHeadText element when step is active. +
        + stepperContentWrapper + + transition-all duration-500 ease-in-out overflow-hidden + + Sets styles to the stepperContent wrapper element. +
        + stepperContent + + absolute left-0 w-full p-4 transition-all duration-500 ease-in-out translate-0 + + Sets styles to the stepperContent element. +
        + stepperVerticalContent + + transition-[height, margin-bottom, padding-top, padding-bottom] left-0 overflow-hidden pb-6 pl-[3.75rem] pr-6 duration-300 ease-in-out + + Sets styles to the stepperContent element in vertical mode. +
        + stepperContentTranslateLeft + + -translate-x-[150%] + + Additional animation style to the stepperContent element when step is active and it is on the left side of the screen. +
        + stepperContentTranslateRight + + translate-x-[150%] + + Additional animation style to the stepperContent element when step is active and it is on the right side of the screen. +
        +
        +
        +
        +
        +
        + + +
        + + +
        + +

        + Events +

        + +
        +
        +
        +
        + + + + + + + + + + + + + +
        + Event type + Description
        + onChange?: (id: number) => void + + Fired when stepper demands to change the active step. It can be triggered in TEStepper element. +
        +
        +
        +
        +
        + + + {{< twsnippet/no-demo id="api-example6" >}} + + + +
        + +
        \ No newline at end of file diff --git a/site/content/docs/react/components/stepper/index-ss.html b/site/content/docs/react/components/stepper/index-ss.html new file mode 100644 index 0000000..faff06c --- /dev/null +++ b/site/content/docs/react/components/stepper/index-ss.html @@ -0,0 +1,12 @@ +--- +--- + +
      • + Basic example +
      • +
      • + Controlled step +
      • +
      • + Vertical +
      • diff --git a/site/content/docs/react/components/stepper/index.html b/site/content/docs/react/components/stepper/index.html new file mode 100644 index 0000000..2d3c592 --- /dev/null +++ b/site/content/docs/react/components/stepper/index.html @@ -0,0 +1,449 @@ +--- +title: "Stepper" +date: 2023-11-20T16:00:58+02:00 +draft: false +main_title: "Stepper" +subheading: "Tailwind CSS React Stepper" +seo_title: "Tailwind CSS React Stepper - Free Examples & Tutorial" +description: "Use responsive stepper component with helper examples for stepper ui, stepper form, vertical stepper, progress steps & more. Free download, open-source license." +image: "https://tecdn.b-cdn.net/img/docs/components/stepper.webp" +video: "https://www.youtube.com/watch?v=-GmnyjgI4Jc&ab_channel=Keepcoding" +url: "docs/react/components/stepper/" +menu: + components: + name: "Stepper" +--- + + +
        + +

        + Basic example +

        + +

        + Use horizontally aligned timeline component to show a series of data in a chronological order. +

        + + + {{< twsnippet/demo-iframe id="example1" iframe="/components/stepper/examples/stepper-basic-example" title="Stepper Basic Example" >}} + + + +
        + + +
        + +
        + + + +
        + +

        + Controlled stepper +

        + +

        + Use the activeStep prop to control the stepper. +

        + + + {{< twsnippet/demo-iframe id="example2" iframe="/components/stepper/examples/stepper-controlled-step" title="Stepper controlled step" >}} + + + + +
        + + + +
        + +

        + Vertical stepper +

        + +

        + Add type='vertical' to change stepper orientation. +

        + + + {{< twsnippet/demo-iframe id="example3" iframe="/components/stepper/examples/stepper-vertical-example" title="Stepper vertical" >}} + + + +
        + + +
        + +

        + If you are looking for more advanced options, try + Bootstrap Stepper + from MDBootstrap. +

        + diff --git a/src/demo/pages.tsx b/src/demo/pages.tsx index 13f469b..9271c70 100644 --- a/src/demo/pages.tsx +++ b/src/demo/pages.tsx @@ -95,6 +95,7 @@ import ToastsExamples from "./pages/components/toasts/exampleList"; import SelectExamples from "./pages/forms/select/exampleList"; import CarouselExamples from "./pages/components/carousel/exampleList"; import VideoCarouselExamples from "./pages/components/video-carousel/exampleList"; +import StepperExamples from "./pages/components/stepper/exampleList"; interface Pages { name: string; @@ -414,6 +415,7 @@ export const examplesPages: Pages[] = [ ...SelectExamples, ...CarouselExamples, ...VideoCarouselExamples, + ...StepperExamples, ]; export default demoPages; diff --git a/src/demo/pages/components/stepper/exampleList.tsx b/src/demo/pages/components/stepper/exampleList.tsx index e69de29..5501faa 100644 --- a/src/demo/pages/components/stepper/exampleList.tsx +++ b/src/demo/pages/components/stepper/exampleList.tsx @@ -0,0 +1,22 @@ +import React from "react"; +import StepperBasicExample from "./examples/StepperBasicExample"; +import StepperControlledStep from "./examples/StepperControlledStep"; +import StepperVerticalExample from "./examples/StepperVerticalExample"; + +export default [ + { + name: "StepperBasicExample", + path: "/components/stepper/examples/stepper-basic-example", + element: , + }, + { + name: "StepperControlledStep", + path: "/components/stepper/examples/stepper-controlled-step", + element: , + }, + { + name: "StepperVerticalExample", + path: "/components/stepper/examples/stepper-vertical-example", + element: , + }, +]; From 56a70b7d866a2bf349827921475cdf1b279b45e6 Mon Sep 17 00:00:00 2001 From: Mateusz Lazaru Date: Mon, 27 Nov 2023 08:07:50 +0100 Subject: [PATCH 09/19] stepper - added custom validation demo --- .../pages/components/stepper/StepperPage.tsx | 11 + .../pages/components/stepper/exampleList.tsx | 6 + .../stepper/examples/StepperLinear.tsx | 196 ++++++++++++++++++ src/lib/components/Stepper/Stepper.tsx | 2 + src/lib/components/Stepper/StepperContext.ts | 2 + .../hooks/useStepperLinearValidation.ts | 28 +++ src/lib/components/Stepper/types.ts | 1 + 7 files changed, 246 insertions(+) create mode 100644 src/demo/pages/components/stepper/examples/StepperLinear.tsx create mode 100644 src/lib/components/Stepper/hooks/useStepperLinearValidation.ts diff --git a/src/demo/pages/components/stepper/StepperPage.tsx b/src/demo/pages/components/stepper/StepperPage.tsx index 66df066..2433100 100644 --- a/src/demo/pages/components/stepper/StepperPage.tsx +++ b/src/demo/pages/components/stepper/StepperPage.tsx @@ -2,6 +2,7 @@ import React from "react"; import StepperControlledStep from "./examples/StepperControlledStep"; import StepperVerticalExample from "./examples/StepperVerticalExample"; import StepperBasicExample from "./examples/StepperBasicExample"; +import StepperLinearExample from "./examples/StepperLinear"; const StepperPage = () => { return ( @@ -38,6 +39,16 @@ const StepperPage = () => {
        + +
        + +

        + Linear stepper +

        + +
        + +
        ); diff --git a/src/demo/pages/components/stepper/exampleList.tsx b/src/demo/pages/components/stepper/exampleList.tsx index 5501faa..f301fcd 100644 --- a/src/demo/pages/components/stepper/exampleList.tsx +++ b/src/demo/pages/components/stepper/exampleList.tsx @@ -2,6 +2,7 @@ import React from "react"; import StepperBasicExample from "./examples/StepperBasicExample"; import StepperControlledStep from "./examples/StepperControlledStep"; import StepperVerticalExample from "./examples/StepperVerticalExample"; +import StepperLinearExample from "./examples/StepperLinear"; export default [ { @@ -19,4 +20,9 @@ export default [ path: "/components/stepper/examples/stepper-vertical-example", element: , }, + { + name: "StepperLinearExample", + path: "/components/stepper/examples/stepper-linear-example", + element: , + }, ]; diff --git a/src/demo/pages/components/stepper/examples/StepperLinear.tsx b/src/demo/pages/components/stepper/examples/StepperLinear.tsx new file mode 100644 index 0000000..6456c0d --- /dev/null +++ b/src/demo/pages/components/stepper/examples/StepperLinear.tsx @@ -0,0 +1,196 @@ +import React, { useState } from "react"; +import { TEStepper, TEStepperStep, TEInput } from "tw-elements-react"; + +const validTheme = { + notchLeadingDefault: "border-green-300 dark:border-green-600", + notchMiddleDefault: "border-green-300 dark:border-green-600", + notchTrailingDefault: "border-green-300 dark:border-green-600", + focusedNotchLeadingDefault: + "shadow-[-1px_0_0_#10b339,_0_1px_0_0_#10b339,_0_-1px_0_0_#10b339] border-green-300 dark:border-green-600", + focusedNotchMiddleDefault: + "shadow-[0_1px_0_0_#10b339] border-green-300 dark:border-green-600", + focusedNotchTrailingDefault: + "shadow-[1px_0_0_#10b339,_0_1px_0_0_#10b339,_0_-1px_0_0_#10b339] border-green-300 dark:border-green-600", + labelDefault: "text-green-500 dark:text-green-600", +}; + +const invalidTheme = { + notchLeadingDefault: "border-red-300 dark:border-red-600", + notchMiddleDefault: "border-red-300 dark:border-red-600", + notchTrailingDefault: "border-red-300 dark:border-red-600", + focusedNotchLeadingDefault: + "shadow-[-1px_0_0_#f87171,_0_1px_0_0_#f87171,_0_-1px_0_0_#f87171] border-red-300 dark:border-red-600", + focusedNotchMiddleDefault: + "shadow-[0_1px_0_0_#f87171] border-red-300 dark:border-red-600", + focusedNotchTrailingDefault: + "shadow-[1px_0_0_#f87171,_0_1px_0_0_#f87171,_0_-1px_0_0_#f87171] border-red-300 dark:border-red-600", + labelDefault: "text-red-300 dark:text-red-600", +}; + +const isFormValid = (formValidity: object) => { + return Object.values(formValidity).every((item) => item); +}; + +export default function StepperLinearExample() { + const [activeStep, setActiveStep] = useState(1); + const [isStepValidated, setIsStepValidated] = useState(false); + + const [step1FormValidity, setStep1FormValidity] = useState({ + email: false, + }); + + const [step2FormValidity, setStep2FormValidity] = useState({ + firstName: false, + lastName: false, + phone: false, + }); + + const handleStepChange = (stepId: number) => { + setIsStepValidated(true); + if ( + (activeStep === 1 && !isFormValid(step1FormValidity)) || + (activeStep === 2 && !isFormValid(step2FormValidity)) + ) { + return; + } + + setActiveStep(stepId); + setIsStepValidated(false); + }; + + return ( + + +
        +

        + Enter your email and start the adventure of your life! +

        + +
        + { + setStep1FormValidity({ email: e.target.checkValidity() }); + }} + /> +
        + + +
        +
        + +
        +

        + You're almost there! Just a few more details! +

        +
        + { + setStep2FormValidity((prev: object) => { + return { ...prev, firstName: e.target.checkValidity() }; + }); + }} + /> +
        +
        + { + setStep2FormValidity((prev: object) => { + return { ...prev, lastName: e.target.checkValidity() }; + }); + }} + /> +
        + +
        + { + setStep2FormValidity((prev: object) => { + return { ...prev, phone: e.target.checkValidity() }; + }); + }} + /> +
        +
        + +
        + + + +
        +
        + +
        +

        + You're all set! We will contact you soon! +

        +
        +
        +
        + ); +} diff --git a/src/lib/components/Stepper/Stepper.tsx b/src/lib/components/Stepper/Stepper.tsx index 450ea46..65486cd 100644 --- a/src/lib/components/Stepper/Stepper.tsx +++ b/src/lib/components/Stepper/Stepper.tsx @@ -14,6 +14,7 @@ const TEStepper: React.FC = ({ children, onChange, type = "horizontal", + linear, style, }) => { const theme = { @@ -51,6 +52,7 @@ const TEStepper: React.FC = ({ setStepperHeight, vertical, stepsAmount: childrenArray.length, + linear, }} >
          void; vertical: boolean; stepsAmount: number; + linear?: boolean; } const StepperContext = createContext({ @@ -18,6 +19,7 @@ const StepperContext = createContext({ setStepperHeight: () => {}, vertical: false, stepsAmount: 0, + linear: false, }); export default StepperContext; diff --git a/src/lib/components/Stepper/hooks/useStepperLinearValidation.ts b/src/lib/components/Stepper/hooks/useStepperLinearValidation.ts new file mode 100644 index 0000000..50d3f2e --- /dev/null +++ b/src/lib/components/Stepper/hooks/useStepperLinearValidation.ts @@ -0,0 +1,28 @@ +import { useContext, useEffect, useState, useMemo } from "react"; +import StepperContext from "../StepperContext"; + +export default function useStepperLinearValidaTion( + itemId: number, + contentRef: React.RefObject +) { + const { activeStep, linear, stepsAmount } = useContext(StepperContext); + const [isStepValid, setIsStepValid] = useState(true); + const isActive = useMemo(() => activeStep === itemId, [activeStep]); + useEffect(() => { + if (!linear || !isActive) return; + // const form = contentRef.current?.querySelector("form"); + // const inputs = form?.querySelectorAll("input, select, textarea"); + // const isFormValid = form?.checkValidity(); + // console.log("isFormValid", isFormValid); + // const isInputsValid = Array.from(inputs || []).every((input) => + // input.checkValidity() + // ); + // const isStepValid = isFormValid && isInputsValid; + // setIsStepValid(isStepValid); + + // if (!isStepValid) { + // form?.reportValidity(); + // } + }, [activeStep, linear, stepsAmount, isStepValid, isActive]); + return true; +} diff --git a/src/lib/components/Stepper/types.ts b/src/lib/components/Stepper/types.ts index d6a3cc2..efc6324 100644 --- a/src/lib/components/Stepper/types.ts +++ b/src/lib/components/Stepper/types.ts @@ -9,6 +9,7 @@ interface StepperThemeProps { type StepperProps = Omit & { activeStep?: number; defaultStep?: number; + linear?: boolean; theme?: StepperThemeProps; type?: "horizontal" | "vertical"; onChange?: (id: number) => void; From 5661ecd5401748f0a542d85774b65675e2306d60 Mon Sep 17 00:00:00 2001 From: Mateusz Lazaru Date: Tue, 28 Nov 2023 10:36:52 +0100 Subject: [PATCH 10/19] stepper - added 'noEditable' option. Updated styles. --- .../pages/components/stepper/StepperPage.tsx | 11 + .../stepper/examples/StepperLinear.tsx | 196 ++++++++++++++++++ .../stepper/examples/StepperNoEditable.tsx | 32 +++ src/lib/components/Stepper/Stepper.tsx | 9 +- src/lib/components/Stepper/StepperContext.ts | 2 + .../Stepper/StepperStep/StepperStep.tsx | 27 ++- .../Stepper/StepperStep/stepperStepTheme.ts | 15 +- .../Stepper/hooks/useHeadIconClasses.ts | 5 + src/lib/components/Stepper/types.ts | 1 + 9 files changed, 282 insertions(+), 16 deletions(-) create mode 100644 src/demo/pages/components/stepper/examples/StepperLinear.tsx create mode 100644 src/demo/pages/components/stepper/examples/StepperNoEditable.tsx diff --git a/src/demo/pages/components/stepper/StepperPage.tsx b/src/demo/pages/components/stepper/StepperPage.tsx index 66df066..44722bd 100644 --- a/src/demo/pages/components/stepper/StepperPage.tsx +++ b/src/demo/pages/components/stepper/StepperPage.tsx @@ -2,6 +2,7 @@ import React from "react"; import StepperControlledStep from "./examples/StepperControlledStep"; import StepperVerticalExample from "./examples/StepperVerticalExample"; import StepperBasicExample from "./examples/StepperBasicExample"; +import StepperNoEditable from "./examples/StepperNoEditable"; const StepperPage = () => { return ( @@ -38,6 +39,16 @@ const StepperPage = () => {
          + +
          + +

          + Stepper with no editable steps +

          + +
          + +
          ); diff --git a/src/demo/pages/components/stepper/examples/StepperLinear.tsx b/src/demo/pages/components/stepper/examples/StepperLinear.tsx new file mode 100644 index 0000000..6456c0d --- /dev/null +++ b/src/demo/pages/components/stepper/examples/StepperLinear.tsx @@ -0,0 +1,196 @@ +import React, { useState } from "react"; +import { TEStepper, TEStepperStep, TEInput } from "tw-elements-react"; + +const validTheme = { + notchLeadingDefault: "border-green-300 dark:border-green-600", + notchMiddleDefault: "border-green-300 dark:border-green-600", + notchTrailingDefault: "border-green-300 dark:border-green-600", + focusedNotchLeadingDefault: + "shadow-[-1px_0_0_#10b339,_0_1px_0_0_#10b339,_0_-1px_0_0_#10b339] border-green-300 dark:border-green-600", + focusedNotchMiddleDefault: + "shadow-[0_1px_0_0_#10b339] border-green-300 dark:border-green-600", + focusedNotchTrailingDefault: + "shadow-[1px_0_0_#10b339,_0_1px_0_0_#10b339,_0_-1px_0_0_#10b339] border-green-300 dark:border-green-600", + labelDefault: "text-green-500 dark:text-green-600", +}; + +const invalidTheme = { + notchLeadingDefault: "border-red-300 dark:border-red-600", + notchMiddleDefault: "border-red-300 dark:border-red-600", + notchTrailingDefault: "border-red-300 dark:border-red-600", + focusedNotchLeadingDefault: + "shadow-[-1px_0_0_#f87171,_0_1px_0_0_#f87171,_0_-1px_0_0_#f87171] border-red-300 dark:border-red-600", + focusedNotchMiddleDefault: + "shadow-[0_1px_0_0_#f87171] border-red-300 dark:border-red-600", + focusedNotchTrailingDefault: + "shadow-[1px_0_0_#f87171,_0_1px_0_0_#f87171,_0_-1px_0_0_#f87171] border-red-300 dark:border-red-600", + labelDefault: "text-red-300 dark:text-red-600", +}; + +const isFormValid = (formValidity: object) => { + return Object.values(formValidity).every((item) => item); +}; + +export default function StepperLinearExample() { + const [activeStep, setActiveStep] = useState(1); + const [isStepValidated, setIsStepValidated] = useState(false); + + const [step1FormValidity, setStep1FormValidity] = useState({ + email: false, + }); + + const [step2FormValidity, setStep2FormValidity] = useState({ + firstName: false, + lastName: false, + phone: false, + }); + + const handleStepChange = (stepId: number) => { + setIsStepValidated(true); + if ( + (activeStep === 1 && !isFormValid(step1FormValidity)) || + (activeStep === 2 && !isFormValid(step2FormValidity)) + ) { + return; + } + + setActiveStep(stepId); + setIsStepValidated(false); + }; + + return ( + + +
          +

          + Enter your email and start the adventure of your life! +

          + +
          + { + setStep1FormValidity({ email: e.target.checkValidity() }); + }} + /> +
          + + +
          +
          + +
          +

          + You're almost there! Just a few more details! +

          +
          + { + setStep2FormValidity((prev: object) => { + return { ...prev, firstName: e.target.checkValidity() }; + }); + }} + /> +
          +
          + { + setStep2FormValidity((prev: object) => { + return { ...prev, lastName: e.target.checkValidity() }; + }); + }} + /> +
          + +
          + { + setStep2FormValidity((prev: object) => { + return { ...prev, phone: e.target.checkValidity() }; + }); + }} + /> +
          +
          + +
          + + + +
          +
          + +
          +

          + You're all set! We will contact you soon! +

          +
          +
          +
          + ); +} diff --git a/src/demo/pages/components/stepper/examples/StepperNoEditable.tsx b/src/demo/pages/components/stepper/examples/StepperNoEditable.tsx new file mode 100644 index 0000000..986b1a5 --- /dev/null +++ b/src/demo/pages/components/stepper/examples/StepperNoEditable.tsx @@ -0,0 +1,32 @@ +import React from "react"; +import { TEStepper, TEStepperStep } from "tw-elements-react"; + +export default function StepperNoEditable() { + return ( + + +

          + After changing the active step, the previous steps can't be accessed + anymore. +

          +
          + +

          + Lorem ipsum dolor, sit amet consectetur adipisicing elit. Pariatur, + tempora esse iusto tempore quod, aspernatur incidunt minima magnam, + quaerat aut vel expedita illum molestias repellendus asperiores id + suscipit saepe. Maxime. +

          +
          + +

          + Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam + voluptatum, quod, voluptate quia, ipsam illum quibusdam dolorum + voluptatibus laboriosam quae voluptates? Quisquam, voluptatibus + voluptas. Quisquam, voluptatibus voluptas. Quisquam, voluptatibus + voluptas. +

          +
          +
          + ); +} diff --git a/src/lib/components/Stepper/Stepper.tsx b/src/lib/components/Stepper/Stepper.tsx index 450ea46..33d0ecf 100644 --- a/src/lib/components/Stepper/Stepper.tsx +++ b/src/lib/components/Stepper/Stepper.tsx @@ -12,6 +12,7 @@ const TEStepper: React.FC = ({ defaultStep = 1, activeStep: activeStepProp, children, + noEditable = false, onChange, type = "horizontal", style, @@ -36,7 +37,8 @@ const TEStepper: React.FC = ({ ) as React.ReactElement[]; }, [children]); - const onChangeHandler = (id: number) => { + const stepChangeHandler = (id: number) => { + if (noEditable && id < activeStep) return; onChange?.(id); setActiveStepState(id); }; @@ -45,12 +47,13 @@ const TEStepper: React.FC = ({
            = ({ itemId: index + 1, activeStep, key: "stepper-step-" + index, - onChange: onChangeHandler, + onChange: stepChangeHandler, }); })}
          diff --git a/src/lib/components/Stepper/StepperContext.ts b/src/lib/components/Stepper/StepperContext.ts index 7d112c4..784359a 100644 --- a/src/lib/components/Stepper/StepperContext.ts +++ b/src/lib/components/Stepper/StepperContext.ts @@ -8,6 +8,7 @@ interface StepperContextProps { setStepperHeight: (height: string) => void; vertical: boolean; stepsAmount: number; + noEditable?: boolean; } const StepperContext = createContext({ @@ -18,6 +19,7 @@ const StepperContext = createContext({ setStepperHeight: () => {}, vertical: false, stepsAmount: 0, + noEditable: false, }); export default StepperContext; diff --git a/src/lib/components/Stepper/StepperStep/StepperStep.tsx b/src/lib/components/Stepper/StepperStep/StepperStep.tsx index a2cf878..52ae3c6 100644 --- a/src/lib/components/Stepper/StepperStep/StepperStep.tsx +++ b/src/lib/components/Stepper/StepperStep/StepperStep.tsx @@ -1,4 +1,4 @@ -import React, { useMemo, useRef, useContext } from "react"; +import React, { useState, useMemo, useRef, useContext, useEffect } from "react"; import clx from "clsx"; import StepperStepTheme from "./stepperStepTheme"; import StepperContext from "../StepperContext"; @@ -22,8 +22,9 @@ const TEStepperStep: React.FC = ({ style, }) => { const headRef = useRef(null); + const [isDisabled, setIsDisabled] = useState(false); const contentRef = useRef(null); - const { activeStep, onChange, vertical, stepsAmount } = + const { activeStep, noEditable, onChange, vertical, stepsAmount } = useContext(StepperContext); const animationDirection = useMemo(() => { @@ -47,9 +48,22 @@ const TEStepperStep: React.FC = ({ const headIconClasses = useHeadIconClasses( isActive, isCompleted, + isDisabled, theme, vertical ); + + const headTextClasses = clx( + isActive ? theme.stepperHeadTextActive : theme.stepperHeadText, + isDisabled && theme.disabledStep + ); + + useEffect(() => { + if (isCompleted && noEditable) { + setIsDisabled(true); + } + }, [isCompleted, noEditable]); + const stepperHeadClasses = clx(useHeadClasses(theme, itemId), headClassName); const stepperStepClasses = clx( vertical @@ -57,6 +71,7 @@ const TEStepperStep: React.FC = ({ ? theme.stepperLastStepVertical : theme.stepperStepVertical : theme.stepperStep, + isDisabled && theme.disabledStep, className ); @@ -87,13 +102,7 @@ const TEStepperStep: React.FC = ({ ref={headRef} > {headIcon} - - {headText} - + {headText}
          { @@ -11,6 +12,7 @@ const useHeadIconClasses = ( stepperHeadIconVertical, stepperHeadIconActiveBg, stepperHeadIconCompletedBg, + stepperHeadIconDisabledBg, } = theme; const headIconTheme = vertical @@ -20,6 +22,9 @@ const useHeadIconClasses = ( if (isActive) { return clsx(headIconTheme, stepperHeadIconActiveBg); } + if (isDisabled) { + return clsx(headIconTheme, stepperHeadIconDisabledBg); + } if (isCompleted) { return clsx(headIconTheme, stepperHeadIconCompletedBg); } diff --git a/src/lib/components/Stepper/types.ts b/src/lib/components/Stepper/types.ts index d6a3cc2..7a5bc9c 100644 --- a/src/lib/components/Stepper/types.ts +++ b/src/lib/components/Stepper/types.ts @@ -9,6 +9,7 @@ interface StepperThemeProps { type StepperProps = Omit & { activeStep?: number; defaultStep?: number; + noEditable?: boolean; theme?: StepperThemeProps; type?: "horizontal" | "vertical"; onChange?: (id: number) => void; From e4bba77b196f5cfea6795561734e9f7e5b331a0c Mon Sep 17 00:00:00 2001 From: Mateusz Lazaru Date: Tue, 30 Jan 2024 23:53:29 +0100 Subject: [PATCH 11/19] fix(stepper) - changed vertical step height calculating to gridTemplateRows animation --- .../Stepper/StepperStep/StepperStep.tsx | 12 ++++-------- .../Stepper/StepperStep/stepperStepTheme.ts | 17 ++++++++++++----- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/src/lib/components/Stepper/StepperStep/StepperStep.tsx b/src/lib/components/Stepper/StepperStep/StepperStep.tsx index a2cf878..3f7da8f 100644 --- a/src/lib/components/Stepper/StepperStep/StepperStep.tsx +++ b/src/lib/components/Stepper/StepperStep/StepperStep.tsx @@ -7,7 +7,6 @@ import useHeadIconClasses from "../hooks/useHeadIconClasses"; import useIsStepCompleted from "../hooks/useIsStepCompleted"; import useStepperHeight from "../hooks/useHorizontalStepperHeight"; import { getTranslateDirection } from "../utils/utils"; -import useVerticalStepHeight from "../hooks/useVerticalStepHeight"; import useHeadClasses from "../hooks/useHeadClasses"; const TEStepperStep: React.FC = ({ @@ -65,7 +64,8 @@ const TEStepperStep: React.FC = ({ const stepperContentClasses = clx( vertical ? theme.stepperVerticalContent : theme.stepperContent, !vertical && theme[dynamicAnimationDirection as keyof typeof theme], - contentClassName + contentClassName, + isActive ? "pb-6" : "pb-0" ); const headClickHandler = () => { @@ -73,11 +73,6 @@ const TEStepperStep: React.FC = ({ }; useStepperHeight(isActive, headRef, contentRef, vertical, children); - const verticalStepHeight = useVerticalStepHeight( - isActive, - contentRef, - children - ); return (
        • @@ -97,8 +92,9 @@ const TEStepperStep: React.FC = ({
        • diff --git a/src/lib/components/Stepper/StepperStep/stepperStepTheme.ts b/src/lib/components/Stepper/StepperStep/stepperStepTheme.ts index 6fe927a..215732b 100644 --- a/src/lib/components/Stepper/StepperStep/stepperStepTheme.ts +++ b/src/lib/components/Stepper/StepperStep/stepperStepTheme.ts @@ -2,6 +2,7 @@ interface StepperStepThemeProps { stepperStep: string; stepperStepVertical: string; stepperLastStepVertical: string; + disabledStep: string; stepperHeadHorizontal: string; stepperFirstStepHeadHorizontal: string; stepperLastStepHeadHorizontal: string; @@ -9,6 +10,7 @@ interface StepperStepThemeProps { stepperHeadIconHorizontal: string; stepperHeadIconVertical: string; stepperHeadIconActiveBg: string; + stepperHeadIconDisabledBg: string; stepperHeadIconCompletedBg: string; stepperHeadText: string; stepperHeadTextActive: string; @@ -24,6 +26,7 @@ const StepperStepTheme: StepperStepThemeProps = { stepperStepVertical: "relative h-fit after:absolute after:left-[2.45rem] after:top-[3.6rem] after:mt-px after:h-[calc(100%-2.45rem)] after:w-px after:bg-[#e0e0e0] dark:after:bg-neutral-600", stepperLastStepVertical: "relative h-fit", + disabledStep: "pointer-events-none", stepperHeadHorizontal: "flex cursor-pointer items-center leading-[1.3rem] no-underline before:mr-2 before:h-px before:w-full before:flex-1 before:bg-[#e0e0e0] before:content-[''] after:ml-2 after:h-px after:w-full after:flex-1 after:bg-[#e0e0e0] after:content-[''] hover:bg-[#f9f9f9] focus:outline-none dark:before:bg-neutral-600 dark:after:bg-neutral-600 dark:hover:bg-[#3b3b3b]", stepperFirstStepHeadHorizontal: @@ -33,11 +36,15 @@ const StepperStepTheme: StepperStepThemeProps = { stepperHeadVertical: "flex cursor-pointer items-center p-6 leading-[1.3rem] no-underline hover:bg-[#f9f9f9] focus:outline-none dark:hover:bg-[#3b3b3b]", stepperHeadIconHorizontal: - "my-6 mr-2 flex h-[1.938rem] w-[1.938rem] items-center justify-center rounded-full bg-[#ebedef] text-sm font-medium text-[#40464f]", + "my-6 mr-2 flex h-[1.938rem] w-[1.938rem] items-center justify-center rounded-full bg-[#6d6d6d] text-sm font-medium text-[#fff] dark:bg-[#757575]", stepperHeadIconVertical: - "mr-3 flex h-[1.938rem] w-[1.938rem] items-center justify-center rounded-full bg-[#ebedef] text-sm font-medium text-[#40464f]", - stepperHeadIconCompletedBg: "!bg-success-100 !text-success-700", - stepperHeadIconActiveBg: "!bg-primary-100 !text-primary-700", + "mr-3 flex h-[1.938rem] w-[1.938rem] items-center justify-center rounded-full bg-[#6d6d6d] text-sm font-medium text-[#fff] dark:bg-[#757575]", + stepperHeadIconCompletedBg: + "!bg-success-100 !text-success-700 dark:!bg-[#04201f] dark:!text-[#72c894]", + stepperHeadIconActiveBg: + "!bg-primary-100 !text-primary-700 dark:!bg-[#0c1728] dark:!text-[#628dd5]", + stepperHeadIconDisabledBg: + "!bg-[#6d6d6d] !text-neutral-300 dark:!bg-[#757575]", stepperHeadText: "text-neutral-500 after:flex after:text-[0.8rem] dark:text-neutral-300", stepperHeadTextActive: @@ -49,7 +56,7 @@ const StepperStepTheme: StepperStepThemeProps = { stepperContentTranslateLeft: "-translate-x-[150%]", stepperContentTranslateRight: "translate-x-[150%]", stepperVerticalContent: - "transition-[height, margin-bottom, padding-top, padding-bottom] left-0 overflow-hidden pb-6 pl-[3.75rem] pr-6 duration-300 ease-in-out", + "transition-[height, margin-bottom, padding-top, padding-bottom] left-0 overflow-hidden pl-[3.75rem] pr-6 duration-300 ease-in-out", }; export default StepperStepTheme; From 7fa782d83e0a3227d1173901c0e6c4fcfef595e1 Mon Sep 17 00:00:00 2001 From: Mateusz Lazaru Date: Mon, 5 Feb 2024 16:30:02 +0100 Subject: [PATCH 12/19] feat(stepper) - added 'linear', 'customValidation', crated demo examples --- .../pages/components/stepper/StepperPage.tsx | 11 + .../pages/components/stepper/exampleList.tsx | 6 + .../examples/StepperControlledStep.tsx | 4 +- .../stepper/examples/StepperLinear.tsx | 194 ++-------------- .../StepperLinearCustomValidation.tsx | 218 ++++++++++++++++++ src/lib/components/Stepper/Stepper.tsx | 78 ++++++- src/lib/components/Stepper/StepperContext.ts | 5 + .../Stepper/StepperStep/StepperStep.tsx | 37 ++- .../Stepper/StepperStep/stepperStepTheme.ts | 3 + .../Stepper/hooks/useHeadIconClasses.ts | 10 +- .../hooks/useHorizontalStepperHeight.ts | 2 +- .../hooks/useStepperLinearValidation.ts | 28 --- .../Stepper/hooks/useVerticalStepHeight.ts | 36 --- src/lib/components/Stepper/types.ts | 10 +- src/lib/components/Stepper/utils/utils.ts | 78 ++++++- 15 files changed, 465 insertions(+), 255 deletions(-) create mode 100644 src/demo/pages/components/stepper/examples/StepperLinearCustomValidation.tsx delete mode 100644 src/lib/components/Stepper/hooks/useStepperLinearValidation.ts delete mode 100644 src/lib/components/Stepper/hooks/useVerticalStepHeight.ts diff --git a/src/demo/pages/components/stepper/StepperPage.tsx b/src/demo/pages/components/stepper/StepperPage.tsx index 2433100..466766c 100644 --- a/src/demo/pages/components/stepper/StepperPage.tsx +++ b/src/demo/pages/components/stepper/StepperPage.tsx @@ -3,6 +3,7 @@ import StepperControlledStep from "./examples/StepperControlledStep"; import StepperVerticalExample from "./examples/StepperVerticalExample"; import StepperBasicExample from "./examples/StepperBasicExample"; import StepperLinearExample from "./examples/StepperLinear"; +import StepperLinearCustomValidationExample from "./examples/StepperLinearCustomValidation"; const StepperPage = () => { return ( @@ -49,6 +50,16 @@ const StepperPage = () => {
          + +
          + +

          + Linear stepper with custom validation +

          + +
          + +
          ); diff --git a/src/demo/pages/components/stepper/exampleList.tsx b/src/demo/pages/components/stepper/exampleList.tsx index f301fcd..8b5c2c3 100644 --- a/src/demo/pages/components/stepper/exampleList.tsx +++ b/src/demo/pages/components/stepper/exampleList.tsx @@ -3,6 +3,7 @@ import StepperBasicExample from "./examples/StepperBasicExample"; import StepperControlledStep from "./examples/StepperControlledStep"; import StepperVerticalExample from "./examples/StepperVerticalExample"; import StepperLinearExample from "./examples/StepperLinear"; +import StepperLinearCustomValidationExample from "./examples/StepperLinearCustomValidation"; export default [ { @@ -25,4 +26,9 @@ export default [ path: "/components/stepper/examples/stepper-linear-example", element: , }, + { + name: "StepperLinearCustomValidationExample", + path: "/components/stepper/examples/stepper-linear-custom-validation-example", + element: , + }, ]; diff --git a/src/demo/pages/components/stepper/examples/StepperControlledStep.tsx b/src/demo/pages/components/stepper/examples/StepperControlledStep.tsx index 6c219dd..ca6d7dc 100644 --- a/src/demo/pages/components/stepper/examples/StepperControlledStep.tsx +++ b/src/demo/pages/components/stepper/examples/StepperControlledStep.tsx @@ -7,8 +7,8 @@ export default function StepperControlledStep(): JSX.Element { return ( { - setActiveStep(stepId); + onChange={(_, next) => { + setActiveStep(next); }} > diff --git a/src/demo/pages/components/stepper/examples/StepperLinear.tsx b/src/demo/pages/components/stepper/examples/StepperLinear.tsx index 6456c0d..a92161f 100644 --- a/src/demo/pages/components/stepper/examples/StepperLinear.tsx +++ b/src/demo/pages/components/stepper/examples/StepperLinear.tsx @@ -1,196 +1,42 @@ -import React, { useState } from "react"; +import React from "react"; import { TEStepper, TEStepperStep, TEInput } from "tw-elements-react"; -const validTheme = { - notchLeadingDefault: "border-green-300 dark:border-green-600", - notchMiddleDefault: "border-green-300 dark:border-green-600", - notchTrailingDefault: "border-green-300 dark:border-green-600", - focusedNotchLeadingDefault: - "shadow-[-1px_0_0_#10b339,_0_1px_0_0_#10b339,_0_-1px_0_0_#10b339] border-green-300 dark:border-green-600", - focusedNotchMiddleDefault: - "shadow-[0_1px_0_0_#10b339] border-green-300 dark:border-green-600", - focusedNotchTrailingDefault: - "shadow-[1px_0_0_#10b339,_0_1px_0_0_#10b339,_0_-1px_0_0_#10b339] border-green-300 dark:border-green-600", - labelDefault: "text-green-500 dark:text-green-600", -}; - -const invalidTheme = { - notchLeadingDefault: "border-red-300 dark:border-red-600", - notchMiddleDefault: "border-red-300 dark:border-red-600", - notchTrailingDefault: "border-red-300 dark:border-red-600", - focusedNotchLeadingDefault: - "shadow-[-1px_0_0_#f87171,_0_1px_0_0_#f87171,_0_-1px_0_0_#f87171] border-red-300 dark:border-red-600", - focusedNotchMiddleDefault: - "shadow-[0_1px_0_0_#f87171] border-red-300 dark:border-red-600", - focusedNotchTrailingDefault: - "shadow-[1px_0_0_#f87171,_0_1px_0_0_#f87171,_0_-1px_0_0_#f87171] border-red-300 dark:border-red-600", - labelDefault: "text-red-300 dark:text-red-600", -}; - -const isFormValid = (formValidity: object) => { - return Object.values(formValidity).every((item) => item); -}; - export default function StepperLinearExample() { - const [activeStep, setActiveStep] = useState(1); - const [isStepValidated, setIsStepValidated] = useState(false); - - const [step1FormValidity, setStep1FormValidity] = useState({ - email: false, - }); - - const [step2FormValidity, setStep2FormValidity] = useState({ - firstName: false, - lastName: false, - phone: false, - }); - - const handleStepChange = (stepId: number) => { - setIsStepValidated(true); - if ( - (activeStep === 1 && !isFormValid(step1FormValidity)) || - (activeStep === 2 && !isFormValid(step2FormValidity)) - ) { - return; - } - - setActiveStep(stepId); - setIsStepValidated(false); - }; - return ( - - -
          + + +

          Enter your email and start the adventure of your life!

          - { - setStep1FormValidity({ email: e.target.checkValidity() }); - }} - /> +
          - - - -
          - -
          + +

          You're almost there! Just a few more details!

          - { - setStep2FormValidity((prev: object) => { - return { ...prev, firstName: e.target.checkValidity() }; - }); - }} - /> +
          - { - setStep2FormValidity((prev: object) => { - return { ...prev, lastName: e.target.checkValidity() }; - }); - }} - /> +
          - { - setStep2FormValidity((prev: object) => { - return { ...prev, phone: e.target.checkValidity() }; - }); - }} - /> +
          - - -
          - - - -
          -
          - -
          -

          - You're all set! We will contact you soon! -

          -
          -
          -
          +
          + +
          +

          + You're all set! We will contact you soon! +

          +
          +
          +
          + ); } diff --git a/src/demo/pages/components/stepper/examples/StepperLinearCustomValidation.tsx b/src/demo/pages/components/stepper/examples/StepperLinearCustomValidation.tsx new file mode 100644 index 0000000..4b3fe54 --- /dev/null +++ b/src/demo/pages/components/stepper/examples/StepperLinearCustomValidation.tsx @@ -0,0 +1,218 @@ +import React, { useState } from "react"; +import { TEStepper, TEStepperStep, TEInput } from "tw-elements-react"; + +const validTheme = { + notchLeadingDefault: "border-green-300 dark:border-green-600", + notchMiddleDefault: "border-green-300 dark:border-green-600", + notchTrailingDefault: "border-green-300 dark:border-green-600", + focusedNotchLeadingDefault: + "shadow-[-1px_0_0_#10b339,_0_1px_0_0_#10b339,_0_-1px_0_0_#10b339] border-green-300 dark:border-green-600", + focusedNotchMiddleDefault: + "shadow-[0_1px_0_0_#10b339] border-green-300 dark:border-green-600", + focusedNotchTrailingDefault: + "shadow-[1px_0_0_#10b339,_0_1px_0_0_#10b339,_0_-1px_0_0_#10b339] border-green-300 dark:border-green-600", + labelDefault: "text-green-500 dark:text-green-600", +}; + +const invalidTheme = { + notchLeadingDefault: "border-red-300 dark:border-red-600", + notchMiddleDefault: "border-red-300 dark:border-red-600", + notchTrailingDefault: "border-red-300 dark:border-red-600", + focusedNotchLeadingDefault: + "shadow-[-1px_0_0_#f87171,_0_1px_0_0_#f87171,_0_-1px_0_0_#f87171] border-red-300 dark:border-red-600", + focusedNotchMiddleDefault: + "shadow-[0_1px_0_0_#f87171] border-red-300 dark:border-red-600", + focusedNotchTrailingDefault: + "shadow-[1px_0_0_#f87171,_0_1px_0_0_#f87171,_0_-1px_0_0_#f87171] border-red-300 dark:border-red-600", + labelDefault: "text-red-300 dark:text-red-600", +}; + +type formItemsValidityTypes = { + email: boolean | null; + firstName: boolean | null; + lastName: boolean | null; + phone: boolean | null; +}; + +export default function StepperLinearCustomValidationExample() { + const [formItemsValidity, setFormItemsValidity] = + useState({ + email: false, + firstName: false, + lastName: false, + phone: false, + }); + + const [validatedSteps, setValidatedSteps] = useState({ + step1: false, + step2: false, + step3: false, + }); + + return ( +
          + { + setValidatedSteps({ + ...validatedSteps, + ["step" + nextStep]: false, + }); + }} + onInvalid={(stepId) => { + setValidatedSteps({ + ...validatedSteps, + ["step" + stepId]: true, + }); + }} + linear + customValidation={(el) => { + let isValid = true; + + switch (el.name) { + case "email": + const isDomainExample = el.value.split("@")[1] === "example.com"; + isValid = isDomainExample; + !isValid && + el.setCustomValidity( + "Please enter a valid email address with @example.com domain" + ); + + el.reportValidity(); + setFormItemsValidity((prev) => { + return { + ...prev, + email: isValid, + }; + }); + break; + + case "firstName": + isValid = el.checkValidity(); + setFormItemsValidity((prev) => { + return { + ...prev, + firstName: isValid, + }; + }); + + break; + + case "lastName": + isValid = el.checkValidity(); + setFormItemsValidity((prev) => { + return { + ...prev, + lastName: isValid, + }; + }); + + break; + + case "phone": + isValid = el.checkValidity(); + setFormItemsValidity((prev) => { + return { + ...prev, + phone: isValid, + }; + }); + + break; + } + + return isValid; + }} + > + +

          + Enter your email and start the adventure of your life! +

          + +
          + { + e.target.setCustomValidity(" "); + }} + onInput={(e) => { + (e.target as HTMLInputElement).setCustomValidity(" "); + }} + required + theme={ + !validatedSteps.step1 + ? {} + : formItemsValidity.email + ? validTheme + : invalidTheme + } + /> +
          +
          + +

          + You're almost there! Just a few more details! +

          +
          + +
          +
          + +
          + +
          + +
          +
          + +
          +

          + You're all set! We will contact you soon! +

          +
          +
          +
          +
          + ); +} diff --git a/src/lib/components/Stepper/Stepper.tsx b/src/lib/components/Stepper/Stepper.tsx index 65486cd..9a7cde3 100644 --- a/src/lib/components/Stepper/Stepper.tsx +++ b/src/lib/components/Stepper/Stepper.tsx @@ -5,6 +5,14 @@ import { StepperStepProps } from "./StepperStep/types"; import StepperTheme from "./stepperTheme"; import useActiveValue from "../../hooks/useActiveValue"; import type { StepperProps } from "./types"; +import { validateStepContent, checkStepsBetweenValidity } from "./utils/utils"; + +interface StepsValidity { + [key: string]: { + wasValidated: boolean; + isValid: boolean; + }; +} const TEStepper: React.FC = ({ theme: customTheme, @@ -13,9 +21,11 @@ const TEStepper: React.FC = ({ activeStep: activeStepProp, children, onChange, + onInvalid, type = "horizontal", linear, style, + customValidation, }) => { const theme = { ...StepperTheme, @@ -29,6 +39,8 @@ const TEStepper: React.FC = ({ const [activeStepState, setActiveStepState] = useState(defaultStep); const activeStep = useActiveValue(activeStepProp, activeStepState); const stepperRef = useRef(null); + const [activeStepContent, setActiveStepContent] = + useState(null); const [stepperHeight, setStepperHeight] = useState("auto"); const childrenArray = useMemo(() => { @@ -37,9 +49,65 @@ const TEStepper: React.FC = ({ ) as React.ReactElement[]; }, [children]); - const onChangeHandler = (id: number) => { - onChange?.(id); - setActiveStepState(id); + const [stepsValidity, setStepsValidity] = useState(() => { + if (!linear) { + return {}; + } + + const obj: StepsValidity = {}; + + childrenArray.forEach((_, i) => { + obj["step" + Number(i + 1)] = { + wasValidated: false, + isValid: false, + }; + }); + + return obj; + }); + + const onChangeHandler = (targetStepId: number) => { + if (linear) { + if (!activeStepContent) { + return; + } + + const isGoingForward = activeStep < targetStepId; + + const isCurrentStepValid = validateStepContent( + activeStepContent as HTMLElement, + customValidation + ); + + const isStepsBetweenValid = checkStepsBetweenValidity( + activeStep, + targetStepId, + stepsValidity, + setStepsValidity + ); + + setStepsValidity((prev) => { + return { + ...prev, + + ["step" + activeStep]: { + wasValidated: true, + isValid: isCurrentStepValid, + }, + + ["step" + targetStepId]: { + wasValidated: prev![`step${targetStepId}`].wasValidated, + isValid: false, + }, + }; + }); + if ((!isCurrentStepValid || !isStepsBetweenValid) && isGoingForward) { + onInvalid?.(activeStep, targetStepId); + return; + } + } + onChange?.(activeStep, targetStepId); + setActiveStepState(targetStepId); }; return ( <> @@ -49,7 +117,10 @@ const TEStepper: React.FC = ({ onChange: onChangeHandler, stepperRef, stepperHeight, + stepsValidity, setStepperHeight, + setActiveStepContent, + vertical, stepsAmount: childrenArray.length, linear, @@ -74,4 +145,5 @@ const TEStepper: React.FC = ({ ); }; +export type { StepsValidity }; export default TEStepper; diff --git a/src/lib/components/Stepper/StepperContext.ts b/src/lib/components/Stepper/StepperContext.ts index 07040a9..413b5e0 100644 --- a/src/lib/components/Stepper/StepperContext.ts +++ b/src/lib/components/Stepper/StepperContext.ts @@ -1,25 +1,30 @@ import { createContext } from "react"; +import { StepsValidity } from "./Stepper"; interface StepperContextProps { activeStep: number; onChange?: (id: number) => void; stepperRef: React.RefObject | null; + stepsValidity: StepsValidity | null; stepperHeight: string; setStepperHeight: (height: string) => void; vertical: boolean; stepsAmount: number; linear?: boolean; + setActiveStepContent: React.Dispatch>; } const StepperContext = createContext({ activeStep: 1, onChange: () => {}, stepperRef: null, + stepsValidity: null, stepperHeight: "0", setStepperHeight: () => {}, vertical: false, stepsAmount: 0, linear: false, + setActiveStepContent: () => {}, }); export default StepperContext; diff --git a/src/lib/components/Stepper/StepperStep/StepperStep.tsx b/src/lib/components/Stepper/StepperStep/StepperStep.tsx index 3f7da8f..87e1e1d 100644 --- a/src/lib/components/Stepper/StepperStep/StepperStep.tsx +++ b/src/lib/components/Stepper/StepperStep/StepperStep.tsx @@ -1,4 +1,4 @@ -import React, { useMemo, useRef, useContext } from "react"; +import React, { useMemo, useRef, useContext, useEffect } from "react"; import clx from "clsx"; import StepperStepTheme from "./stepperStepTheme"; import StepperContext from "../StepperContext"; @@ -20,10 +20,28 @@ const TEStepperStep: React.FC = ({ children, style, }) => { + const stepRef = useRef(null); const headRef = useRef(null); const contentRef = useRef(null); - const { activeStep, onChange, vertical, stepsAmount } = - useContext(StepperContext); + const { + activeStep, + onChange, + vertical, + linear, + stepsAmount, + setActiveStepContent, + stepsValidity, + } = useContext(StepperContext); + + const isInvalid = useMemo(() => { + if (!linear) { + return false; + } + return ( + stepsValidity?.["step" + itemId].wasValidated && + !stepsValidity?.["step" + itemId].isValid + ); + }, [stepsValidity, itemId]); const animationDirection = useMemo(() => { return getTranslateDirection(activeStep, itemId); @@ -44,10 +62,11 @@ const TEStepperStep: React.FC = ({ }; const headIconClasses = useHeadIconClasses( + theme, + vertical, isActive, isCompleted, - theme, - vertical + isInvalid ); const stepperHeadClasses = clx(useHeadClasses(theme, itemId), headClassName); const stepperStepClasses = clx( @@ -68,6 +87,12 @@ const TEStepperStep: React.FC = ({ isActive ? "pb-6" : "pb-0" ); + useEffect(() => { + if (isActive && contentRef.current && setActiveStepContent) { + setActiveStepContent(contentRef.current); + } + }, [isActive, contentRef, children]); + const headClickHandler = () => { itemId != activeStep && onChange?.(itemId); }; @@ -75,7 +100,7 @@ const TEStepperStep: React.FC = ({ useStepperHeight(isActive, headRef, contentRef, vertical, children); return ( -
        • +
        • { const { stepperHeadIconHorizontal, stepperHeadIconVertical, stepperHeadIconActiveBg, stepperHeadIconCompletedBg, + stepperHeadIconInvalidBg, } = theme; const headIconTheme = vertical ? stepperHeadIconVertical : stepperHeadIconHorizontal; + if (isInvalid) { + return clsx(headIconTheme, stepperHeadIconInvalidBg); + } + if (isActive) { return clsx(headIconTheme, stepperHeadIconActiveBg); } diff --git a/src/lib/components/Stepper/hooks/useHorizontalStepperHeight.ts b/src/lib/components/Stepper/hooks/useHorizontalStepperHeight.ts index 801c91d..dfbfba3 100644 --- a/src/lib/components/Stepper/hooks/useHorizontalStepperHeight.ts +++ b/src/lib/components/Stepper/hooks/useHorizontalStepperHeight.ts @@ -15,7 +15,7 @@ const useStepperHeight = ( const headHeight = headRef.current?.offsetHeight || 0; const handleResize = (entries: Array) => { - if (!isActive) { + if (!isActive || !stepRef.current) { return; } const stepHeight = entries[0].contentRect.height; diff --git a/src/lib/components/Stepper/hooks/useStepperLinearValidation.ts b/src/lib/components/Stepper/hooks/useStepperLinearValidation.ts deleted file mode 100644 index 50d3f2e..0000000 --- a/src/lib/components/Stepper/hooks/useStepperLinearValidation.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { useContext, useEffect, useState, useMemo } from "react"; -import StepperContext from "../StepperContext"; - -export default function useStepperLinearValidaTion( - itemId: number, - contentRef: React.RefObject -) { - const { activeStep, linear, stepsAmount } = useContext(StepperContext); - const [isStepValid, setIsStepValid] = useState(true); - const isActive = useMemo(() => activeStep === itemId, [activeStep]); - useEffect(() => { - if (!linear || !isActive) return; - // const form = contentRef.current?.querySelector("form"); - // const inputs = form?.querySelectorAll("input, select, textarea"); - // const isFormValid = form?.checkValidity(); - // console.log("isFormValid", isFormValid); - // const isInputsValid = Array.from(inputs || []).every((input) => - // input.checkValidity() - // ); - // const isStepValid = isFormValid && isInputsValid; - // setIsStepValid(isStepValid); - - // if (!isStepValid) { - // form?.reportValidity(); - // } - }, [activeStep, linear, stepsAmount, isStepValid, isActive]); - return true; -} diff --git a/src/lib/components/Stepper/hooks/useVerticalStepHeight.ts b/src/lib/components/Stepper/hooks/useVerticalStepHeight.ts deleted file mode 100644 index 89a4bc8..0000000 --- a/src/lib/components/Stepper/hooks/useVerticalStepHeight.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { useState, useEffect, RefObject } from "react"; - -export default function useVerticalStepHeight( - isActive: boolean, - contentWrapperRef: RefObject, - children: React.ReactNode | React.ReactNode[] -) { - const [height, setHeight] = useState("0px"); - - useEffect(() => { - const observer = new ResizeObserver((entries) => { - if (!isActive) { - return setHeight("0px"); - } - const contentWrapperHeight = entries[0].contentRect.height; - const computed = window.getComputedStyle( - contentWrapperRef.current as Element - ); - - const offsetY = - parseFloat(computed.paddingTop) + - parseFloat(computed.paddingBottom) + - parseFloat(computed.marginBottom) + - parseFloat(computed.marginTop); - - setHeight(`${contentWrapperHeight + offsetY}px`); - }); - - observer.observe(contentWrapperRef.current as Element); - return () => { - observer.disconnect(); - }; - }, [isActive, children]); - - return height; -} diff --git a/src/lib/components/Stepper/types.ts b/src/lib/components/Stepper/types.ts index efc6324..4029e2f 100644 --- a/src/lib/components/Stepper/types.ts +++ b/src/lib/components/Stepper/types.ts @@ -6,16 +6,22 @@ interface StepperThemeProps { stepperVertical: string; } +type customValidationType = ( + validableElement: HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement +) => boolean; + type StepperProps = Omit & { activeStep?: number; defaultStep?: number; linear?: boolean; theme?: StepperThemeProps; type?: "horizontal" | "vertical"; - onChange?: (id: number) => void; + onChange?: (prevStepId: number, nextStepId: number) => void; + onInvalid?: (prevStepId: number, nextStepId: number) => void; children: | React.ReactElement[] | React.ReactElement; + customValidation?: customValidationType; }; -export type { StepperProps }; +export type { StepperProps, customValidationType }; diff --git a/src/lib/components/Stepper/utils/utils.ts b/src/lib/components/Stepper/utils/utils.ts index 3ce0979..c1b0751 100644 --- a/src/lib/components/Stepper/utils/utils.ts +++ b/src/lib/components/Stepper/utils/utils.ts @@ -1,3 +1,6 @@ +import type { customValidationType } from "../../Stepper/types"; +import type { StepsValidity } from "../Stepper"; + const getTranslateDirection = (activeStep: number, step: number) => { if (activeStep > step) { return "Left"; @@ -7,4 +10,77 @@ const getTranslateDirection = (activeStep: number, step: number) => { } }; -export { getTranslateDirection }; +const validateStepContent = ( + stepContent: HTMLElement, + customValidation?: customValidationType +): boolean => { + let isFormValid = true; + const validableElements: HTMLInputElement[] = Array.from( + stepContent.querySelectorAll( + "input[required], select[required], textarea[required]" + ) + ); + + if (customValidation) { + validableElements.forEach((el) => { + const isElementValid = customValidation(el); + if (!isElementValid) { + isFormValid = false; + } + }); + } + + // in this case we expect native-like validation, which means we'll stop the check + // after the first invalid element is found + if (!customValidation) { + validableElements.every((el) => { + const isElementValid = el.checkValidity(); + if (!isElementValid) { + isFormValid = false; + el.reportValidity(); + } + return isElementValid; + }); + } + + return isFormValid; +}; + +/** + * Checks the validity of steps between the active step and the target step. + * @return {boolean} Returns true if all the steps between the active step and the target step ID are valid, otherwise false + */ +const checkStepsBetweenValidity = ( + activeStep: number, + targetStepId: number, + stepsValidity: StepsValidity, + setStepsValidity: React.Dispatch> +) => { + if (activeStep > targetStepId) { + [activeStep, targetStepId] = [targetStepId, activeStep]; + } + + for (let step = activeStep + 1; step < targetStepId; step++) { + setStepsValidity((prev: StepsValidity) => { + return { + ...prev, + [`step${step}`]: { + ...prev[`step${step}`], + wasValidated: true, + }, + }; + }); + + if (!stepsValidity[`step${step}`].isValid) { + return false; + } + } + + return true; +}; + +export { + getTranslateDirection, + validateStepContent, + checkStepsBetweenValidity, +}; From 494f0b3546c4de3c2c8d79e4f6b9e43491c8d92a Mon Sep 17 00:00:00 2001 From: Mateusz Lazaru Date: Tue, 13 Feb 2024 11:16:55 +0100 Subject: [PATCH 13/19] fix(stepper) - fixed types in stepper, stepperTheme, stepperStepTheme --- .../Stepper/StepperStep/stepperStepTheme.ts | 44 +++++++++---------- src/lib/components/Stepper/stepperTheme.ts | 8 +++- src/lib/components/Stepper/types.ts | 8 +--- 3 files changed, 31 insertions(+), 29 deletions(-) diff --git a/src/lib/components/Stepper/StepperStep/stepperStepTheme.ts b/src/lib/components/Stepper/StepperStep/stepperStepTheme.ts index 92dc65b..3bcd87e 100644 --- a/src/lib/components/Stepper/StepperStep/stepperStepTheme.ts +++ b/src/lib/components/Stepper/StepperStep/stepperStepTheme.ts @@ -1,25 +1,25 @@ interface StepperStepThemeProps { - stepperStep: string; - stepperStepVertical: string; - stepperLastStepVertical: string; - disabledStep: string; - stepperHeadHorizontal: string; - stepperFirstStepHeadHorizontal: string; - stepperLastStepHeadHorizontal: string; - stepperHeadVertical: string; - stepperHeadIconHorizontal: string; - stepperHeadIconVertical: string; - stepperHeadIconActiveBg: string; - stepperHeadIconInvalidBg: string; - stepperHeadIconDisabledBg: string; - stepperHeadIconCompletedBg: string; - stepperHeadText: string; - stepperHeadTextActive: string; - stepperContent: string; - stepperContentTranslateLeft: string; - stepperContentTranslateRight: string; - stepperVerticalContent: string; - stepperContentWrapper: string; + stepperStep?: string; + stepperStepVertical?: string; + stepperLastStepVertical?: string; + disabledStep?: string; + stepperHeadHorizontal?: string; + stepperFirstStepHeadHorizontal?: string; + stepperLastStepHeadHorizontal?: string; + stepperHeadVertical?: string; + stepperHeadIconHorizontal?: string; + stepperHeadIconVertical?: string; + stepperHeadIconActiveBg?: string; + stepperHeadIconInvalidBg?: string; + stepperHeadIconDisabledBg?: string; + stepperHeadIconCompletedBg?: string; + stepperHeadText?: string; + stepperHeadTextActive?: string; + stepperContent?: string; + stepperContentTranslateLeft?: string; + stepperContentTranslateRight?: string; + stepperVerticalContent?: string; + stepperContentWrapper?: string; } const StepperStepTheme: StepperStepThemeProps = { @@ -59,7 +59,7 @@ const StepperStepTheme: StepperStepThemeProps = { stepperContentTranslateLeft: "-translate-x-[150%]", stepperContentTranslateRight: "translate-x-[150%]", stepperVerticalContent: - "transition-[height, margin-bottom, padding-top, padding-bottom] left-0 overflow-hidden pl-[3.75rem] pr-6 duration-300 ease-in-out", + "transition-[height,_margin-bottom,_padding-top,_padding-bottom] left-0 overflow-hidden pl-[3.75rem] pr-6 duration-300 ease-in-out", }; export default StepperStepTheme; diff --git a/src/lib/components/Stepper/stepperTheme.ts b/src/lib/components/Stepper/stepperTheme.ts index 1f5fd88..10be936 100644 --- a/src/lib/components/Stepper/stepperTheme.ts +++ b/src/lib/components/Stepper/stepperTheme.ts @@ -1,8 +1,14 @@ -const StepperTheme = { +interface StepperThemeProps { + stepperHorizontal?: string; + stepperVertical?: string; +} + +const StepperTheme: StepperThemeProps = { stepperHorizontal: "relative m-0 flex list-none justify-between overflow-hidden p-0 transition-[height] duration-200 ease-in-out", stepperVertical: "relative m-0 w-full list-none overflow-hidden p-0 transition-[height] duration-200 ease-in-out", }; +export type { StepperThemeProps }; export default StepperTheme; diff --git a/src/lib/components/Stepper/types.ts b/src/lib/components/Stepper/types.ts index 4029e2f..ee965fc 100644 --- a/src/lib/components/Stepper/types.ts +++ b/src/lib/components/Stepper/types.ts @@ -1,10 +1,6 @@ import { BaseComponent } from "../../types/baseComponent"; import type { StepperStepProps } from "./StepperStep/types"; - -interface StepperThemeProps { - stepper: string; - stepperVertical: string; -} +import type { StepperThemeProps } from "./stepperTheme"; type customValidationType = ( validableElement: HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement @@ -24,4 +20,4 @@ type StepperProps = Omit & { customValidation?: customValidationType; }; -export type { StepperProps, customValidationType }; +export type { StepperProps, StepperThemeProps, customValidationType }; From f53127a58f59e47aecc0f2cf395b6bb750c26779 Mon Sep 17 00:00:00 2001 From: Mateusz Lazaru Date: Tue, 13 Feb 2024 11:18:16 +0100 Subject: [PATCH 14/19] refactor(stepper) - minor refactoring, changed inline style to tailwind class --- .../components/Stepper/StepperStep/StepperStep.tsx | 14 +++++++------- src/lib/components/Stepper/hooks/useHeadClasses.ts | 12 +++++++++--- .../Stepper/hooks/useHorizontalStepperHeight.ts | 4 +++- 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/src/lib/components/Stepper/StepperStep/StepperStep.tsx b/src/lib/components/Stepper/StepperStep/StepperStep.tsx index 87e1e1d..0bc9746 100644 --- a/src/lib/components/Stepper/StepperStep/StepperStep.tsx +++ b/src/lib/components/Stepper/StepperStep/StepperStep.tsx @@ -12,8 +12,6 @@ import useHeadClasses from "../hooks/useHeadClasses"; const TEStepperStep: React.FC = ({ theme: customTheme, className, - contentClassName, - headClassName, itemId = 1, headIcon = "", headText = "", @@ -68,7 +66,7 @@ const TEStepperStep: React.FC = ({ isCompleted, isInvalid ); - const stepperHeadClasses = clx(useHeadClasses(theme, itemId), headClassName); + const stepperHeadClasses = clx(useHeadClasses(theme, itemId)); const stepperStepClasses = clx( vertical ? isLastStep @@ -80,10 +78,14 @@ const TEStepperStep: React.FC = ({ ); const dynamicAnimationDirection: string = `stepperContentTranslate${animationDirection}`; + const stepperContentWrapperClasses = clx( + theme.stepperContentWrapper, + isActive ? "visible" : "invisible", + vertical ? "grid" : "block" + ); const stepperContentClasses = clx( vertical ? theme.stepperVerticalContent : theme.stepperContent, !vertical && theme[dynamicAnimationDirection as keyof typeof theme], - contentClassName, isActive ? "pb-6" : "pb-0" ); @@ -117,11 +119,9 @@ const TEStepperStep: React.FC = ({
          {children} diff --git a/src/lib/components/Stepper/hooks/useHeadClasses.ts b/src/lib/components/Stepper/hooks/useHeadClasses.ts index 87edb5b..cd44ee9 100644 --- a/src/lib/components/Stepper/hooks/useHeadClasses.ts +++ b/src/lib/components/Stepper/hooks/useHeadClasses.ts @@ -14,8 +14,14 @@ export default function useHeadClasses( [itemId, stepsAmount] ); - if (vertical) return theme.stepperHeadVertical; - if (isFirstStep) return theme.stepperFirstStepHeadHorizontal; - if (isLastStep) return theme.stepperLastStepHeadHorizontal; + if (vertical) { + return theme.stepperHeadVertical; + } + if (isFirstStep) { + return theme.stepperFirstStepHeadHorizontal; + } + if (isLastStep) { + return theme.stepperLastStepHeadHorizontal; + } return theme.stepperHeadHorizontal; } diff --git a/src/lib/components/Stepper/hooks/useHorizontalStepperHeight.ts b/src/lib/components/Stepper/hooks/useHorizontalStepperHeight.ts index dfbfba3..dad07c0 100644 --- a/src/lib/components/Stepper/hooks/useHorizontalStepperHeight.ts +++ b/src/lib/components/Stepper/hooks/useHorizontalStepperHeight.ts @@ -11,7 +11,9 @@ const useStepperHeight = ( const { setStepperHeight } = useContext(StepperContext); useEffect(() => { - if (vertical) return; + if (vertical) { + return; + } const headHeight = headRef.current?.offsetHeight || 0; const handleResize = (entries: Array) => { From 571f2f716c3fc43a0a4990ccfbdab8b5f5991b48 Mon Sep 17 00:00:00 2001 From: Mateusz Lazaru Date: Tue, 13 Feb 2024 13:47:29 +0100 Subject: [PATCH 15/19] doc(stepper) - added stepper linear and linear with custom validation examples --- .../react/components/stepper/index-ss.html | 6 + .../docs/react/components/stepper/index.html | 321 +++++++++++++++++- 2 files changed, 323 insertions(+), 4 deletions(-) diff --git a/site/content/docs/react/components/stepper/index-ss.html b/site/content/docs/react/components/stepper/index-ss.html index faff06c..080959c 100644 --- a/site/content/docs/react/components/stepper/index-ss.html +++ b/site/content/docs/react/components/stepper/index-ss.html @@ -10,3 +10,9 @@
        • Vertical
        • +
        • + Linear +
        • +
        • + Linear with custom validation +
        • diff --git a/site/content/docs/react/components/stepper/index.html b/site/content/docs/react/components/stepper/index.html index 2d3c592..89ad83f 100644 --- a/site/content/docs/react/components/stepper/index.html +++ b/site/content/docs/react/components/stepper/index.html @@ -39,7 +39,7 @@ import React from "react"; import { TEStepper, TEStepperStep } from "tw-elements-react"; - export default function StepperBasicExample(): JSX.Element { + export default function StepperBasicExample() { return ( @@ -353,8 +353,8 @@ import React, { useState } from "react"; import { TEStepper, TEStepperStep } from "tw-elements-react"; - export default function StepperControlledStep(): JSX.Element { - const [activeStep, setActiveStep] = useState(1); + export default function StepperControlledStep() { + const [activeStep, setActiveStep] = useState(1); return ( @@ -435,6 +435,319 @@ + +
          + +

          + Linear stepper +

          + +

          + Add linear to enable step validation before proceeding to next step. +

          + + + {{< twsnippet/demo-iframe id="example4" iframe="/components/stepper/examples/stepper-linear-example" title="Stepper linear" >}} + + + +
          + + + +
          + +

          + Linear stepper with custom validation +

          + +

          + Use customValidation to enable custom step validation before proceeding to next step. +

          + + + {{< twsnippet/demo-iframe id="example5" iframe="/components/stepper/examples/stepper-linear-custom-validation-example" title="Stepper linear" >}} + + + +
          + +

          From c09a7b3360d3ed189f31b512d9bdd888bf08d31f Mon Sep 17 00:00:00 2001 From: Mateusz Lazaru Date: Thu, 15 Feb 2024 09:20:02 +0100 Subject: [PATCH 16/19] feat(stepper) - added 'noEditable' option --- .../pages/components/stepper/StepperPage.tsx | 11 +++++++ .../stepper/examples/StepperNoEditable.tsx | 32 +++++++++++++++++++ src/lib/components/Stepper/Stepper.tsx | 7 +++- src/lib/components/Stepper/StepperContext.ts | 2 ++ .../Stepper/StepperStep/StepperStep.tsx | 29 +++++++++++------ .../Stepper/hooks/useHeadIconClasses.ts | 9 +++++- 6 files changed, 78 insertions(+), 12 deletions(-) create mode 100644 src/demo/pages/components/stepper/examples/StepperNoEditable.tsx diff --git a/src/demo/pages/components/stepper/StepperPage.tsx b/src/demo/pages/components/stepper/StepperPage.tsx index 466766c..4db7b89 100644 --- a/src/demo/pages/components/stepper/StepperPage.tsx +++ b/src/demo/pages/components/stepper/StepperPage.tsx @@ -2,6 +2,7 @@ import React from "react"; import StepperControlledStep from "./examples/StepperControlledStep"; import StepperVerticalExample from "./examples/StepperVerticalExample"; import StepperBasicExample from "./examples/StepperBasicExample"; +import StepperNoEditable from "./examples/StepperNoEditable"; import StepperLinearExample from "./examples/StepperLinear"; import StepperLinearCustomValidationExample from "./examples/StepperLinearCustomValidation"; @@ -43,6 +44,16 @@ const StepperPage = () => {


          +

          + Stepper with no editable steps +

          + +
          + +
          + +
          +

          Linear stepper

          diff --git a/src/demo/pages/components/stepper/examples/StepperNoEditable.tsx b/src/demo/pages/components/stepper/examples/StepperNoEditable.tsx new file mode 100644 index 0000000..986b1a5 --- /dev/null +++ b/src/demo/pages/components/stepper/examples/StepperNoEditable.tsx @@ -0,0 +1,32 @@ +import React from "react"; +import { TEStepper, TEStepperStep } from "tw-elements-react"; + +export default function StepperNoEditable() { + return ( + + +

          + After changing the active step, the previous steps can't be accessed + anymore. +

          +
          + +

          + Lorem ipsum dolor, sit amet consectetur adipisicing elit. Pariatur, + tempora esse iusto tempore quod, aspernatur incidunt minima magnam, + quaerat aut vel expedita illum molestias repellendus asperiores id + suscipit saepe. Maxime. +

          +
          + +

          + Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam + voluptatum, quod, voluptate quia, ipsam illum quibusdam dolorum + voluptatibus laboriosam quae voluptates? Quisquam, voluptatibus + voluptas. Quisquam, voluptatibus voluptas. Quisquam, voluptatibus + voluptas. +

          +
          +
          + ); +} diff --git a/src/lib/components/Stepper/Stepper.tsx b/src/lib/components/Stepper/Stepper.tsx index 9a7cde3..33b82ad 100644 --- a/src/lib/components/Stepper/Stepper.tsx +++ b/src/lib/components/Stepper/Stepper.tsx @@ -20,6 +20,7 @@ const TEStepper: React.FC = ({ defaultStep = 1, activeStep: activeStepProp, children, + noEditable = false, onChange, onInvalid, type = "horizontal", @@ -67,6 +68,10 @@ const TEStepper: React.FC = ({ }); const onChangeHandler = (targetStepId: number) => { + if (noEditable && targetStepId < activeStep) { + return; + } + if (linear) { if (!activeStepContent) { return; @@ -120,10 +125,10 @@ const TEStepper: React.FC = ({ stepsValidity, setStepperHeight, setActiveStepContent, - vertical, stepsAmount: childrenArray.length, linear, + noEditable, }} >
            >; + noEditable?: boolean; } const StepperContext = createContext({ @@ -25,6 +26,7 @@ const StepperContext = createContext({ stepsAmount: 0, linear: false, setActiveStepContent: () => {}, + noEditable: false, }); export default StepperContext; diff --git a/src/lib/components/Stepper/StepperStep/StepperStep.tsx b/src/lib/components/Stepper/StepperStep/StepperStep.tsx index 0bc9746..63732a4 100644 --- a/src/lib/components/Stepper/StepperStep/StepperStep.tsx +++ b/src/lib/components/Stepper/StepperStep/StepperStep.tsx @@ -1,4 +1,4 @@ -import React, { useMemo, useRef, useContext, useEffect } from "react"; +import React, { useState, useMemo, useRef, useContext, useEffect } from "react"; import clx from "clsx"; import StepperStepTheme from "./stepperStepTheme"; import StepperContext from "../StepperContext"; @@ -18,11 +18,13 @@ const TEStepperStep: React.FC = ({ children, style, }) => { + const [isDisabled, setIsDisabled] = useState(false); const stepRef = useRef(null); const headRef = useRef(null); const contentRef = useRef(null); const { activeStep, + noEditable, onChange, vertical, linear, @@ -64,8 +66,21 @@ const TEStepperStep: React.FC = ({ vertical, isActive, isCompleted, - isInvalid + isInvalid, + isDisabled ); + + const headTextClasses = clx( + isActive ? theme.stepperHeadTextActive : theme.stepperHeadText, + isDisabled && theme.disabledStep + ); + + useEffect(() => { + if (isCompleted && noEditable && !isActive) { + setIsDisabled(true); + } + }, [isCompleted, noEditable, isActive]); + const stepperHeadClasses = clx(useHeadClasses(theme, itemId)); const stepperStepClasses = clx( vertical @@ -73,7 +88,7 @@ const TEStepperStep: React.FC = ({ ? theme.stepperLastStepVertical : theme.stepperStepVertical : theme.stepperStep, - + isDisabled && theme.disabledStep, className ); @@ -109,13 +124,7 @@ const TEStepperStep: React.FC = ({ ref={headRef} > {headIcon} - - {headText} - + {headText}
            { const { stepperHeadIconHorizontal, @@ -13,6 +14,7 @@ const useHeadIconClasses = ( stepperHeadIconActiveBg, stepperHeadIconCompletedBg, stepperHeadIconInvalidBg, + stepperHeadIconDisabledBg, } = theme; const headIconTheme = vertical @@ -26,6 +28,11 @@ const useHeadIconClasses = ( if (isActive) { return clsx(headIconTheme, stepperHeadIconActiveBg); } + + if (isDisabled) { + return clsx(headIconTheme, stepperHeadIconDisabledBg); + } + if (isCompleted) { return clsx(headIconTheme, stepperHeadIconCompletedBg); } From 7971810198ae351d551ff5df2855b6cae1a47949 Mon Sep 17 00:00:00 2001 From: Mateusz Lazaru Date: Thu, 15 Feb 2024 09:20:39 +0100 Subject: [PATCH 17/19] Revert "stepper - added 'noEditable' option. Updated styles." This reverts commit 5661ecd5401748f0a542d85774b65675e2306d60. --- .../pages/components/stepper/StepperPage.tsx | 11 - .../stepper/examples/StepperLinear.tsx | 196 ------------------ .../stepper/examples/StepperNoEditable.tsx | 32 --- src/lib/components/Stepper/Stepper.tsx | 9 +- src/lib/components/Stepper/StepperContext.ts | 2 - .../Stepper/StepperStep/StepperStep.tsx | 27 +-- .../Stepper/StepperStep/stepperStepTheme.ts | 15 +- .../Stepper/hooks/useHeadIconClasses.ts | 5 - src/lib/components/Stepper/types.ts | 1 - 9 files changed, 16 insertions(+), 282 deletions(-) delete mode 100644 src/demo/pages/components/stepper/examples/StepperLinear.tsx delete mode 100644 src/demo/pages/components/stepper/examples/StepperNoEditable.tsx diff --git a/src/demo/pages/components/stepper/StepperPage.tsx b/src/demo/pages/components/stepper/StepperPage.tsx index 44722bd..66df066 100644 --- a/src/demo/pages/components/stepper/StepperPage.tsx +++ b/src/demo/pages/components/stepper/StepperPage.tsx @@ -2,7 +2,6 @@ import React from "react"; import StepperControlledStep from "./examples/StepperControlledStep"; import StepperVerticalExample from "./examples/StepperVerticalExample"; import StepperBasicExample from "./examples/StepperBasicExample"; -import StepperNoEditable from "./examples/StepperNoEditable"; const StepperPage = () => { return ( @@ -39,16 +38,6 @@ const StepperPage = () => {
            - -
            - -

            - Stepper with no editable steps -

            - -
            - -
            ); diff --git a/src/demo/pages/components/stepper/examples/StepperLinear.tsx b/src/demo/pages/components/stepper/examples/StepperLinear.tsx deleted file mode 100644 index 6456c0d..0000000 --- a/src/demo/pages/components/stepper/examples/StepperLinear.tsx +++ /dev/null @@ -1,196 +0,0 @@ -import React, { useState } from "react"; -import { TEStepper, TEStepperStep, TEInput } from "tw-elements-react"; - -const validTheme = { - notchLeadingDefault: "border-green-300 dark:border-green-600", - notchMiddleDefault: "border-green-300 dark:border-green-600", - notchTrailingDefault: "border-green-300 dark:border-green-600", - focusedNotchLeadingDefault: - "shadow-[-1px_0_0_#10b339,_0_1px_0_0_#10b339,_0_-1px_0_0_#10b339] border-green-300 dark:border-green-600", - focusedNotchMiddleDefault: - "shadow-[0_1px_0_0_#10b339] border-green-300 dark:border-green-600", - focusedNotchTrailingDefault: - "shadow-[1px_0_0_#10b339,_0_1px_0_0_#10b339,_0_-1px_0_0_#10b339] border-green-300 dark:border-green-600", - labelDefault: "text-green-500 dark:text-green-600", -}; - -const invalidTheme = { - notchLeadingDefault: "border-red-300 dark:border-red-600", - notchMiddleDefault: "border-red-300 dark:border-red-600", - notchTrailingDefault: "border-red-300 dark:border-red-600", - focusedNotchLeadingDefault: - "shadow-[-1px_0_0_#f87171,_0_1px_0_0_#f87171,_0_-1px_0_0_#f87171] border-red-300 dark:border-red-600", - focusedNotchMiddleDefault: - "shadow-[0_1px_0_0_#f87171] border-red-300 dark:border-red-600", - focusedNotchTrailingDefault: - "shadow-[1px_0_0_#f87171,_0_1px_0_0_#f87171,_0_-1px_0_0_#f87171] border-red-300 dark:border-red-600", - labelDefault: "text-red-300 dark:text-red-600", -}; - -const isFormValid = (formValidity: object) => { - return Object.values(formValidity).every((item) => item); -}; - -export default function StepperLinearExample() { - const [activeStep, setActiveStep] = useState(1); - const [isStepValidated, setIsStepValidated] = useState(false); - - const [step1FormValidity, setStep1FormValidity] = useState({ - email: false, - }); - - const [step2FormValidity, setStep2FormValidity] = useState({ - firstName: false, - lastName: false, - phone: false, - }); - - const handleStepChange = (stepId: number) => { - setIsStepValidated(true); - if ( - (activeStep === 1 && !isFormValid(step1FormValidity)) || - (activeStep === 2 && !isFormValid(step2FormValidity)) - ) { - return; - } - - setActiveStep(stepId); - setIsStepValidated(false); - }; - - return ( - - -
            -

            - Enter your email and start the adventure of your life! -

            - -
            - { - setStep1FormValidity({ email: e.target.checkValidity() }); - }} - /> -
            - - -
            -
            - -
            -

            - You're almost there! Just a few more details! -

            -
            - { - setStep2FormValidity((prev: object) => { - return { ...prev, firstName: e.target.checkValidity() }; - }); - }} - /> -
            -
            - { - setStep2FormValidity((prev: object) => { - return { ...prev, lastName: e.target.checkValidity() }; - }); - }} - /> -
            - -
            - { - setStep2FormValidity((prev: object) => { - return { ...prev, phone: e.target.checkValidity() }; - }); - }} - /> -
            -
            - -
            - - - -
            -
            - -
            -

            - You're all set! We will contact you soon! -

            -
            -
            -
            - ); -} diff --git a/src/demo/pages/components/stepper/examples/StepperNoEditable.tsx b/src/demo/pages/components/stepper/examples/StepperNoEditable.tsx deleted file mode 100644 index 986b1a5..0000000 --- a/src/demo/pages/components/stepper/examples/StepperNoEditable.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import React from "react"; -import { TEStepper, TEStepperStep } from "tw-elements-react"; - -export default function StepperNoEditable() { - return ( - - -

            - After changing the active step, the previous steps can't be accessed - anymore. -

            -
            - -

            - Lorem ipsum dolor, sit amet consectetur adipisicing elit. Pariatur, - tempora esse iusto tempore quod, aspernatur incidunt minima magnam, - quaerat aut vel expedita illum molestias repellendus asperiores id - suscipit saepe. Maxime. -

            -
            - -

            - Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam - voluptatum, quod, voluptate quia, ipsam illum quibusdam dolorum - voluptatibus laboriosam quae voluptates? Quisquam, voluptatibus - voluptas. Quisquam, voluptatibus voluptas. Quisquam, voluptatibus - voluptas. -

            -
            -
            - ); -} diff --git a/src/lib/components/Stepper/Stepper.tsx b/src/lib/components/Stepper/Stepper.tsx index 33d0ecf..450ea46 100644 --- a/src/lib/components/Stepper/Stepper.tsx +++ b/src/lib/components/Stepper/Stepper.tsx @@ -12,7 +12,6 @@ const TEStepper: React.FC = ({ defaultStep = 1, activeStep: activeStepProp, children, - noEditable = false, onChange, type = "horizontal", style, @@ -37,8 +36,7 @@ const TEStepper: React.FC = ({ ) as React.ReactElement[]; }, [children]); - const stepChangeHandler = (id: number) => { - if (noEditable && id < activeStep) return; + const onChangeHandler = (id: number) => { onChange?.(id); setActiveStepState(id); }; @@ -47,13 +45,12 @@ const TEStepper: React.FC = ({
              = ({ itemId: index + 1, activeStep, key: "stepper-step-" + index, - onChange: stepChangeHandler, + onChange: onChangeHandler, }); })}
            diff --git a/src/lib/components/Stepper/StepperContext.ts b/src/lib/components/Stepper/StepperContext.ts index 784359a..7d112c4 100644 --- a/src/lib/components/Stepper/StepperContext.ts +++ b/src/lib/components/Stepper/StepperContext.ts @@ -8,7 +8,6 @@ interface StepperContextProps { setStepperHeight: (height: string) => void; vertical: boolean; stepsAmount: number; - noEditable?: boolean; } const StepperContext = createContext({ @@ -19,7 +18,6 @@ const StepperContext = createContext({ setStepperHeight: () => {}, vertical: false, stepsAmount: 0, - noEditable: false, }); export default StepperContext; diff --git a/src/lib/components/Stepper/StepperStep/StepperStep.tsx b/src/lib/components/Stepper/StepperStep/StepperStep.tsx index 52ae3c6..a2cf878 100644 --- a/src/lib/components/Stepper/StepperStep/StepperStep.tsx +++ b/src/lib/components/Stepper/StepperStep/StepperStep.tsx @@ -1,4 +1,4 @@ -import React, { useState, useMemo, useRef, useContext, useEffect } from "react"; +import React, { useMemo, useRef, useContext } from "react"; import clx from "clsx"; import StepperStepTheme from "./stepperStepTheme"; import StepperContext from "../StepperContext"; @@ -22,9 +22,8 @@ const TEStepperStep: React.FC = ({ style, }) => { const headRef = useRef(null); - const [isDisabled, setIsDisabled] = useState(false); const contentRef = useRef(null); - const { activeStep, noEditable, onChange, vertical, stepsAmount } = + const { activeStep, onChange, vertical, stepsAmount } = useContext(StepperContext); const animationDirection = useMemo(() => { @@ -48,22 +47,9 @@ const TEStepperStep: React.FC = ({ const headIconClasses = useHeadIconClasses( isActive, isCompleted, - isDisabled, theme, vertical ); - - const headTextClasses = clx( - isActive ? theme.stepperHeadTextActive : theme.stepperHeadText, - isDisabled && theme.disabledStep - ); - - useEffect(() => { - if (isCompleted && noEditable) { - setIsDisabled(true); - } - }, [isCompleted, noEditable]); - const stepperHeadClasses = clx(useHeadClasses(theme, itemId), headClassName); const stepperStepClasses = clx( vertical @@ -71,7 +57,6 @@ const TEStepperStep: React.FC = ({ ? theme.stepperLastStepVertical : theme.stepperStepVertical : theme.stepperStep, - isDisabled && theme.disabledStep, className ); @@ -102,7 +87,13 @@ const TEStepperStep: React.FC = ({ ref={headRef} > {headIcon} - {headText} + + {headText} +
            { @@ -12,7 +11,6 @@ const useHeadIconClasses = ( stepperHeadIconVertical, stepperHeadIconActiveBg, stepperHeadIconCompletedBg, - stepperHeadIconDisabledBg, } = theme; const headIconTheme = vertical @@ -22,9 +20,6 @@ const useHeadIconClasses = ( if (isActive) { return clsx(headIconTheme, stepperHeadIconActiveBg); } - if (isDisabled) { - return clsx(headIconTheme, stepperHeadIconDisabledBg); - } if (isCompleted) { return clsx(headIconTheme, stepperHeadIconCompletedBg); } diff --git a/src/lib/components/Stepper/types.ts b/src/lib/components/Stepper/types.ts index 7a5bc9c..d6a3cc2 100644 --- a/src/lib/components/Stepper/types.ts +++ b/src/lib/components/Stepper/types.ts @@ -9,7 +9,6 @@ interface StepperThemeProps { type StepperProps = Omit & { activeStep?: number; defaultStep?: number; - noEditable?: boolean; theme?: StepperThemeProps; type?: "horizontal" | "vertical"; onChange?: (id: number) => void; From 6e2612b0a65201294f0eb8cc0b78961dd2c05ff9 Mon Sep 17 00:00:00 2001 From: Mateusz Lazaru Date: Thu, 15 Feb 2024 09:31:49 +0100 Subject: [PATCH 18/19] stepper - added padding to stepperContentWrapper --- src/lib/components/Stepper/StepperStep/StepperStep.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lib/components/Stepper/StepperStep/StepperStep.tsx b/src/lib/components/Stepper/StepperStep/StepperStep.tsx index 63732a4..e5f7b78 100644 --- a/src/lib/components/Stepper/StepperStep/StepperStep.tsx +++ b/src/lib/components/Stepper/StepperStep/StepperStep.tsx @@ -96,12 +96,12 @@ const TEStepperStep: React.FC = ({ const stepperContentWrapperClasses = clx( theme.stepperContentWrapper, isActive ? "visible" : "invisible", - vertical ? "grid" : "block" + vertical ? "grid" : "block", + isActive ? "pb-6" : "pb-0" ); const stepperContentClasses = clx( vertical ? theme.stepperVerticalContent : theme.stepperContent, - !vertical && theme[dynamicAnimationDirection as keyof typeof theme], - isActive ? "pb-6" : "pb-0" + !vertical && theme[dynamicAnimationDirection as keyof typeof theme] ); useEffect(() => { From d224658537133f52b334f8957397d0a62b144d9b Mon Sep 17 00:00:00 2001 From: Mateusz Lazaru Date: Thu, 15 Feb 2024 10:50:50 +0100 Subject: [PATCH 19/19] doc(stepper) - added noEditable demo, updated api section --- .../docs/react/components/stepper/a.html | 181 +++++++++++++----- .../react/components/stepper/index-ss.html | 3 + .../docs/react/components/stepper/index.html | 69 ++++++- site/static/search-react.json | 6 + .../pages/components/stepper/exampleList.tsx | 6 + .../components/Stepper/StepperStep/types.ts | 2 - src/lib/components/Stepper/types.ts | 4 +- 7 files changed, 216 insertions(+), 55 deletions(-) diff --git a/site/content/docs/react/components/stepper/a.html b/site/content/docs/react/components/stepper/a.html index becbf99..9fab41e 100644 --- a/site/content/docs/react/components/stepper/a.html +++ b/site/content/docs/react/components/stepper/a.html @@ -95,6 +95,24 @@ In most cases the value should be managed with onChange handler. + + + customValidation + + + (validableElement: HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement) => boolean; + + + - + + + Sets custom validation for each validable element in linear stepper. + + + + + linear + + + bolean + + + false + + + Linear stepper prevents going back to the previous step after it was completed. + + + + + type + + + 'vertical' | 'horizontal' + + + 'horizontal' + + + Sets stepper view mode. + +
            @@ -171,42 +225,6 @@ - - - contentClassName - - - String - - - '' - - - Adds custom classes to the content wrapper. - - - - - headClassName - - - String - - - '' - - - Adds custom classes to the step head. - - stepperHorizontal - "relative m-0 flex list-none justify-between overflow-hidden p-0 transition-[height] duration-200 ease-in-out", + relative m-0 flex list-none justify-between overflow-hidden p-0 transition-[height] duration-200 ease-in-out Sets styles to the TEStepper element in horizontal mode. @@ -330,7 +348,7 @@ stepperVertical - "relative m-0 w-full list-none overflow-hidden p-0 transition-[height] duration-200 ease-in-out", + relative m-0 w-full list-none overflow-hidden p-0 transition-[height] duration-200 ease-in-out Sets styles to the TEStepper element in vertical mode. @@ -406,6 +424,17 @@ Sets styles to the last stepperStep element in vertical mode. + + + disabledStep + + + pointer-events-none + + + Sets styles to disabled step in noEditable mode. + + stepperHeadHorizontal @@ -455,7 +484,7 @@ stepperHeadIconHorizontal - my-6 mr-2 flex h-[1.938rem] w-[1.938rem] items-center justify-center rounded-full bg-[#ebedef] text-sm font-medium text-[#40464f] + my-6 mr-2 flex h-[1.938rem] w-[1.938rem] items-center justify-center rounded-full bg-[#6d6d6d] text-sm font-medium text-[#fff] dark:bg-[#757575] Sets styles to the stepperHeadIcon element in horizontal mode. @@ -466,7 +495,7 @@ stepperHeadIconVertical - mr-3 flex h-[1.938rem] w-[1.938rem] items-center justify-center rounded-full bg-[#ebedef] text-sm font-medium text-[#40464f] + mr-3 flex h-[1.938rem] w-[1.938rem] items-center justify-center rounded-full bg-[#6d6d6d] text-sm font-medium text-[#fff] dark:bg-[#757575] Sets styles to the stepperHeadIcon element in vertical mode. @@ -477,7 +506,7 @@ stepperHeadIconCompletedBg - !bg-success-100 !text-success-700 + !bg-success-100 !text-success-700 dark:!bg-[#04201f] dark:!text-[#72c894] Sets background color applied to the stepperHeadIcon element when step is completed. @@ -488,10 +517,32 @@ stepperHeadIconActiveBg - !bg-primary-100 !text-primary-700 + !bg-primary-100 !text-primary-700 dark:!bg-[#0c1728] dark:!text-[#628dd5] - Sets background color applied to the stepperHeadIcon element when step is active. + Sets background color applied to the stepperHeadIcon element when the step is active. + + + + + stepperHeadIconInvalidBg + + + !bg-danger-100 !text-danger-700 dark:!bg-[#2c0f14] dark:!text-[#e37083] + + + Sets background color applied to the stepperHeadIcon element when the step is invalid. + + + + + stepperHeadIconDisabledBg + + + !bg-[#6d6d6d] !text-neutral-300 dark:!bg-[#757575] + + + Sets background color applied to the stepperHeadIcon element when the step is disabled. @@ -543,7 +594,7 @@ stepperVerticalContent - transition-[height, margin-bottom, padding-top, padding-bottom] left-0 overflow-hidden pb-6 pl-[3.75rem] pr-6 duration-300 ease-in-out + transition-[height,_margin-bottom,_padding-top,_padding-bottom] left-0 overflow-hidden pl-[3.75rem] pr-6 duration-300 ease-in-out Sets styles to the stepperContent element in vertical mode. @@ -603,7 +654,17 @@ - Event type + Name + + + Type + + + Default Description @@ -611,12 +672,38 @@ + + onChange + + + (prevStepId: number, targetStepId: number) => void; + + + - + + + Event fires when the stepper demands to change step. + + + + + onInvalid + - onChange?: (id: number) => void + (prevStepId: number, targetStepId: number) => void; + + + - - Fired when stepper demands to change the active step. It can be triggered in TEStepper element. + Event fires when the stepper demands to change step, but linear option prevents it. diff --git a/site/content/docs/react/components/stepper/index-ss.html b/site/content/docs/react/components/stepper/index-ss.html index 080959c..5238d28 100644 --- a/site/content/docs/react/components/stepper/index-ss.html +++ b/site/content/docs/react/components/stepper/index-ss.html @@ -10,6 +10,9 @@
          • Vertical
          • +
          • + No editable +
          • Linear
          • diff --git a/site/content/docs/react/components/stepper/index.html b/site/content/docs/react/components/stepper/index.html index 89ad83f..7128596 100644 --- a/site/content/docs/react/components/stepper/index.html +++ b/site/content/docs/react/components/stepper/index.html @@ -435,6 +435,67 @@ + +
            + +

            + No editable stepper +

            + +

            + Add noEditable to disable editing a completed step. +

            + + + {{< twsnippet/demo-iframe id="example4" iframe="/components/stepper/examples/stepper-no-editable-example" title="Stepper no editable" >}} + + + +
            + +
            @@ -450,10 +511,10 @@

            - {{< twsnippet/demo-iframe id="example4" iframe="/components/stepper/examples/stepper-linear-example" title="Stepper linear" >}} + {{< twsnippet/demo-iframe id="example5" iframe="/components/stepper/examples/stepper-linear-example" title="Stepper linear" >}} -