Pourquoi "click outside" est un casse-tête pour chaque développeur front-end ?
\n\nImaginez : vous créez une fenêtre modale, un menu déroulant ou une liste déroulante. L'utilisateur l'ouvre, puis clique quelque part dans le vide. Logique ? Oui. Simple à implémenter ? C'est là que les ennuis commencent.
\n\nLa question "How do I detect a click outside an element?" est l'une des plus populaires sur Stack Overflow. Des milliers de développeurs se cassent les dents en essayant de comprendre pourquoi leur code fonctionne tantôt, tantôt pas. Aujourd'hui, nous allons analyser les erreurs les plus courantes lors de la détection d'un clic en dehors d'un élément en JavaScript et apprendre à le faire correctement.
\n\nCet article est rédigé pour le portail éducatif pythonlib.ru, mais le code sera en JavaScript pur — aucun framework, que du hardcore. C'est parti !
\n\nErreur n°1 : Écouter le clic sur l'élément lui-même
\n\nL'erreur la plus naïve, mais courante. Les débutants tentent d'attacher un gestionnaire à l'élément et d'attendre que l'utilisateur clique à côté. Cela ressemble à : "Je ne sais pas quand l'utilisateur cliquera en dehors, mais je vais marquer cet événement".
\n\nCela ne fonctionne pas. L'événement click ne se déclenche que lorsque l'utilisateur clique sur l'élément. Si le clic est en dehors, l'événement n'est pas généré. Vous ne saurez tout simplement pas qu'un clic a eu lieu.
Comment faire correctement ?
\nIl faut écouter le clic sur le document (ou sur window), puis vérifier si le clic a eu lieu à l'intérieur ou à l'extérieur de votre élément.
const myElement = document.querySelector('.my-dropdown');\n\ndocument.addEventListener('click', function(event) {\n // Vérifie si on a cliqué à l'intérieur de myElement\n if (!myElement.contains(event.target)) {\n // Clic à l'extérieur — on ferme !\n myElement.style.display = 'none';\n }\n});\n\nC'est la base. Mais il y a aussi des pièges. Passons à l'erreur suivante.
\n\nErreur n°2 : Ignorer la propagation des événements (Event Bubbling)
\n\nVous avez écrit le code ci-dessus, mais il ne fonctionne toujours pas ? Le problème vient probablement de la propagation des événements. Lorsque vous cliquez sur un élément enfant, l'événement se déclenche d'abord sur celui-ci, puis "remonte" dans l'arbre DOM jusqu'au document.
Si quelque part sur le chemin se trouve un event.stopPropagation(), l'événement mourra et n'atteindra pas le document. Votre gestionnaire ne se déclenchera pas.
Comment l'éviter ?
\nN'utilisez jamais stopPropagation() sans nécessité absolue. Si vous écrivez un composant qui intercepte le clic (par exemple, un bouton dans une liste déroulante), utilisez event.stopImmediatePropagation() uniquement si vous êtes sûr que cela ne cassera pas les gestionnaires externes.
La meilleure solution est de ne pas compter sur la propagation, mais d'attacher le gestionnaire en phase de capture (capture phase). Pour cela, passez le troisième argument true à addEventListener :
document.addEventListener('click', function(event) {\n // Votre code\n}, true); // Phase capture : l'événement atteint d'abord document, puis descend\n\nMaintenant, l'événement sera intercepté avant que quelqu'un ne l'arrête. Mais c'est une arme à double tranchant — utilisez-la avec précaution.
\n\nErreur n°3 : Comparaison incorrecte avec event.target
\n\nUn classique : vous vérifiez event.target === myElement. Cela fonctionne uniquement si l'utilisateur clique exactement sur l'élément lui-même. Mais que faire si à l'intérieur de l'élément il y a une image, un bouton ou simplement du texte ?
Un clic sur un nœud enfant donnera event.target égal à ce nœud enfant, et non à votre élément. La condition ne sera pas remplie et l'élément ne se fermera pas.
Solution : utilisez Node.contains()
\nLa méthode contains() vérifie si le nœud passé est un descendant du nœud donné (ou le nœud lui-même). C'est parfait pour notre tâche.
// Correct\nif (!myElement.contains(event.target)) {\n // Clic à l'extérieur\n}\n\n// Incorrect\nif (event.target !== myElement) {\n // Ignorera les clics sur les éléments enfants\n}