Création d'un composant de réaction de mise au point externe et de gestionnaire de clic

react-foco
) à partir de zéro. Pour tirer le meilleur parti de cet article, vous aurez besoin d'une compréhension de base des classes JavaScript, de la délégation d'événements DOM et de React. À la fin de l'article, vous saurez comment utiliser les propriétés d'instance de classe JavaScript et la délégation d'événements pour créer un composant React qui vous aide à détecter un clic ou un focus en dehors de tout composant React.Souvent, nous devons détecter quand un clic s'est produit en dehors d'un élément ou lorsque le focus s'est déplacé en dehors de celui-ci. Certains des exemples évidents de ce cas d'utilisation sont les menus déroulants, les listes déroulantes, les info-bulles et les popovers. Commençons le processus de création de cette fonctionnalité de détection.
La méthode DOM pour détecter l'extérieur Cliquez
Si on vous a demandé d'écrire du code pour détecter si un clic s'est produit à l'intérieur ou à l'extérieur d'un nœud DOM , Qu'est-ce que tu ferais? Il y a de fortes chances que vous utilisiez l'API DOM Node.contains
. Voici comment MDN l'explique:
La méthode
Node.contains ()
renvoie une valeurBoolean
indiquant si un nœud est un descendant d'un nœud donné, c'est-à-dire le nœud lui-même, un de ses enfants directs (childNodes
), l'un des enfants directs des enfants, etc.
Testons-le rapidement. Créons un élément pour lequel nous voulons détecter les clics extérieurs. Je lui ai attribué une classe click-text
.
click inside and outside me
const concernElement = document.querySelector (". Click-text");
document.addEventListener ("mousedown", (événement) => {
if (concernElement.contains (event.target)) {
console.log ("Cliqué à l'intérieur");
} autre {
console.log ("Cliqué à l'extérieur / ailleurs");
}
});
Nous avons fait les choses suivantes:
- Nous avons sélectionné l'élément HTML avec la classe
click-text
. - Placez un écouteur d'événement avec la souris sur le document
- Dans la fonction de rappel, nous vérifions si l'élément concerné – pour lequel nous devons détecter le clic extérieur – contient l'élément (lui-même compris) qui a déclenché l'événement
mousedown
(event.target
).
Si l'élément qui a déclenché l'événement mouse down est soit notre élément concerné, soit tout élément qui se trouve à l'intérieur de l'élément concerné, cela signifie que nous avons cliqué à l'intérieur de l'élément concerné. [19659021] Cliquons à l'intérieur et à l'extérieur de l'élément dans la boîte de code ci-dessous, et vérifions la console.
Emballage de la logique de détection basée sur la hiérarchie DOM dans un composant React
Excellent! Jusqu'à présent, nous avons vu comment utiliser l'API DOM Node.contains
pour détecter un clic en dehors d'un élément. Nous pouvons envelopper cette logique dans un composant React. Nous pourrions nommer notre nouveau composant React OutsideClickHandler
. Notre composant OutsideClickHandler
fonctionnera comme ceci:
{
console.log ("Je suis appelé chaque fois qu'un clic se produit en dehors du composant 'AnyOtherReactComponent'")
}}
>
OutsideClickHandler
comprend deux accessoires:
children
Il peut s'agir de n'importe quel enfant React valide. Dans l'exemple ci-dessus, nous transmettons le composantAnyOtherReactComponent
comme enfant deOutsideClickHandler
.onOutsideClick
Cette fonction sera appelée si une un clic se produit n'importe où en dehors du composantAnyOtherReactComponent
.
Ça sonne bien jusqu'à présent? Commençons à construire notre composant OutsideClickHandler
.
import React de 'react';
La classe OutsideClickHandler étend React.Component {
render () {
retournez this.props.children;
}
}
Juste un composant de base de React. Jusqu'à présent, nous n'en faisons pas grand-chose. Nous renvoyons simplement les enfants au fur et à mesure qu'ils sont transmis à notre composant OutsideClickHandler
. Emballons les children
avec un élément div et y attacher une référence React.
import React, {createRef} from 'react';
La classe OutsideClickHandler étend React.Component {
wrapperRef = createRef ();
render () {
revenir (
{this.props.children}
)
}
}
Nous utiliserons cette ref
pour accéder à l'objet nœud DOM associé à l'élément div
. En utilisant cela, nous allons recréer la logique de détection extérieure que nous avons faite ci-dessus.
Attachons l'événement mousedown
sur le document à l'intérieur de componentDidMount
Méthode du cycle de vie de réaction, et nettoyons cet événement à l'intérieur componentWillUnmount
Méthode de cycle de vie React.
classe OutsideClickHandler étend React.Component {
componentDidMount () {
document
.addEventListener ('mousedown', this.handleClickOutside);
}
componentWillUnmount () {
document
.removeEventListener ('mousedown', this.handleClickOutside);
}
handleClickOutside = (événement) => {
// Ici, nous écrirons le même clic extérieur
// logique de détection comme nous l'avons utilisé auparavant.
}
}
Maintenant, écrivons le code de détection dans la fonction de gestionnaire de handleClickOutside
.
La classe OutsideClickHandler étend React.Component {
componentDidMount () {
document
.addEventListener ('mousedown', this.handleClickOutside);
}
componentWillUnmount () {
document
.removeEventListener ('mousedown', this.handleClickOutside);
}
handleClickOutside = (événement) => {
si (
this.wrapperRef.current &&
! this.wrapperRef.current.contains (event.target)
) {
this.props.onOutsideClick ();
}
}
}
La logique de la méthode handleClickOutside
dit ce qui suit:
Si le nœud DOM sur lequel on a cliqué (
event.target
) n'était ni notre conteneur div (ce .wrapperRef.current
) ni aucun nœud à l'intérieur (! this.wrapperRef.current.contains (event.target)
), nous appelons le proponOutsideClick
.
Cela devrait fonctionner de la même manière que la détection des clics extérieurs fonctionnait auparavant. Essayons de cliquer en dehors de l’élément de texte gris dans la zone de code ci-dessous et observons la console:
The Problem With DOM Hierarchy Based Outside Click Detection Logic
Mais il y a un problème. Notre composant React ne fonctionne pas si l'un de ses enfants est rendu dans un portail React.
Mais que sont les portails React?
«Les portails fournissent un moyen de première classe pour rendre les enfants dans un nœud DOM qui existe en dehors du Hiérarchie DOM du composant parent. »

Dans l'image ci-dessus, vous pouvez voir que Tooltip
React component est un enfant de Container
React component, si nous inspectons le DOM, nous constatons que le nœud DOM Tooltip réside en fait dans une structure DOM complètement séparée, c'est-à-dire qu'il n'est pas à l'intérieur du nœud DOM Container.
Le problème est que dans notre logique de détection externe jusqu'à présent, nous supposons que les enfants de OutsideClickHandler
sera ses descendants directs dans l'arborescence DOM. Ce qui n'est pas le cas pour les portails React. Si les enfants de notre composant sont rendus dans un portail React – c'est-à-dire qu'ils sont rendus dans un nœud DOM séparé qui est en dehors de la hiérarchie de notre container div
dans lequel notre composant OutsideClickHandler
rend son enfants – alors la logique Node.contains
échoue.
Mais comment échouerait-elle? Si vous essayez de cliquer sur les enfants de notre composant OutsideClickHandler
– qui effectue le rendu dans un nœud DOM séparé à l'aide des portails React – notre composant enregistrera un clic extérieur, ce qui ne devrait pas être le cas. Voyez par vous-même:
Node .contains
l'enregistre à tort comme clic extérieur. « /> Node.contains
pour détecter le clic extérieur du composant React donne un résultat erroné pour les enfants rendus dans un portail React. ( Grand aperçu )Essayez-le:
Même si le popover qui s'ouvre en cliquant sur le bouton, est un enfant du composant OutsideClickHandler il ne parvient pas à détecter qu'il n'est pas en dehors de celui-ci
Utilisation de la propriété d'instance de classe et de la délégation d'événements pour détecter l'extérieur Cliquez sur
Alors, quelle pourrait être la solution? comptez sur DOM pour nous dire si le clic se produit à l'extérieur n'importe où. Nous devrons faire quelque chose avec JavaScript en réécrivant la mise en œuvre de OutsideClickHandler .
Commençons par une ardoise vierge. Donc en ce moment OutsideClickHandler est une classe React vide.
Le point crucial pour détecter correctement les clics extérieurs est:
- Ne pas se fier à la structure DOM.
- Pour stocker l'état 'cliqué' quelque part dans le code JavaScript.
Pour cet événement, la délégation viendra à notre aide. Prenons un exemple du même exemple de bouton et de popover que nous avons vu ci-dessus dans le GIF ci-dessus.
Nous avons deux enfants de notre fonction OutsideClickHandler . Un bouton et un popover – qui est rendu dans un portail en dehors de la hiérarchie DOM de OutsideClickHandler en cliquant sur le bouton, comme ceci:
https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_800/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/a1a55a0e-2033-4e26- 9019-dad3d5340071 / 2-creation-outside-focus-click-handler-react-component.png 800w,
https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_1200/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/a1a55a0e-2033-4e26- 9019-dad3d5340071 / 2-creation-outside-focus-click-handler-react-component.png 1200w,
https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_1600/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/a1a55a0e-2033-4e26- 9019-dad3d5340071 / 2-creation-outside-focus-click-handler-react-component.png 1600w,
https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_2000/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/a1a55a0e-2033-4e26- 9019-dad3d5340071 / 2-creation-outside-focus-click-handler-react-component.png 2000w "src =" https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https: //cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/a1a55a0e-2033-4e26-9019-dad3d5340071/2-creating-outside-focus-click-handler-react-component.png "tailles = "100vw" alt = "Diagramme montrant la hiérarchie du document
le composant OutsideClickHandler React et ses enfants rendus dans le portail React. « />
le composant OutsideClickHandler React et ses enfants rendus dans le portail React. ( Grand aperçu ) Lorsque l'un de nos enfants est cliqué, nous définissons une variable clickCaptured
sur true
. Si vous cliquez sur quelque chose en dehors d'eux, la valeur de clickCaptured
restera false
.
Nous stockerons la valeur de clickCaptured
dans:
- A propriété d'instance de classe, si vous utilisez un composant réactif de classe.
- Une référence, si vous utilisez un composant React fonctionnel.
Nous n'utilisons pas l'état React pour stocker la valeur de clickCaptured
car nous ne rendons rien basé sur ces données clickCaptured
. Le but de clickCaptured
est éphémère et prend fin dès que nous avons détecté si le clic s'est produit à l'intérieur ou à l'extérieur.
Voyons dans l'image ci-dessous la logique de réglage clickCaptured
:

Chaque fois qu'un clic se produit n'importe où, il bouillonne dans React par défaut. Il atteindra éventuellement le document
.
https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_800/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/35d75325-af9e-4160- a667-f4857839dbeb / 4-creation-outside-focus-click-handler-react-component.png 800w,
https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_1200/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/35d75325-af9e-4160- a667-f4857839dbeb / 4-creation-outside-focus-click-handler-react-component.png 1200w,
https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_1600/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/35d75325-af9e-4160- a667-f4857839dbeb / 4-creation-outside-focus-click-handler-react-component.png 1600w,
https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_2000/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/35d75325-af9e-4160- a667-f4857839dbeb / 4-creation-outside-focus-click-handler-react-component.png 2000w "src =" https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https: //cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/35d75325-af9e-4160-a667-f4857839dbeb/4-creating-outside-focus-click-handler-react-component "tailles" "100vw" alt = "Diagramme montrant la valeur de la variable clickCaptured lorsque l'événement mousedown remonte jusqu'au document, pour les cas de clics intérieurs et extérieurs. « />
Lorsque le clic atteint le document
deux choses peuvent se produire:
-
clickCaptured
sera vrai, si les enfants où vous avez cliqué. -
clickCaptured
sera faux, si vous avez cliqué n'importe où en dehors d'eux.
Dans l'écouteur d'événements du document, nous allons faire deux choses maintenant:
- Si
clickCaptured
est vrai, nous déclenchons un gestionnaire de clics externes que l'utilisateur de OutsideClickHandler aurait pu nous donner via un accessoire. - Nous remettons
clickCaptured
à false afin que nous soyons prêts pour une autre détection de clic.
https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_800/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/8157d7f6-976d-43f0- 9197-40f1bf28fddb / 5-creation-outside-focus-click-handler-react-component.png 800w,
https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_1200/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/8157d7f6-976d-43f0- 9197-40f1bf28fddb / 5-creation-outside-focus-click-handler-react-component.png 1200w,
https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_1600/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/8157d7f6-976d-43f0- 9197-40f1bf28fddb / 5-creation-outside-focus-click-handler-react-component.png 1600w,
https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_2000/https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/8157d7f6-976d-43f0- 9197-40f1bf28fddb / 5-creation-outside-focus-click-handler-react-component.png 2000w "src =" https://res.cloudinary.com/indysigner/image/fetch/f_auto,q_auto/w_400/https: //cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/8157d7f6-976d-43f0-9197-40f1bf28fddb/5-creating-outside-focus-click-handler-react-component.png "100vw" alt = "Diagramme montrant la détection d'un clic à l'intérieur ou à l'extérieur du composant React en vérifiant la valeur de clickCapture lorsque l'événement mousedown atteint le document. « />
Traduisons ceci en code.
import React de "react"
La classe OutsideClickHandler étend React.Component {
clickCaptured = false;
render () {
if (typeof this.props.children === 'fonction') {
retourne this.props.children (this.getProps ())
}
renvoie this.renderComponent ()
}
}
Nous avons les éléments suivants:
- définissez la valeur initiale de la propriété d'instance
clickCaptured
surfalse
. - Dans la méthode
render
nous vérifier sienfants
prop est une fonction. Si c'est le cas, nous l'appelons et lui passons tous les accessoires que nous voulons lui donner en appelant la méthode de classegetProps
. Nous n'avons pas encore implémentégetProps
. - Si l'accessoire
children
n'est pas une fonction, nous appelons la méthoderenderComponent
. Implémentons cette méthode maintenant.
la classe OutsideClickHandler étend React.Component {
renderComponent () {
retourner React.createElement (
this.props.component || 'portée',
this.getProps (),
this.props.children
)
}
}
Comme nous n'utilisons pas JSX, nous utilisons directement l'API createElement de React pour envelopper nos enfants dans this.props.component
ou dans un span
]. this.props.component
peut être un composant React ou n'importe quel nom de balise de l'élément HTML comme 'div', 'section', etc. Nous transmettons tous les accessoires que nous voulons passer à notre élément nouvellement créé en appelant la méthode de classe getProps
comme deuxième argument.
Écrivons maintenant la méthode getProps
:
class OutsideClickHandler extend React.Component {
getProps () {
revenir {
onMouseDown: this.innerClick,
onTouchStart: this.innerClick
};
}
}
Notre élément React nouvellement créé aura les accessoires suivants qui lui seront transmis: onMouseDown
et onTouchStart
pour les appareils tactiles. Leurs deux valeurs sont la méthode de classe innerClick
.
class OutsideClickHandler extend React.Component {
innerClick = () => {
this.clickCaptured = true;
}
}
Si l'on clique sur notre nouveau composant React ou tout ce qu'il contient – qui pourrait être un portail React -, nous définissons la propriété d'occurrence de la classe clickCaptured
sur true. Ajoutons maintenant les événements mousedown
et touchstart
au document, afin que nous puissions capturer l'événement qui bouillonne d'en bas.
La classe OutsideClickHandler étend React.Component {
componentDidMount () {
document.addEventListener ('mousedown', this.documentClick);
document.addEventListener ('touchstart', this.documentClick);
}
componentWillUnmount () {
document.removeEventListener ('mousedown', this.documentClick);
document.removeEventListener ('touchstart', this.documentClick);
}
documentClick = (événement) => {
if (! this.clickCaptured && this.props.onClickOutside) {
this.props.onClickOutside (événement);
}
this.clickCaptured = false;
};
}
Dans les gestionnaires d'événements mousedown et touchstart nous vérifions si clickCaptured
est faux.
-
clickCaptured
seraitvrai
seulement si les enfants de notre composant React ont été cliqués. - Si vous avez cliqué sur quelque chose d'autre
clickCaptured
seraitfalse
et nous ' Je sais qu'un clic extérieur s'est produit.
Si clickCaptured
est faux, nous appellerons la méthode onClickOutside transmise dans un accessoire à notre composant OutsideClickHandler .
Voilà! Confirmons que si nous cliquons à l'intérieur du popover, il ne se ferme pas maintenant, comme c'était le cas auparavant:

Essayons-le:
Merveilleux!
Détection de mise au point extérieure
Allons maintenant plus loin. Ajoutons également des fonctionnalités pour détecter lorsque le focus s'est déplacé en dehors d'un composant React. La mise en œuvre sera très similaire à celle que nous avons réalisée avec la détection des clics. Écrivons le code.
la classe OutsideClickHandler étend React.Component {
focusCaptured = false // 1. pour ajouter ceci
innerFocus = () => {
this.focusCaptured = vrai;
}
componentDidMount () {
document.addEventListener ('mousedown', this.documentClick);
document.addEventListener ('touchstart', this.documentClick);
document.addEventListener ('focusin', this.documentFocus);
}
componentWillUnmount () {
document.removeEventListener ('mousedown', this.documentClick);
document.removeEventListener ('touchstart', this.documentClick);
document.removeEventListener ('focusin', this.documentFocus);
}
documentFocus = (événement) => {
if (! this.focusCaptured && this.props.onFocusOutside) {
this.props.onFocusOutside (événement);
}
this.focusCaptured = faux;
};
// 2. pour indenter le morceau de code suivant
// 3. Ce morceau de code n'est pas copié dans le presse-papiers en cliquant sur le bouton "Copier"
getProps () {return {onMouseDown: this.innerClick, onTouchStart: this.innerClick, onFocus: this.innerFocus}; }
Tout est ajouté pour la plupart de la même manière, sauf une chose. Vous avez peut-être remarqué que bien que nous ajoutions un gestionnaire d'événements onFocus
react sur nos enfants, nous définissons un écouteur d'événements focusin
à notre document. Pourquoi pas un événement focus
dites-vous? Parce que, 🥁🥁🥁, À partir de la v17, React mappe maintenant onFocus
React event to focusin
natif événement en interne.
In Si vous utilisez la version 16 ou antérieure, au lieu d'ajouter un gestionnaire d'événements focusin
au document, vous devrez ajouter un événement focus
en phase de capture à la place. Ce sera donc:
document.addEventListener ('focus', this.documentFocus, true);
Pourquoi en phase de capture vous pourriez demander? Car aussi étrange soit-il, l’événement de mise au point ne bouillonne pas .
Puisque j’utilise la v17 dans tous mes exemples, je vais continuer à utiliser le premier. Voyons ce que nous avons ici:

Essayons-le nous-mêmes, essayez de cliquer à l'intérieur et à l'extérieur de l'arrière-plan rose. Utilisez également les touches Tab et Maj + Tab (dans Chrome, Firefox, Edge) ou Opt / Alt + Tab et Opt / Alt + Maj + Tab (dans Safari) pour basculer la mise au point entre le bouton interne et externe et voir comment l'état de la mise au point change. [19659125] Conclusion
Dans cet article, nous avons appris que le moyen le plus simple de détecter un clic en dehors d'un nœud DOM en JavaScript est d'utiliser Node.contains
DOM API. J'ai expliqué l'importance de savoir pourquoi utiliser la même méthode pour détecter les clics en dehors d'un composant React ne fonctionne pas lorsque le composant React a des enfants qui s'affichent dans un portail React. En outre, vous savez maintenant comment utiliser une propriété d'instance de classe avec une délégation d'événement pour détecter correctement si un clic s'est produit en dehors d'un composant React, ainsi que comment étendre la même technique de détection à la détection de focus extérieur d'un composant React avec le focusin
event caveat.
Related Resources
- React Foco Github Repository
- documentation mdn pour
Node.contains
DOM api - Docs React pour les portails
- React
createElement
API - React Github codebase Pull Request for mapping
onFocus
etonBlur
méthodes pour utiliser en internefocusin
etfocusout
événements natifs. - Délégation d'événements Focus et Blur

Source link