Skip to content

Commit e75e3b0

Browse files
feat: implement pokemone controlled component version
1 parent 4457f6b commit e75e3b0

File tree

3 files changed

+309
-82
lines changed

3 files changed

+309
-82
lines changed

โ€Žsrc/course/02-lessons/02-Silver/Controlled/exercise/exercise.tsxโ€Ž

Lines changed: 127 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -7,36 +7,49 @@ import { useEffect, useRef, useState } from 'react';
77
import FocusLock from 'react-focus-lock';
88
import { Button } from '@shared/components/Button/Button.component';
99

10-
interface IModal {
10+
interface IEvolutionModal {
1111
isVisible: boolean;
1212
onClose: () => void;
13+
onConfirm: () => void;
1314
id: string;
14-
title: string;
15-
children: React.ReactNode | React.ReactNode[];
15+
pokemon: {
16+
name: string;
17+
level: number;
18+
currentSprite: string;
19+
};
20+
evolution: {
21+
name: string;
22+
sprite: string;
23+
requirement: string;
24+
};
1625
}
1726

1827
// For the full guide to making an accessible modal you can follow below to get every instance
1928
// โ™ฟ๏ธ WCAG Modal Resource: https://www.w3.org/WAI/ARIA/apg/patterns/alertdialog/examples/alertdialog/
2029
// ๐Ÿ’ฃ You can get rid of this eslint error comment when finished.
2130
// @ts-ignore
2231
// eslint-disable-next-line @typescript-eslint/no-unused-vars
23-
const Modal = ({
32+
const EvolutionModal = ({
2433
isVisible,
2534
// ๐Ÿ’ฃ You can get rid of this eslint error comment when finished.
2635
// @ts-ignore
2736
// eslint-disable-next-line @typescript-eslint/no-unused-vars
2837
onClose,
38+
// ๐Ÿ’ฃ You can get rid of this eslint error comment when finished.
39+
// @ts-ignore
40+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
41+
onConfirm,
2942
id,
30-
title,
31-
children
32-
}: IModal) => {
33-
// 2a ๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ป Create a useRef<HTMLDivElement> and bind the ref to the div on line 58
43+
pokemon,
44+
evolution
45+
}: IEvolutionModal) => {
46+
// 2a ๐Ÿ‘จ๐Ÿป๐Ÿ’ป Create a useRef<HTMLDivElement> and bind the ref to the div on line 70
3447

3548
useEffect(() => {
3649
// โœ๐Ÿป When a modal is visible you want to navigate the focus from
3750
// the actioner (what caused the modal to open) to the content
3851
// โ™ฟ๏ธ It helps the screenreader not get lost on the page
39-
// 2b - ๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ป Check if isVisible is true and the modal.current is defined before setting focus to the modal
52+
// 2b - ๐Ÿ‘จ๐Ÿป๐Ÿ’ป Check if isVisible is true and the modal.current is defined before setting focus to the modal
4053
}, [isVisible]);
4154

4255
// ๐Ÿ’ฃ You can get rid of this eslint error comment when finished.
@@ -61,56 +74,139 @@ const Modal = ({
6174
// 2c - ๐Ÿ’„ Add an object as the second param with flex: isVisible and hidden !isVisible
6275
)}
6376
role="button"
64-
// 2d - ๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ป Pass the onClose event from the props to the onClick event.
77+
// 2d - ๐Ÿ‘จ๐Ÿป๐Ÿ’ป Pass the onClose event from the props to the onClick event.
6578
tabIndex={0}
6679
>
6780
<div
6881
id={id}
6982
// 2e - Add the following HTML attributes to the div:
7083
// * โ™ฟ๏ธ role="dialog" - this changes how the screenreader understands the html element
71-
// * โ™ฟ๏ธ aria-labelledby={`modal_title_${id}`} - so when focus lands the title is read out
72-
// * โ™ฟ๏ธ aria-describedby={`modal_body_${id}`} - so when focus lands the content is read out
84+
// * โ™ฟ๏ธ aria-labelledby={`evolution_title_${id}`} - so when focus lands the title is read out
85+
// * โ™ฟ๏ธ aria-describedby={`evolution_body_${id}`} - so when focus lands the content is read out
7386
// ๐Ÿค” โฌ†๏ธ It's always good to make these unique with an id prop so you have unique ids on the page
7487
// * โ™ฟ๏ธ aria-modal="true" - indicates whether an element is modal when displayed.
7588
// * โ™ฟ๏ธ hidden={!isVisible} - we do not want the screenreader to pick this up when it's hidden
7689
// * โ™ฟ๏ธ tabIndex={0} - allows the browser to focus on it via a keyboard
7790
// * onClick={onModalPress} - description in onModalPress.
78-
className="bg-white rounded-2xl p-5 relative z-20"
91+
className="bg-white rounded-2xl p-6 relative z-20 max-w-md mx-4"
7992
>
8093
{/* โœ๐Ÿป SUPER important for meeting the WCAG quidelines is that you need focus, but locked within this div */}
8194
{/* When focus is landed in this box with a keyboard you can no longer get out so make sure you have a close button */}
82-
{/* โ™ฟ๏ธ Another requirement is to return focus to the actioner, but FocusLock does that for us when this component unmounts! ๐Ÿฆธ๐Ÿปโ€โ™€๏ธ */}
83-
<FocusLock>
95+
{/* โ™ฟ๏ธ Another requirement is to return focus to the actioner, but FocusLock does that for us when this component unmounts! ๐Ÿฆธ๐Ÿปโ™€๏ธ */}
96+
<FocusLock returnFocus={true}>
8497
<div>
85-
{/* 2f - ๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ปโ™ฟ๏ธ Add id={`modal_title_${id}`} - this creates the relationship between the title and modal */}
86-
<h2>{title}</h2>
87-
{/* 2g - ๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ป Add onClick={onClose} going back to the pattern, we want outside to control the visibility of the modal */}
88-
<Button>Close Dialog</Button>
98+
{/* 2f - ๐Ÿ‘จ๐Ÿป๐Ÿ’ปโ™ฟ๏ธ Add id={`evolution_title_${id}`} - this creates the relationship between the title and modal */}
99+
<h2 className="text-2xl font-bold text-center mb-4 text-blue-800">
100+
โœจ Evolution Time! โœจ
101+
</h2>
102+
103+
<div className="text-center mb-6">
104+
<div className="flex items-center justify-center gap-4 mb-4">
105+
<div className="text-center">
106+
<img
107+
src={pokemon.currentSprite}
108+
alt={pokemon.name}
109+
className="w-20 h-20 mx-auto"
110+
/>
111+
<p className="font-bold">{pokemon.name}</p>
112+
<p className="text-sm text-gray-600">
113+
Level {pokemon.level}
114+
</p>
115+
</div>
116+
117+
<div className="text-4xl">โ†’</div>
118+
119+
<div className="text-center">
120+
<img
121+
src={evolution.sprite}
122+
alt={evolution.name}
123+
className="w-20 h-20 mx-auto"
124+
/>
125+
<p className="font-bold">{evolution.name}</p>
126+
<p className="text-xs text-blue-600">
127+
{evolution.requirement}
128+
</p>
129+
</div>
130+
</div>
131+
</div>
132+
133+
<div className="flex gap-3 justify-center">
134+
{/* 2g - ๐Ÿ‘จ๐Ÿป๐Ÿ’ป Add onClick={onConfirm} for evolution confirmation */}
135+
<Button className="bg-green-600 hover:bg-green-700">
136+
โœจ Evolve!
137+
</Button>
138+
{/* 2h - ๐Ÿ‘จ๐Ÿป๐Ÿ’ป Add onClick={onClose} going back to the pattern, we want outside to control the visibility of the modal */}
139+
<Button className="bg-gray-600 hover:bg-gray-700">
140+
Cancel
141+
</Button>
142+
</div>
143+
</div>
144+
{/* 2i - ๐Ÿ‘จ๐Ÿป๐Ÿ’ปโ™ฟ๏ธ Add id={`evolution_body_${id}`} - this creates the relationship between the content and modal */}
145+
<div className="mt-4 text-center text-sm text-gray-600">
146+
Your {pokemon.name} is ready to evolve into{' '}
147+
{evolution.name}!
89148
</div>
90-
{/* 2h - ๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ปโ™ฟ๏ธ Add id={`modal_body_${id}`} - this creates the relationship between the title and modal */}
91-
<div>{children}</div>
92149
</FocusLock>
93150
</div>
94151
</div>
95152
);
96153
};
97154

98155
export const Exercise = () => {
99-
// 1a ๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ป Create a state hook variable with isVisible and setIsVisible
156+
// 1a ๐Ÿ‘จ๐Ÿป๐Ÿ’ป Create a state hook variable with isEvolutionVisible and setIsEvolutionVisible
157+
158+
// 1b ๐Ÿ‘จ๐Ÿป๐Ÿ’ป Create an onClose event that sets isEvolutionVisible to false
100159

101-
// 1b ๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ป Create an onClose event that sets isVisible to false
160+
// 1c ๐Ÿ‘จ๐Ÿป๐Ÿ’ป Create an onConfirm event that handles evolution and closes modal
102161

103-
// 1c ๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ป Create an onOpen event that sets isVisible to true
162+
// 1d ๐Ÿ‘จ๐Ÿป๐Ÿ’ป Create an onCheckEvolution event that sets isEvolutionVisible to true
163+
164+
const pokemon = {
165+
name: 'Charmander',
166+
level: 16,
167+
currentSprite:
168+
'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/4.png'
169+
};
170+
171+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
172+
const evolution = {
173+
name: 'Charmeleon',
174+
sprite:
175+
'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/5.png',
176+
requirement: 'Level 16 reached!'
177+
};
104178

105179
return (
106-
<>
107-
{/* 1d ๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ป Add the onClick={onOpen} event to the button
108-
โœ๐Ÿป This is an example of a Controlled component but in a HTML context.
180+
<div className="p-6 bg-blue-50 rounded-lg border-2 border-blue-200">
181+
<h2 className="text-2xl font-bold mb-4 text-blue-800">
182+
๐ŸŽฎ Pokemon Evolution System
183+
</h2>
184+
185+
<div className="text-center mb-6">
186+
<img
187+
src={pokemon.currentSprite}
188+
alt={pokemon.name}
189+
className="w-32 h-32 mx-auto mb-4"
190+
/>
191+
<h3 className="text-xl font-bold">{pokemon.name}</h3>
192+
<p className="text-gray-600">Level {pokemon.level}</p>
193+
<p className="text-green-600 font-semibold mt-2">
194+
Ready to evolve! ๐ŸŒŸ
195+
</p>
196+
</div>
197+
198+
{/* 1e ๐Ÿ‘จ๐Ÿป๐Ÿ’ป Add the onClick={onCheckEvolution} event to the button
199+
โœ๐Ÿป This is an example of a Controlled component but in a Pokemon context.
109200
As a developer, we are providing the button with those props for the button
110201
to behave how we want it to behave, otherwise, it does nothing. */}
111-
<button type="button">Open Modal</button>
112-
{/* 1e ๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ป Check if isVisible (๐Ÿ’… Conditional Render Pattern) to render the Modal */}
113-
{/* Map the isVisible and onClose props to the Modal. The other props can be whatever you want */}
114-
</>
202+
<div className="text-center">
203+
<Button className="bg-yellow-500 hover:bg-yellow-600">
204+
โšก Check Evolution
205+
</Button>
206+
</div>
207+
208+
{/* 1f ๐Ÿ‘จ๐Ÿป๐Ÿ’ป Check if isEvolutionVisible (๐Ÿ’… Conditional Render Pattern) to render the EvolutionModal */}
209+
{/* Map the isVisible, onClose, onConfirm props to the EvolutionModal. The other props can be whatever you want */}
210+
</div>
115211
);
116212
};

0 commit comments

Comments
ย (0)