Klick außerhalb der React-Komponente erkennen


410

Ich suche nach einer Möglichkeit, um festzustellen, ob ein Klickereignis außerhalb einer Komponente aufgetreten ist, wie in diesem Artikel beschrieben . jQuery next () wird verwendet, um festzustellen, ob das Ziel eines Klickereignisses das dom-Element als eines seiner übergeordneten Elemente hat. Wenn eine Übereinstimmung vorliegt, gehört das Klickereignis zu einem der untergeordneten Elemente und wird daher nicht als außerhalb der Komponente befindlich betrachtet.

Daher möchte ich in meiner Komponente einen Klick-Handler an das Fenster anhängen. Wenn der Handler ausgelöst wird, muss ich das Ziel mit den dom-Kindern meiner Komponente vergleichen.

Das Klickereignis enthält Eigenschaften wie "Pfad", der den Dom-Pfad zu enthalten scheint, den das Ereignis zurückgelegt hat. Ich bin mir nicht sicher, was ich vergleichen oder wie ich es am besten durchqueren soll, und ich denke, jemand muss das bereits in eine clevere Utility-Funktion gesteckt haben ... Nein?


Könnten Sie den Klick-Handler eher an das übergeordnete als an das Fenster anhängen?
J. Mark Stevens

Wenn Sie dem übergeordneten Element einen Klick-Handler hinzufügen, wissen Sie, wann auf dieses Element oder eines seiner untergeordneten Elemente geklickt wird. Ich muss jedoch alle anderen angeklickten Stellen erkennen , sodass der Handler an das Fenster angehängt werden muss.
Thijs Koerselman

Ich habe mir den Artikel nach der vorherigen Antwort angesehen. Wie wäre es, wenn Sie einen clickState in der obersten Komponente festlegen und Klickaktionen von den Kindern übergeben. Dann würden Sie die Requisiten in den Kindern überprüfen, um den offenen Schließzustand zu verwalten.
J. Mark Stevens

Die oberste Komponente wäre meine App. Die Hörkomponente ist jedoch mehrere Ebenen tief und hat keine strenge Position im Dom. Ich kann unmöglich allen Komponenten in meiner App Klick-Handler hinzufügen, nur weil einer von ihnen interessiert ist, ob Sie irgendwo außerhalb darauf geklickt haben. Andere Komponenten sollten sich dieser Logik nicht bewusst sein, da dies zu schrecklichen Abhängigkeiten und Boilerplate-Code führen würde.
Thijs Koerselman

5
Ich möchte Ihnen eine sehr schöne Bibliothek empfehlen. Erstellt von AirBnb: github.com/airbnb/react-outside-click-handler
Osoian Marcel

Antworten:


683

Refs Verwendung in React 16.3+ geändert.

Die folgende Lösung verwendet ES6 und befolgt Best Practices zum Binden sowie zum Festlegen des Verweises durch eine Methode.

Um es in Aktion zu sehen:

Klassenimplementierung:

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

Implementierung von Hooks:

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

3
Wie testest du das? Wie senden Sie ein gültiges event.target an die Funktion handleClickOutside?
Csilk

5
Wie können Sie die synthetischen Ereignisse von React anstelle der document.addEventListenerhier verwendeten verwenden?
Ralph David Abernathy

9
@ polkovnikov.ph 1. contextist nur dann als Argument im Konstruktor erforderlich, wenn es verwendet wird. Es wird in diesem Fall nicht verwendet. Der Grund, den das Reaktionsteam propsals Argument im Konstruktor empfiehlt, ist, dass die Verwendung von this.propsvor dem Aufruf super(props)undefiniert ist und zu Fehlern führen kann. contextist immer noch auf inneren Knoten verfügbar, das ist der ganze Zweck von context. Auf diese Weise müssen Sie es nicht wie bei Requisiten von Komponente zu Komponente weitergeben. 2. Dies ist nur eine stilistische Präferenz und rechtfertigt in diesem Fall keine Debatte.
Ben Bud

7
Ich erhalte die folgende Fehlermeldung: "this.wrapperRef.contains ist keine Funktion"
Bogdan

5
@ Bogdan, ich habe den gleichen Fehler erhalten, als ich eine gestylte Komponente verwendet habe.
Erwägen

149

Hier ist die Lösung, die für mich am besten funktioniert hat, ohne Ereignisse an den Container anzuhängen:

Bestimmte HTML-Elemente können einen sogenannten " Fokus " haben, beispielsweise Eingabeelemente. Diese Elemente reagieren auch auf das Unschärfeereignis , wenn sie diesen Fokus verlieren.

Um einem Element die Fähigkeit zu geben, sich zu konzentrieren, stellen Sie einfach sicher, dass sein tabindex-Attribut auf etwas anderes als -1 gesetzt ist. In normalem HTML würde dies durch Setzen des tabindexAttributs geschehen, aber in React müssen Sie verwenden tabIndex(beachten Sie das Großbuchstaben I).

Sie können dies auch über JavaScript mit tun element.setAttribute('tabindex',0)

Dafür habe ich es verwendet, um ein benutzerdefiniertes DropDown-Menü zu erstellen.

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

10
Würde dies nicht zu Problemen mit der Zugänglichkeit führen? Da Sie ein zusätzliches Element fokussierbar machen, nur um zu erkennen, wann es ein- oder ausgeht, sollte die Interaktion auf den Dropdown-Werten liegen, nein?
Thijs Koerselman

2
Dies ist ein sehr interessanter Ansatz. Funktionierte perfekt für meine Situation, aber ich würde gerne erfahren, wie sich dies auf die Barrierefreiheit auswirken könnte.
klinore

17
Ich habe dieses "Arbeiten" bis zu einem gewissen Grad, es ist ein cooler kleiner Hack. Mein Problem ist, dass mein Dropdown-Inhalt display:absoluteso ist , dass das Dropdown die Größe des übergeordneten Divs nicht beeinflusst. Das heißt, wenn ich auf ein Element in der Dropdown-Liste klicke, wird die Unschärfe ausgelöst.
Dave

2
Ich hatte Probleme mit diesem Ansatz. Jedes Element im Dropdown-Inhalt löst keine Ereignisse wie onClick aus.
AnimaSola

8
Möglicherweise treten andere Probleme auf, wenn der Dropdown-Inhalt einige fokussierbare Elemente enthält, z. B. Formulareingaben. Sie stehlen Ihren Fokus, onBlur werden auf Ihrem Dropdown-Container ausgelöst und expanded auf false gesetzt.
Kamagatos

106

Ich war in der gleichen Frage festgefahren. Ich bin etwas spät zur Party hier, aber für mich ist das eine wirklich gute Lösung. Hoffentlich hilft es jemand anderem. Sie müssen Import findDOMNodevonreact-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
        });
    }
}

React Hooks Approach (16.8 +)

Sie können einen wiederverwendbaren Hook namens erstellen 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 };
}

Dann möchten Sie in der Komponente die Funktionalität hinzufügen, um Folgendes zu tun:

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

}

Hier finden Sie ein Code- und Box- Beispiel.


1
@LeeHanKyeol Nicht vollständig - Diese Antwort ruft die Ereignishandler während der Erfassungsphase der Ereignisbehandlung auf, während die Antwort, die mit verknüpft ist, die Ereignishandler während der Blasenphase aufruft.
Stevejay

3
Dies sollte die akzeptierte Antwort sein. Funktionierte perfekt für Dropdown-Menüs mit absoluter Positionierung.
GeschirrspülerMitProgrammingSkill

1
ReactDOM.findDOMNodeund ist veraltet, sollte refRückrufe verwenden: github.com/yannickcr/eslint-plugin-react/issues/…
dain

2
tolle und saubere Lösung
Karim

1
Dies funktionierte für mich, obwohl ich Ihre Komponente hacken musste, um das sichtbare Element nach 200 ms wieder auf "true" zurückzusetzen (andernfalls wird mein Menü nie wieder angezeigt). Vielen Dank!
Lee Moe

87

Nachdem ich hier viele Methoden ausprobiert hatte, entschied ich mich, github.com/Pomax/react-onclickoutside zu verwenden, da es vollständig ist.

Ich habe das Modul über npm installiert und in meine Komponente importiert:

import onClickOutside from 'react-onclickoutside'

Dann habe ich in meiner Komponentenklasse die handleClickOutsideMethode definiert :

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

Und beim Exportieren meiner Komponente habe ich sie eingepackt onClickOutside():

export default onClickOutside(NameOfComponent)

Das ist es.


2
Gibt es konkrete Vorteile dieser über den tabIndex/ onBlurAnsatz vorgeschlagen von Pablo ? Wie funktioniert die Implementierung und wie unterscheidet sich ihr Verhalten vom onBlurAnsatz?
Mark Amery

4
Es ist besser, eine dedizierte Komponente zu verwenden, als einen tabIndex-Hack zu verwenden. Upvoting es!
Atul Yadav

5
@ MarkAmery - Ich habe einen Kommentar zum tabIndex/onBlurAnsatz abgegeben. Es funktioniert nicht, wenn sich das Dropdown-Menü befindet position:absolute, z. B. wenn ein Menü über anderen Inhalten schwebt.
Beau Smith

4
Ein weiterer Vorteil der Verwendung gegenüber Tabindex besteht darin, dass die Tabindex-Lösung auch ein Unschärfeereignis auslöst, wenn Sie sich auf ein
untergeordnetes

1
@BeauSmith - Vielen Dank! Rettete meinen Tag!
Mika

38

Ich habe dank Ben Alpert auf diskut.reactjs.org eine Lösung gefunden . Der vorgeschlagene Ansatz fügt dem Dokument einen Handler hinzu, der sich jedoch als problematisch herausstellte. Das Klicken auf eine der Komponenten in meinem Baum führte zu einem erneuten Rendern, bei dem das angeklickte Element beim Aktualisieren entfernt wurde. Da das erneute Rendern von React erfolgt, bevor der Dokumentkörper-Handler aufgerufen wird, wurde das Element nicht als "innerhalb" des Baums erkannt.

Die Lösung hierfür bestand darin, den Handler zum Anwendungsstammelement hinzuzufügen.

Main:

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

Komponente:

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

8
Dies funktioniert bei React 0.14.7 nicht mehr für mich - möglicherweise hat React etwas geändert, oder ich habe einen Fehler gemacht, als ich den Code an alle Änderungen an React angepasst habe. Ich verwende stattdessen github.com/Pomax/react-onclickoutside, was wie ein Zauber funktioniert.
Nicole

1
Hmm. Ich sehe keinen Grund, warum das funktionieren sollte. Warum sollte ein Handler auf dem Root-DOM-Knoten einer App garantiert ausgelöst werden, bevor ein Renderer von einem anderen Handler ausgelöst wird, wenn einer auf dem documentnicht ist?
Mark Amery

1
Ein reiner UX-Punkt: mousedownwäre wahrscheinlich ein besserer Handler als clickhier. In den meisten Anwendungen tritt das Verhalten beim Schließen des Menüs durch Klicken nach außen in dem Moment auf, in dem Sie mit der Maus nach unten drücken, nicht beim Loslassen. Versuchen Sie es, zum Beispiel, mit Stapelüberlauf der Flagge oder Aktien Dialogen oder mit einem der Dropdown - Listen in der oberen Menüleiste des Browsers.
Mark Amery

ReactDOM.findDOMNodeund Zeichenfolge refsind veraltet, sollten refRückrufe verwenden: github.com/yannickcr/eslint-plugin-react/issues/…
dain

Dies ist die einfachste Lösung und funktioniert perfekt für mich, selbst wenn ich sie am document
7.

37

Hook-Implementierung basierend auf Tanner Linsleys ausgezeichnetem Vortrag auf der JSConf Hawaii 2020 :

useOuterClick Hook-API

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

Implementierung

/*
  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>

useOuterClickverwendet veränderbare Verweise , um eine schlanke API für die zu erstellen Client. Ein Rückruf kann eingestellt werden, ohne dass Sie ihn über merken müssen useCallback. Der Rückrufkörper hat weiterhin Zugriff auf die neuesten Requisiten und den neuesten Status (keine veralteten Schließungswerte).


Hinweis für iOS-Benutzer

iOS behandelt nur bestimmte Elemente als anklickbar. Um dieses Verhalten zu umgehen, wählen Sie einen anderen äußeren Klick-Listener als document- nichts nach oben, einschließlich body. Sie können divbeispielsweise im obigen Beispiel einen Listener zum React-Stamm hinzufügen und dessen Höhe ( height: 100vhoder ähnlich) erweitern, um alle externen Klicks zu erfassen. Quellen: quirksmode.org und diese Antwort


funktioniert nicht auf dem Handy so unglücklich
Omar

@Omar hat gerade die Codesandbox in meinem Beitrag mit Firefox 67.0.3 auf einem Android-Gerät getestet - funktioniert alles einwandfrei. Können Sie erläutern, welchen Browser Sie verwenden, was der Fehler ist usw.?
Ford04

Keine Fehler, funktioniert aber nicht auf Chrome, iPhone 7+. Es erkennt die Wasserhähne draußen nicht. In Chrome Dev Tools auf Mobilgeräten funktioniert es, aber in einem echten iOS-Gerät funktioniert es nicht.
Omar

1
@ ford04 danke, dass du das ausgegraben hast, ich bin auf einen anderen Thread gestoßen , der den folgenden Trick vorschlug. @media (hover: none) and (pointer: coarse) { body { cursor:pointer } }Das Hinzufügen in meinen globalen Stilen scheint das Problem behoben zu haben.
Kostas Sarantopoulos

1
@ ford04 Ja, übrigens gibt es laut diesem Thread eine noch spezifischere Medienabfrage @supports (-webkit-overflow-scrolling: touch), die nur auf IOS abzielt, obwohl ich nicht mehr weiß, wie viel! Ich habe es gerade getestet und es funktioniert.
Kostas Sarantopoulos

30

Keine der anderen Antworten hier hat bei mir funktioniert. Ich habe versucht, ein Popup bei Unschärfe auszublenden, aber da der Inhalt absolut positioniert war, wurde der onBlur auch beim Klicken auf den inneren Inhalt ausgelöst.

Hier ist ein Ansatz, der für mich funktioniert hat:

// 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.
    }
},

Hoffe das hilft.


Sehr gut! Vielen Dank. In meinem Fall war es jedoch ein Problem, den "aktuellen Ort" mit der absoluten Position für das Div zu erfassen. Daher verwende ich "OnMouseLeave" für das Div mit Eingabe und Dropdown-Kalender, um alle Divs zu deaktivieren, wenn die Maus das Div verlässt.
Alex

1
Vielen Dank! Hilf mir sehr.
Angelo Lucas

1
Ich mag das besser als diese Methoden document. Vielen Dank.
Kyw

Damit dies funktioniert, müssen Sie sicherstellen, dass das angeklickte Element (relatedTarget) fokussierbar ist. Siehe stackoverflow.com/questions/42764494/…
xabitrigo

21

[Update] Lösung mit React ^ 16.8 mit Hooks

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;

Lösung mit Reaktion ^ 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;

3
Dies ist jetzt die Lösung. Wenn Sie den Fehler erhalten .contains is not a function, kann es sein, dass Sie die Ref-Requisite an eine benutzerdefinierte Komponente übergeben und nicht an ein echtes DOM-Element wie ein<div>
willlma

Für diejenigen, die versuchen, refRequisiten an eine benutzerdefinierte Komponente zu übergeben, möchten Sie vielleicht einen Blick darauf werfenReact.forwardRef
onoya

4

Hier ist mein Ansatz (Demo - https://jsfiddle.net/agymay93/4/ ):

Ich habe eine spezielle Komponente namens erstellt WatchClickOutsideund sie kann wie folgt verwendet werden (ich nehme die JSXSyntax an):

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

Hier ist der Code der WatchClickOutsideKomponente:

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

Tolle Lösung - für meine Verwendung habe ich bei Seiten mit kurzem Inhalt das Dokument anstelle des Textes angehört
Billy Moon

und verwendete span anstelle von div, da es weniger wahrscheinlich ist, dass das Layout geändert wird, und fügte eine Demo zur Behandlung von Klicks außerhalb des Körpers hinzu: jsfiddle.net/agymay93/10
Billy Moon

String refist veraltet, sollte refRückrufe verwenden: reactjs.org/docs/refs-and-the-dom.html
dain

4

Dies hat bereits viele Antworten, aber sie adressieren e.stopPropagation()und verhindern nicht das Klicken auf Reaktionslinks außerhalb des Elements, das Sie schließen möchten.

Aufgrund der Tatsache, dass React über einen eigenen künstlichen Ereignishandler verfügt, können Sie das Dokument nicht als Basis für Ereignis-Listener verwenden. Sie müssen dies e.stopPropagation()vorher tun, da React das Dokument selbst verwendet. Wenn Sie document.querySelector('body')stattdessen zum Beispiel verwenden. Sie können das Klicken über den Link Reagieren verhindern. Im Folgenden finden Sie ein Beispiel für die Implementierung von Click Outside und Close.
Dies verwendet ES6 und 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;

3

Für diejenigen, die eine absolute Positionierung benötigen, ist eine einfache Option, für die ich mich entschieden habe, das Hinzufügen einer Wrapper-Komponente, die so gestaltet ist, dass sie die gesamte Seite mit einem transparenten Hintergrund abdeckt. Anschließend können Sie einen onClick auf dieses Element hinzufügen, um Ihre interne Komponente zu schließen.

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

Wie im Moment, wenn Sie einen Klick-Handler für Inhalte hinzufügen, wird das Ereignis auch an das obere Div weitergegeben und löst daher den handlerOutsideClick aus. Wenn dies nicht Ihr gewünschtes Verhalten ist, stoppen Sie einfach die Ereignisprogation auf Ihrem Handler.

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

`


Das Problem bei diesem Ansatz ist, dass Sie keine Klicks auf den Inhalt haben können - der Div erhält stattdessen den Klick.
Rbonick

Da sich der Inhalt im div befindet, erhalten beide den Klick, wenn Sie die Ereignisausbreitung nicht stoppen. Dies ist häufig ein gewünschtes Verhalten. Wenn Sie jedoch nicht möchten, dass der Klick an das div weitergegeben wird, stoppen Sie einfach die eventPropagation für Ihren Content onClick-Handler. Ich habe meine Antwort aktualisiert, um zu zeigen, wie.
Anthony Garant

3

Um die akzeptierte Antwort von Ben Bud zu erweitern , wenn Sie gestylte Komponenten verwenden, erhalten Sie beim Übergeben von Refs auf diese Weise einen Fehler wie "this.wrapperRef.contains ist keine Funktion".

Die in den Kommentaren vorgeschlagene Korrektur, die gestaltete Komponente mit einem Div zu versehen und den Ref dort zu übergeben, funktioniert. Allerdings erklären sie in ihren Dokumenten bereits den Grund dafür und die ordnungsgemäße Verwendung von Refs in gestalteten Komponenten:

Wenn Sie eine Ref-Requisite an eine gestaltete Komponente übergeben, erhalten Sie eine Instanz des StyledComponent-Wrappers, jedoch nicht an den zugrunde liegenden DOM-Knoten. Dies liegt daran, wie Refs funktionieren. Es ist nicht möglich, DOM-Methoden wie focus direkt auf unsere Wrapper aufzurufen. Übergeben Sie den Rückruf stattdessen an die innerRef-Requisite, um eine Referenz zum tatsächlichen, umschlossenen DOM-Knoten zu erhalten.

Wie so:

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

Dann können Sie direkt über die Funktion "handleClickOutside" darauf zugreifen:

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

Dies gilt auch für den "onBlur" -Ansatz:

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


2

Mein größtes Anliegen bei allen anderen Antworten ist es, Klickereignisse vom Stammverzeichnis / übergeordneten Element nach unten zu filtern. Ich fand, der einfachste Weg war, einfach ein Geschwisterelement mit der Position festzusetzen: fest, einen Z-Index 1 hinter dem Dropdown und das Klickereignis auf das feste Element innerhalb derselben Komponente zu behandeln. Hält alles zentral für eine bestimmte Komponente.

Beispielcode

#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
Hmm geschickt. Würde es mit mehreren Instanzen funktionieren? Oder würde jedes feste Element möglicherweise den Klick für die anderen blockieren?
Thijs Koerselman

2
componentWillMount(){

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

handleClickOutside(event) {

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

Ereignis path[0]ist das letzte angeklickte Element


3
Stellen Sie sicher, dass Sie den Ereignis-Listener entfernen, wenn die Bereitstellung der Komponente aufgehoben wird, um Speicherverwaltungsprobleme usw. zu vermeiden.
agm1984

2

Ich habe dieses Modul verwendet (ich habe keine Verbindung zum Autor)

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

Es hat den Job gut gemacht.


Das funktioniert fantastisch! Weitaus einfacher als viele andere Antworten hier und im Gegensatz zu mindestens drei anderen Lösungen hier, bei denen ich für alle erhalten habe, dass dies sofort "Support for the experimental syntax 'classProperties' isn't currently enabled"funktioniert hat. Denken Sie bei jedem Versuch mit dieser Lösung daran, den verbleibenden Code zu löschen, den Sie möglicherweise kopiert haben, wenn Sie die anderen Lösungen ausprobieren. Das Hinzufügen von Ereignis-Listenern scheint dem zu widersprechen.
Frikster

2

Ich habe dies teilweise getan, indem ich dies befolgt habe und indem ich den offiziellen React-Dokumenten zum Umgang mit Refs gefolgt bin, die eine Reaktion erfordern ^ 16.3. Dies ist das einzige, was für mich funktioniert hat, nachdem ich einige der anderen Vorschläge hier ausprobiert habe ...

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

Ein Beispiel mit Strategie

Ich mag die bereitgestellten Lösungen, die das Gleiche tun, indem sie einen Wrapper um die Komponente erstellen.

Da dies eher ein Verhalten ist, dachte ich an Strategie und kam auf Folgendes.

Ich bin neu bei React und brauche ein bisschen Hilfe, um in den Anwendungsfällen etwas Boilerplate zu sparen

Bitte überprüfen Sie und sagen Sie mir, was Sie denken.

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

Beispielnutzung

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

Anmerkungen

Ich dachte daran, die übergebenen componentLifecycle-Hooks zu speichern und sie mit ähnlichen Methoden zu überschreiben:

const baseDidMount = component.componentDidMount;

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

componentist die Komponente, die an den Konstruktor von übergeben wird ClickOutsideBehavior.
Dadurch wird das Aktivierungs- / Deaktivierungs-Boilerplate vom Benutzer dieses Verhaltens entfernt, aber es sieht nicht besonders gut aus


1

In meinem DROPDOWN-Fall funktionierte die Lösung von Ben Bud gut, aber ich hatte eine separate Umschalttaste mit einem onClick-Handler. Die äußere Klicklogik widersprach also der Schaltfläche onClick toggler. Hier ist, wie ich es gelöst habe, indem ich auch den Ref des Buttons übergeben habe:

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

0

Wenn Sie eine winzige Komponente (466 Byte gzipped) verwenden möchten, die für diese Funktionalität bereits vorhanden ist, können Sie diesen React-Outclick dieser Bibliothek überprüfen .

Das Gute an der Bibliothek ist, dass Sie damit auch Klicks außerhalb einer Komponente und innerhalb einer anderen Komponente erkennen können. Es unterstützt auch das Erkennen anderer Arten von Ereignissen.


0

Wenn Sie eine winzige Komponente (466 Byte gzipped) verwenden möchten, die für diese Funktionalität bereits vorhanden ist, können Sie diesen React-Outclick dieser Bibliothek überprüfen . Es erfasst Ereignisse außerhalb einer Reaktionskomponente oder eines jsx-Elements.

Das Gute an der Bibliothek ist, dass Sie damit auch Klicks außerhalb einer Komponente und innerhalb einer anderen Komponente erkennen können. Es unterstützt auch das Erkennen anderer Arten von Ereignissen.


0

Ich weiß, dass dies eine alte Frage ist, aber ich stoße immer wieder darauf und hatte große Probleme, dies in einem einfachen Format herauszufinden. Wenn dies jemandem das Leben ein bisschen leichter machen würde, verwenden Sie OutsideClickHandler von airbnb. Es ist das einfachste Plugin, um diese Aufgabe zu erledigen, ohne eigenen Code zu schreiben.

Beispiel:

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

0
import { useClickAway } from "react-use";

useClickAway(ref, () => console.log('OUTSIDE CLICKED'));

0

Sie können Ihnen einen einfachen Weg, um Ihr Problem zu lösen, ich zeige Ihnen:

....

const [dropDwonStatus , setDropDownStatus] = useState(false)

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

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

das funktioniert bei mir, viel glück;)


-1

Ich fand dies aus dem Artikel unten:

render () {return ({this.node = node;}}> Popover umschalten {this.state.popupVisible && (Ich bin ein Popover!)}); }}

Hier ist ein großartiger Artikel zu diesem Problem: "Klicks außerhalb von React-Komponenten verarbeiten" https://larsgraubner.com/handle-outside-clicks-react/


Willkommen bei Stack Overflow! Antworten, die nur ein Link sind, werden im Allgemeinen verpönt: meta.stackoverflow.com/questions/251006/… . Fügen Sie in Zukunft ein Zitat oder ein Codebeispiel mit den für die Frage relevanten Informationen hinzu. Prost!
Fred Stark

-1

Fügen Sie onClickIhrem Container der obersten Ebene einen Handler hinzu und erhöhen Sie einen Statuswert, wenn der Benutzer darauf klickt. Übergeben Sie diesen Wert an die entsprechende Komponente, und wenn sich der Wert ändert, können Sie arbeiten.

In diesem Fall rufen wir auf, this.closeDropdown()wenn sich der clickCountWert ändert.

Die incrementClickCountMethode wird innerhalb des .appContainers ausgelöst, jedoch nicht, .dropdownweil wir sie verwenden event.stopPropagation(), um das Sprudeln von Ereignissen zu verhindern.

Ihr Code sieht möglicherweise folgendermaßen aus:

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

Ich bin mir nicht sicher, wer das Downvoting durchführt, aber diese Lösung ist im Vergleich zu bindenden / nicht bindenden Ereignis-Listenern ziemlich sauber. Ich hatte keine Probleme mit dieser Implementierung.
wdm

-1

Damit die "Fokus" -Lösung für Dropdown-Listen mit Ereignis-Listenern funktioniert, können Sie sie mit dem onMouseDown- Ereignis anstelle von onClick hinzufügen . Auf diese Weise wird das Ereignis ausgelöst und danach wird das Popup wie folgt geschlossen:

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

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

Und vergessen Sie nichtimport ReactDOM from 'react-dom';
dipole_moment

-1

Ich habe für alle Gelegenheiten eine Lösung gefunden.

Sie sollten eine Komponente höherer Ordnung verwenden, um die Komponente, die Sie auf Klicks außerhalb der Komponente warten möchten, zu verpacken.

Dieses Komponentenbeispiel enthält nur eine Requisite: "onClickedOutside", die eine Funktion empfängt.

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

-1

UseOnClickOutside Hook - Reagiere 16.8 +

Erstellen Sie eine allgemeine useOnOutsideClick-Funktion

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

Verwenden Sie dann den Haken in einer beliebigen Funktionskomponente.

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

};

Link zur Demo

Teilweise inspiriert von @ pau1fitzgerald Antwort.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.