Cours JavaScript et TypeScript · Module 8: Événements et DOM

Supprimer les écouteurs d'événements et nettoyer la mémoire

6 min de lecture

Parfois, il faut supprimer des écouteurs d'événements pour qu'ils n'occupent plus de mémoire ou qu'ils cessent de réagir aux événements. C'est comme éteindre les systèmes d'alarme devenus inutiles dans le parc.

« La pire alarme, c'est celle qui surveille un enclos vide », dit Ray Arnold. « L'enclos a été démonté il y a un mois, mais le capteur consomme toujours du courant et réveille de temps en temps toute l'équipe de sécurité. » Dans une application, c'est pareil : un écouteur que personne n'a retiré continue de réagir aux événements et garde en mémoire tout ce qu'il touche.

Bien gérer les écouteurs d'événements

Pour supprimer un écouteur, on utilise la méthode removeEventListener(). Elle prend les mêmes arguments que addEventListener() : le type d'événement et la fonction de gestion.

1// Fonction qui gère les clics
2function handleEmergencyClick() {
3  console.log("Activation du protocole d'urgence !");
4  evacuatePark();
5}
6
7const emergencyButton = document.getElementById('emergency-btn');
8
9// Ajout de l'écouteur d'événement
10emergencyButton.addEventListener('click', handleEmergencyClick);
11
12// Suppression de l'écouteur d'événement (il faut la même fonction !)
13emergencyButton.removeEventListener('click', handleEmergencyClick);

Le bouton d'alarme est de nouveau sourd, car le navigateur a trouvé dans sa liste exactement la fonction qu'on avait attachée plus tôt. Voici le cycle de vie complet d'un écouteur : l'ajout, le déclenchement de l'événement, l'exécution de la fonction et, pour finir, la suppression. Le bouton lui-même ne disparaît nulle part, seul son capteur disparaît.

Le piège : la fonction anonyme

L'erreur la plus fréquente lors du nettoyage a l'air innocente. Deux fonctions fléchées identiques semblent être la même chose, mais pour JavaScript, ce sont deux objets différents.

1// Ceci NE supprimera PAS l'écouteur - deux fonctions différentes avec le même code
2emergencyButton.addEventListener('click', () => evacuatePark());
3emergencyButton.removeEventListener('click', () => evacuatePark());

L'écouteur reste en place, car JavaScript compare les références des fonctions, et non leur code source. Si tu prévois une suppression, enregistre la fonction sous un nom ou dans une variable. Pense aussi au drapeau de la phase de capture : un écouteur ajouté avec true ne peut être supprimé que par un appel qui passe lui aussi true.

Un écouteur temporaire

Parfois, un capteur ne doit fonctionner que pendant une durée précise, par exemple pendant un exercice d'alarme. On le retire alors une fois le délai écoulé, dans setTimeout().

1// Exemple avec un écouteur d'événement temporaire
2function temporaryEventHandler() {
3  console.log('Cet écouteur ne sera actif que pendant 10 secondes');
4}
5
6const tempButton = document.getElementById('temp-button');
7tempButton.addEventListener('click', temporaryEventHandler);
8
9// Suppression automatique au bout de 10 secondes
10setTimeout(() => {
11  tempButton.removeEventListener('click', temporaryEventHandler);
12  console.log("L'écouteur a été supprimé");
13}, 10000);

Au bout de dix secondes, les clics sur le bouton ne font plus rien, alors que le bouton est toujours sur la page. Quand un écouteur ne doit agir qu'une fois, il est plus simple d'utiliser l'option { once: true } vue dans la leçon sur les écouteurs, car le navigateur le retirera lui-même après le premier appel.

AbortController - un seul interrupteur pour plusieurs capteurs

Quand il y a plusieurs écouteurs, garder la trace de toutes les références devient fatigant. C'est là que AbortController vient à la rescousse : tu crées un contrôleur, tu passes son signal dans les options de l'écouteur, et un seul appel à abort() retire tout d'un coup. Voyons cela avec le défilement infini du catalogue des dinosaures.

1const scrollController = new AbortController();
2
3window.addEventListener('scroll', () => {
4  const { scrollTop, scrollHeight, clientHeight } = document.documentElement;
5
6  if (scrollTop + clientHeight >= scrollHeight - 100) {
7    loadMoreDinosaurs();
8  }
9}, { signal: scrollController.signal });
10
11// Le catalogue est terminé - on coupe le capteur d'un seul geste
12function onCatalogEnd() {
13  scrollController.abort();
14}

La propriété scrollTop indique de combien la page a défilé, clientHeight est la hauteur de la fenêtre visible, et scrollHeight la hauteur de toute la page. Quand il reste moins de 100 pixels jusqu'à la fin, on charge le lot suivant. Remarque que la fonction de gestion n'a pas de nom, et pourtant on peut la supprimer, car le navigateur l'a liée au signal du contrôleur. Tu peux passer le même signal à autant d'écouteurs que tu veux. Un contrôleur déjà utilisé ne fonctionne plus, alors pour de nouveaux écouteurs, tu en crées un nouveau.

D'où viennent les fuites de mémoire

Une fuite de mémoire, c'est une situation dans laquelle un objet devenu inutile ne peut pas être libéré, car quelque chose pointe encore vers lui. Le moteur JavaScript ne libère que ce vers quoi plus aucune référence ne mène. Avec les événements, le scénario typique est le suivant : un écouteur est attaché à window ou à document, qui vivent aussi longtemps que la page, et sa fonction fait référence à une carte supprimée. La carte a disparu de l'écran, mais pas de la mémoire, car l'écouteur la retient toujours. Une variable oubliée qui pointe encore vers un élément supprimé fait la même chose.

Le nettoyage en quatre étapes

Supprimer en toute sécurité un élément équipé d'un capteur suit toujours la même procédure, comme la fermeture d'un enclos selon une checklist.

1let raptorCard = document.getElementById('raptor-card');
2
3function handleCardClick() {
4  console.log('Carte du raptor cliquée');
5}
6
7raptorCard.addEventListener('click', handleCardClick);
8
9// Nettoyage, quand la carte doit disparaître
10raptorCard.removeEventListener('click', handleCardClick);
11raptorCard.remove();
12raptorCard = null;

Cette procédure est une bonne habitude : conserve une référence à la fonction de gestion, appelle removeEventListener() avec cette même référence, retire l'élément du DOM, et pour finir, remets les variables à zéro en leur affectant null. Les moteurs actuels libèrent un élément retiré avec ses propres écouteurs dès que plus rien n'y mène, donc pour la mémoire ce qui compte le plus, ce sont les variables et les écouteurs sur window ou document qui font encore référence à la carte.

Mon ordre de choix des outils : { once: true } pour les réactions ponctuelles, AbortController quand un composant a plusieurs écouteurs, et removeEventListener() quand tu retires un capteur précis. Dans le bac à sable ci-dessous, tu peux ajouter et supprimer des écouteurs en observant ce qui se passe. Dans la leçon sur les composants, nous réunirons tout le nettoyage dans une seule méthode destroy().

Retiens bien : chaque capteur que tu allumes dans le parc doit avoir une extinction prévue.

Code de cette leçon : index.html
1<!DOCTYPE html>
2<html lang="en">
3<head>
4    <meta charset="UTF-8">
5    <meta name="viewport" content="width=device-width, initial-scale=1.0">
6    <title>Supprimer les écouteurs d'événements</title>
7    <style>
8        body {
9            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
10            background: linear-gradient(135deg, #2c3e50 0%, #34495e 100%);
11            margin: 0;
12            padding: 20px;
13            color: white;
14            min-height: 100vh;
15        }
16
17        .container {
18            max-width: 800px;
19            margin: 0 auto;
20        }
21
22        h1 {
23            text-align: center;
24            margin-bottom: 30px;
25        }
26
27        .demo-section {
28            background: rgba(0, 0, 0, 0.7);
29            border-radius: 15px;
30            padding: 30px;
31            margin-bottom: 20px;
32            border: 2px solid #34495e;
33        }
34
35        h2 {
36            color: #3498db;
37            margin-bottom: 20px;
38        }
39
40        .button-group {
41            display: grid;
42            grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
43            gap: 15px;
44            margin: 20px 0;
45        }
46
47        button {
48            padding: 12px 20px;
49            border: none;
50            border-radius: 8px;
51            font-size: 16px;
52            font-weight: bold;
53            cursor: pointer;
54            transition: all 0.3s ease;
55        }
56
57        .btn-start {
58            background: linear-gradient(45deg, #27ae60, #2ecc71);
59            color: white;
60        }
61
62        .btn-stop {
63            background: linear-gradient(45deg, #e74c3c, #c0392b);
64            color: white;
65        }
66
67        .btn-action {
68            background: linear-gradient(45deg, #3498db, #2980b9);
69            color: white;
70        }
71
72        button:hover {
73            transform: translateY(-2px);
74            box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
75        }
76
77        button:disabled {
78            background: #7f8c8d;
79            cursor: not-allowed;
80            transform: none;
81        }
82
83        .dino-box {
84            background: rgba(52, 152, 219, 0.2);
85            border: 2px solid #3498db;
86            border-radius: 10px;
87            padding: 30px;
88            text-align: center;
89            margin: 20px 0;
90            cursor: pointer;
91            transition: all 0.3s ease;
92        }
93
94        .dino-box:hover {
95            transform: scale(1.05);
96            box-shadow: 0 10px 30px rgba(52, 152, 219, 0.3);
97        }
98
99        .dino-box.inactive {
100            opacity: 0.5;
101            border-color: #7f8c8d;
102            cursor: not-allowed;
103        }
104
105        .dino-emoji {
106            font-size: 80px;
107            display: block;
108            margin-bottom: 10px;
109        }
110
111        .status {
112            display: inline-block;
113            padding: 5px 15px;
114            border-radius: 20px;
115            margin-top: 10px;
116            font-weight: bold;
117        }
118
119        .status.active {
120            background: #27ae60;
121        }
122
123        .status.inactive {
124            background: #e74c3c;
125        }
126
127        .log {
128            background: #000;
129            border-radius: 8px;
130            padding: 15px;
131            max-height: 200px;
132            overflow-y: auto;
133            font-family: 'Courier New', monospace;
134            font-size: 13px;
135            margin-top: 20px;
136        }
137
138        .log-entry {
139            padding: 3px 0;
140            border-bottom: 1px solid #333;
141        }
142
143        .log-entry.add {
144            color: #2ecc71;
145        }
146
147        .log-entry.remove {
148            color: #e74c3c;
149        }
150
151        .log-entry.event {
152            color: #3498db;
153        }
154
155        .timer-display {
156            font-size: 48px;
157            text-align: center;
158            margin: 20px 0;
159            color: #3498db;
160            font-weight: bold;
161        }
162
163        .info-box {
164            background: rgba(241, 196, 15, 0.1);
165            border: 1px solid #f1c40f;
166            border-radius: 8px;
167            padding: 15px;
168            margin: 20px 0;
169            color: #f1c40f;
170        }
171    </style>
172</head>
173<body>
174    <div class="container">
175        <h1>Supprimer les écouteurs d'événements</h1>
176
177        <!-- Demo 1: Ajout/suppression basique -->
178        <div class="demo-section">
179            <h2>Démo 1 : zone de dinosaure cliquable</h2>
180            <p>Clique sur « Démarrer l'écouteur » pour activer le champ, puis « Arrêter l'écouteur » pour le désactiver.</p>
181
182            <div class="button-group">
183                <button id="start-listener" class="btn-start">Démarrer l'écouteur</button>
184                <button id="stop-listener" class="btn-stop">Arrêter l'écouteur</button>
185            </div>
186
187            <div id="dino-box" class="dino-box inactive">
188                <span class="dino-emoji">◆</span>
189                <h3>T-Rex</h3>
190                <p>Clique-moi si je suis actif !</p>
191                <span class="status inactive" id="dino-status">INACTIF</span>
192            </div>
193
194            <div class="log" id="demo1-log"></div>
195        </div>
196
197        <!-- Demo 2: Timer avec suppression automatique -->
198        <div class="demo-section">
199            <h2>Démo 2 : minuteur temporaire</h2>
200            <p>Le minuteur ne sera actif que 10 secondes, puis s'éteindra automatiquement.</p>
201
202            <div class="button-group">
203                <button id="start-timer" class="btn-start">Démarrer le minuteur (10s)</button>
204                <button id="stop-timer" class="btn-stop">Arrêter le minuteur</button>
205            </div>
206
207            <div class="timer-display" id="timer-display">00:00</div>
208
209            <div class="info-box" id="timer-info">
210                Press "Start Timer" to begin countdown. Event listener will be automatically removed after 10 seconds.
211            </div>
212
213            <div class="log" id="demo2-log"></div>
214        </div>
215
216        <!-- Demo 3: Multiple listeners -->
217        <div class="demo-section">
218            <h2>Démo 3 : gérer plusieurs écouteurs</h2>
219            <p>Contrôle différents types d'événements indépendamment.</p>
220
221            <div class="button-group">
222                <button id="add-click" class="btn-start">Ajouter Click</button>
223                <button id="remove-click" class="btn-stop">Retirer Click</button>
224                <button id="add-hover" class="btn-start">Ajouter Hover</button>
225                <button id="remove-hover" class="btn-stop">Retirer Hover</button>
226            </div>
227
228            <div id="multi-dino" class="dino-box">
229                <span class="dino-emoji">◆</span>
230                <h3>Triceratops</h3>
231                <p>Teste différents événements !</p>
232                <div>
233                    <span class="status inactive" id="click-status">Click: OFF</span>
234                    <span class="status inactive" id="hover-status">Hover: OFF</span>
235                </div>
236            </div>
237
238            <div class="log" id="demo3-log"></div>
239        </div>
240    </div>
241
242    <script>
243        console.log('Event Listener Management System - START');
244
245        // DEMO 1: Ajout/suppression basique
246        const dinoBox = document.getElementById('dino-box');
247        const dinoStatus = document.getElementById('dino-status');
248        const demo1Log = document.getElementById('demo1-log');
249
250        function logDemo1(message, type = 'event') {
251            const entry = document.createElement('div');
252            entry.className = `log-entry ${type}`;
253            entry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
254            demo1Log.appendChild(entry);
255            demo1Log.scrollTop = demo1Log.scrollHeight;
256        }
257
258        // Click handler function - MUST be named to be removable
259        function handleDinoClick() {
260            console.log('T-Rex was clicked!');
261            logDemo1('T-Rex was clicked!', 'event');
262            dinoBox.style.transform = 'scale(1.1)';
263            setTimeout(() => {
264                dinoBox.style.transform = 'scale(1)';
265            }, 200);
266        }
267
268        // Start listener
269        document.getElementById('start-listener').addEventListener('click', function() {
270            dinoBox.addEventListener('click', handleDinoClick);
271            dinoBox.classList.remove('inactive');
272            dinoStatus.textContent = 'ACTIF';
273            dinoStatus.className = 'status active';
274            logDemo1('Event listener AJOUTÉ', 'add');
275            console.log('Event listener ajouté au T-Rex');
276        });
277
278        // Stop listener
279        document.getElementById('stop-listener').addEventListener('click', function() {
280            dinoBox.removeEventListener('click', handleDinoClick);
281            dinoBox.classList.add('inactive');
282            dinoStatus.textContent = 'INACTIF';
283            dinoStatus.className = 'status inactive';
284            logDemo1('Event listener REMOVED', 'remove');
285            console.log('Event listener removed from T-Rex');
286        });
287
288        // DEMO 2: Timer avec suppression automatique
289        const timerDisplay = document.getElementById('timer-display');
290        const timerInfo = document.getElementById('timer-info');
291        const demo2Log = document.getElementById('demo2-log');
292
293        let timerInterval = null;
294        let timerTimeout = null;
295        let seconds = 0;
296
297        function logDemo2(message, type = 'event') {
298            const entry = document.createElement('div');
299            entry.className = `log-entry ${type}`;
300            entry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
301            demo2Log.appendChild(entry);
302            demo2Log.scrollTop = demo2Log.scrollHeight;
303        }
304
305        function updateTimer() {
306            seconds++;
307            const mins = Math.floor(seconds / 60).toString().padStart(2, '0');
308            const secs = (seconds % 60).toString().padStart(2, '0');
309            timerDisplay.textContent = `${mins}:${secs}`;
310        }
311
312        function stopTimer() {
313            if (timerInterval) {
314                clearInterval(timerInterval);
315                timerInterval = null;
316                logDemo2('Timer ARRÊTÉ', 'remove');
317                timerInfo.textContent = 'Timer has been stopped.';
318                timerInfo.style.borderColor = '#e74c3c';
319            }
320            if (timerTimeout) {
321                clearTimeout(timerTimeout);
322                timerTimeout = null;
323            }
324        }
325
326        document.getElementById('start-timer').addEventListener('click', function() {
327            stopTimer(); // Arrête le timer précédent
328            seconds = 0;
329            timerDisplay.textContent = '00:00';
330
331            timerInterval = setInterval(updateTimer, 1000);
332            logDemo2('Timer DÉMARRÉ (arrêt automatique dans 10s)', 'add');
333            timerInfo.textContent = 'Timer active! Auto-disable in 10 seconds...';
334            timerInfo.style.borderColor = '#f1c40f';
335
336            // Auto-remove after 10 seconds
337            timerTimeout = setTimeout(() => {
338                stopTimer();
339                logDemo2('Timer automatically DISABLED after 10 seconds', 'remove');
340                timerInfo.textContent = 'Timer was automatically disabled after 10 seconds. Event listener has been removed!';
341                timerInfo.style.borderColor = '#27ae60';
342                console.log('Timer automatiquement arrêté et nettoyé');
343            }, 10000);
344        });
345
346        document.getElementById('stop-timer').addEventListener('click', stopTimer);
347
348        // DEMO 3: Multiple listeners
349        const multiDino = document.getElementById('multi-dino');
350        const clickStatus = document.getElementById('click-status');
351        const hoverStatus = document.getElementById('hover-status');
352        const demo3Log = document.getElementById('demo3-log');
353
354        function logDemo3(message, type = 'event') {
355            const entry = document.createElement('div');
356            entry.className = `log-entry ${type}`;
357            entry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
358            demo3Log.appendChild(entry);
359            demo3Log.scrollTop = demo3Log.scrollHeight;
360        }
361
362        // Handler functions - must be named
363        function handleMultiClick() {
364            logDemo3('Triceratops CLICKED!', 'event');
365            console.log('Click event');
366        }
367
368        function handleMultiMouseEnter() {
369            logDemo3('Survol du Triceratops', 'event');
370            multiDino.style.borderColor = '#2ecc71';
371        }
372
373        function handleMultiMouseLeave() {
374            multiDino.style.borderColor = '#3498db';
375        }
376
377        // Ajout du Click Listener
378        document.getElementById('add-click').addEventListener('click', function() {
379            multiDino.addEventListener('click', handleMultiClick);
380            clickStatus.textContent = 'Click: ON';
381            clickStatus.className = 'status active';
382            logDemo3('Click listener AJOUTÉ', 'add');
383        });
384
385        // Suppression du Click Listener
386        document.getElementById('remove-click').addEventListener('click', function() {
387            multiDino.removeEventListener('click', handleMultiClick);
388            clickStatus.textContent = 'Click: OFF';
389            clickStatus.className = 'status inactive';
390            logDemo3('Click listener REMOVED', 'remove');
391        });
392
393        // Adding Hover Listeners
394        document.getElementById('add-hover').addEventListener('click', function() {
395            multiDino.addEventListener('mouseenter', handleMultiMouseEnter);
396            multiDino.addEventListener('mouseleave', handleMultiMouseLeave);
397            hoverStatus.textContent = 'Hover: ON';
398            hoverStatus.className = 'status active';
399            logDemo3('Hover listeners AJOUTÉS', 'add');
400        });
401
402        // Removing Hover Listeners
403        document.getElementById('remove-hover').addEventListener('click', function() {
404            multiDino.removeEventListener('mouseenter', handleMultiMouseEnter);
405            multiDino.removeEventListener('mouseleave', handleMultiMouseLeave);
406            hoverStatus.textContent = 'Hover: OFF';
407            hoverStatus.className = 'status inactive';
408            multiDino.style.borderColor = '#3498db';
409            logDemo3('Hover listeners REMOVED', 'remove');
410        });
411
412        // Cleanup on page close
413        window.addEventListener('beforeunload', function() {
414            stopTimer();
415            console.log('All event listeners cleaned up before closing');
416        });
417
418        logDemo1('Système prêt à fonctionner', 'add');
419        logDemo2('Système prêt à fonctionner', 'add');
420        logDemo3('Système prêt à fonctionner', 'add');
421    </script>
422</body>
423</html>

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. 1. Qu'est-ce qui peut provoquer une fuite mémoire liée aux écouteurs d'événements ?

  2. 2. Pourquoi est-il important de supprimer les écouteurs d'événements quand un élément est retiré du DOM ?

Voici 2 des 3 questions de cette leçon. Réponds aux autres dans le jeu.

Tâches pratiques dans le jeu

  • Tri vertical

    Place les éléments pour utiliser AbortController afin de supprimer un écouteur d'événement :

  • Éditeur de code

    Le fichier index.html contient une galerie #gallery avec trois miniatures (l'attribut data-full indique la grande version) et une lightbox cachée #lightbox (style.css l'affiche avec la classe open). Complète les blancs du script : ___BLANK1___ est l'événement de clic, et ___BLANK2___ est la propriété de l'objet event qui désigne l'élément cliqué (on vérifie si c'est un IMG). Un clic sur une photo doit ouvrir la lightbox avec la grande version de cette photo et la légende tirée de alt, et un clic dans l'espace entre les photos ne doit pas l'ouvrir.

  • Tri vertical

    Place dans l'ordre les phases du cycle de vie d'un écouteur d'événement :

  • Éditeur de code

    Le fichier index.html contient une liste défilante #dino-list (style.css lui donne une hauteur de 300 px et overflow-y: auto). Le script charge les dinosaures par lots de 10. Complète les blancs : ___BLANK1___ est la méthode qui ajoute un nouvel élément li à la fin de la liste, et ___BLANK2___ est la propriété qui donne la hauteur de tout le contenu de la liste (scrollTop correspond aux pixels défilés, clientHeight à la hauteur de la zone visible). Quand il reste moins de 50 pixels avant la fin de la liste, elle doit charger les 10 éléments suivants, et ne rien ajouter avant.

  • Éditeur de code

    Le fichier index.html contient un bouton #menu-toggle et un menu #main-menu. Sur un écran étroit, style.css n'affiche le menu que s'il a la classe open (sur un écran de plus de 900 px, le menu est toujours visible et le bouton est caché). Complète les blancs : ___BLANK1___ est l'événement de clic, et ___BLANK2___ est la méthode de classList qui ajoute la classe open quand elle manque et la retire quand elle est présente (en renvoyant true ou false). Le premier clic doit déplier le menu et mettre aria-expanded="true", le second le replier et mettre aria-expanded="false".

  • Éditeur de code

    Le fichier index.html contient un champ #dino-search et une liste de suggestions #suggestions, et le script a un tableau d'espèces de dinosaures. Complète les blancs : ___BLANK1___ est l'événement que le champ envoie après chaque modification du texte saisi, et ___BLANK2___ est la méthode de chaîne qui vérifie si un nom commence par le fragment saisi. Après avoir tapé « tri », la liste doit afficher seulement Triceratops (les majuscules ne comptent pas), un champ vide ou un texte sans correspondance vide la liste, et un clic sur une suggestion la place dans le champ.

  • Éditeur de code

    Le fichier index.html contient un panneau avec un paragraphe #status, et le script a un seul écouteur keydown sur le document. Complète les blancs : ___BLANK1___ est la méthode de l'objet event qui empêche le navigateur d'enregistrer la page avec Ctrl+S, ___BLANK2___ est la valeur de event.key pour le raccourci Ctrl+O (lettre minuscule), et ___BLANK3___ est la valeur de event.key pour la touche Échap. Ctrl+S doit enregistrer, Ctrl+O ouvrir le rapport, Échap fermer le panneau, et la touche S seule, sans Ctrl, ne doit rien changer.

  • Éditeur de code

    Le fichier index.html contient trois onglets (role="tab") avec leurs panneaux et un écouteur keydown sur le conteneur #dino-tabs. La flèche droite déplace le focus vers l'onglet suivant, et Entrée choisit l'onglet qui a le focus. Complète les blancs : ___BLANK1___ est la valeur de event.key pour la flèche gauche (elle déplace le focus vers l'onglet précédent, du premier vers le dernier), et ___BLANK2___ est une condition vraie quand on appuie sur Espace (vérifie event.key ou event.code). Entrée et Espace doivent afficher le panneau de l'onglet qui a le focus, et les flèches et Espace ne doivent pas faire défiler la page.

  • Tri horizontal

    Place les éléments d'un gestionnaire qui arrête la propagation d'un événement :

  • Tri vertical

    Place dans l'ordre les étapes pour nettoyer les écouteurs d'événements lors de la suppression d'un élément du DOM :

Articles utiles