diff --git a/README.md b/README.md
index 2890401..d9c6de3 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={}
wrapper={}
+ sidebarAndStepWrapper={}
>
@@ -137,6 +147,7 @@ Used to retrieve all methods and properties related to your wizard. Make sure `W
| stepCount | number | The total number of steps of the wizard |
| isFirstStep | boolean | Indicate if the current step is the first step (aka no previous step) |
| isLastStep | boolean | Indicate if the current step is the last step (aka no next step) |
+| steps | { name?: string; number?: string }[] | An array of objects for each step in the wizard. Each object has a `name` and `number` property corresponding to the step's name and number. |
| |
#### Example
diff --git a/playground/components/asyncStep.tsx b/playground/components/asyncStep.tsx
index 37cc2ad..ce74783 100644
--- a/playground/components/asyncStep.tsx
+++ b/playground/components/asyncStep.tsx
@@ -2,10 +2,11 @@ import { styled } from 'goober';
import * as React from 'react';
import { useWizard } from '../../dist';
+import { BaseWizardStep } from '../../dist/types';
import { useMockMutation } from '../hooks';
-type Props = {
- number: number;
+type Props = BaseWizardStep & {
+ content: string;
};
const MOCK = [
diff --git a/playground/components/sidebar.tsx b/playground/components/sidebar.tsx
new file mode 100644
index 0000000..197f943
--- /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, steps } = useWizard();
+
+ return (
+
+ );
+};
+
+export default Sidebar;
diff --git a/playground/components/step.tsx b/playground/components/step.tsx
index 70a7caf..64c9b64 100644
--- a/playground/components/step.tsx
+++ b/playground/components/step.tsx
@@ -2,9 +2,9 @@ import { styled } from 'goober';
import * as React from 'react';
import { useWizard } from '../../dist';
+import { BaseWizardStep } from '../../dist/types';
-type Props = {
- number: number;
+type Props = BaseWizardStep & {
withCallback?: boolean;
};
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={}
>
-
-
-
-
+
+
+
+
);
diff --git a/src/index.ts b/src/index.ts
index be42baa..0629d7a 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,5 +1,6 @@
// Type re-export workaround, to stay compatible with TS 3.7 and lower
import {
+ BaseWizardStep as _BaseWizardStep,
Handler as _Handler,
WizardProps as _WizardProps,
WizardValues as _WizardValues,
@@ -10,5 +11,6 @@ import { default as Wizard } from './wizard';
export type WizardProps = _WizardProps;
export type WizardValues = _WizardValues;
export type Handler = _Handler;
+export interface BaseWizardStep extends _BaseWizardStep {}
export { Wizard, useWizard };
diff --git a/src/types.ts b/src/types.ts
index a54d06b..58b57ae 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
* }>
@@ -17,10 +19,27 @@ 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;
};
+export interface BaseWizardStep {
+ /** The step number */
+ number?: number;
+ /** The step name */
+ name?: string;
+}
+
export type WizardValues = {
/**
* Go to the next step
@@ -55,6 +74,8 @@ 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 */
+ steps: BaseWizardStep[];
};
/** Console log levels */
diff --git a/src/wizard.tsx b/src/wizard.tsx
index 43260ed..0961b57 100644
--- a/src/wizard.tsx
+++ b/src/wizard.tsx
@@ -1,17 +1,19 @@
import * as React from 'react';
import * as logger from './logger';
-import { Handler, WizardProps } from './types';
+import { BaseWizardStep, Handler, WizardProps } from './types';
import WizardContext from './wizardContext';
const Wizard: React.FC> = React.memo(
({
header,
+ sidebar,
footer,
children,
onStepChange,
wrapper: Wrapper,
startIndex = 0,
+ sidebarAndStepWrapper: SidebarAndStepWrapper,
}) => {
const [activeStep, setActiveStep] = React.useState(startIndex);
const [isLoading, setIsLoading] = React.useState(false);
@@ -19,6 +21,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 steps = 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 BaseWizardStep[];
hasNextStep.current = activeStep < stepCount - 1;
hasPreviousStep.current = activeStep > 0;
@@ -96,6 +110,7 @@ const Wizard: React.FC> = React.memo(
isFirstStep: !hasPreviousStep.current,
isLastStep: !hasNextStep.current,
goToStep,
+ steps,
}),
[
doNextStep,
@@ -104,6 +119,7 @@ const Wizard: React.FC> = React.memo(
activeStep,
stepCount,
goToStep,
+ steps,
],
);
@@ -126,6 +142,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 +154,7 @@ const Wizard: React.FC> = React.memo(
}
return reactChildren[activeStep];
- }, [activeStep, children, header, footer]);
+ }, [activeStep, children, header, sidebar, footer]);
const enhancedActiveStepContent = React.useMemo(
() =>
@@ -143,10 +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}
- {enhancedActiveStepContent}
+ {sidebar
+ ? enhancedActiveStepContentWithSidebar
+ : enhancedActiveStepContent}
+
{footer}
);