So warten Sie auf Klickereignisse, die sich außerhalb einer Komponente befinden

89

Ich möchte ein Dropdown-Menü schließen, wenn ein Klick außerhalb der Dropdown-Komponente auftritt.

Wie mache ich das?

Allan Hortle
quelle

Antworten:

58

In dem Element, das ich hinzugefügt habe mousedownund das mouseupgefällt:

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

Dann mache ich beim Elternteil Folgendes:

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

Das showCalist ein Boolescher Wert, der truein meinem Fall einen Kalender anzeigt und falseversteckt.

Robbert van Elk
quelle
Dies bindet den Klick jedoch speziell an eine Maus. Klickereignisse können durch Berührungsereignisse erzeugt werden und auch Tasten eingeben, auf die diese Lösung nicht reagieren kann, was sie für mobile Zwecke und Zugänglichkeitszwecke ungeeignet macht = (
Mike 'Pomax' Kamermans
@ Mike'Pomax'Kamermans Sie können jetzt onTouchStart und onTouchEnd für Handys verwenden. facebook.github.io/react/docs/events.html#touch-events
naoufal
3
Diese existieren schon seit langer Zeit, spielen aber mit Android nicht gut, da Sie preventDefault()die Ereignisse sofort aufrufen müssen oder das native Android-Verhalten einsetzt, das durch die Vorverarbeitung von React beeinträchtigt wird. Ich habe seitdem npmjs.com/package/react-onclickoutside geschrieben .
Mike 'Pomax' Kamermans
Ich liebe es! Empfohlen. Das Entfernen des Ereignis-Listeners beim Mousedown ist hilfreich. componentWillUnmount = () => window.removeEventListener ('mousedown', this.pageClick, false);
Juni Brosas
72

Mithilfe der Lebenszyklusmethoden können Sie dem Dokument Ereignis-Listener hinzufügen und daraus entfernen.

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

Überprüfen Sie die Zeilen 48-54 dieser Komponente: https://github.com/i-like-robots/react-tube-tracker/blob/91dc0129a1f6077bef57ea4ad9a860be0c600e9d/app/component/tube-tracker.jsx#L48-54

i_like_robots
quelle
Dadurch wird dem Dokument eine hinzugefügt. Dies bedeutet jedoch, dass alle Klickereignisse auf der Komponente auch das Dokumentereignis auslösen. Dies verursacht eigene Probleme, da das Ziel des Dokumentlisteners immer ein niedriger Div am Ende einer Komponente ist, nicht die Komponente selbst.
Allan Hortle
Ich bin mir nicht ganz sicher, ob ich Ihrem Kommentar folge, aber wenn Sie Ereignis-Listener an das Dokument binden, können Sie alle Klickereignisse filtern, die dazu aufsteigen, indem Sie entweder einen Selektor (Standard-Ereignisdelegation) oder andere willkürliche Anforderungen (EG war) abgleichen das Zielelement nicht innerhalb eines anderen Elements).
i_like_robots
3
Dies verursacht Probleme, wie @AllanHortle hervorhebt. stopPropagation für ein Reaktionsereignis verhindert nicht, dass die Dokumentereignishandler das Ereignis empfangen.
Matt Crinklaw-Vogt
4
Für alle Interessierten hatte ich Probleme mit stopPropagation bei der Verwendung des Dokuments. Wenn Sie Ihren Ereignis-Listener an das Fenster anhängen, scheint dieses Problem behoben zu sein?
Titus
10
Wie bereits erwähnt, ist this.getDOMNode () veraltet. Verwenden Sie stattdessen ReactDOM wie folgt: ReactDOM.findDOMNode (this) .contains (e.target)
Arne H. Bitubekk
17

Sehen Sie sich das Ziel des Ereignisses an. Wenn sich das Ereignis direkt auf der Komponente oder untergeordneten Elementen dieser Komponente befand, befand sich der Klick im Inneren. Ansonsten war es draußen.

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

Wenn dies in vielen Komponenten verwendet werden soll, ist es mit einem Mixin schöner:

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

Siehe Beispiel hier: https://jsfiddle.net/0Lshs7mg/1/

ja
quelle
@ Mike'Pomax'Kamermans, der behoben wurde, ich denke, diese Antwort fügt nützliche Informationen hinzu, vielleicht kann Ihr Kommentar jetzt entfernt werden.
Ja
Sie haben meine Änderungen aus einem falschen Grund rückgängig gemacht. this.refs.componentbezieht sich auf das DOM-Element ab
0.14
@GajusKuizinas - gut, um diese Änderung vorzunehmen, sobald 0.14 die neueste Version ist (jetzt ist Beta).
Ja
Was ist Dollar?
Ben Sinclair
2
Ich hasse es, jQuery mit React zu stupsen, da es sich um das Framework von zwei Ansichten handelt.
Abdennour TOUMI
11

Für Ihren speziellen Anwendungsfall ist die derzeit akzeptierte Antwort etwas überarbeitet. Wenn Sie warten möchten, wenn ein Benutzer aus einer Dropdown-Liste klickt, verwenden Sie einfach eine <select>Komponente als übergeordnetes Element und fügen Sie eine hinzuonBlur Handler hinzu.

Der einzige Nachteil dieses Ansatzes besteht darin, dass davon ausgegangen wird, dass der Benutzer den Fokus auf das Element bereits beibehalten hat, und dass er sich auf ein Formularsteuerelement stützt (das möglicherweise das ist, was Sie möchten oder nicht, wenn Sie berücksichtigen, dass der tabSchlüssel auch Elemente fokussiert und verwischt ) - aber diese Nachteile sind nur eine Grenze für kompliziertere Anwendungsfälle. In diesem Fall könnte eine kompliziertere Lösung erforderlich sein.

 var Dropdown = React.createClass({

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

   render: function() {
     return (
       <select onBlur={this.handleBlur}>
         ...
       </select>
     );
   }
 });
Rasiermesser
quelle
10
onBlurdiv funktioniert auch mit div, wenn es tabIndexAttribut hat
gorpacrate
Für mich war dies die Lösung, die ich am einfachsten und am einfachsten zu verwenden fand. Ich verwende sie für ein Schaltflächenelement und arbeite wie ein Zauber. Vielen Dank!
Amir5000
5

Ich habe einen generischen Ereignishandler für Ereignisse geschrieben, die außerhalb der Komponente " Reagieren außerhalb des Ereignisses" entstehen .

Die Implementierung selbst ist einfach:

  • Wenn eine Komponente bereitgestellt wird, wird eine Ereignisbehandlungsroutine an das windowObjekt angehängt .
  • Wenn ein Ereignis eintritt, prüft die Komponente, ob das Ereignis aus der Komponente stammt. Ist dies nicht der Fall, wird onOutsideEventdie Zielkomponente ausgelöst.
  • Wenn die Komponente nicht gemountet ist, wird der Ereignishandler entfernt.
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} />;
        }
    }
};

Um die Komponente zu verwenden, müssen Sie die Deklaration der Zielkomponentenklasse mit der Komponente höherer Ordnung umbrechen und die Ereignisse definieren, die Sie behandeln möchten:

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
quelle
4

Ich habe eine der Antworten gewählt, obwohl es bei mir nicht funktioniert hat. Es führte mich schließlich zu dieser Lösung. Ich habe die Reihenfolge der Operationen leicht geändert. Ich höre auf mouseDown auf dem Ziel und mouseUp auf dem Ziel. Wenn einer von beiden TRUE zurückgibt, schließen wir das Modal nicht. Sobald ein Klick registriert ist, werden diese beiden Booleschen Werte {mouseDownOnModal, mouseUpOnModal} auf false zurückgesetzt.

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

Dies hat den Vorteil, dass der gesamte Code bei der untergeordneten Komponente und nicht bei der übergeordneten Komponente liegt. Dies bedeutet, dass bei der Wiederverwendung dieser Komponente kein Code für das Boilerplate kopiert werden muss.

Steve Zelaznik
quelle
3
  1. Erstellen Sie eine feste Ebene, die sich über den gesamten Bildschirm erstreckt (.backdrop ).
  2. Haben Sie das Zielelement ( .target) außerhalb des .backdropElements und mit einem größeren Stapelindex ( z-index).

Dann wird jeder Klick auf das .backdropElement als "außerhalb des .targetElements" betrachtet.

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

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

Du könntest benutzen ref s verwenden, um dies zu erreichen. So etwas wie das Folgende sollte funktionieren.

Fügen Sie das refzu Ihrem Element hinzu:

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

Sie können dann eine Funktion zum Behandeln des Klicks außerhalb des Elements wie folgt hinzufügen:

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

Fügen Sie dann schließlich die Ereignis-Listener hinzu und entfernen Sie sie, um sie zu aktivieren und zu deaktivieren.

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
quelle
Modifiziert Ihre Antwort, hatte es möglich , Fehler in Bezug auf Aufruf handleClickOutsidein document.addEventListener()durch Zugabe von thisReferenz. Andernfalls gibt es Uncaught ReferenceError: handleClickOutside ist nicht definiert incomponentWillMount()
Muhammad Hannan
1

Super spät zur Party, aber ich hatte Erfolg damit, ein Unschärfeereignis für das übergeordnete Element der Dropdown-Liste mit dem zugehörigen Code festzulegen, um die Dropdown-Liste zu schließen, und dem übergeordneten Element einen Mousedown-Listener hinzuzufügen, der prüft, ob die Dropdown-Liste geöffnet ist oder nicht, und stoppt die Ereignisausbreitung, wenn sie geöffnet ist, damit das Unschärfeereignis nicht ausgelöst wird.

Da das Mousedown-Ereignis in die Luft sprudelt, wird verhindert, dass Mousedown bei Kindern zu Unschärfe bei den Eltern führt.

/* 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 () sollte nicht aufgerufen werden müssen, soweit ich das beurteilen kann, aber Firefox spielt ohne es aus irgendeinem Grund nicht gut. Funktioniert unter Chrome, Firefox und Safari.

Tanner Stults
quelle
0

Ich habe einen einfacheren Weg gefunden.

Sie müssen nur onHide(this.closeFunction)das Modal hinzufügen

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

Angenommen, Sie haben eine Funktion zum Schließen des Modals.

MR Murazza
quelle
-1

Verwenden Sie das ausgezeichnete React-on-Click-Outside- Mixin:

npm install --save react-onclickoutside

Und dann

var Component = React.createClass({
  mixins: [
    require('react-onclickoutside')
  ],
  handleClickOutside: function(evt) {
    // ...handling code goes here... 
  }
});
e18r
quelle
4
Zu Ihrer Information: Mix-Ins sind in React now veraltet.
Machineghost