Détecter le clic en dehors du composant React

412

Je recherche un moyen de détecter si un événement de clic s'est produit en dehors d'un composant, comme décrit dans cet article . jQuery le plus proche () est utilisé pour voir si la cible d'un événement click a l'élément dom comme l'un de ses parents. S'il y a correspondance, l'événement click appartient à l'un des enfants et n'est donc pas considéré comme étant en dehors du composant.

Donc, dans mon composant, je veux attacher un gestionnaire de clics à la fenêtre. Lorsque le gestionnaire se déclenche, je dois comparer la cible avec les enfants dom de mon composant.

L'événement click contient des propriétés telles que "chemin" qui semble contenir le chemin dom que l'événement a parcouru. Je ne sais pas quoi comparer ou comment le traverser au mieux, et je pense que quelqu'un doit déjà avoir mis cela dans une fonction utilitaire intelligente ... Non?

Thijs Koerselman
la source
Pourriez-vous attacher le gestionnaire de clics au parent plutôt qu'à la fenêtre?
J. Mark Stevens
Si vous attachez un gestionnaire de clics au parent, vous savez quand cet élément ou l'un de leurs enfants est cliqué, mais j'ai besoin de détecter tous les autres endroits sur lesquels vous cliquez, le gestionnaire doit donc être attaché à la fenêtre.
Thijs Koerselman
J'ai regardé l'article après la réponse précédente. Que diriez-vous de définir un clickState dans le composant supérieur et de transmettre les actions de clic des enfants. Ensuite, vous vérifieriez les accessoires des enfants pour gérer l'état de fermeture ouvert.
J. Mark Stevens
Le composant supérieur serait mon application. Mais la composante d'écoute est profonde de plusieurs niveaux et n'a pas de position stricte dans le dom. Je ne peux pas ajouter de gestionnaires de clics à tous les composants de mon application simplement parce que l'un d'eux souhaite savoir si vous avez cliqué quelque part en dehors de celui-ci. D'autres composants ne devraient pas être conscients de cette logique car cela créerait de terribles dépendances et du code passe-partout.
Thijs Koerselman
5
Je voudrais vous recommander une très belle lib. créé par AirBnb: github.com/airbnb/react-outside-click-handler
Osoian Marcel

Réponses:

685

L'utilisation des références dans React 16.3+ a changé.

La solution suivante utilise ES6 et suit les meilleures pratiques pour la liaison ainsi que la définition de la référence via une méthode.

Pour le voir en action:

Implémentation de classe:

import React, { Component } from 'react';

/**
 * Component that alerts if you click outside of it
 */
export default class OutsideAlerter extends Component {
    constructor(props) {
        super(props);

        this.wrapperRef = React.createRef();
        this.setWrapperRef = this.setWrapperRef.bind(this);
        this.handleClickOutside = this.handleClickOutside.bind(this);
    }

    componentDidMount() {
        document.addEventListener('mousedown', this.handleClickOutside);
    }

    componentWillUnmount() {
        document.removeEventListener('mousedown', this.handleClickOutside);
    }

    /**
     * Alert if clicked on outside of element
     */
    handleClickOutside(event) {
        if (this.wrapperRef && !this.wrapperRef.current.contains(event.target)) {
            alert('You clicked outside of me!');
        }
    }

    render() {
        return <div ref={this.wrapperRef}>{this.props.children}</div>;
    }
}

OutsideAlerter.propTypes = {
    children: PropTypes.element.isRequired,
};

Mise en œuvre des crochets:

import React, { useRef, useEffect } from "react";

/**
 * Hook that alerts clicks outside of the passed ref
 */
function useOutsideAlerter(ref) {
    useEffect(() => {
        /**
         * Alert if clicked on outside of element
         */
        function handleClickOutside(event) {
            if (ref.current && !ref.current.contains(event.target)) {
                alert("You clicked outside of me!");
            }
        }

        // Bind the event listener
        document.addEventListener("mousedown", handleClickOutside);
        return () => {
            // Unbind the event listener on clean up
            document.removeEventListener("mousedown", handleClickOutside);
        };
    }, [ref]);
}

/**
 * Component that alerts if you click outside of it
 */
export default function OutsideAlerter(props) {
    const wrapperRef = useRef(null);
    useOutsideAlerter(wrapperRef);

    return <div ref={wrapperRef}>{props.children}</div>;
}
Ben Bud
la source
3
Comment testez-vous cela cependant? Comment envoyer un event.target valide à la fonction handleClickOutside?
csilk
5
Comment pouvez-vous utiliser les événements synthétiques de React, au lieu de ceux- document.addEventListenerci?
Ralph David Abernathy
10
@ polkovnikov.ph 1. contextn'est nécessaire comme argument dans le constructeur que s'il est utilisé. Il n'est pas utilisé dans ce cas. La raison pour laquelle l'équipe de réaction recommande d'avoir propscomme argument dans le constructeur est que l'utilisation de this.propsavant l'appel super(props)ne sera pas définie et peut conduire à des erreurs. contextest toujours disponible sur les nœuds internes, c'est tout le but de context. De cette façon, vous n'avez pas à le transmettre d'un composant à l'autre comme vous le faites avec les accessoires. 2. Il s'agit simplement d'une préférence stylistique et ne justifie pas un débat dans ce cas.
Ben Bud, le
7
J'obtiens l'erreur suivante: "this.wrapperRef.contains n'est pas une fonction"
Bogdan
5
@Bogdan, j'obtenais la même erreur lors de l'utilisation d'un composant de style. Envisagez d'utiliser un <div> au niveau supérieur
user3040068
151

Voici la solution qui a fonctionné le mieux pour moi sans attacher d'événements au conteneur:

Certains éléments HTML peuvent avoir ce qui est appelé " focus ", par exemple des éléments d'entrée. Ces éléments répondront également au flou événement , lorsqu'ils perdront cette concentration.

Pour donner à n'importe quel élément la capacité d'avoir le focus, assurez-vous simplement que son attribut tabindex est défini sur autre chose que -1. En HTML standard, ce serait en définissant l' tabindexattribut, mais dans React, vous devez utiliser tabIndex(notez la majusculeI ).

Vous pouvez également le faire via JavaScript avec element.setAttribute('tabindex',0)

C'est pour cela que je l'utilisais, pour créer un menu DropDown personnalisé.

var DropDownMenu = React.createClass({
    getInitialState: function(){
        return {
            expanded: false
        }
    },
    expand: function(){
        this.setState({expanded: true});
    },
    collapse: function(){
        this.setState({expanded: false});
    },
    render: function(){
        if(this.state.expanded){
            var dropdown = ...; //the dropdown content
        } else {
            var dropdown = undefined;
        }

        return (
            <div className="dropDownMenu" tabIndex="0" onBlur={ this.collapse } >
                <div className="currentValue" onClick={this.expand}>
                    {this.props.displayValue}
                </div>
                {dropdown}
            </div>
        );
    }
});
Pablo Barría Urenda
la source
10
Cela ne créerait-il pas des problèmes d'accessibilité? Puisque vous faites un élément supplémentaire focalisable juste pour détecter quand il entre / sort, mais l'interaction devrait être sur les valeurs déroulantes non?
Thijs Koerselman
2
Il s'agit d'une approche très intéressante. Fonctionne parfaitement pour ma situation, mais je serais intéressé d'apprendre comment cela pourrait affecter l'accessibilité.
klinore
17
J'ai ce "travail" dans une certaine mesure, c'est un petit hack cool. Mon problème est que mon contenu déroulant est de display:absolutesorte que le menu déroulant n'affecte pas la taille de la div parent. Cela signifie que lorsque je clique sur un élément dans la liste déroulante, le flou se déclenche.
Dave
2
J'ai eu des problèmes avec cette approche, aucun élément du contenu déroulant ne déclenche des événements tels que onClick.
AnimaSola
8
Vous pourriez rencontrer d'autres problèmes si le contenu de la liste déroulante contient des éléments pouvant être mis au point comme des entrées de formulaire par exemple. Ils vous voleront votre attention, onBlur seront déclenchés sur votre conteneur déroulant et expanded seront définis sur faux.
Kamagatos
107

J'étais coincé sur le même problème. Je suis un peu en retard à la fête ici, mais pour moi, c'est une très bonne solution. J'espère que cela sera utile à quelqu'un d'autre. Vous devez importer findDOMNodedereact-dom

import ReactDOM from 'react-dom';
// ... ✂

componentDidMount() {
    document.addEventListener('click', this.handleClickOutside, true);
}

componentWillUnmount() {
    document.removeEventListener('click', this.handleClickOutside, true);
}

handleClickOutside = event => {
    const domNode = ReactDOM.findDOMNode(this);

    if (!domNode || !domNode.contains(event.target)) {
        this.setState({
            visible: false
        });
    }
}

Approche React Hooks (16.8 +)

Vous pouvez créer un crochet réutilisable appelé useComponentVisible.

import { useState, useEffect, useRef } from 'react';

export default function useComponentVisible(initialIsVisible) {
    const [isComponentVisible, setIsComponentVisible] = useState(initialIsVisible);
    const ref = useRef(null);

    const handleClickOutside = (event) => {
        if (ref.current && !ref.current.contains(event.target)) {
            setIsComponentVisible(false);
        }
    };

    useEffect(() => {
        document.addEventListener('click', handleClickOutside, true);
        return () => {
            document.removeEventListener('click', handleClickOutside, true);
        };
    });

    return { ref, isComponentVisible, setIsComponentVisible };
}

Ensuite, dans le composant, vous souhaitez ajouter la fonctionnalité pour effectuer les opérations suivantes:

const DropDown = () => {
    const { ref, isComponentVisible } = useComponentVisible(true);
    return (
       <div ref={ref}>
          {isComponentVisible && (<p>Dropdown Component</p>)}
       </div>
    );

}

Trouvez un exemple de code et de boîte ici.

Paul Fitzgerald
la source
1
@LeeHanKyeol Pas entièrement - cette réponse appelle les gestionnaires d'événements pendant la phase de capture de la gestion des événements, tandis que la réponse liée à appelle les gestionnaires d'événements pendant la phase de bulle.
stevejay
3
Ce devrait être la réponse acceptée. Fonctionne parfaitement pour les menus déroulants avec positionnement absolu.
lave
1
ReactDOM.findDOMNodeet est obsolète, devrait utiliser des refrappels: github.com/yannickcr/eslint-plugin-react/issues/…
dain
2
grande et propre solution
Karim
1
Cela a fonctionné pour moi, même si j'ai dû pirater votre composant pour réinitialiser le visible à `` vrai '' après 200 ms (sinon mon menu ne s'affiche plus). Je vous remercie!
Lee Moe
87

Après avoir essayé de nombreuses méthodes ici, j'ai décidé d'utiliser github.com/Pomax/react-onclickoutside en raison de sa complétude.

J'ai installé le module via npm et l'ai importé dans mon composant:

import onClickOutside from 'react-onclickoutside'

Ensuite, dans ma classe de composants, j'ai défini la handleClickOutsideméthode:

handleClickOutside = () => {
  console.log('onClickOutside() method called')
}

Et lors de l'exportation de mon composant, je l'ai enveloppé dans onClickOutside():

export default onClickOutside(NameOfComponent)

C'est ça.

Beau Smith
la source
2
Y a-t- il des avantages concrets à ce sur la tabIndex/ onBlurapproche proposée par Pablo ? Comment fonctionne la mise en œuvre et en quoi son comportement diffère-t-il de l' onBlurapproche?
Mark Amery
4
Il vaut mieux utiliser un composant dédié puis utiliser un hack tabIndex. Le voter!
Atul Yadav
5
@MarkAmery - J'ai mis un commentaire sur l' tabIndex/onBlurapproche. Cela ne fonctionne pas lorsque la liste déroulante est position:absolute, comme un menu survolant un autre contenu.
Beau Smith
4
Un autre avantage d'utiliser ceci par rapport à tabindex est que la solution tabindex déclenche également un événement flou si vous vous concentrez sur un élément enfant
Jemar Jones
1
@BeauSmith - Merci beaucoup! J'ai sauvé ma journée!
Mika
39

J'ai trouvé une solution grâce à Ben Alpert sur discuter.reactjs.org . L'approche suggérée associe un gestionnaire au document, mais cela s'est avéré problématique. En cliquant sur l'un des composants de mon arborescence, un nouveau rendu a supprimé l'élément cliqué lors de la mise à jour. Étant donné que le rendu de React se produit avant l'appel du gestionnaire de corps de document, l'élément n'a pas été détecté comme «à l'intérieur» de l'arborescence.

La solution était d'ajouter le gestionnaire sur l'élément racine de l'application.

principale:

window.__myapp_container = document.getElementById('app')
React.render(<App/>, window.__myapp_container)

composant:

import { Component, PropTypes } from 'react';
import ReactDOM from 'react-dom';

export default class ClickListener extends Component {

  static propTypes = {
    children: PropTypes.node.isRequired,
    onClickOutside: PropTypes.func.isRequired
  }

  componentDidMount () {
    window.__myapp_container.addEventListener('click', this.handleDocumentClick)
  }

  componentWillUnmount () {
    window.__myapp_container.removeEventListener('click', this.handleDocumentClick)
  }

  /* using fat arrow to bind to instance */
  handleDocumentClick = (evt) => {
    const area = ReactDOM.findDOMNode(this.refs.area);

    if (!area.contains(evt.target)) {
      this.props.onClickOutside(evt)
    }
  }

  render () {
    return (
      <div ref='area'>
       {this.props.children}
      </div>
    )
  }
}
Thijs Koerselman
la source
8
Cela ne fonctionne plus pour moi avec React 0.14.7 - peut-être que React a changé quelque chose, ou peut-être que j'ai fait une erreur lors de l'adaptation du code à toutes les modifications apportées à React. J'utilise plutôt github.com/Pomax/react-onclickoutside qui fonctionne comme un charme.
Nicole
1
Hmm. Je ne vois aucune raison pour laquelle cela devrait fonctionner. Pourquoi un gestionnaire sur le nœud DOM racine de l'application devrait-il être garanti de se déclencher avant un nouveau rendu déclenché par un autre gestionnaire s'il ne l' documentest pas?
Mark Amery
1
Un point UX pur: mousedownserait probablement un meilleur gestionnaire clickqu'ici. Dans la plupart des applications, le comportement de fermeture du menu en cliquant sur l'extérieur se produit au moment où vous cliquez avec le bouton droit de la souris, pas lorsque vous relâchez. Essayez-le, par exemple, avec l' indicateur Stack Overflow ou les dialogues de partage ou avec l'une des listes déroulantes de la barre de menu supérieure de votre navigateur.
Mark Amery
ReactDOM.findDOMNodeet la chaîne refsont obsolètes, doivent utiliser des refrappels: github.com/yannickcr/eslint-plugin-react/issues/…
dain
c'est la solution la plus simple et fonctionne parfaitement pour moi même lors de la fixation audocument
Flion
37

Mise en œuvre du crochet basée sur l'excellent discours de Tanner Linsley au JSConf Hawaii 2020 :

useOuterClick API de crochet

const Client = () => {
  const innerRef = useOuterClick(ev => { /* what you want do on outer click */ });
  return <div ref={innerRef}> Inside </div> 
};

la mise en oeuvre

/*
  Custom Hook
*/
function useOuterClick(callback) {
  const innerRef = useRef();
  const callbackRef = useRef();

  // set current callback in ref, before second useEffect uses it
  useEffect(() => { // useEffect wrapper to be safe for concurrent mode
    callbackRef.current = callback;
  });

  useEffect(() => {
    document.addEventListener("click", handleClick);
    return () => document.removeEventListener("click", handleClick);

    // read most recent callback and innerRef dom node from refs
    function handleClick(e) {
      if (
        innerRef.current && 
        callbackRef.current &&
        !innerRef.current.contains(e.target)
      ) {
        callbackRef.current(e);
      }
    }
  }, []); // no need for callback + innerRef dep
  
  return innerRef; // return ref; client can omit `useRef`
}

/*
  Usage 
*/
const Client = () => {
  const [counter, setCounter] = useState(0);
  const innerRef = useOuterClick(e => {
    // counter state is up-to-date, when handler is called
    alert(`Clicked outside! Increment counter to ${counter + 1}`);
    setCounter(c => c + 1);
  });
  return (
    <div>
      <p>Click outside!</p>
      <div id="container" ref={innerRef}>
        Inside, counter: {counter}
      </div>
    </div>
  );
};

ReactDOM.render(<Client />, document.getElementById("root"));
#container { border: 1px solid red; padding: 20px; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.12.0/umd/react.production.min.js" integrity="sha256-Ef0vObdWpkMAnxp39TYSLVS/vVUokDE8CDFnx7tjY6U=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.12.0/umd/react-dom.production.min.js" integrity="sha256-p2yuFdE8hNZsQ31Qk+s8N+Me2fL5cc6NKXOC0U9uGww=" crossorigin="anonymous"></script>
<script> var {useRef, useEffect, useCallback, useState} = React</script>
<div id="root"></div>

useOuterClickutilise des références mutables pour créer une API allégée pour le Client. Un rappel peut être défini sans avoir à le mémoriser via useCallback. Le corps de rappel a toujours accès aux accessoires et à l'état les plus récents (pas de valeurs de fermeture périmées).


Remarque pour les utilisateurs iOS

iOS ne traite que certains éléments comme cliquables. Pour contourner ce comportement, choisissez un écouteur de clic externe différent de document- rien vers le haut, y compris body. Par exemple, vous pouvez ajouter un écouteur sur la racine React divdans l'exemple ci-dessus et augmenter sa hauteur ( height: 100vhou similaire) pour intercepter tous les clics extérieurs. Sources: quirksmode.org et cette réponse

ford04
la source
ne fonctionne pas sur mobile si imprévu
Omar
@Omar vient de tester le Codesandbox dans mon article avec Firefox 67.0.3 sur un appareil Android - fonctionne très bien. Pourriez-vous préciser quel navigateur vous utilisez, quelle est l'erreur, etc.?
ford04
Aucune erreur mais ne fonctionne pas sur Chrome, iPhone 7+. Il ne détecte pas les robinets à l'extérieur. Dans les outils de développement Chrome sur mobile, cela fonctionne, mais dans un véritable appareil iOS, cela ne fonctionne pas.
Omar
1
@ ford04 merci d'avoir creusé cela, je suis tombé sur un autre fil qui suggérait l'astuce suivante. @media (hover: none) and (pointer: coarse) { body { cursor:pointer } }L'ajout de cela dans mes styles globaux semble avoir résolu le problème.
Kostas Sarantopoulos
1
@ ford04 ouais, btw selon ce fil, il y a une requête média encore plus spécifique @supports (-webkit-overflow-scrolling: touch)qui cible uniquement IOS même si je n'en sais pas plus! Je viens de le tester et ça marche.
Kostas Sarantopoulos
31

Aucune des autres réponses ici n'a fonctionné pour moi. J'essayais de cacher une fenêtre contextuelle sur le flou, mais comme le contenu était absolument positionné, onBlur tirait même au clic du contenu intérieur.

Voici une approche qui a fonctionné pour moi:

// Inside the component:
onBlur(event) {
    // currentTarget refers to this component.
    // relatedTarget refers to the element where the user clicked (or focused) which
    // triggered this event.
    // So in effect, this condition checks if the user clicked outside the component.
    if (!event.currentTarget.contains(event.relatedTarget)) {
        // do your thing.
    }
},

J'espère que cela t'aides.

Niyaz
la source
Très bien! Merci. Mais dans mon cas, c'était un problème pour attraper "endroit actuel" avec la position absolue pour le div, donc j'utilise "OnMouseLeave" pour div avec entrée et calendrier déroulant pour désactiver simplement tous les div lorsque la souris quitte le div.
Alex
1
Merci! Aide-moi beaucoup.
Ângelo Lucas
1
J'aime mieux cela que les méthodes qui s'y rattachent document. Merci.
kyw
Pour que cela fonctionne, vous devez vous assurer que l'élément cliqué (relatedTarget) est focalisable. Voir stackoverflow.com/questions/42764494/…
xabitrigo
21

[Mise à jour] Solution avec React ^ 16.8 utilisant des crochets

CodeSandbox

import React, { useEffect, useRef, useState } from 'react';

const SampleComponent = () => {
    const [clickedOutside, setClickedOutside] = useState(false);
    const myRef = useRef();

    const handleClickOutside = e => {
        if (!myRef.current.contains(e.target)) {
            setClickedOutside(true);
        }
    };

    const handleClickInside = () => setClickedOutside(false);

    useEffect(() => {
        document.addEventListener('mousedown', handleClickOutside);
        return () => document.removeEventListener('mousedown', handleClickOutside);
    });

    return (
        <button ref={myRef} onClick={handleClickInside}>
            {clickedOutside ? 'Bye!' : 'Hello!'}
        </button>
    );
};

export default SampleComponent;

Solution avec React ^ 16.3 :

CodeSandbox

import React, { Component } from "react";

class SampleComponent extends Component {
  state = {
    clickedOutside: false
  };

  componentDidMount() {
    document.addEventListener("mousedown", this.handleClickOutside);
  }

  componentWillUnmount() {
    document.removeEventListener("mousedown", this.handleClickOutside);
  }

  myRef = React.createRef();

  handleClickOutside = e => {
    if (!this.myRef.current.contains(e.target)) {
      this.setState({ clickedOutside: true });
    }
  };

  handleClickInside = () => this.setState({ clickedOutside: false });

  render() {
    return (
      <button ref={this.myRef} onClick={this.handleClickInside}>
        {this.state.clickedOutside ? "Bye!" : "Hello!"}
      </button>
    );
  }
}

export default SampleComponent;
onoya
la source
3
C'est la solution incontournable maintenant. Si vous obtenez l'erreur .contains is not a function, cela peut être dû au fait que vous transmettez l'hélice ref à un composant personnalisé plutôt qu'à un véritable élément DOM comme un<div>
willlma
Pour ceux qui essaient de passer l' refhélice à un composant personnalisé, vous voudrez peut-être jeter un œil àReact.forwardRef
onoya
4

Voici mon approche (démo - https://jsfiddle.net/agymay93/4/ ):

J'ai créé un composant spécial appelé WatchClickOutsideet il peut être utilisé comme (je suppose que la JSXsyntaxe):

<WatchClickOutside onClickOutside={this.handleClose}>
  <SomeDropdownEtc>
</WatchClickOutside>

Voici le code du WatchClickOutsidecomposant:

import React, { Component } from 'react';

export default class WatchClickOutside extends Component {
  constructor(props) {
    super(props);
    this.handleClick = this.handleClick.bind(this);
  }

  componentWillMount() {
    document.body.addEventListener('click', this.handleClick);
  }

  componentWillUnmount() {
    // remember to remove all events to avoid memory leaks
    document.body.removeEventListener('click', this.handleClick);
  }

  handleClick(event) {
    const {container} = this.refs; // get container that we'll wait to be clicked outside
    const {onClickOutside} = this.props; // get click outside callback
    const {target} = event; // get direct click event target

    // if there is no proper callback - no point of checking
    if (typeof onClickOutside !== 'function') {
      return;
    }

    // if target is container - container was not clicked outside
    // if container contains clicked target - click was not outside of it
    if (target !== container && !container.contains(target)) {
      onClickOutside(event); // clicked outside - fire callback
    }
  }

  render() {
    return (
      <div ref="container">
        {this.props.children}
      </div>
    );
  }
}
pie6k
la source
Excellente solution - pour mon usage, j'ai changé pour écouter le document au lieu du corps dans le cas des pages avec un contenu court, et changé la référence de chaîne en référence dom: jsfiddle.net/agymay93/9
Billy Moon
et utilisé span au lieu de div car il est moins susceptible de changer de disposition, et ajout d'une démo de gestion des clics en dehors du corps: jsfiddle.net/agymay93/10
Billy Moon
La chaîne refest obsolète, doit utiliser des refrappels: reactjs.org/docs/refs-and-the-dom.html
dain
4

Cela a déjà de nombreuses réponses, mais elles ne répondent pas e.stopPropagation()et empêchent de cliquer sur les liens de réaction en dehors de l'élément que vous souhaitez fermer.

Étant donné que React possède son propre gestionnaire d'événements artificiels, vous ne pouvez pas utiliser le document comme base pour les écouteurs d'événements. Vous devez le faire e.stopPropagation()avant car React utilise le document lui-même. Si vous utilisez par exemple à la document.querySelector('body')place. Vous pouvez empêcher le clic du lien React. Voici un exemple de la façon dont j'implémente le clic à l'extérieur et la fermeture.
Il utilise ES6 et React 16.3 .

import React, { Component } from 'react';

class App extends Component {
  constructor(props) {
    super(props);

    this.state = {
      isOpen: false,
    };

    this.insideContainer = React.createRef();
  }

  componentWillMount() {
    document.querySelector('body').addEventListener("click", this.handleClick, false);
  }

  componentWillUnmount() {
    document.querySelector('body').removeEventListener("click", this.handleClick, false);
  }

  handleClick(e) {
    /* Check that we've clicked outside of the container and that it is open */
    if (!this.insideContainer.current.contains(e.target) && this.state.isOpen === true) {
      e.preventDefault();
      e.stopPropagation();
      this.setState({
        isOpen: false,
      })
    }
  };

  togggleOpenHandler(e) {
    e.preventDefault();

    this.setState({
      isOpen: !this.state.isOpen,
    })
  }

  render(){
    return(
      <div>
        <span ref={this.insideContainer}>
          <a href="#open-container" onClick={(e) => this.togggleOpenHandler(e)}>Open me</a>
        </span>
        <a href="/" onClick({/* clickHandler */})>
          Will not trigger a click when inside is open.
        </a>
      </div>
    );
  }
}

export default App;
Richard Herries
la source
3

Pour ceux qui ont besoin d'un positionnement absolu, une option simple que j'ai choisie consiste à ajouter un composant wrapper conçu pour couvrir toute la page avec un arrière-plan transparent. Ensuite, vous pouvez ajouter un onClick sur cet élément pour fermer votre composant interne.

<div style={{
        position: 'fixed',
        top: '0', right: '0', bottom: '0', left: '0',
        zIndex: '1000',
      }} onClick={() => handleOutsideClick()} >
    <Content style={{position: 'absolute'}}/>
</div>

Comme c'est le cas actuellement si vous ajoutez un gestionnaire de clics sur le contenu, l'événement sera également propagé à la division supérieure et déclenchera donc le gestionnaireOutsideClick. Si ce n'est pas le comportement souhaité, arrêtez simplement la progression de l'événement sur votre gestionnaire.

<Content style={{position: 'absolute'}} onClick={e => {
                                          e.stopPropagation();
                                          desiredFunctionCall();
                                        }}/>

"

Anthony Garant
la source
Le problème avec cette approche est que vous ne pouvez pas avoir de clics sur le contenu - le div recevra le clic à la place.
rbonick
Puisque le contenu est dans la div si vous n'arrêtez pas la propagation de l'événement, les deux recevront le clic. C'est souvent un comportement souhaité, mais si vous ne voulez pas que le clic soit propagé à la div, arrêtez simplement la propagation d'événement sur votre gestionnaire de contenu onClick. J'ai mis à jour ma réponse pour montrer comment.
Anthony Garant
3

Pour prolonger la réponse acceptée de Ben Bud , si vous utilisez des composants de style, transmettre des références de cette façon vous donnera une erreur telle que "this.wrapperRef.contains n'est pas une fonction".

La correction suggérée, dans les commentaires, pour envelopper le composant stylisé avec un div et y passer la référence, fonctionne. Cela dit, dans leurs documents, ils expliquent déjà la raison de cela et la bonne utilisation des références dans les composants de style:

Passer un accessoire ref à un composant stylé vous donnera une instance de l'encapsuleur StyledComponent, mais pas au nœud DOM sous-jacent. Cela est dû au fonctionnement des refs. Il n'est pas possible d'appeler directement des méthodes DOM, comme focus, sur nos wrappers. Pour obtenir une référence au nœud DOM enveloppé réel, passez plutôt le rappel à la propriété innerRef.

Ainsi:

<StyledDiv innerRef={el => { this.el = el }} />

Ensuite, vous pouvez y accéder directement dans la fonction "handleClickOutside":

handleClickOutside = e => {
    if (this.el && !this.el.contains(e.target)) {
        console.log('clicked outside')
    }
}

Cela vaut également pour l'approche "onBlur":

componentDidMount(){
    this.el.focus()
}
blurHandler = () => {
    console.log('clicked outside')
}
render(){
    return(
        <StyledDiv
            onBlur={this.blurHandler}
            tabIndex="0"
            innerRef={el => { this.el = el }}
        />
    )
}
Flatlinediver
la source
2

Ma plus grande préoccupation avec toutes les autres réponses est de devoir filtrer les événements de clic de la racine / du parent vers le bas. J'ai trouvé que le moyen le plus simple était de simplement définir un élément frère avec position: fixed, un z-index 1 derrière la liste déroulante et de gérer l'événement click sur l'élément fixe à l'intérieur du même composant. Garde tout centralisé dans un composant donné.

Exemple de code

#HTML
<div className="parent">
  <div className={`dropdown ${this.state.open ? open : ''}`}>
    ...content
  </div>
  <div className="outer-handler" onClick={() => this.setState({open: false})}>
  </div>
</div>

#SASS
.dropdown {
  display: none;
  position: absolute;
  top: 0px;
  left: 0px;
  z-index: 100;
  &.open {
    display: block;
  }
}
.outer-handler {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    opacity: 0;
    z-index: 99;
    display: none;
    &.open {
      display: block;
    }
}
metroninja
la source
1
Hmm chouette. Cela fonctionnerait-il avec plusieurs instances? Ou bien chaque élément fixe pourrait-il bloquer le clic des autres?
Thijs Koerselman
2
componentWillMount(){

  document.addEventListener('mousedown', this.handleClickOutside)
}

handleClickOutside(event) {

  if(event.path[0].id !== 'your-button'){
     this.setState({showWhatever: false})
  }
}

L'événement path[0]est le dernier élément cliqué

Ivan Sanchez
la source
3
Assurez-vous de supprimer l'écouteur d'événements lorsque le composant se démonte pour éviter les problèmes de gestion de la mémoire, etc.
agm1984
2

J'ai utilisé ce module (je n'ai aucun lien avec l'auteur)

npm install react-onclickout --save

const ClickOutHandler = require('react-onclickout');
 
class ExampleComponent extends React.Component {
 
  onClickOut(e) {
    if (hasClass(e.target, 'ignore-me')) return;
    alert('user clicked outside of the component!');
  }
 
  render() {
    return (
      <ClickOutHandler onClickOut={this.onClickOut}>
        <div>Click outside of me!</div>
      </ClickOutHandler>
    );
  }
}

Cela a bien fonctionné.

ErichBSchulz
la source
Cela fonctionne à merveille! Beaucoup plus simple que de nombreuses autres réponses ici et contrairement à au moins 3 autres solutions ici où pour toutes, j'ai reçu "Support for the experimental syntax 'classProperties' isn't currently enabled"cela, cela a fonctionné directement. Pour tous ceux qui essaient cette solution, n'oubliez pas d'effacer tout code persistant que vous pourriez avoir copié lors de l'essai des autres solutions. Par exemple, l'ajout d'écouteurs d'événements semble entrer en conflit avec cela.
Frikster
2

Je l'ai fait en partie en suivant cela et en suivant les documents officiels de React sur la gestion des références, ce qui nécessite de réagir ^ 16.3. C'est la seule chose qui a fonctionné pour moi après avoir essayé certaines des autres suggestions ici ...

class App extends Component {
  constructor(props) {
    super(props);
    this.inputRef = React.createRef();
  }
  componentWillMount() {
    document.addEventListener("mousedown", this.handleClick, false);
  }
  componentWillUnmount() {
    document.removeEventListener("mousedown", this.handleClick, false);
  }
  handleClick = e => {
    if (this.inputRef.current === e.target) {
      return;
    }
    this.handleclickOutside();
  };
handleClickOutside(){
...***code to handle what to do when clicked outside***...
}
render(){
return(
<div>
...***code for what's outside***...
<span ref={this.inputRef}>
...***code for what's "inside"***...
</span>
...***code for what's outside***
)}}
RawCode
la source
1

Un exemple avec la stratégie

J'aime les solutions fournies qui permettent de faire la même chose en créant un wrapper autour du composant.

Puisqu'il s'agit plus d'un comportement, j'ai pensé à la stratégie et j'ai proposé ce qui suit.

Je suis nouveau avec React et j'ai besoin d'un peu d'aide pour économiser du passe-partout dans les cas d'utilisation

Vérifiez et dites moi ce que vous en pensez s'il vous plait.

ClickOutsideBehavior

import ReactDOM from 'react-dom';

export default class ClickOutsideBehavior {

  constructor({component, appContainer, onClickOutside}) {

    // Can I extend the passed component's lifecycle events from here?
    this.component = component;
    this.appContainer = appContainer;
    this.onClickOutside = onClickOutside;
  }

  enable() {

    this.appContainer.addEventListener('click', this.handleDocumentClick);
  }

  disable() {

    this.appContainer.removeEventListener('click', this.handleDocumentClick);
  }

  handleDocumentClick = (event) => {

    const area = ReactDOM.findDOMNode(this.component);

    if (!area.contains(event.target)) {
        this.onClickOutside(event)
    }
  }
}

Exemple d'utilisation

import React, {Component} from 'react';
import {APP_CONTAINER} from '../const';
import ClickOutsideBehavior from '../ClickOutsideBehavior';

export default class AddCardControl extends Component {

  constructor() {
    super();

    this.state = {
      toggledOn: false,
      text: ''
    };

    this.clickOutsideStrategy = new ClickOutsideBehavior({
      component: this,
      appContainer: APP_CONTAINER,
      onClickOutside: () => this.toggleState(false)
    });
  }

  componentDidMount () {

    this.setState({toggledOn: !!this.props.toggledOn});
    this.clickOutsideStrategy.enable();
  }

  componentWillUnmount () {
    this.clickOutsideStrategy.disable();
  }

  toggleState(isOn) {

    this.setState({toggledOn: isOn});
  }

  render() {...}
}

Remarques

J'ai pensé à stocker les componentcrochets de cycle de vie passés et à les remplacer par des méthodes similaires à ceci:

const baseDidMount = component.componentDidMount;

component.componentDidMount = () => {
  this.enable();
  baseDidMount.call(component)
}

componentest le composant passé au constructeur de ClickOutsideBehavior.
Cela supprimera l'activation / désactivation du passe-partout de l'utilisateur de ce comportement, mais cela n'a pas l'air très agréable

kidroca
la source
1

Dans mon cas DROPDOWN, la solution de Ben Bud fonctionnait bien, mais j'avais un bouton bascule séparé avec un gestionnaire onClick. Ainsi, la logique de clic extérieur était en conflit avec le bouton du bouton bascule. Voici comment je l'ai résolu en passant également la référence du bouton:

import React, { useRef, useEffect, useState } from "react";

/**
 * Hook that triggers onClose when clicked outside of ref and buttonRef elements
 */
function useOutsideClicker(ref, buttonRef, onOutsideClick) {
  useEffect(() => {

    function handleClickOutside(event) {
      /* clicked on the element itself */
      if (ref.current && !ref.current.contains(event.target)) {
        return;
      }

      /* clicked on the toggle button */
      if (buttonRef.current && !buttonRef.current.contains(event.target)) {
        return;
      }

      /* If it's something else, trigger onClose */
      onOutsideClick();
    }

    // Bind the event listener
    document.addEventListener("mousedown", handleClickOutside);
    return () => {
      // Unbind the event listener on clean up
      document.removeEventListener("mousedown", handleClickOutside);
    };
  }, [ref]);
}

/**
 * Component that alerts if you click outside of it
 */
export default function DropdownMenu(props) {
  const wrapperRef = useRef(null);
  const buttonRef = useRef(null);
  const [dropdownVisible, setDropdownVisible] = useState(false);

  useOutsideClicker(wrapperRef, buttonRef, closeDropdown);

  const toggleDropdown = () => setDropdownVisible(visible => !visible);

  const closeDropdown = () => setDropdownVisible(false);

  return (
    <div>
      <button onClick={toggleDropdown} ref={buttonRef}>Dropdown Toggler</button>
      {dropdownVisible && <div ref={wrapperRef}>{props.children}</div>}
    </div>
  );
}
Ilyas Assainov
la source
0

Si vous souhaitez utiliser un petit composant (466 octets compressé) qui existe déjà pour cette fonctionnalité, vous pouvez consulter cette bibliothèque react-outclick .

La bonne chose à propos de la bibliothèque est qu'elle vous permet également de détecter les clics à l'extérieur d'un composant et à l'intérieur d'un autre. Il prend également en charge la détection d'autres types d'événements.

Tushar Sharma
la source
0

Si vous souhaitez utiliser un petit composant (466 octets compressé) qui existe déjà pour cette fonctionnalité, vous pouvez consulter cette bibliothèque react-outclick . Il capture les événements en dehors d'un composant React ou d'un élément jsx.

La bonne chose à propos de la bibliothèque est qu'elle vous permet également de détecter les clics à l'extérieur d'un composant et à l'intérieur d'un autre. Il prend également en charge la détection d'autres types d'événements.

Tushar Sharma
la source
0

Je sais que c'est une vieille question, mais je continue de la rencontrer et j'ai eu beaucoup de mal à la comprendre dans un format simple. Donc, si cela faciliterait la vie de quiconque, utilisez OutsideClickHandler par airbnb. C'est le plugin le plus simple pour accomplir cette tâche sans écrire votre propre code.

Exemple:

hideresults(){
   this.setState({show:false})
}
render(){
 return(
 <div><div onClick={() => this.setState({show:true})}>SHOW</div> {(this.state.show)? <OutsideClickHandler onOutsideClick={() => 
  {this.hideresults()}} > <div className="insideclick"></div> </OutsideClickHandler> :null}</div>
 )
}
Sam Nikaein
la source
0
import { useClickAway } from "react-use";

useClickAway(ref, () => console.log('OUTSIDE CLICKED'));
Melounek
la source
0

vous pouvez vous un moyen simple de résoudre votre problème, je vous montre:

....

const [dropDwonStatus , setDropDownStatus] = useState(false)

const openCloseDropDown = () =>{
 setDropDownStatus(prev => !prev)
}

const closeDropDown = ()=> {
 if(dropDwonStatus){
   setDropDownStatus(false)
 }
}
.
.
.
<parent onClick={closeDropDown}>
 <child onClick={openCloseDropDown} />
</parent>

cela fonctionne pour moi, bonne chance;)

Moshtaba Darzi
la source
-1

J'ai trouvé cela dans l'article ci-dessous:

render () {return ({this.node = node;}}> Toggle Popover {this.state.popupVisible && (Je suis un popover!)}); }}

Voici un excellent article sur ce problème: "Gérer les clics en dehors des composants React" https://larsgraubner.com/handle-outside-clicks-react/

Amanda Koster
la source
Bienvenue dans Stack Overflow! Les réponses qui ne sont qu'un lien sont généralement mal vues : meta.stackoverflow.com/questions/251006/… . À l'avenir, incluez un exemple de devis ou de code avec les informations pertinentes à la question. À votre santé!
Fred Stark
-1

Ajoutez un onClickgestionnaire à votre conteneur de niveau supérieur et incrémentez une valeur d'état chaque fois que l'utilisateur clique dessus. Transmettez cette valeur au composant approprié et chaque fois que la valeur change, vous pouvez travailler.

Dans ce cas, nous appelons this.closeDropdown()chaque fois que la clickCountvaleur change.

La incrementClickCountméthode se déclenche dans le .appconteneur, mais pas .dropdownparce que nous l'utilisons event.stopPropagation()pour empêcher la propagation d'événements.

Votre code peut finir par ressembler à ceci:

class App extends Component {
    constructor(props) {
        super(props);
        this.state = {
            clickCount: 0
        };
    }
    incrementClickCount = () => {
        this.setState({
            clickCount: this.state.clickCount + 1
        });
    }
    render() {
        return (
            <div className="app" onClick={this.incrementClickCount}>
                <Dropdown clickCount={this.state.clickCount}/>
            </div>
        );
    }
}
class Dropdown extends Component {
    constructor(props) {
        super(props);
        this.state = {
            open: false
        };
    }
    componentDidUpdate(prevProps) {
        if (this.props.clickCount !== prevProps.clickCount) {
            this.closeDropdown();
        }
    }
    toggleDropdown = event => {
        event.stopPropagation();
        return (this.state.open) ? this.closeDropdown() : this.openDropdown();
    }
    render() {
        return (
            <div className="dropdown" onClick={this.toggleDropdown}>
                ...
            </div>
        );
    }
}
wdm
la source
Je ne sais pas qui vote en aval, mais cette solution est assez propre par rapport aux écouteurs d'événements contraignants / non contraignants. Je n'ai eu aucun problème avec cette implémentation.
wdm
-1

Pour que la solution «focus» fonctionne pour la liste déroulante avec les écouteurs d'événements, vous pouvez les ajouter avec l' événement onMouseDown au lieu de onClick . De cette façon, l'événement se déclenchera et après cela, le popup se fermera comme suit:

<TogglePopupButton
                    onClick = { this.toggleDropup }
                    tabIndex = '0'
                    onBlur = { this.closeDropup }
                />
                { this.state.isOpenedDropup &&
                <ul className = { dropupList }>
                    { this.props.listItems.map((item, i) => (
                        <li
                            key = { i }
                            onMouseDown = { item.eventHandler }
                        >
                            { item.itemName}
                        </li>
                    ))}
                </ul>
                }
idtmc
la source
-1
import ReactDOM from 'react-dom' ;

class SomeComponent {

  constructor(props) {
    // First, add this to your constructor
    this.handleClickOutside = this.handleClickOutside.bind(this);
  }

  componentWillMount() {
    document.addEventListener('mousedown', this.handleClickOutside, false); 
  }

  // Unbind event on unmount to prevent leaks
  componentWillUnmount() {
    window.removeEventListener('mousedown', this.handleClickOutside, false);
  }

  handleClickOutside(event) {
    if(!ReactDOM.findDOMNode(this).contains(event.path[0])){
       console.log("OUTSIDE");
    }
  }
}
moment dipolaire
la source
Et n'oubliez pas deimport ReactDOM from 'react-dom';
dipole_moment
-1

J'ai fait une solution pour toutes les occasions.

Vous devez utiliser un composant d'ordre élevé pour envelopper le composant que vous souhaitez écouter pour les clics en dehors de celui-ci.

Cet exemple de composant n'a qu'un seul accessoire: "onClickedOutside" qui reçoit une fonction.

ClickedOutside.js
import React, { Component } from "react";

export default class ClickedOutside extends Component {
  componentDidMount() {
    document.addEventListener("mousedown", this.handleClickOutside);
  }

  componentWillUnmount() {
    document.removeEventListener("mousedown", this.handleClickOutside);
  }

  handleClickOutside = event => {
    // IF exists the Ref of the wrapped component AND his dom children doesnt have the clicked component 
    if (this.wrapperRef && !this.wrapperRef.contains(event.target)) {
      // A props callback for the ClikedClickedOutside
      this.props.onClickedOutside();
    }
  };

  render() {
    // In this piece of code I'm trying to get to the first not functional component
    // Because it wouldn't work if use a functional component (like <Fade/> from react-reveal)
    let firstNotFunctionalComponent = this.props.children;
    while (typeof firstNotFunctionalComponent.type === "function") {
      firstNotFunctionalComponent = firstNotFunctionalComponent.props.children;
    }

    // Here I'm cloning the element because I have to pass a new prop, the "reference" 
    const children = React.cloneElement(firstNotFunctionalComponent, {
      ref: node => {
        this.wrapperRef = node;
      },
      // Keeping all the old props with the new element
      ...firstNotFunctionalComponent.props
    });

    return <React.Fragment>{children}</React.Fragment>;
  }
}
Higor Lorenzon
la source
-1

Crochet UseOnClickOutside - React 16.8 +

Créer une fonction useOnOutsideClick générale

export const useOnOutsideClick = handleOutsideClick => {
  const innerBorderRef = useRef();

  const onClick = event => {
    if (
      innerBorderRef.current &&
      !innerBorderRef.current.contains(event.target)
    ) {
      handleOutsideClick();
    }
  };

  useMountEffect(() => {
    document.addEventListener("click", onClick, true);
    return () => {
      document.removeEventListener("click", onClick, true);
    };
  });

  return { innerBorderRef };
};

const useMountEffect = fun => useEffect(fun, []);

Utilisez ensuite le crochet dans n'importe quel composant fonctionnel.

const OutsideClickDemo = ({ currentMode, changeContactAppMode }) => {

  const [open, setOpen] = useState(false);
  const { innerBorderRef } = useOnOutsideClick(() => setOpen(false));

  return (
    <div>
      <button onClick={() => setOpen(true)}>open</button>
      {open && (
        <div ref={innerBorderRef}>
           <SomeChild/>
        </div>
      )}
    </div>
  );

};

Lien vers la démo

Partiellement inspiré par la réponse @ pau1fitzgerald.

Ben Carp
la source