Comment écouter les événements de clic qui sont en dehors d'un composant

89

Je souhaite fermer un menu déroulant lorsqu'un clic se produit en dehors du composant déroulant.

Comment je fais ça?

Allan Hortle
la source

Réponses:

58

Dans l'élément que j'ai ajouté mousedownet mouseupcomme ceci:

onMouseDown={this.props.onMouseDown} onMouseUp={this.props.onMouseUp}

Ensuite, chez le parent, je fais ceci:

componentDidMount: function () {
    window.addEventListener('mousedown', this.pageClick, false);
},

pageClick: function (e) {
  if (this.mouseIsDownOnCalendar) {
      return;
  }

  this.setState({
      showCal: false
  });
},

mouseDownHandler: function () {
    this.mouseIsDownOnCalendar = true;
},

mouseUpHandler: function () {
    this.mouseIsDownOnCalendar = false;
}

Le showCalest un booléen qui truemontre dans mon cas un calendrier et le falsecache.

Robbert van Elk
la source
cela lie le clic spécifiquement à une souris, cependant. Les événements de clic peuvent être générés par des événements tactiles et saisir des clés également, auxquels cette solution ne pourra pas réagir, ce qui la rend inadaptée à des fins mobiles et d'accessibilité = (
Mike 'Pomax' Kamermans
@ Mike'Pomax'Kamermans Vous pouvez désormais utiliser onTouchStart et onTouchEnd pour mobile. facebook.github.io/react/docs/events.html#touch-events
naoufal
3
Ceux-ci existent depuis longtemps, mais ne fonctionnent pas bien avec Android, car vous devez appeler preventDefault()les événements immédiatement ou le comportement natif d'Android entre en action, ce qui interfère avec le prétraitement de React. J'ai écrit npmjs.com/package/react-onclickoutside depuis.
Mike 'Pomax' Kamermans
J'aime cela! Félicité. La suppression de l'écouteur d'événement sur mousedown sera utile. componentWillUnmount = () => window.removeEventListener ('mousedown', this.pageClick, false);
Juni Brosas
73

À l'aide des méthodes de cycle de vie, ajoutez et supprimez des écouteurs d'événements dans le document.

React.createClass({
    handleClick: function (e) {
        if (this.getDOMNode().contains(e.target)) {
            return;
        }
    },

    componentWillMount: function () {
        document.addEventListener('click', this.handleClick, false);
    },

    componentWillUnmount: function () {
        document.removeEventListener('click', this.handleClick, false);
    }
});

Consultez les lignes 48-54 de ce composant: https://github.com/i-like-robots/react-tube-tracker/blob/91dc0129a1f6077bef57ea4ad9a860be0c600e9d/app/component/tube-tracker.jsx#L48-54

i_like_robots
la source
Cela en ajoute un au document, mais cela signifie que tout événement de clic sur le composant déclenche également l'événement de document. Cela provoque ses propres problèmes car la cible de l'écouteur de document est toujours une div basse à la fin d'un composant, pas le composant lui-même.
Allan Hortle
Je ne suis pas tout à fait sûr de suivre votre commentaire, mais si vous liez des écouteurs d'événements au document, vous pouvez filtrer tous les événements de clic qui s'y trouvent, soit en faisant correspondre un sélecteur (délégation d'événement standard), soit par toute autre exigence arbitraire (EG était l'élément cible pas dans un autre élément).
i_like_robots
3
Cela pose des problèmes comme le souligne @AllanHortle. stopPropagation sur un événement react n'empêchera pas les gestionnaires d'événements de document de recevoir l'événement.
Matt Crinklaw-Vogt
4
Pour toute personne intéressée, j'avais des problèmes avec stopPropagation lors de l'utilisation du document. Si vous attachez votre écouteur d'événements à la fenêtre, cela semble résoudre ce problème?
Titus
10
Comme mentionné, this.getDOMNode () est obsolète. utilisez plutôt ReactDOM comme ceci: ReactDOM.findDOMNode (this) .contains (e.target)
Arne H.Bitubekk
17

Regardez la cible de l'événement, si l'événement était directement sur le composant, ou les enfants de ce composant, alors le clic était à l'intérieur. Sinon, c'était à l'extérieur.

React.createClass({
    clickDocument: function(e) {
        var component = React.findDOMNode(this.refs.component);
        if (e.target == component || $(component).has(e.target).length) {
            // Inside of the component.
        } else {
            // Outside of the component.
        }

    },
    componentDidMount: function() {
        $(document).bind('click', this.clickDocument);
    },
    componentWillUnmount: function() {
        $(document).unbind('click', this.clickDocument);
    },
    render: function() {
        return (
            <div ref='component'>
                ...
            </div> 
        )
    }
});

Si cela doit être utilisé dans de nombreux composants, c'est mieux avec un mixin:

var ClickMixin = {
    _clickDocument: function (e) {
        var component = React.findDOMNode(this.refs.component);
        if (e.target == component || $(component).has(e.target).length) {
            this.clickInside(e);
        } else {
            this.clickOutside(e);
        }
    },
    componentDidMount: function () {
        $(document).bind('click', this._clickDocument);
    },
    componentWillUnmount: function () {
        $(document).unbind('click', this._clickDocument);
    },
}

Voir l'exemple ici: https://jsfiddle.net/0Lshs7mg/1/

ja
la source
@ Mike'Pomax'Kamermans qui a été corrigé, je pense que cette réponse ajoute des informations utiles, peut-être que votre commentaire peut maintenant être supprimé.
ja
vous avez annulé mes modifications pour une mauvaise raison. this.refs.componentfait référence à l'élément DOM à partir de 0,14 facebook.github.io/react/blog/2015/07/03/…
Gajus
@GajusKuizinas - bien pour faire ce changement une fois que la version 0.14 est la dernière version (maintenant est la version bêta).
ja
Qu'est-ce que le dollar?
Ben Sinclair
2
Je déteste pousser jQuery avec React car ils sont deux cadres de vue.
Abdennour TOUMI
11

Pour votre cas d'utilisation spécifique, la réponse actuellement acceptée est un peu sur-conçue. Si vous voulez écouter quand un utilisateur clique hors d'une liste déroulante, utilisez simplement un <select>composant comme élément parent et attachez unonBlur gestionnaire.

Le seul inconvénient de cette approche est qu'elle suppose que l'utilisateur a déjà maintenu le focus sur l'élément, et qu'elle repose sur un contrôle de formulaire (qui peut ou non être ce que vous voulez si vous prenez en compte que la tabclé se concentre également et brouille les éléments ) - mais ces inconvénients ne sont vraiment qu'une limite pour des cas d'utilisation plus compliqués, auquel cas une solution plus compliquée pourrait être nécessaire.

 var Dropdown = React.createClass({

   handleBlur: function(e) {
     // do something when user clicks outside of this element
   },

   render: function() {
     return (
       <select onBlur={this.handleBlur}>
         ...
       </select>
     );
   }
 });
razorbeard
la source
10
onBlurdiv fonctionne également avec div s'il a un tabIndexattribut
gorpacrate
Pour moi, c'est la solution que j'ai trouvée la plus simple et la plus facile à utiliser, je l'utilise sur un élément de bouton et fonctionne comme un charme. Merci!
Amir5000
5

J'ai écrit un gestionnaire d'événements générique pour les événements qui proviennent de l'extérieur du composant, react-outside-event .

La mise en œuvre elle-même est simple:

  • Lorsque le composant est monté, un gestionnaire d'événements est attaché au window objet.
  • Lorsqu'un événement se produit, le composant vérifie si l'événement provient de l'intérieur du composant. Si ce n'est pas le cas, cela déclencheonOutsideEvent sur le composant cible.
  • Lorsque le composant est démonté, le gestionnaire d'événements est détacthé.
import React from 'react';
import ReactDOM from 'react-dom';

/**
 * @param {ReactClass} Target The component that defines `onOutsideEvent` handler.
 * @param {String[]} supportedEvents A list of valid DOM event names. Default: ['mousedown'].
 * @return {ReactClass}
 */
export default (Target, supportedEvents = ['mousedown']) => {
    return class ReactOutsideEvent extends React.Component {
        componentDidMount = () => {
            if (!this.refs.target.onOutsideEvent) {
                throw new Error('Component does not defined "onOutsideEvent" method.');
            }

            supportedEvents.forEach((eventName) => {
                window.addEventListener(eventName, this.handleEvent, false);
            });
        };

        componentWillUnmount = () => {
            supportedEvents.forEach((eventName) => {
                window.removeEventListener(eventName, this.handleEvent, false);
            });
        };

        handleEvent = (event) => {
            let target,
                targetElement,
                isInside,
                isOutside;

            target = this.refs.target;
            targetElement = ReactDOM.findDOMNode(target);
            isInside = targetElement.contains(event.target) || targetElement === event.target;
            isOutside = !isInside;



            if (isOutside) {
                target.onOutsideEvent(event);
            }
        };

        render() {
            return <Target ref='target' {... this.props} />;
        }
    }
};

Pour utiliser le composant, vous devez encapsuler la déclaration de classe de composant cible à l'aide du composant d'ordre supérieur et définir les événements que vous souhaitez gérer:

import React from 'react';
import ReactDOM from 'react-dom';
import ReactOutsideEvent from 'react-outside-event';

class Player extends React.Component {
    onOutsideEvent = (event) => {
        if (event.type === 'mousedown') {

        } else if (event.type === 'mouseup') {

        }
    }

    render () {
        return <div>Hello, World!</div>;
    }
}

export default ReactOutsideEvent(Player, ['mousedown', 'mouseup']);
Gajus
la source
4

J'ai voté pour l'une des réponses même si cela n'a pas fonctionné pour moi. Cela a fini par m'amener à cette solution. J'ai légèrement changé l'ordre des opérations. J'écoute mouseDown sur la cible et mouseUp sur la cible. Si l'un de ceux-ci retourne TRUE, nous ne fermons pas le modal. Dès qu'un clic est enregistré, n'importe où, ces deux booléens {mouseDownOnModal, mouseUpOnModal} sont remis à false.

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

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

_handlePageClick(e) {
    var wasDown = this.mouseDownOnModal;
    var wasUp = this.mouseUpOnModal;
    this.mouseDownOnModal = false;
    this.mouseUpOnModal = false;
    if (!wasDown && !wasUp)
        this.close();
},

_handleMouseDown() {
    this.mouseDownOnModal = true;
},

_handleMouseUp() {
    this.mouseUpOnModal = true;
},

render() {
    return (
        <Modal onMouseDown={this._handleMouseDown} >
               onMouseUp={this._handleMouseUp}
            {/* other_content_here */}
        </Modal>
    );
}

Cela présente l'avantage que tout le code repose sur le composant enfant et non sur le parent. Cela signifie qu'il n'y a pas de code standard à copier lors de la réutilisation de ce composant.

Steve Zelaznik
la source
3
  1. Créez un calque fixe qui couvre tout l'écran ( .backdrop).
  2. Avoir l'élément cible ( .target) en dehors de l' .backdropélément et avec un index d'empilement supérieur ( z-index).

Ensuite, tout clic sur l' .backdropélément sera considéré "en dehors de l' .targetélément".

.click-overlay {
    position: fixed;
    left: 0;
    right: 0;
    top: 0;
    bottom: 0;
    z-index: 1;
}

.target {
    position: relative;
    z-index: 2;
}
Federico
la source
2

Vous pourriez utiliser ref s pour y parvenir, quelque chose comme ce qui suit devrait fonctionner.

Ajoutez le refà votre élément:

<div ref={(element) => { this.myElement = element; }}></div>

Vous pouvez ensuite ajouter une fonction pour gérer le clic en dehors de l'élément comme ceci:

handleClickOutside(e) {
  if (!this.myElement.contains(e)) {
    this.setState({ myElementVisibility: false });
  }
}

Puis enfin, ajoutez et supprimez les écouteurs d'événement sur le montage et le démontage.

componentWillMount() {
  document.addEventListener('click', this.handleClickOutside, false);  // assuming that you already did .bind(this) in constructor
}

componentWillUnmount() {
  document.removeEventListener('click', this.handleClickOutside, false);  // assuming that you already did .bind(this) in constructor
}
Chris Borrowdale
la source
Modification de votre réponse, il y avait une erreur possible en référence à l'appel handleClickOutsideen document.addEventListener()ajoutant une thisréférence. Sinon, cela donne Uncaught ReferenceError: handleClickOutside n'est pas défini danscomponentWillMount()
Muhammad Hannan
1

Super tard à la fête, mais j'ai réussi à définir un événement de flou sur l'élément parent de la liste déroulante avec le code associé pour fermer la liste déroulante, et également à attacher un écouteur mousedown à l'élément parent qui vérifie si la liste déroulante est ouverte ou non, et arrêtera la propagation de l'événement s'il est ouvert afin que l'événement de flou ne soit pas déclenché.

Puisque l'événement mousedown bouillonne, cela empêchera tout mousedown sur les enfants de provoquer un flou sur le parent.

/* Some react component */
...

showFoo = () => this.setState({ showFoo: true });

hideFoo = () => this.setState({ showFoo: false });

clicked = e => {
    if (!this.state.showFoo) {
        this.showFoo();
        return;
    }
    e.preventDefault()
    e.stopPropagation()
}

render() {
    return (
        <div 
            onFocus={this.showFoo}
            onBlur={this.hideFoo}
            onMouseDown={this.clicked}
        >
            {this.state.showFoo ? <FooComponent /> : null}
        </div>
    )
}

...

e.preventDefault () ne devrait pas avoir à être appelé pour autant que je sache, mais Firefox ne joue pas bien sans lui pour une raison quelconque. Fonctionne sur Chrome, Firefox et Safari.

Tanner Stults
la source
0

J'ai trouvé un moyen plus simple à ce sujet.

Il vous suffit d'ajouter onHide(this.closeFunction)le modal

<Modal onHide={this.closeFunction}>
...
</Modal>

En supposant que vous ayez une fonction pour fermer le modal.

M. Murazza
la source
-1

Utilisez l'excellent mixin réact-onclickoutside :

npm install --save react-onclickoutside

Puis

var Component = React.createClass({
  mixins: [
    require('react-onclickoutside')
  ],
  handleClickOutside: function(evt) {
    // ...handling code goes here... 
  }
});
e18r
la source
4
Les mix-ins FYI sont désormais obsolètes dans React.
machineghost