From 2af9d8f0c2de091dba55a748ec86b2c5601012b4 Mon Sep 17 00:00:00 2001 From: John Fu Date: Tue, 21 May 2024 11:42:40 +1000 Subject: [PATCH 1/6] feat: support sidebar property --- src/types.ts | 4 +++- src/wizard.tsx | 9 ++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/types.ts b/src/types.ts index a54d06b..120e506 100644 --- a/src/types.ts +++ b/src/types.ts @@ -3,12 +3,14 @@ export type Handler = (() => Promise) | (() => void) | null; export type WizardProps = { /** Optional header that is shown above the active step */ header?: React.ReactNode; + /** Optional sidebar that is shown before the active step */ + sidebar?: React.ReactNode; /** Optional footer that is shown below the active step */ footer?: React.ReactNode; /** Optional start index @default 0 */ startIndex?: number; /** - * Optional wrapper that is exclusively wrapped around the active step component. It is not wrapped around the `header` and `footer` + * Optional wrapper that is exclusively wrapped around the active step component. It is not wrapped around the `header`, `sidebar` and `footer` * @example With `framer-motion` - `` * ```jsx * }> diff --git a/src/wizard.tsx b/src/wizard.tsx index 43260ed..d5f11e3 100644 --- a/src/wizard.tsx +++ b/src/wizard.tsx @@ -7,6 +7,7 @@ import WizardContext from './wizardContext'; const Wizard: React.FC> = React.memo( ({ header, + sidebar, footer, children, onStepChange, @@ -126,6 +127,11 @@ const Wizard: React.FC> = React.memo( if (header && !React.isValidElement(header)) { logger.log('error', 'Invalid header passed to '); } + // Invalid sidebar element + if (sidebar && !React.isValidElement(sidebar)) { + logger.log('error', 'Invalid sidebar passed to '); + } + // Invalid footer element if (footer && !React.isValidElement(footer)) { logger.log('error', 'Invalid footer passed to '); @@ -133,7 +139,7 @@ const Wizard: React.FC> = React.memo( } return reactChildren[activeStep]; - }, [activeStep, children, header, footer]); + }, [activeStep, children, header, sidebar, footer]); const enhancedActiveStepContent = React.useMemo( () => @@ -146,6 +152,7 @@ const Wizard: React.FC> = React.memo( return ( {header} + {sidebar} {enhancedActiveStepContent} {footer} From e82abff2384459f18f5b5135fbff2996a8db6090 Mon Sep 17 00:00:00 2001 From: John Fu Date: Tue, 21 May 2024 11:43:12 +1000 Subject: [PATCH 2/6] feat: expose Step Names --- src/types.ts | 9 +++++++++ src/wizard.tsx | 16 +++++++++++++++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/types.ts b/src/types.ts index 120e506..0186268 100644 --- a/src/types.ts +++ b/src/types.ts @@ -23,6 +23,11 @@ export type WizardProps = { onStepChange?: (stepIndex: number) => void; }; +export type StepName = { + number: number; + name: string; +}; + export type WizardValues = { /** * Go to the next step @@ -57,6 +62,10 @@ export type WizardValues = { isFirstStep: boolean; /** Indicate if the current step is the last step (aka no next step) */ isLastStep: boolean; + /** The labels of each step */ + stepNames: ({ + name: any; + } | null)[]; }; /** Console log levels */ diff --git a/src/wizard.tsx b/src/wizard.tsx index d5f11e3..2d0d149 100644 --- a/src/wizard.tsx +++ b/src/wizard.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import * as logger from './logger'; -import { Handler, WizardProps } from './types'; +import { Handler, StepName, WizardProps } from './types'; import WizardContext from './wizardContext'; const Wizard: React.FC> = React.memo( @@ -20,6 +20,18 @@ const Wizard: React.FC> = React.memo( const hasPreviousStep = React.useRef(false); const nextStepHandler = React.useRef(() => {}); const stepCount = React.Children.toArray(children).length; + const stepsArray = React.Children.toArray(children); + const stepNames = stepsArray + .map((child) => { + if (React.isValidElement(child)) { + const number = child.props.number; + const name = child.props.name || `Step ${number}`; + + return { name, number }; + } + return null; + }) + .filter(Boolean) as StepName[]; hasNextStep.current = activeStep < stepCount - 1; hasPreviousStep.current = activeStep > 0; @@ -97,6 +109,7 @@ const Wizard: React.FC> = React.memo( isFirstStep: !hasPreviousStep.current, isLastStep: !hasNextStep.current, goToStep, + stepNames, }), [ doNextStep, @@ -105,6 +118,7 @@ const Wizard: React.FC> = React.memo( activeStep, stepCount, goToStep, + stepNames, ], ); From dab1824ffaaf4085ecc677fb3e7f57b99f21e304 Mon Sep 17 00:00:00 2001 From: John Fu Date: Tue, 21 May 2024 11:43:46 +1000 Subject: [PATCH 3/6] feat: add sidebarAndStepWrapper prop Gives ability to add a wrapper around the sidebar and the active step. Useful for styling. --- src/types.ts | 14 +++++++++++--- src/wizard.tsx | 27 +++++++++++++++++++++++++-- 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/src/types.ts b/src/types.ts index 0186268..45c4502 100644 --- a/src/types.ts +++ b/src/types.ts @@ -19,6 +19,16 @@ export type WizardProps = { * ``` */ wrapper?: React.ReactElement; + /** + * Optional wrapper that is exclusively wrapped around the sidebar and active step component. It is not wrapped around the `header` and `footer` + * @example With `framer-motion` - `` + * ```jsx + * }> + * ... + * + * ``` + */ + sidebarAndStepWrapper?: React.ReactElement; /** Callback that will be invoked with the new step index when the wizard changes steps */ onStepChange?: (stepIndex: number) => void; }; @@ -63,9 +73,7 @@ export type WizardValues = { /** Indicate if the current step is the last step (aka no next step) */ isLastStep: boolean; /** The labels of each step */ - stepNames: ({ - name: any; - } | null)[]; + stepNames: StepName[]; }; /** Console log levels */ diff --git a/src/wizard.tsx b/src/wizard.tsx index 2d0d149..6ab1a3b 100644 --- a/src/wizard.tsx +++ b/src/wizard.tsx @@ -13,6 +13,7 @@ const Wizard: React.FC> = React.memo( onStepChange, wrapper: Wrapper, startIndex = 0, + sidebarAndStepWrapper: SidebarAndStepWrapper, }) => { const [activeStep, setActiveStep] = React.useState(startIndex); const [isLoading, setIsLoading] = React.useState(false); @@ -163,11 +164,33 @@ const Wizard: React.FC> = React.memo( [Wrapper, activeStepContent], ); + const enhancedActiveStepContentWithSidebar = React.useMemo( + () => + SidebarAndStepWrapper ? ( + React.cloneElement(SidebarAndStepWrapper, { + children: ( + <> + {sidebar} + {enhancedActiveStepContent} + + ), + }) + ) : ( + <> + {sidebar} + {enhancedActiveStepContent} + + ), + [SidebarAndStepWrapper, sidebar, enhancedActiveStepContent], + ); + return ( {header} - {sidebar} - {enhancedActiveStepContent} + {sidebar + ? enhancedActiveStepContentWithSidebar + : enhancedActiveStepContent} + {footer} ); From 40fc02ff528ee1f266fc99e9f0f53c74d4358b83 Mon Sep 17 00:00:00 2001 From: John Fu Date: Tue, 21 May 2024 11:54:45 +1000 Subject: [PATCH 4/6] feat: implement Sidebar usage on Simple playground example. --- playground/components/asyncStep.tsx | 1 + playground/components/sidebar.tsx | 53 ++++++++++++++++++++++ playground/components/step.tsx | 1 + playground/modules/wizard/simple/index.tsx | 22 +++++++-- 4 files changed, 73 insertions(+), 4 deletions(-) create mode 100644 playground/components/sidebar.tsx diff --git a/playground/components/asyncStep.tsx b/playground/components/asyncStep.tsx index 37cc2ad..9bfd0b1 100644 --- a/playground/components/asyncStep.tsx +++ b/playground/components/asyncStep.tsx @@ -6,6 +6,7 @@ import { useMockMutation } from '../hooks'; type Props = { number: number; + name?: string; }; const MOCK = [ diff --git a/playground/components/sidebar.tsx b/playground/components/sidebar.tsx new file mode 100644 index 0000000..1707948 --- /dev/null +++ b/playground/components/sidebar.tsx @@ -0,0 +1,53 @@ +import { styled } from 'goober'; +import * as React from 'react'; + +import { useWizard } from '../../dist'; +import { Button } from '../modules/common'; + +export const Nav = styled('nav')` + display: flex; + justify-content: center; + flex-direction: column; + gap: 0; + + & > ul { + list-style: none; + margin: 0; + padding: 0; + } + + @media screen and (min-width: 768px) { + flex-direction: row; + gap: 1rem; + + & > p { + margin: initial; + } + } +`; + +const Sidebar: React.FC = () => { + const { activeStep, stepCount, goToStep, stepNames } = useWizard(); + + return ( + + ); +}; + +export default Sidebar; diff --git a/playground/components/step.tsx b/playground/components/step.tsx index 70a7caf..cf91dd2 100644 --- a/playground/components/step.tsx +++ b/playground/components/step.tsx @@ -6,6 +6,7 @@ import { useWizard } from '../../dist'; type Props = { number: number; withCallback?: boolean; + name?: string; }; const Container = styled('div')` diff --git a/playground/modules/wizard/simple/index.tsx b/playground/modules/wizard/simple/index.tsx index e4973fc..00b34b8 100644 --- a/playground/modules/wizard/simple/index.tsx +++ b/playground/modules/wizard/simple/index.tsx @@ -1,20 +1,34 @@ +import { styled } from 'goober'; import * as React from 'react'; import { Wizard } from '../../../../dist'; import { AsyncStep, Footer, Step } from '../../../components'; +import Sidebar from '../../../components/sidebar'; import Section from '../../common/section'; +const Flex = styled('div')` + display: flex; + width: 100%; + gap: 1rem; + + & > :nth-child(2) { + flex-grow: 1; + } +`; + const SimpleSection: React.FC = () => { return (
} + sidebar={} onStepChange={(stepIndex) => alert(`New step index is ${stepIndex}`)} + sidebarAndStepWrapper={} > - - - - + + + +
); From 74129d7ad1473a452ade0b874f87eaa577ef9595 Mon Sep 17 00:00:00 2001 From: John Fu Date: Tue, 21 May 2024 12:09:55 +1000 Subject: [PATCH 5/6] docs: update readme --- README.md | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 2890401..9bee2e1 100644 --- a/README.md +++ b/README.md @@ -83,9 +83,11 @@ Example: pass a footer component that contains a "previous" and "next" button to | ---------- | --------------- | ---------------------------------------------------------- | -------- | ------- | | startIndex | number | Indicate the wizard to start at the given step | ❌ | 0 | | header | React.ReactNode | Header that is shown above the active step | ❌ | | +| sidebar | React.ReactNode | Sidebar that is shown after the header, before the active step | ❌ | | | footer | React.ReactNode | Footer that is shown below the active stepstep | ❌ | | | onStepChange | (stepIndex) | Callback that will be invoked with the new step index when the wizard changes steps | ❌ | | - | wrapper | React.React.ReactElement | Optional wrapper that is exclusively wrapped around the active step component. It is not wrapped around the `header` and `footer` | ❌ | | +| wrapper | React.React.ReactElement | Optional wrapper that is exclusively wrapped around the active step component. It is not wrapped around the `header`, `sidebar` and `footer` | ❌ | | +| sidebarAndStepWrapper | React.React.ReactElement | Optional wrapper that is exclusively wrapped around the sidebar and active step component. It is not wrapped around the `header` and `footer` | ❌ | | | children | React.ReactNode | Each child component will be treated as an individual step | ✔️ | #### Example @@ -94,19 +96,27 @@ Example: pass a footer component that contains a "previous" and "next" button to // Example: show the active step in the header const Header = () =>

I am the header component

; +// Example: show the a sidebar +const Sidebar = () =>

I am the sidebar component

; + // Example: show the "previous" and "next" buttons in the footer const Footer = () =>

I am the footer component

; -// Example: Wrap steps in an ` ; +// Example: Wrap sidebar and steps in a Flexbox +const SidebarAndStepWrapper = () =>
; + const App = () => { return ( - } + sidebar={} footer={