From d0702213c372854325e14314ab187c2b9b80ea6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Herv=C3=A9=20Dombya?= <135591453+hervedombya@users.noreply.github.com> Date: Wed, 8 Nov 2023 13:47:28 +0100 Subject: [PATCH] Stepper Component Add Stepper component to library. Add wrapper component to Stepper Add empty fragment to Stepper component. Refactor Stepper component to use Box instead of Wrap. Add gap between stepper components Update Stepper component and add Stepper stories Add padding to Stepper component and update Storybook metadata. --- .../components/steppers/Stepper.component.tsx | 80 +++++++++++ .../steppers/Steppers.component.tsx | 4 +- src/lib/components/steppers/types.ts | 48 +++++++ src/lib/index.ts | 1 + stories/stepper.stories.tsx | 124 ++++++++++++++++++ 5 files changed, 255 insertions(+), 2 deletions(-) create mode 100644 src/lib/components/steppers/Stepper.component.tsx create mode 100644 src/lib/components/steppers/types.ts create mode 100644 stories/stepper.stories.tsx diff --git a/src/lib/components/steppers/Stepper.component.tsx b/src/lib/components/steppers/Stepper.component.tsx new file mode 100644 index 0000000000..547ddcc13c --- /dev/null +++ b/src/lib/components/steppers/Stepper.component.tsx @@ -0,0 +1,80 @@ +import { createContext, useContext, useState } from 'react'; +import { + Add, + ExctractProps, + StepperContextType, + Steps, + Subtract, +} from './types'; +import { Steppers } from './Steppers.component'; +import { Box } from '../box/Box'; + +const StepperContext = createContext(null); + +export const useStepper = < + T extends any[], + StepIndex extends number, + NextIndex = Add, + PrevIndex = Subtract, +>( + index: StepIndex, + steps: readonly [...Steps], +): (NextIndex extends number + ? { + next: (props: ExctractProps) => void; + } + : Record) & + (PrevIndex extends -1 + ? Record + : PrevIndex extends number + ? { + prev: (props: ExctractProps) => void; + } + : Record) => { + const context = useContext(StepperContext); + + if (context === null) { + throw new Error('Cannot use useStepper outside of Stepper'); + } + const { next, prev } = context; + + //@ts-expect-error generic type + return { next, prev }; +}; + +export const Stepper = ({ + steps, +}: { + steps: readonly [...Steps]; +}) => { + const [currentStep, setCurrentStep] = useState(0); + const [stepProps, setStepProps] = useState>({}); + + const next = (props: Record) => { + setCurrentStep(currentStep + 1); + setStepProps(props); + }; + + const prev = (props: Record) => { + setCurrentStep(currentStep - 1); + setStepProps(props); + }; + + const { Component } = steps[currentStep]; + + return ( + + + { + return { + title: step.label, + }; + })} + /> + + + + ); +}; diff --git a/src/lib/components/steppers/Steppers.component.tsx b/src/lib/components/steppers/Steppers.component.tsx index 12856032da..a537e688d7 100644 --- a/src/lib/components/steppers/Steppers.component.tsx +++ b/src/lib/components/steppers/Steppers.component.tsx @@ -6,8 +6,8 @@ import { Loader } from '../loader/Loader.component'; import { getTheme, getThemePropSelector } from '../../utils'; import { Icon } from '../icon/Icon.component'; type StepProps = { - title: JSX.Element; - content: JSX.Element; + title: React.ReactNode; + content?: React.ReactNode; active?: boolean; completed?: boolean; isLast?: boolean; diff --git a/src/lib/components/steppers/types.ts b/src/lib/components/steppers/types.ts new file mode 100644 index 0000000000..13170ad460 --- /dev/null +++ b/src/lib/components/steppers/types.ts @@ -0,0 +1,48 @@ +import { ReactNode } from 'react'; + +type MAXIMUM_DEPTH = 20; + +type GetResults = T extends Step ? Step : never; + +type Length = T extends { length: infer L } ? L : never; + +type BuildTuple = T extends { + length: L; +} + ? T + : BuildTuple; + +export interface Step { + label: string; + Component: (args: T) => ReactNode; +} + +export interface StepperContextType { + next: (props: Record) => void; + prev: (props: Record) => void; +} + +export declare type Steps< + T extends any[], + Result extends any[] = [], + Depth extends ReadonlyArray = [], +> = Depth['length'] extends MAXIMUM_DEPTH + ? Step[] + : T extends [] + ? [] + : T extends [infer Head] + ? [...Result, GetResults] + : T extends [infer Head, ...infer Tail] + ? Steps<[...Tail], [...Result, GetResults], [...Depth, 1]> + : unknown[] extends T + ? T + : never; + +export type Add = Length< + [...BuildTuple, ...BuildTuple] +>; + +export type Subtract = + BuildTuple extends [...infer U, ...BuildTuple] ? Length : -1; + +export type ExctractProps = T extends Step ? Props : never; diff --git a/src/lib/index.ts b/src/lib/index.ts index 0d42d4b28f..0b497b67e8 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -69,3 +69,4 @@ export { Dropzone } from './components/dropzone/Dropzone'; export { Toast } from './components/toast/Toast.component'; export { ToastProvider, useToast } from './components/toast/ToastProvider'; export { useMutationsHandler } from './components/toast/useMutationsHandler'; +export { Stepper } from './components/steppers/Stepper.component'; diff --git a/stories/stepper.stories.tsx b/stories/stepper.stories.tsx new file mode 100644 index 0000000000..2ca1667cbd --- /dev/null +++ b/stories/stepper.stories.tsx @@ -0,0 +1,124 @@ +import React from 'react'; +import { + Stepper, + useStepper, +} from '../src/lib/components/steppers/Stepper.component'; +import styled from 'styled-components'; +import { Box } from '../src/lib/components/box/Box'; +import { Stack } from '../src/lib/spacing'; +import { Button } from '../src/lib/components/buttonv2/Buttonv2.component'; +import { Text } from '../src/lib/components/text/Text.component'; +import { Wrapper as StoryWrapper } from './common'; +import type { Meta, StoryObj } from '@storybook/react'; + +const Wrapper = styled.div` + display: flex; + flex-direction: column; + align-items: stretch; + height: 100%; +`; + +const FirstStepComponent = (props: Record) => { + const { next } = useStepper(StepIndexes.Step1, STEPS); + return ( + + + + First Step +