web-dev-qa-db-fra.com

Détecter un clic en dehors du composant React

Je cherche 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 article . jQuery la plus proche () est utilisée pour voir si l'élément cible d'un événement click a l'élément dom comme l'un de ses parents. S'il existe une 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 clic à window. Lorsque le gestionnaire déclenche, je dois comparer la cible aux dom enfants de mon composant.

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

224
Thijs Koerselman

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

Pour le voir en action: Code Sandbox demo

import React, { Component } from 'react';
import PropTypes from 'prop-types';

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

    this.setWrapperRef = this.setWrapperRef.bind(this);
    this.handleClickOutside = this.handleClickOutside.bind(this);
  }

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

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

  /**
   * Set the wrapper ref
   */
  setWrapperRef(node) {
    this.wrapperRef = node;
  }

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

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

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

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

Certains éléments HTML peuvent avoir ce qu’on appelle " focus ", par exemple des éléments en entrée. Ces éléments réagiront également à l'événement blur lorsqu'ils perdront ce focus. 

Pour donner à un élément la capacité d'avoir le focus, assurez-vous que son attribut tabindex est défini sur autre chose que -1. En HTML normal, ce serait en définissant l'attribut tabindex, mais en React, vous devez utiliser tabIndex (notez le capital I).

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>
        );
    }
});
117

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 je l'ai importé dans mon composant:

import onClickOutside from 'react-onclickoutside'

Ensuite, dans ma classe de composant, j'ai défini la méthode handleClickOutside:

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 tout.

79
Beau Smith

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 aidera quelqu'un d'autre. Vous devez importer findDOMNode à partir de react-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
        });
    }
}

Crochet de réaction (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 codesandbox example ici.

46
Paul Fitzgerald

J'ai trouvé une solution grâce à Ben Alpert sur discussion.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 arbre, une ré-émérite a supprimé l'élément cliqué lors de la mise à jour. Etant donné que la restitution de React se produit avant le gestionnaire de corps du document est appelé, l'élément n'a pas été détecté comme "à l'intérieur" de l'arborescence.

La solution à cela é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>
    )
  }
}
37
Thijs Koerselman

Aucune des autres réponses ici ne fonctionnait pour moi. J'essayais de cacher une fenêtre contextuelle sur le flou, mais comme le contenu était parfaitement positionné, l'OnBlur se déclenche 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.

22
Niyaz

[Mise à jour] Solution avec React ^ 16.8 en utilisant 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;
11
onoya

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

J'ai créé un composant spécial appelé WatchClickOutside et qui peut être utilisé comme suit (je suppose que la syntaxe JSX)

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

Voici le code du composant WatchClickOutside:

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>
    );
  }
}
4
pie6k

Pour ceux qui ont besoin d'un positionnement absolu, une option simple que j'ai choisie consiste à ajouter un composant wrapper stylisé de manière à 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>

Dans l'état actuel des choses, si vous ajoutez un gestionnaire de clics sur le contenu, l'événement sera également propagé au div supérieur et déclenchera donc le gestionnaireOutsideClick. Si ce n'est pas votre comportement souhaité, arrêtez simplement la progression de l'événement sur votre gestionnaire.

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

`

2
Anthony Garant

Pour prolonger la réponse acceptée faite par Ben Bud , si vous utilisez des composants stylés, le fait de passer les références de cette façon vous donnera une erreur du type "this.wrapperRef.contains n'est pas une fonction".

La solution suggérée, dans les commentaires, pour envelopper le composant stylé avec une div et y passer l’arbitre, ça marche . Cela dit, dans leur docs ils expliquent déjà la raison de ceci et le bon utilisation de références dans des composants stylés:

Passer un prop prop à 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 arbitres. Il n'est pas possible d'appeler directement des méthodes DOM, telles que focus, sur nos wrappers . Pour obtenir une référence au nœud DOM encapsulé réel, transmettez le rappel à la propriété innerRef.

Ainsi:

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

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

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

Ceci s'applique également à 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 }}
        />
    )
}
2

J'ai utilisé ce module (je n'ai aucune association 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>
    );
  }
}

Il a bien fait le travail.

2
ErichBSchulz

Solution avec crochets de réaction (16.8 +)

tl; dr ( Codesandbox ):

Composant "interne"

function OutsideClickComp() {
  const innerRef = useRef(null);
  useOuterClickNotifier(
    // if you want to call it once only,
    // don't provide an anonymous function here
    e => alert("clicked outside of this component!"),
    innerRef
  );
  return (
    <div ref={innerRef}>
      inside component
    </div>
  );
}

Crochet Personnalisé

function useOuterClickNotifier(onOuterClick, innerRef) {
  useLayoutEffect(
    () => {
      document.addEventListener("click", handleClick);
      // called for each previous layout effect
      return () => document.removeEventListener("click", handleClick);

      function handleClick(e) {
        !innerRef.current.contains(e.target) && onOuterClick(e);
      }
    },
    [onOuterClick, innerRef] // invoke again, if inputs have changed
  );
}

Voici ma solution: je devais mettre en place un type de menu contextuel qui se ferme automatiquement lorsque vous cliquez en dehors du conteneur de menus. En utilisant des crochets, IMO présente les avantages suivants:

  • la logique d’effet latéral/dynamique de l’enregistrement de clics extérieurs peut être complètement abstraite et alléger le composant "interne"
  • réutilisation de useOuterClickNotifier partout
  • pas de composant supplémentaire d'emballage (remplacé par la logique de raccordement)

Ensuite, en fonction de votre cas d'utilisation, vous pouvez créer quelque chose dans le rappel de clic externe, qui est stubé ici pour plus de simplicité avec alert("clicked outside of this component!"). Par exemple. définir un état avec useState hook ou invoquer un rappel à bascule (dans mon cas) pour ouvrir/fermer un menu contextuel.

Edit: useLayoutEffect vs useEffect

useEffect est déclenché après la phase de rendu pendant un événement deferred, alors que useLayoutEffect est appelée synchrone après chaque rendu. Ainsi, useLayoutEffect se comporte comme les méthodes de cycle de vie componentDidMount et componentDidUpdate dans les composants de classe. Il est utilisé pour s'assurer que, lorsque les entrées de la fonction de hook useOuterClickNotifier ont changé, un écouteur de clic sur un document précédent ne peut pas se déclencher avant d'être désenregistré. Voir timings de crochet d'effet et useLayoutEffect

J'espère que cela pourra aider.

2
ford04
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é

2
Iván

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

En raison du fait que React dispose de son propre gestionnaire d'événements artificiel, vous ne pouvez pas utiliser le document comme base pour les écouteurs d'événements. Vous devez préalablement appeler e.stopPropagation() car React utilise le document lui-même. Si vous utilisez par exemple document.querySelector('body') à la place. Vous pouvez empêcher le clic du lien Réagir. Voici un exemple de la façon dont je mets en œuvre les clics en dehors et à l’extérieur.
.__ Ceci utilise ES6 et Réagit 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;
1
Richard Herries

Je l’ai fait en partie en suivant this et en respectant la documentation officielle de React sur le traitement des demandes de référence qui nécessite une réaction ^ 16.3. C’est la seule chose qui a fonctionné pour moi après avoir essayé certaines des suggestions proposées 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***
)}}
1
RawCode

Un exemple avec Strategy

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

Comme il s’agit plus d’un comportement, j’ai pensé à Strategy et proposé ce qui suit. 

Je suis nouveau chez React et j’ai besoin d’un peu d’aide pour économiser de l’énergie 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 points d'ancrage passés du cycle de vie component et les remplacer par des méthodes similaires à ceci:

const baseDidMount = component.componentDidMount;

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

component est le composant transmis au constructeur de ClickOutsideBehavior.
Cela supprimera l’activation/désactivation du passe-partout de l’utilisateur de ce comportement, mais il n’a pas l’air très agréable

1
kidroca

Ma plus grande préoccupation avec toutes les autres réponses est de devoir filtrer les événements de clic de la racine/parent vers le bas. J'ai trouvé le moyen le plus simple de définir simplement 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 du même composant. Garde tout centralisé sur 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;
    }
}
1
metroninja

Pour que la solution 'focus' fonctionne avec des écouteurs d’événement, vous pouvez les ajouter avec l’événement onMouseDown au lieu de onClick. De cette façon, l’événement se déclenchera et 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>
                }
0
idtmc

J'ai trouvé ceci de 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: "Gestion des clics en dehors des composants de React"https://larsgraubner.com/handle-outside-clicks-react/

0
Amanda Koster

Crochet UseOnClickOutside - React 16.8 +

Créer une fonction générale useOnOutsideClick

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, []);

Ensuite, utilisez 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.

0
Ben Carp

J'ai fait une solution pour toutes les occasions.

Vous devez utiliser un composant de niveau élevé pour encapsuler le composant que vous souhaitez écouter pour les clics en dehors de celui-ci.

Cet exemple de composant n'a qu'un seul objet: "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>;
  }
}
0
Higor Lorenzon

Ajoutez un gestionnaire onClick à 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 valeur clickCount change.

La méthode incrementClickCount est déclenchée dans le conteneur .app mais pas le .dropdown car nous 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>
        );
    }
}
0
wdm
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");
    }
  }
}
0
dipole_moment