Cours JavaScript et React · Module 8: Patterns React avancés
Accessibilité (a11y) en React
Dans cette leçon9
Sur une station spatiale, chaque membre d'équipage doit pouvoir accéder aux systèmes de contrôle - qu'il les utilise par la vue, le toucher ou des commandes vocales. L'accessibilité (a11y en abrégé) dans les applications web fonctionne exactement de la même manière. Quand on construit une interface React, on doit s'assurer que chaque utilisateur peut s'en servir - les personnes aveugles utilisant un lecteur d'écran, celles qui naviguent uniquement au clavier, ou celles ayant des déficiences de la vision des couleurs.
Pourquoi l'accessibilité est-elle importante ?
Selon l'OMS, plus d'un milliard de personnes dans le monde vivent avec une forme de handicap. Cela signifie qu'une application inaccessible exclut un énorme groupe d'utilisateurs potentiels. Au-delà de l'aspect éthique, l'accessibilité :
- Est légalement requise dans de nombreux pays (WCAG, ADA)
- Améliore le SEO et la qualité générale du code
- Rend l'application plus facile à utiliser pour TOUS les utilisateurs (par exemple sur les appareils mobiles)
- Rend l'interface plus prévisible et testable
HTML sémantique en React
Le fondement de l'accessibilité est l'utilisation des bons éléments HTML. React encourage la création de composants, mais il est facile de tomber dans le piège de la surutilisation des <div> :
1// MAUVAIS - « soupe de div » - le lecteur d'écran ne comprend pas la structure
2function BadNavigation() {
3 return (
4 <div className="nav">
5 <div className="nav-item" onClick={goHome}>Accueil</div>
6 <div className="nav-item" onClick={goMissions}>Missions</div>
7 <div className="nav-item" onClick={goCrewPanel}>Équipage</div>
8 </div>
9 );
10}
11
12// BON - éléments HTML sémantiques
13function GoodNavigation() {
14 return (
15 <nav aria-label="Menu principal de la station">
16 <ul>
17 <li><a href="/">Accueil</a></li>
18 <li><a href="/missions">Missions</a></li>
19 <li><a href="/crew">Équipage</a></li>
20 </ul>
21 </nav>
22 );
23}Les éléments sémantiques clés incluent : <nav>, <main>, <header>, <footer>, <section>, <article>, <aside>, <button>, <form>, <label>.
Attributs ARIA en React
ARIA (Accessible Rich Internet Applications) est un ensemble d'attributs qui ajoutent des informations sur le rôle et l'état des éléments pour les technologies d'assistance. En React, nous les utilisons en camelCase ou avec des traits d'union :
1function MissionAlert({ message, severity }) {
2 return (
3 <div
4 role="alert"
5 aria-live="assertive"
6 aria-atomic="true"
7 className={severity}
8 >
9 {message}
10 </div>
11 );
12}
13
14function SearchField() {
15 const [results, setResults] = useState([]);
16 const [isOpen, setIsOpen] = useState(false);
17
18 return (
19 <div>
20 <label htmlFor="galaxy-search">Rechercher des galaxies :</label>
21 <input
22 id="galaxy-search"
23 type="search"
24 role="combobox"
25 aria-expanded={isOpen}
26 aria-controls="search-results"
27 aria-autocomplete="list"
28 aria-haspopup="listbox"
29 />
30 {isOpen && (
31 <ul id="search-results" role="listbox">
32 {results.map(r => (
33 <li key={r.id} role="option">{r.name}</li>
34 ))}
35 </ul>
36 )}
37 </div>
38 );
39}Les attributs ARIA les plus couramment utilisés en React :
| Attribut | Cas d'usage |
|---|---|
role | Définit le rôle de l'élément (alert, dialog, tabpanel) |
aria-label | Étiquette textuelle de l'élément |
aria-labelledby | ID de l'élément qui sert d'étiquette |
aria-describedby | ID de l'élément avec une description supplémentaire |
aria-live | Annonce les changements de contenu dynamiques |
aria-expanded | Indique si l'élément est déplié |
aria-hidden | Cache l'élément au lecteur d'écran |
aria-disabled | Indique un élément désactivé |
Gestion du focus
Dans les applications SPA (Single Page Application), la gestion du focus clavier est critique. Quand un utilisateur navigue au clavier, le focus doit se déplacer dans un ordre logique :
1import { useRef, useEffect } from 'react';
2
3function MissionModal({ isOpen, onClose, title, children }) {
4 const modalRef = useRef(null);
5 const closeButtonRef = useRef(null);
6
7 // Déplacer le focus vers la modale après l'ouverture
8 useEffect(() => {
9 if (isOpen && closeButtonRef.current) {
10 closeButtonRef.current.focus();
11 }
12 }, [isOpen]);
13
14 // Focus trap - Tab ne quitte pas la modale
15 const handleKeyDown = (e) => {
16 if (e.key === 'Escape') {
17 onClose();
18 }
19
20 if (e.key === 'Tab') {
21 const focusableElements = modalRef.current.querySelectorAll(
22 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
23 );
24 const firstElement = focusableElements[0];
25 const lastElement = focusableElements[focusableElements.length - 1];
26
27 if (e.shiftKey && document.activeElement === firstElement) {
28 e.preventDefault();
29 lastElement.focus();
30 } else if (!e.shiftKey && document.activeElement === lastElement) {
31 e.preventDefault();
32 firstElement.focus();
33 }
34 }
35 };
36
37 if (!isOpen) return null;
38
39 return (
40 <div
41 className="modal-overlay"
42 role="dialog"
43 aria-modal="true"
44 aria-labelledby="modal-title"
45 ref={modalRef}
46 onKeyDown={handleKeyDown}
47 >
48 <div className="modal-content">
49 <h2 id="modal-title">{title}</h2>
50 {children}
51 <button ref={closeButtonRef} onClick={onClose}>
52 Fermer
53 </button>
54 </div>
55 </div>
56 );
57}Navigation au clavier
Chaque élément interactif doit être accessible au clavier. React prend en charge la gestion des touches via onKeyDown, onKeyUp et onKeyPress :
1function SpaceshipSelector({ ships, onSelect }) {
2 const [activeIndex, setActiveIndex] = useState(0);
3
4 const handleKeyDown = (e) => {
5 switch (e.key) {
6 case 'ArrowDown':
7 e.preventDefault();
8 setActiveIndex(prev => Math.min(prev + 1, ships.length - 1));
9 break;
10 case 'ArrowUp':
11 e.preventDefault();
12 setActiveIndex(prev => Math.max(prev - 1, 0));
13 break;
14 case 'Enter':
15 case ' ':
16 e.preventDefault();
17 onSelect(ships[activeIndex]);
18 break;
19 }
20 };
21
22 return (
23 <ul
24 role="listbox"
25 aria-label="Sélectionner un vaisseau"
26 tabIndex={0}
27 onKeyDown={handleKeyDown}
28 >
29 {ships.map((ship, index) => (
30 <li
31 key={ship.id}
32 role="option"
33 aria-selected={index === activeIndex}
34 onClick={() => onSelect(ship)}
35 style={{
36 background: index === activeIndex ? '#1a3a5c' : 'transparent',
37 }}
38 >
39 {ship.name} - {ship.type}
40 </li>
41 ))}
42 </ul>
43 );
44}Annoncer les changements dynamiques (Live Regions)
Quand le contenu de la page change dynamiquement (par exemple, une notification apparaît), le lecteur d'écran doit en être informé. Nous utilisons aria-live :
1function MissionStatusBoard() {
2 const [status, setStatus] = useState('En attente du lancement');
3 const [logs, setLogs] = useState([]);
4
5 const updateStatus = (newStatus) => {
6 setStatus(newStatus);
7 setLogs(prev => [...prev, newStatus]);
8 };
9
10 return (
11 <div>
12 {/* aria-live="polite" - attend que le lecteur termine le texte en cours */}
13 <div aria-live="polite" aria-atomic="true">
14 <h2>Statut de la mission : {status}</h2>
15 </div>
16
17 {/* aria-live="assertive" - interrompt la lecture en cours */}
18 <div aria-live="assertive" className="sr-only">
19 {logs.length > 0 && logs[logs.length - 1]}
20 </div>
21
22 <button onClick={() => updateStatus('Compte à rebours démarré')}>
23 Démarrer le compte à rebours
24 </button>
25 <button onClick={() => updateStatus('Lancement !')}>
26 Lancer
27 </button>
28 </div>
29 );
30}La classe sr-only (screen reader only)
Parfois nous avons besoin de texte visible uniquement par les lecteurs d'écran :
1// Style CSS pour cacher visuellement, mais pas aux lecteurs d'écran
2const srOnlyStyle = {
3 position: 'absolute',
4 width: '1px',
5 height: '1px',
6 padding: 0,
7 margin: '-1px',
8 overflow: 'hidden',
9 clip: 'rect(0, 0, 0, 0)',
10 whiteSpace: 'nowrap',
11 borderWidth: 0,
12};
13
14function IconButton({ icon, label, onClick }) {
15 return (
16 <button onClick={onClick} aria-label={label}>
17 <span aria-hidden="true">{icon}</span>
18 <span style={srOnlyStyle}>{label}</span>
19 </button>
20 );
21}
22
23// Utilisation
24<IconButton icon="▲" label="Démarrer la mission" onClick={startMission} />Formulaires accessibles
Les formulaires sont l'un des domaines les plus importants de l'accessibilité. Chaque champ doit avoir une étiquette, et les erreurs doivent être clairement communiquées :
1function CrewRegistrationForm() {
2 const [errors, setErrors] = useState({});
3
4 return (
5 <form aria-label="Inscription d'un membre d'équipage">
6 <div>
7 <label htmlFor="crew-name">Nom complet :</label>
8 <input
9 id="crew-name"
10 type="text"
11 aria-required="true"
12 aria-invalid={!!errors.name}
13 aria-describedby={errors.name ? 'name-error' : undefined}
14 />
15 {errors.name && (
16 <span id="name-error" role="alert" style={{ color: 'red' }}>
17 {errors.name}
18 </span>
19 )}
20 </div>
21
22 <fieldset>
23 <legend>Spécialisation :</legend>
24 <label>
25 <input type="radio" name="role" value="pilot" />
26 Pilote
27 </label>
28 <label>
29 <input type="radio" name="role" value="engineer" />
30 Ingénieur
31 </label>
32 <label>
33 <input type="radio" name="role" value="scientist" />
34 Scientifique
35 </label>
36 </fieldset>
37
38 <button type="submit">Rejoindre l'équipage</button>
39 </form>
40 );
41}Résumé
L'accessibilité en React n'est pas un ajout, mais une partie intégrante du processus de création d'interface :
- HTML sémantique est le fondement - utilise
<nav>,<main>,<button>au lieu de<div>pour tout - Attributs ARIA complètent la sémantique quand les éléments natifs ne suffisent pas
- Gestion du focus est critique dans les SPA - une modale doit « piéger » le focus, la navigation doit être logique
- Navigation au clavier - chaque élément interactif doit être accessible sans souris
- Live regions (
aria-live) annoncent les changements de contenu dynamiques - Formulaires nécessitent des étiquettes, des descriptions d'erreurs et des rôles appropriés
Tout comme les systèmes d'une station spatiale doivent être conçus pour que chaque membre d'équipage puisse les utiliser en toutes conditions - même lorsque les lumières s'éteignent et que l'écran tactile cesse de fonctionner - une application React accessible garantit que chaque utilisateur peut l'utiliser efficacement.
Code de cette leçon : App.jsx
1import React, { useState, useRef, useEffect } from 'react';
2
3// ============================================
4// DEMO : Accessibilite (a11y) en React
5// ============================================
6
7// --- Composant 1 : Navigation avec ARIA ---
8function SpaceNavigation() {
9 return (
10 <nav aria-label="Menu principal de la station spatiale">
11 <ul style={navListStyle}>
12 {[
13 { label: 'Panneau de contrôle', icon: '' },
14 { label: 'Carte des étoiles', icon: '' },
15 { label: 'Équipage', icon: '' },
16 { label: 'Systèmes', icon: '' },
17 ].map(item => (
18 <li key={item.label}>
19 <button
20 style={navBtnStyle}
21 aria-label={item.label}
22 onClick={() => alert(item.label)}
23 >
24 <span aria-hidden="true">{item.icon}</span>
25 <span>{item.label}</span>
26 </button>
27 </li>
28 ))}
29 </ul>
30 </nav>
31 );
32}
33
34// --- Composant 2 : Modale avec focus trap ---
35function AccessibleModal({ isOpen, onClose, title, children }) {
36 const modalRef = useRef(null);
37 const closeRef = useRef(null);
38
39 useEffect(() => {
40 if (isOpen && closeRef.current) {
41 closeRef.current.focus();
42 }
43 }, [isOpen]);
44
45 const handleKeyDown = (e) => {
46 if (e.key === 'Escape') onClose();
47
48 if (e.key === 'Tab' && modalRef.current) {
49 const focusable = modalRef.current.querySelectorAll(
50 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
51 );
52 const first = focusable[0];
53 const last = focusable[focusable.length - 1];
54
55 if (e.shiftKey && document.activeElement === first) {
56 e.preventDefault();
57 last.focus();
58 } else if (!e.shiftKey && document.activeElement === last) {
59 e.preventDefault();
60 first.focus();
61 }
62 }
63 };
64
65 if (!isOpen) return null;
66
67 return (
68 <div style={overlayStyle} onClick={onClose}>
69 <div
70 ref={modalRef}
71 role="dialog"
72 aria-modal="true"
73 aria-labelledby="modal-title"
74 style={modalStyle}
75 onClick={(e) => e.stopPropagation()}
76 onKeyDown={handleKeyDown}
77 >
78 <h2 id="modal-title" style={{ color: '#00d4ff', marginBottom: '12px' }}>
79 {title}
80 </h2>
81 {children}
82 <button ref={closeRef} onClick={onClose} style={closeBtnStyle}>
83 Fermer (Esc)
84 </button>
85 </div>
86 </div>
87 );
88}
89
90// --- Composant 3 : Liste avec navigation au clavier ---
91function ShipSelector() {
92 const ships = [
93 { id: 1, name: 'Aurora', type: 'Cruiser', crew: 120 },
94 { id: 2, name: 'Nebula', type: 'Frégate', crew: 45 },
95 { id: 3, name: 'Vortex', type: 'Fighter', crew: 2 },
96 { id: 4, name: 'Horizon', type: 'Transporter', crew: 300 },
97 { id: 5, name: 'Eclipse', type: 'Éclaireur', crew: 8 },
98 ];
99
100 const [activeIndex, setActiveIndex] = useState(0);
101 const [selected, setSelected] = useState(null);
102
103 const handleKeyDown = (e) => {
104 switch (e.key) {
105 case 'ArrowDown':
106 e.preventDefault();
107 setActiveIndex(prev => Math.min(prev + 1, ships.length - 1));
108 break;
109 case 'ArrowUp':
110 e.preventDefault();
111 setActiveIndex(prev => Math.max(prev - 1, 0));
112 break;
113 case 'Enter':
114 case ' ':
115 e.preventDefault();
116 setSelected(ships[activeIndex]);
117 break;
118 }
119 };
120
121 return (
122 <div>
123 <h3 style={headingStyle}>Choisis un vaisseau (navigue avec les flèches)</h3>
124 <ul
125 role="listbox"
126 aria-label="Liste des vaisseaux spatiaux"
127 tabIndex={0}
128 onKeyDown={handleKeyDown}
129 style={{ listStyle: 'none', padding: 0, outline: 'none' }}
130 >
131 {ships.map((ship, index) => (
132 <li
133 key={ship.id}
134 role="option"
135 aria-selected={index === activeIndex}
136 onClick={() => { setActiveIndex(index); setSelected(ship); }}
137 style={{
138 padding: '10px 14px',
139 background: index === activeIndex ? '#1a3a5c' : '#0d1b2a',
140 borderRadius: '6px',
141 marginBottom: '4px',
142 cursor: 'pointer',
143 border: index === activeIndex ? '1px solid #00d4ff' : '1px solid transparent',
144 display: 'flex',
145 justifyContent: 'space-between',
146 }}
147 >
148 <span>{ship.name} - {ship.type}</span>
149 <span style={{ opacity: 0.5 }}>Équipage : {ship.crew}</span>
150 </li>
151 ))}
152 </ul>
153 {selected && (
154 <div aria-live="polite" style={selectedStyle}>
155 Vaisseau sélectionné : <strong>{selected.name}</strong> ({selected.type})
156 </div>
157 )}
158 </div>
159 );
160}
161
162// --- Composant 4 : Formulaire avec validation ---
163function CrewForm() {
164 const [name, setName] = useState('');
165 const [role, setRole] = useState('');
166 const [errors, setErrors] = useState({});
167 const [submitted, setSubmitted] = useState(false);
168
169 const validate = () => {
170 const newErrors = {};
171 if (!name.trim()) newErrors.name = 'Le nom est requis';
172 if (!role) newErrors.role = 'Choisis une spécialisation';
173 setErrors(newErrors);
174 return Object.keys(newErrors).length === 0;
175 };
176
177 const handleSubmit = (e) => {
178 e.preventDefault();
179 if (validate()) {
180 setSubmitted(true);
181 setName('');
182 setRole('');
183 setTimeout(() => setSubmitted(false), 3000);
184 }
185 };
186
187 return (
188 <form onSubmit={handleSubmit} aria-label="Inscription d'un membre de l'équipage">
189 <h3 style={headingStyle}>Inscription de l'équipage</h3>
190
191 <div style={{ marginBottom: '12px' }}>
192 <label htmlFor="crew-name" style={labelStyle}>Nom et prénom :</label>
193 <input
194 id="crew-name"
195 type="text"
196 value={name}
197 onChange={(e) => setName(e.target.value)}
198 aria-required="true"
199 aria-invalid={!!errors.name}
200 aria-describedby={errors.name ? 'name-error' : undefined}
201 style={inputStyle}
202 />
203 {errors.name && (
204 <span id="name-error" role="alert" style={errorStyle}>
205 {errors.name}
206 </span>
207 )}
208 </div>
209
210 <fieldset style={{ border: '1px solid #1a3a5c', borderRadius: '6px', padding: '12px', marginBottom: '12px' }}>
211 <legend style={{ color: '#00d4ff', padding: '0 8px' }}>Spécialisation :</legend>
212 {['pilote', 'ingénieur', 'scientifique', 'médecin'].map(r => (
213 <label key={r} style={{ display: 'block', marginBottom: '6px', cursor: 'pointer' }}>
214 <input
215 type="radio"
216 name="role"
217 value={r}
218 checked={role === r}
219 onChange={(e) => setRole(e.target.value)}
220 style={{ marginRight: '8px' }}
221 />
222 {r.charAt(0).toUpperCase() + r.slice(1)}
223 </label>
224 ))}
225 {errors.role && (
226 <span role="alert" style={errorStyle}>{errors.role}</span>
227 )}
228 </fieldset>
229
230 <button type="submit" style={submitBtnStyle}>
231 Rejoindre l'équipage
232 </button>
233
234 {submitted && (
235 <div aria-live="assertive" role="alert" style={successStyle}>
236 Inscription terminée avec succès !
237 </div>
238 )}
239 </form>
240 );
241}
242
243// --- Composant 5 : Live region ---
244function MissionStatus() {
245 const [status, setStatus] = useState('En attente');
246 const statuses = ['En attente', 'Compte à rebours', 'Décollage !', 'En vol', 'Orbite atteinte'];
247
248 return (
249 <div>
250 <h3 style={headingStyle}>Statut de la mission (aria-live)</h3>
251 <div aria-live="polite" aria-atomic="true" style={statusBoxStyle}>
252 Status: <strong style={{ color: '#ffd700' }}>{status}</strong>
253 </div>
254 <div style={{ display: 'flex', gap: '6px', marginTop: '8px', flexWrap: 'wrap' }}>
255 {statuses.map(s => (
256 <button
257 key={s}
258 onClick={() => setStatus(s)}
259 style={{
260 ...navBtnStyle,
261 background: status === s ? '#00d4ff' : '#1a2a3a',
262 color: status === s ? '#000' : '#fff',
263 }}
264 >
265 {s}
266 </button>
267 ))}
268 </div>
269 </div>
270 );
271}
272
273// --- Styles ---
274const navListStyle = { listStyle: 'none', padding: 0, display: 'flex', gap: '6px', flexWrap: 'wrap' };
275const navBtnStyle = {
276 padding: '8px 14px', background: '#1a2a3a', border: 'none',
277 borderRadius: '6px', color: '#fff', cursor: 'pointer', fontSize: '13px',
278 display: 'flex', gap: '6px', alignItems: 'center',
279};
280const overlayStyle = {
281 position: 'fixed', top: 0, left: 0, right: 0, bottom: 0,
282 background: 'rgba(0,0,0,0.7)', display: 'flex',
283 alignItems: 'center', justifyContent: 'center', zIndex: 1000,
284};
285const modalStyle = {
286 background: '#0d1b2a', borderRadius: '12px', padding: '24px',
287 maxWidth: '400px', width: '90%', border: '1px solid #1a3a5c',
288};
289const closeBtnStyle = {
290 marginTop: '16px', padding: '8px 16px', background: '#f44336',
291 border: 'none', borderRadius: '6px', color: '#fff', cursor: 'pointer',
292};
293const headingStyle = { color: '#00d4ff', marginBottom: '10px', fontSize: '15px' };
294const inputStyle = {
295 width: '100%', padding: '8px', background: '#0d1b2a',
296 border: '1px solid #1a3a5c', borderRadius: '6px', color: '#fff',
297 fontSize: '14px', boxSizing: 'border-box',
298};
299const labelStyle = { display: 'block', marginBottom: '4px', color: '#00d4ff', fontSize: '13px' };
300const errorStyle = { color: '#f44336', fontSize: '12px', display: 'block', marginTop: '4px' };
301const submitBtnStyle = {
302 padding: '10px 20px', background: '#4CAF50', border: 'none',
303 borderRadius: '6px', color: '#fff', cursor: 'pointer', fontSize: '14px',
304};
305const successStyle = {
306 marginTop: '12px', padding: '10px', background: '#1a3a1a',
307 borderRadius: '6px', color: '#4CAF50', border: '1px solid #4CAF50',
308};
309const selectedStyle = {
310 marginTop: '8px', padding: '10px', background: '#1a2a3a',
311 borderRadius: '6px', border: '1px solid #00d4ff',
312};
313const statusBoxStyle = {
314 padding: '12px', background: '#0d1b2a', borderRadius: '6px',
315 border: '1px solid #1a3a5c',
316};
317const sectionStyle = {
318 marginBottom: '24px', padding: '16px',
319 background: 'rgba(255,255,255,0.03)', borderRadius: '12px',
320 border: '1px solid rgba(255,255,255,0.08)',
321};
322
323function App() {
324 const [modalOpen, setModalOpen] = useState(false);
325
326 return (
327 <div style={{ padding: '20px', background: '#0a0e1a', color: '#e0e0e0', minHeight: '100vh', fontFamily: 'system-ui' }}>
328 <h1 style={{ color: '#ffd700', textAlign: 'center', marginBottom: '6px' }}>
329 Accessibility (a11y) en React
330 </h1>
331 <p style={{ textAlign: 'center', opacity: 0.6, marginBottom: '24px', fontSize: '13px' }}>
332 Teste Tab, flèches, Enter, Esc
333 </p>
334
335 <section style={sectionStyle}>
336 <h2 style={{ color: '#00d4ff', marginBottom: '10px' }}>Navigation sémantique</h2>
337 <SpaceNavigation />
338 </section>
339
340 <section style={sectionStyle}>
341 <h2 style={{ color: '#00d4ff', marginBottom: '10px' }}>Modale avec focus trap</h2>
342 <button onClick={() => setModalOpen(true)} style={navBtnStyle}>
343 Ouvrir la modale de mission
344 </button>
345 <AccessibleModal
346 isOpen={modalOpen}
347 onClose={() => setModalOpen(false)}
348 title="Détails de la mission Alpha"
349 >
350 <p>Objectif : Exploration du système Kepler-442.</p>
351 <p>Durée : 18 mois.</p>
352 <input placeholder="Ajouter une note..." style={{ ...inputStyle, marginTop: '8px' }} />
353 </AccessibleModal>
354 </section>
355
356 <section style={sectionStyle}>
357 <ShipSelector />
358 </section>
359
360 <section style={sectionStyle}>
361 <MissionStatus />
362 </section>
363
364 <section style={sectionStyle}>
365 <CrewForm />
366 </section>
367 </div>
368 );
369}
370
371export default App;Tu as repéré une erreur dans cette leçon ?
Teste tes connaissances
Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.
1. Quel est le but principal du pattern Adapter en React ?
2. Comment implémente-t-on le plus souvent le pattern Facade en React ?
Voici 2 des 4 questions de cette leçon. Réponds aux autres dans le jeu.
Tâches pratiques dans le jeu
- Clique dans l'ordre
Ordonne les attributs d'accessibilité d'un champ de saisie avec une erreur de validation :
- Éditeur de code
Liste virtualisée avec optimisations
- Clique dans l'ordre
Ordonne la syntaxe de React.forwardRef :
- Tri vertical
Ordonne l'implémentation du HOC :
- Éditeur de code
Error Boundary avancé