Skip to content

Commit 7b343d7

Browse files
authored
Merge pull request #86 from devrnt/feat/wizard-wrapper
Feat/wizard wrapper
2 parents b5abeb7 + f435bce commit 7b343d7

9 files changed

Lines changed: 124 additions & 68 deletions

File tree

.vscode/settings.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"typescript.tsdk": "node_modules\\typescript\\lib"
3+
}

README.md

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ Example: pass a footer component that contains a "previous" and "next" button to
8484
| startIndex | number | Indicate the wizard to start at the given step || 0 |
8585
| header | React.ReactNode | Header that is shown above the active step || |
8686
| footer | React.ReactNode | Footer that is shown below the active stepstep || |
87+
| wrapper | React.React.ReactElement | Optional wrapper that is exclusively wrapped around the active step component. It is not wrapped around the `header` and `footer` || |
8788
| children | React.ReactNode | Each child component will be treated as an individual step | ✔️ |
8889

8990
#### Example
@@ -95,9 +96,17 @@ const Header = () => <p>I am the header component</p>;
9596
// Example: show the "previous" and "next" buttons in the footer
9697
const Footer = () => <p>I am the footer component</p>;
9798

99+
// Example: Wrap steps in an `<AnimatePresence` from framer-motion
100+
const Wrapper = () => <AnimatePresence exitBeforeEnter />;
101+
98102
const App = () => {
99103
return (
100-
<Wizard startIndex={0} header={<Header />} footer={<Footer />}>
104+
<Wizard
105+
startIndex={0}
106+
header={<Header />}
107+
footer={<Footer />}
108+
wrapper={<Wrapper />}
109+
>
101110
<Step1 />
102111
<Step2 />
103112
<Step3 />
@@ -236,7 +245,7 @@ If an async function is attached to `handleStep` the `isLoading` property will i
236245

237246
Since `react-use-wizard` is focused to manage the logic of a wizard it doesn't mean you can't add some animation by your own. Add any animation library that you like. I highly suggest [framer-motion](https://www.framer.com/motion/) to add your animations.
238247

239-
Checkout this [example](https://github.com/devrnt/react-use-wizard/blob/main/playground/components/animatedStep.tsx) to see how a step can be animated with framer motion.
248+
Checkout this [example](https://github.com/devrnt/react-use-wizard/tree/feat/wizard-wrapper/playground/modules/wizard/animated) to see how a step can be animated with framer motion.
240249

241250
## IE11
242251

Lines changed: 36 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
import { motion } from 'framer-motion';
1+
import { motion, Variants } from 'framer-motion';
22
import * as React from 'react';
33

44
import { useWizard } from '../../dist';
55

6-
const variants = {
6+
const variants: Variants = {
77
enter: (direction: number) => {
88
return {
9-
x: direction < 0 ? 1000 : -1000,
9+
x: direction > 0 ? 800 : -800,
1010
opacity: 0,
1111
};
1212
},
@@ -15,32 +15,46 @@ const variants = {
1515
x: 0,
1616
opacity: 1,
1717
},
18+
exit: (direction: number) => {
19+
return {
20+
zIndex: 0,
21+
x: direction < 0 ? 800 : -800,
22+
opacity: 0,
23+
};
24+
},
1825
};
1926

2027
type Props = {
2128
previousStep: React.MutableRefObject<number>;
2229
};
2330

24-
const AnimatedStep: React.FC<Props> = ({
25-
children,
26-
previousStep: previousStepIndex,
27-
}) => {
28-
const { activeStep } = useWizard();
31+
const AnimatedStep: React.FC<Props> = React.memo(
32+
({ children, previousStep: previousStepIndex }) => {
33+
const { activeStep } = useWizard();
2934

30-
React.useEffect(() => {
31-
previousStepIndex.current = activeStep;
32-
}, [activeStep, previousStepIndex]);
35+
React.useEffect(() => {
36+
return () => {
37+
previousStepIndex.current = activeStep;
38+
};
39+
}, [activeStep, previousStepIndex]);
3340

34-
return (
35-
<motion.div
36-
custom={activeStep - previousStepIndex.current}
37-
variants={variants}
38-
initial="enter"
39-
animate="center"
40-
>
41-
{children}
42-
</motion.div>
43-
);
44-
};
41+
return (
42+
<motion.div
43+
custom={activeStep - previousStepIndex.current}
44+
variants={variants}
45+
initial="enter"
46+
animate="center"
47+
exit="exit"
48+
transition={{
49+
type: 'spring',
50+
stiffness: 300,
51+
damping: 30,
52+
}}
53+
>
54+
{children}
55+
</motion.div>
56+
);
57+
},
58+
);
4559

4660
export default AnimatedStep;

playground/modules/wizard/animated/index.tsx

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,20 @@ const AnimatedSection: React.FC = () => {
1010

1111
return (
1212
<Section title="Animated wizard" description="animation by framer motion">
13-
<AnimatePresence>
14-
<Wizard footer={<Footer />}>
15-
{Array(4)
16-
.fill(null)
17-
.map((_, index) => {
18-
return (
19-
<AnimatedStep key={index} previousStep={previousStep}>
20-
<Step number={index + 1} withCallback={false}></Step>
21-
</AnimatedStep>
22-
);
23-
})}
24-
</Wizard>
25-
</AnimatePresence>
13+
<Wizard
14+
footer={<Footer />}
15+
wrapper={<AnimatePresence initial={false} exitBeforeEnter />}
16+
>
17+
{Array(4)
18+
.fill(null)
19+
.map((_, index) => {
20+
return (
21+
<AnimatedStep key={index} previousStep={previousStep}>
22+
<Step number={index + 1} withCallback={false}></Step>
23+
</AnimatedStep>
24+
);
25+
})}
26+
</Wizard>
2627
</Section>
2728
);
2829
};

playground/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"build": "parcel build index.html --public-url /react-use-wizard/"
1010
},
1111
"dependencies": {
12-
"framer-motion": "^4.0.0",
12+
"framer-motion": "^4.1.17",
1313
"goober": "^2.0.35",
1414
"react-app-polyfill": "^2.0.0",
1515
"react-query": "^3.13.0"

playground/yarn.lock

Lines changed: 25 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -3270,23 +3270,25 @@ format@^0.2.0:
32703270
resolved "https://registry.yarnpkg.com/format/-/format-0.2.2.tgz#d6170107e9efdc4ed30c9dc39016df942b5cb58b"
32713271
integrity sha1-1hcBB+nv3E7TDJ3DkBbflCtctYs=
32723272

3273-
framer-motion@^4.0.0:
3274-
version "4.0.0"
3275-
resolved "https://registry.yarnpkg.com/framer-motion/-/framer-motion-4.0.0.tgz#3eab6d79e9a662ed8dfcfd05e680ba3a6b841672"
3276-
integrity sha512-j7lPnXEZdpkAelLRU8FhaZC3UbqnZwSTT/W9rSRwb3cKX4eWrekakK86cVkUE79v1HRE9IPzdPVNP9/e8CPRag==
3273+
framer-motion@^4.1.17:
3274+
version "4.1.17"
3275+
resolved "https://registry.yarnpkg.com/framer-motion/-/framer-motion-4.1.17.tgz#4029469252a62ea599902e5a92b537120cc89721"
3276+
integrity sha512-thx1wvKzblzbs0XaK2X0G1JuwIdARcoNOW7VVwjO8BUltzXPyONGAElLu6CiCScsOQRI7FIk/45YTFtJw5Yozw==
32773277
dependencies:
3278-
framesync "5.2.0"
3278+
framesync "5.3.0"
32793279
hey-listen "^1.0.8"
3280-
popmotion "9.3.1"
3281-
style-value-types "4.1.1"
3280+
popmotion "9.3.6"
3281+
style-value-types "4.1.4"
32823282
tslib "^2.1.0"
32833283
optionalDependencies:
32843284
"@emotion/is-prop-valid" "^0.8.2"
32853285

3286-
framesync@5.2.0:
3287-
version "5.2.0"
3288-
resolved "https://registry.yarnpkg.com/framesync/-/framesync-5.2.0.tgz#f14480654cd05a6af4c72c9890cad93556841643"
3289-
integrity sha512-dcl92w5SHc0o6pRK3//VBVNvu6WkYkiXmHG6ZIXrVzmgh0aDYMDAaoA3p3LH71JIdN5qmhDcfONFA4Lmq22tNA==
3286+
framesync@5.3.0:
3287+
version "5.3.0"
3288+
resolved "https://registry.yarnpkg.com/framesync/-/framesync-5.3.0.tgz#0ecfc955e8f5a6ddc8fdb0cc024070947e1a0d9b"
3289+
integrity sha512-oc5m68HDO/tuK2blj7ZcdEBRx3p1PjrgHazL8GYEpvULhrtGIFbQArN6cQS2QhW8mitffaB+VYzMjDqBxxQeoA==
3290+
dependencies:
3291+
tslib "^2.1.0"
32903292

32913293
fs.realpath@^1.0.0:
32923294
version "1.0.0"
@@ -4554,15 +4556,15 @@ pn@^1.1.0:
45544556
resolved "https://registry.yarnpkg.com/pn/-/pn-1.1.0.tgz#e2f4cef0e219f463c179ab37463e4e1ecdccbafb"
45554557
integrity sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA==
45564558

4557-
popmotion@9.3.1:
4558-
version "9.3.1"
4559-
resolved "https://registry.yarnpkg.com/popmotion/-/popmotion-9.3.1.tgz#134319ed4b9b8e3ec506c99064f7b2f14bc05781"
4560-
integrity sha512-Qozvg8rz2OGeZwWuIjqlSXqqgWto/+QL24ll8sAAc0n71KY/wvN1W4sAASxTuHv8YWdDnk9u9IdadyPo2DGvDA==
4559+
popmotion@9.3.6:
4560+
version "9.3.6"
4561+
resolved "https://registry.yarnpkg.com/popmotion/-/popmotion-9.3.6.tgz#b5236fa28f242aff3871b9e23721f093133248d1"
4562+
integrity sha512-ZTbXiu6zIggXzIliMi8LGxXBF5ST+wkpXGEjeTUDUOCdSQ356hij/xjeUdv0F8zCQNeqB1+PR5/BB+gC+QLAPw==
45614563
dependencies:
4562-
framesync "5.2.0"
4564+
framesync "5.3.0"
45634565
hey-listen "^1.0.8"
4564-
style-value-types "4.1.1"
4565-
tslib "^1.10.0"
4566+
style-value-types "4.1.4"
4567+
tslib "^2.1.0"
45664568

45674569
postcss-calc@^8.0.0:
45684570
version "8.0.0"
@@ -5512,13 +5514,13 @@ strip-ansi@^6.0.0, strip-ansi@^6.0.1:
55125514
dependencies:
55135515
ansi-regex "^5.0.1"
55145516

5515-
style-value-types@4.1.1:
5516-
version "4.1.1"
5517-
resolved "https://registry.yarnpkg.com/style-value-types/-/style-value-types-4.1.1.tgz#15d08bd9bb17ac3f5d4a863bf9d5e4eb8e4f0686"
5518-
integrity sha512-cNLrl6jk+I1T18ZI2KIp/fcqKVuykcNELDrOz7y+TYZR97xmNdN0ewupURvVFnQxVrRJv98TMBq92VMsggq3kw==
5517+
style-value-types@4.1.4:
5518+
version "4.1.4"
5519+
resolved "https://registry.yarnpkg.com/style-value-types/-/style-value-types-4.1.4.tgz#80f37cb4fb024d6394087403dfb275e8bb627e75"
5520+
integrity sha512-LCJL6tB+vPSUoxgUBt9juXIlNJHtBMy8jkXzUJSBzeHWdBu6lhzHqCvLVkXFGsFIlNa2ln1sQHya/gzaFmB2Lg==
55195521
dependencies:
55205522
hey-listen "^1.0.8"
5521-
tslib "^1.10.0"
5523+
tslib "^2.1.0"
55225524

55235525
stylehacks@^5.0.1:
55245526
version "5.0.1"
@@ -5648,11 +5650,6 @@ tr46@^1.0.1:
56485650
dependencies:
56495651
punycode "^2.1.0"
56505652

5651-
tslib@^1.10.0:
5652-
version "1.14.1"
5653-
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
5654-
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
5655-
56565653
tslib@^2.1.0:
56575654
version "2.1.0"
56585655
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.1.0.tgz#da60860f1c2ecaa5703ab7d39bc05b6bf988b97a"

src/types.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,17 @@ export type WizardProps = {
77
footer?: React.ReactNode;
88
/** Optional start index @default 0 */
99
startIndex?: number;
10+
/**
11+
* Optional wrapper that is exclusively wrapped around the active step component. It is not wrapped around the `header` and `footer`
12+
*
13+
* @example With `framer-motion` - `<AnimatePresence />`
14+
* ```jsx
15+
* <Wizard wrapper={<AnimatePresence exitBeforeEnter />}>
16+
* ...
17+
* </Wizard>
18+
* ```
19+
*/
20+
wrapper?: React.ReactElement;
1021
};
1122

1223
export type WizardValues = {

src/wizard.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { Handler, WizardProps } from './types';
55
import WizardContext from './wizardContext';
66

77
const Wizard: React.FC<React.PropsWithChildren<WizardProps>> = React.memo(
8-
({ header, footer, children, startIndex = 0 }) => {
8+
({ header, footer, children, wrapper: Wrapper, startIndex = 0 }) => {
99
const [activeStep, setActiveStep] = React.useState(startIndex);
1010
const [isLoading, setIsLoading] = React.useState(false);
1111
const hasNextStep = React.useRef(true);
@@ -111,10 +111,18 @@ const Wizard: React.FC<React.PropsWithChildren<WizardProps>> = React.memo(
111111
return reactChildren[activeStep];
112112
}, [activeStep, children, header, footer]);
113113

114+
const enhancedActiveStepContent = React.useMemo(
115+
() =>
116+
Wrapper
117+
? React.cloneElement(Wrapper, { children: activeStepContent })
118+
: activeStepContent,
119+
[Wrapper, activeStepContent],
120+
);
121+
114122
return (
115123
<WizardContext.Provider value={wizardValue}>
116124
{header}
117-
{activeStepContent}
125+
{enhancedActiveStepContent}
118126
{footer}
119127
</WizardContext.Provider>
120128
);

test/wizard.test.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,13 @@ const ComponentWithFooter = (
2424
</Wizard>
2525
);
2626

27+
const ComponentWithWrapper = (
28+
<Wizard wrapper={<main />}>
29+
<p>step 1</p>
30+
<p>step 2</p>
31+
</Wizard>
32+
);
33+
2734
describe('Wizard', () => {
2835
test('should render first step', () => {
2936
const { queryByText } = render(Component);
@@ -60,4 +67,10 @@ describe('Wizard', () => {
6067

6168
expect(queryByText('footer')).toBeInTheDocument();
6269
});
70+
71+
test('should render wrapper', () => {
72+
const { queryByRole } = render(ComponentWithWrapper);
73+
74+
expect(queryByRole('main')).toBeInTheDocument();
75+
});
6376
});

0 commit comments

Comments
 (0)