Skip to content

Commit 1ea04fa

Browse files
feat: implement the portals course
1 parent 5729ea2 commit 1ea04fa

File tree

8 files changed

+334
-7
lines changed

8 files changed

+334
-7
lines changed

src/course/01-introduction/01-Welcome.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ Each lesson is broken down in an exercise file and a final file. The exercise fi
4949
- 09 - [The State Reducer pattern](?path=/docs/lessons-09-state-reducer-pattern-01-lesson--docs)
5050
- 10 - [Compound components pattern](?path=/docs/lessons-10-compound-components-pattern-🚧-01-lesson--docs)
5151
- 11 - [Slots pattern](?path=/docs/lessons-11-slots-01-lesson--docs)
52-
- 12 - [Portals pattern](?path=/docs/lessons-12-portals-🚧-01-lesson--docs)
52+
- 12 - [Portals pattern](?path=/docs/lessons-12-portals-01-lesson--docs)
5353

5454
### Recipes section
5555

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import classNames from 'classnames';
2+
import { useEffect, useRef } from 'react';
3+
// 👨🏻‍💻 1B - import { createPortal } from 'react-dom';
4+
import FocusLock from 'react-focus-lock';
5+
import { Button } from '../../../../shared/components/Button/Button.component';
6+
7+
interface IModal {
8+
isVisible: boolean;
9+
onClose: () => void;
10+
id: string;
11+
title: string;
12+
children: React.ReactNode | React.ReactNode[];
13+
}
14+
15+
export const Modal = ({
16+
isVisible,
17+
onClose,
18+
id,
19+
title,
20+
children
21+
}: IModal) => {
22+
const modal = useRef<HTMLDivElement>(null);
23+
24+
useEffect(() => {
25+
if (isVisible && modal.current) {
26+
modal.current.focus();
27+
}
28+
}, [isVisible]);
29+
30+
const onModalPress = (event: React.MouseEvent) => {
31+
event.stopPropagation();
32+
event.preventDefault();
33+
};
34+
35+
// 👨🏻‍💻 1C - call createPortal(modalCode, document.body);
36+
// 🧪 Test the storybook and look at how you can all of a sudden click the pay now button
37+
// This isn't saying the solution to override z-index is to use portal but more of the sense that if you need something
38+
// put at the root of the DOM but do not wish to implement something extremely complex or app level then portal is handy for this.
39+
return (
40+
<div
41+
className={classNames(
42+
'bg-modal-bg fixed top-0 left-0 right-0 bottom-0 justify-center items-center z-20',
43+
{ flex: isVisible, hidden: !isVisible }
44+
)}
45+
role="button"
46+
onClick={onClose}
47+
tabIndex={0}
48+
>
49+
<div
50+
role="dialog"
51+
id={id}
52+
aria-labelledby={`modal_title_${id}`}
53+
aria-describedby={`modal_body_${id}`}
54+
aria-modal="true"
55+
hidden={!isVisible}
56+
tabIndex={0}
57+
ref={modal}
58+
className="bg-white rounded-2xl p-5 relative z-20"
59+
onClick={onModalPress}
60+
>
61+
<FocusLock>
62+
<div>
63+
<h2 id={`modal_title_${id}`}>{title}</h2>
64+
<Button onClick={onClose}>Close Dialog</Button>
65+
</div>
66+
<div id={`modal_body_${id}`}>{children}</div>
67+
</FocusLock>
68+
</div>
69+
</div>
70+
);
71+
};

src/course/02- lessons/12-Portals/exercise.stories.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react';
33
import { Exercise } from './exercise';
44

55
const meta: Meta<typeof Exercise> = {
6-
title: 'Lessons/12 - Portals 🚧/02-Exercise',
6+
title: 'Lessons/12 - Portals/02-Exercise',
77
component: Exercise
88
};
99

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,77 @@
1+
import { useState } from 'react';
2+
import { Modal } from './components/modal';
3+
import { Button } from '../../../shared/components/Button/Button.component';
4+
5+
// 👨🏻‍💻 1A - have a look at the current implementation of the modal and then go to components/modal.tsx
6+
17
export const Exercise = () => {
2-
return null;
8+
const [isVisible, setIsVisible] = useState(false);
9+
const [isComplete, setIsComplete] = useState(false);
10+
11+
const onClose = () => {
12+
setIsVisible(false);
13+
};
14+
15+
const onOpen = () => {
16+
setIsVisible(true);
17+
};
18+
19+
const onCheckout = () => {
20+
setIsComplete(true);
21+
};
22+
23+
return (
24+
// 🧪 We have z-index 10 on the section and then z-9998 on a div that's purposely there. Our Modal has a z-20 which means:
25+
// section z-10
26+
// modal z-20 (but this means z-20 within the z-10) think of it as a sub layer.
27+
// the bug is 9998 and a css hack for the pay now is 9999
28+
<section className="z-10 relative h-screen">
29+
<div className="z-[9998] absolute top-0 left-0 right-0 bottom-0" />
30+
{isComplete && (
31+
<>
32+
<h1 className="text-xl font-semibold">
33+
Payment Successful
34+
</h1>
35+
<p className="text-md mb-2">Well done you did it!</p>
36+
</>
37+
)}
38+
39+
{!isComplete && (
40+
<>
41+
<h1 className="text-xl font-semibold">Payment Page</h1>
42+
43+
<p className="text-md mb-2">
44+
Please see your selected options from the previous steps
45+
before continuing.
46+
</p>
47+
48+
<section className="my-6">
49+
<h2 className="text-lg font-semibold mb-2">
50+
Delivery Details
51+
</h2>
52+
<address className="border border-grey-300 rounded-md p-3 mb-2 block">
53+
<p>12 john doe street, Manchester, M12 3RT</p>
54+
</address>
55+
</section>
56+
57+
<section className="z-[9999] relative">
58+
<h2 className="text-lg font-semibold mb-2">
59+
Make Payment
60+
</h2>
61+
<Button onClick={onOpen}>Pay now</Button>
62+
</section>
63+
</>
64+
)}
65+
{isVisible && !isComplete && (
66+
<Modal
67+
id="modal"
68+
onClose={onClose}
69+
isVisible={isVisible}
70+
title="Some fancy payment form..."
71+
>
72+
<Button onClick={onCheckout}>Pay now</Button>
73+
</Modal>
74+
)}
75+
</section>
76+
);
377
};

src/course/02- lessons/12-Portals/lesson.mdx

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,52 @@
11
import { Meta } from '@storybook/blocks';
22

3-
<Meta title="Lessons/12 - Portals 🚧/01-Lesson" />
3+
<Meta title="Lessons/12 - Portals/01-Lesson" />
44

5-
# Portals Pattern 🚧
5+
# Portals Pattern
6+
7+
A React portal lets you render some children into a different part of the DOM. When you call the **createPortal** it will trigger the creation of the portal. When you unmount, the portal removes itself. This is how it looks in React:
8+
9+
```jsx
10+
import { createPortal } from 'react-dom';
11+
12+
// ...
13+
14+
<div>
15+
<p>This child is placed in the parent div.</p>
16+
{createPortal(
17+
<p>This child is placed in the document body.</p>,
18+
document.body
19+
)}
20+
</div>;
21+
```
22+
23+
Which in html, will translate to:
24+
25+
```html
26+
<html>
27+
<head>
28+
<title>My react app</title>
29+
</head>
30+
<body>
31+
<div id="app">
32+
<div>
33+
<p>This child is placed in the parent div.</p>
34+
</div>
35+
</div>
36+
37+
<p>This child is placed in the document body.</p>
38+
</body>
39+
</html>
40+
```
41+
42+
Portal benefits:
43+
44+
- Simplified state management
45+
- No clashes with z-index as the modal is at the root of the DOM.
46+
47+
## Exercise
48+
49+
In the current application when a customer clicks the checkout cta and the payment popup appears, the customer cannot click the pay now button in the modal.
650

751
## Feedback
852

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import classNames from 'classnames';
2+
import { useEffect, useRef } from 'react';
3+
import { createPortal } from 'react-dom';
4+
import FocusLock from 'react-focus-lock';
5+
import { Button } from '../../../../shared/components/Button/Button.component';
6+
7+
interface IModal {
8+
isVisible: boolean;
9+
onClose: () => void;
10+
id: string;
11+
title: string;
12+
children: React.ReactNode | React.ReactNode[];
13+
}
14+
15+
export const Modal = ({
16+
isVisible,
17+
onClose,
18+
id,
19+
title,
20+
children
21+
}: IModal) => {
22+
const modal = useRef<HTMLDivElement>(null);
23+
24+
useEffect(() => {
25+
if (isVisible && modal.current) {
26+
modal.current.focus();
27+
}
28+
}, [isVisible]);
29+
30+
const onModalPress = (event: React.MouseEvent) => {
31+
event.stopPropagation();
32+
event.preventDefault();
33+
};
34+
35+
return createPortal(
36+
<div
37+
className={classNames(
38+
'bg-modal-bg fixed top-0 left-0 right-0 bottom-0 justify-center items-center z-20',
39+
{ flex: isVisible, hidden: !isVisible }
40+
)}
41+
role="button"
42+
onClick={onClose}
43+
tabIndex={0}
44+
>
45+
<div
46+
role="dialog"
47+
id={id}
48+
aria-labelledby={`modal_title_${id}`}
49+
aria-describedby={`modal_body_${id}`}
50+
aria-modal="true"
51+
hidden={!isVisible}
52+
tabIndex={0}
53+
ref={modal}
54+
className="bg-white rounded-2xl p-5 relative z-20"
55+
onClick={onModalPress}
56+
>
57+
<FocusLock>
58+
<div>
59+
<h2 id={`modal_title_${id}`}>{title}</h2>
60+
<Button onClick={onClose}>Close Dialog</Button>
61+
</div>
62+
<div id={`modal_body_${id}`}>{children}</div>
63+
</FocusLock>
64+
</div>
65+
</div>,
66+
document.body
67+
);
68+
};

src/course/02-solutions/12-Portals/final.stories.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react';
33
import { Final } from './final';
44

55
const meta: Meta<typeof Final> = {
6-
title: 'Lessons/12 - Portals 🚧/03-Final',
6+
title: 'Lessons/12 - Portals/03-Final',
77
component: Final
88
};
99

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,71 @@
1-
export const Final = () => null;
1+
import { useState } from 'react';
2+
import { Modal } from './components/modal';
3+
import { Button } from '../../../shared/components/Button/Button.component';
4+
5+
export const Final = () => {
6+
const [isVisible, setIsVisible] = useState(false);
7+
const [isComplete, setIsComplete] = useState(false);
8+
9+
const onClose = () => {
10+
setIsVisible(false);
11+
};
12+
13+
const onOpen = () => {
14+
setIsVisible(true);
15+
};
16+
17+
const onCheckout = () => {
18+
setIsComplete(true);
19+
};
20+
21+
return (
22+
<section className="z-10 relative h-screen">
23+
<div className="z-[9998] absolute top-0 left-0 right-0 bottom-0" />
24+
{isComplete && (
25+
<>
26+
<h1 className="text-xl font-semibold">
27+
Payment Successful
28+
</h1>
29+
<p className="text-md mb-2">Checkout has been successful</p>
30+
</>
31+
)}
32+
33+
{!isComplete && (
34+
<>
35+
<h1 className="text-xl font-semibold">Payment Page</h1>
36+
37+
<p className="text-md mb-2">
38+
Please see your selected options from the previous steps
39+
before continuing.
40+
</p>
41+
42+
<section className="my-6">
43+
<h2 className="text-lg font-semibold mb-2">
44+
Delivery Details
45+
</h2>
46+
<address className="border border-grey-300 rounded-md p-3 mb-2 block">
47+
<p>12 john doe street, Manchester, M12 3RT</p>
48+
</address>
49+
</section>
50+
51+
<section className="z-[9999] relative">
52+
<h2 className="text-lg font-semibold mb-2">
53+
Make Payment
54+
</h2>
55+
<Button onClick={onOpen}>Pay now</Button>
56+
</section>
57+
</>
58+
)}
59+
{isVisible && !isComplete && (
60+
<Modal
61+
id="modal"
62+
onClose={onClose}
63+
isVisible={isVisible}
64+
title="Some fancy payment form..."
65+
>
66+
<Button onClick={onCheckout}>Pay now</Button>
67+
</Modal>
68+
)}
69+
</section>
70+
);
71+
};

0 commit comments

Comments
 (0)