Détecter le clic à L'extérieur 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 . jQuery closest () est utilisé pour voir si la cible d'un événement de clic a l'élément dom comme l'un de ses parents. S'il y a 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 à la fenêtre. Lorsque le gestionnaire se déclenche je dois comparer la cible avec les enfants dom de ma composante.

L'événement click contient des propriétés comme "path" 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à l'avoir mis dans une fonction utilitaire intelligente... Non?

173
demandé sur Thijs Koerselman 2015-09-13 21:34:00

20 réponses

La solution suivante utilise ES6 et suit les meilleures pratiques pour la liaison ainsi que la définition de la ref à travers une méthode.

Pour le voir en action: Code Sandbox démo

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,
};
269
répondu Ben Bud 2018-04-12 15:14:41

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 d'entrée. Ces éléments répondront également à l'événement flou , lorsqu'ils perdront cette focalisation.

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 normal, ce serait en définissant l'attribut tabindex, mais dans React vous devez utiliser tabIndex (notez la capitale I).

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

C'est ce que je l'utilisais pour faire un menu déroulant 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>
        );
    }
});
109
répondu Pablo Barría Urenda 2017-04-02 08:20:41

Après avoir essayé de nombreuses méthodes ici, j'ai décidé d'utiliser github.com/Pomax/react-onclickoutside à cause de la façon dont il est complet.

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

import onClickOutside from 'react-onclickoutside'

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

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

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

export default onClickOutside(NameOfComponent)

C'est ça.

71
répondu Beau Smith 2017-08-25 18:16:23

J'ai trouvé une solution grâce à Ben Alpert sur discuss.reactjs.org . L'approche suggérée attache un gestionnaire au document mais cela s'est avéré problématique. En cliquant sur l'un des composants de mon arborescence, un rerender a supprimé l'élément cliqué lors de la mise à jour. Parce que le redirection de React se produit avant que le gestionnaire de corps du document soit 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 le élément racine de l'application.

Principal:

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
répondu Thijs Koerselman 2016-05-13 08:40:31

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. Espérons qu'il sera utile à quelqu'un d'autre. Vous devez importer findDOMNode depuis 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
        });
    }
}
35
répondu Paul Fitzgerald 2018-03-16 16:06:15

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

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

17
répondu Niyaz 2017-06-05 22:45:45

Voici ma solution avec Réagir 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;
6
répondu onoya 2018-05-28 03:32:58

Voici ma démarche (démo - https://jsfiddle.net/agymay93/4/):

J'ai créé composant spécial appelé WatchClickOutside et il peut être utilisé comme (je suppose JSX syntaxe):

<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>
    );
  }
}
3
répondu pie6k 2017-01-14 17:28:28

Pour ceux qui ont besoin d'un positionnement absolu, une option simple que j'ai choisie est d'ajouter un composant wrapper qui est 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 intérieur composant.

<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 clic sur le contenu, l'événement sera également propagé à la div supérieure et déclenchera donc le handlerOutsideClick. 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
répondu Anthony Garant 2017-06-02 23:51:23

MA plus grande préoccupation avec toutes les autres réponses est d'avoir à filtrer les événements de clic de la racine / parent vers le bas. J'ai trouvé 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 gérer l'événement click sur l'élément fixe à l'intérieur du même composant. Garde tout centralisé à 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
répondu metroninja 2017-05-10 18:39:04

Un exemple avec la Stratégie

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

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

Je suis nouveau avec React et j'ai besoin d'un peu d'aide pour économiser de l'argent dans les cas d'utilisation

Veuillez passer en revue et me dire ce que vous en pensez.

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

Échantillon 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() {...}
}

Notes

J'ai pensé à stocker les crochets de cycle de vie component passés 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'enable / disable boilerplate de l'utilisateur de ce comportement mais cela n'a pas l'air très agréable

1
répondu kidroca 2017-07-21 17:50:13
componentWillMount(){

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

handleClickOutside(event) {

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

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

1
répondu ISanchez 2017-11-29 17:41:57

J'ai utilisé ce module (je n'ai pas d'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.

0
répondu ErichBSchulz 2018-04-22 03:05:08

J'ai trouvé ceci de l'article ci-dessous:

Rendu() { retourner ( { ce.node = nœud; }} > Bascule Liste {ce.état.popupVisible && ( Je suis une liste! )} ); } }

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

0
répondu Amanda Koster 2018-05-02 17:52:21

Ajoutez un gestionnaire onClick à votre conteneur de niveau supérieur et incrémentez une valeur d'état chaque fois que l'utilisateur clique dessus. Passez cette valeur au composant concerné 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 se déclenche dans le conteneur .app mais pas dans le .dropdown car nous utilisons event.stopPropagation() pour empêcher le bouillonnement des é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
répondu wdm 2018-05-04 18:05:15

Je l'ai fait en partie en suivant this et en suivant les docs officiels React sur la gestion des refs qui nécessitent react ^ 16.3. C'est la seule chose qui a fonctionné pour moi, après avoir essayé quelques 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***
)}}
0
répondu RawCode 2018-05-07 08:44:21

Pour étendre sur la réponse acceptée faite par Ben Bud, Si vous utilisez des composants stylés, passer des refs de cette façon vous donnera une erreur telle que " this.wrapperRef.contient n'est pas une fonction".

Le correctif suggéré, dans les commentaires, pour envelopper le composant stylé avec un div et passer la ref là, fonctionne. Cela dit, dans leurs documents , ils expliquent déjà la raison de cela et l'utilisation correcte des refs dans les composants stylés:

Passer un prop ref à un composant styled vous donnera une instance du wrapper StyledComponent, mais pas au nœud DOM sous-jacent. Cela est dû à la façon dont les refs fonctionnent. Il n'est pas possible d'appeler les méthodes DOM, comme focus, sur nos wrappers directement. Pour obtenir une référence au nœud DOM réel, enveloppé, passez le rappel à la prop innerRef à la place.

Comme ça:

<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 le " onBlur" approche:

componentDidMount(){
    this.el.focus()
}
blurHandler = () => {
    console.log('clicked outside')
}
render(){
    return(
        <StyledDiv
            onBlur={this.blurHandler}
            tabIndex="0"
            innerRef={el => { this.el = el }}
        />
    )
}
0
répondu Eric Delli Compagni 2018-07-05 02:36:42

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

En raison du fait que React a son propre gestionnaire d'événements artificiel, vous ne pouvez pas utiliser le document comme base pour les écouteurs d'événements. Vous devez e.stopPropagation() avant cela car React utilise le document lui-même. Si vous utilisez par exemple document.querySelector('body') à la place. Vous êtes en mesure d'empêcher le clic du lien React. Voici un exemple de la façon dont j'implémente click outside et fermer.
Il utilise ES6 et Réagir 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;
0
répondu Richard Herries 2018-07-25 06:28:17

Pour que la solution 'focus' fonctionne pour la liste déroulante avec les écouteurs d'événements, vous pouvez les ajouter avec OnMouseDown événement au lieu deonClick . 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>
                }
0
répondu idtmc 2018-08-06 21:58:01

Vous pouvez simplement installer un gestionnaire de double-clic sur le corps et un autre sur cet élément. Dans le gestionnaire de cet élément, il suffit de retourner false pour empêcher la propagation de l'événement. Ainsi, lorsqu'un double clic se produit s'il se trouve sur l'élément, il sera intercepté et ne se propagera pas au gestionnaire sur le corps. Sinon, il sera pris par le gestionnaire sur le corps.

Update: si vous ne voulez vraiment pas empêcher la propagation des événements, il vous suffit d'utiliser closest pour vérifier si le clic arrivé sur votre élément ou un de ses enfants:

<html>
<head>
<script src="https://code.jquery.com/jquery-2.1.4.min.js"></script>
<script>
$(document).on('click', function(event) {
    if (!$(event.target).closest('#div3').length) {
    alert("outside");
    }
});
</script>
</head>
<body>
    <div style="background-color:blue;width:100px;height:100px;" id="div1"></div>
    <div style="background-color:red;width:100px;height:100px;" id="div2"></div>
    <div style="background-color:green;width:100px;height:100px;" id="div3"></div>
    <div style="background-color:yellow;width:100px;height:100px;" id="div4"></div>
    <div style="background-color:grey;width:100px;height:100px;" id="div5"></div>
</body>
</html>

Mise à jour: sans jQuery:

<html>
<head>
<script>
function findClosest (element, fn) {
  if (!element) return undefined;
  return fn(element) ? element : findClosest(element.parentElement, fn);
}
document.addEventListener("click", function(event) {
    var target = findClosest(event.target, function(el) {
        return el.id == 'div3'
    });
    if (!target) {
        alert("outside");
    }
}, false);
</script>
</head>
<body>
    <div style="background-color:blue;width:100px;height:100px;" id="div1"></div>
    <div style="background-color:red;width:100px;height:100px;" id="div2"></div>
    <div style="background-color:green;width:100px;height:100px;" id="div3">
        <div style="background-color:pink;width:50px;height:50px;" id="div6"></div>
    </div>
    <div style="background-color:yellow;width:100px;height:100px;" id="div4"></div>
    <div style="background-color:grey;width:100px;height:100px;" id="div5"></div>
</body>
</html>
-6
répondu benohead 2015-09-14 18:47:40