Rileva clic all'esterno del componente Reagisci


411

Sto cercando un modo per rilevare se un evento clic si è verificato al di fuori di un componente, come descritto in questo articolo . jQuery latest () viene utilizzato per vedere se la destinazione di un evento click ha l'elemento dom come uno dei suoi genitori. Se esiste una corrispondenza, l'evento click appartiene a uno dei figli e non viene quindi considerato esterno al componente.

Quindi nel mio componente voglio collegare un gestore di clic alla finestra. Quando il gestore spara, devo confrontare l'obiettivo con i figli dom del mio componente.

L'evento click contiene proprietà come "path" che sembra contenere il percorso dom che ha percorso l'evento. Non sono sicuro di cosa confrontare o come attraversarlo al meglio, e sto pensando che qualcuno deve averlo già messo in una funzione di utilità intelligente ... No?


Potresti collegare il gestore di clic al genitore anziché alla finestra?
J. Mark Stevens,

Se colleghi un gestore di clic al genitore, sai quando viene fatto clic su quell'elemento o su uno dei loro figli, ma devo rilevare tutti gli altri luoghi su cui si fa clic, quindi il gestore deve essere collegato alla finestra.
Thijs Koerselman,

Ho guardato l'articolo dopo la risposta precedente. Che ne dici di impostare un clickState nel componente principale e passare azioni di clic dai bambini. Quindi controlleresti gli oggetti di scena nei bambini per gestire lo stato aperto vicino.
J. Mark Stevens,

Il componente principale sarebbe la mia app. Ma la componente di ascolto è profonda diversi livelli e non ha una posizione rigida nel dom. Non posso aggiungere gestori di clic a tutti i componenti della mia app solo perché uno di loro è interessato a sapere se hai fatto clic da qualche parte al di fuori di esso. Gli altri componenti non dovrebbero essere consapevoli di questa logica perché ciò creerebbe dipendenze terribili e codice del boilerplate.
Thijs Koerselman,

5
Vorrei raccomandarti una bella lib. creato da AirBnb: github.com/airbnb/react-outside-click-handler
Osoian Marcel

Risposte:


684

L'utilizzo dei riferimenti in React 16.3+ è cambiato.

La seguente soluzione utilizza ES6 e segue le migliori pratiche per l'associazione e l'impostazione dell'arbitro tramite un metodo.

Per vederlo in azione:

Implementazione di classe:

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

Implementazione di ami:

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
Come si fa a provare questo? Come si invia un event.target valido alla funzione handleClickOutside?
csilk,

5
Come puoi usare gli eventi sintetici di React, invece di document.addEventListenerqui?
Ralph David Abernathy,

10
@ polkovnikov.ph 1. contextè necessario solo come argomento nel costruttore se viene utilizzato. In questo caso non viene utilizzato. Il motivo per cui il team di reazione consiglia di avere propscome argomento nel costruttore è perché l'uso di this.propsprima della chiamata super(props)non è definito e può causare errori. contextè ancora disponibile su nodi interni, questo è lo scopo di context. In questo modo non devi passarlo da un componente all'altro come fai con gli oggetti di scena. 2. Questa è solo una preferenza stilistica, e in questo caso non merita dibattiti.
Ben Bud,

7
Ottengo il seguente errore: "this.wrapperRef.contains non è una funzione"
Bogdan,

5
@Bogdan, ho riscontrato lo stesso errore durante l'utilizzo di un componente con stile. Prendi in considerazione l'utilizzo di un <div> al livello superiore
user3040068,

151

Ecco la soluzione che ha funzionato meglio per me senza associare eventi al contenitore:

Alcuni elementi HTML possono avere ciò che è noto come " focus ", ad esempio elementi di input. Questi elementi risponderanno anche all'evento di sfocatura , quando perdono tale attenzione.

Per dare a qualsiasi elemento la capacità di focalizzarsi, assicurati solo che il suo attributo tabindex sia impostato su un valore diverso da -1. Nel normale HTML ciò sarebbe impostando l' tabindexattributo, ma in React devi usare tabIndex(nota la maiuscola I).

Puoi anche farlo tramite JavaScript con element.setAttribute('tabindex',0)

Questo è quello per cui lo stavo usando, per creare un menu a discesa personalizzato.

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
Ciò non creerebbe problemi con l'accessibilità? Dal momento che rendi un elemento aggiuntivo focalizzabile solo per rilevare quando sta andando in / out focus, ma l'interazione dovrebbe essere sui valori a discesa no?
Thijs Koerselman,

2
Questo è un approccio molto interessante. Ha funzionato perfettamente per la mia situazione, ma sarei interessato a sapere come ciò potrebbe influire sull'accessibilità.
klinore,

17
Ho questo "lavoro" in una certa misura, è un piccolo trucco interessante. Il mio problema è che il contenuto del mio menu a discesa è display:absolutetale che il menu a discesa non influirà sulla dimensione del div parent. Ciò significa che quando faccio clic su un elemento nel menu a discesa, viene attivato lo sfocato.
Dave,

2
Ho avuto problemi con questo approccio, qualsiasi elemento nel contenuto a discesa non genera eventi come onClick.
AnimaSola,

8
Potresti dover affrontare alcuni altri problemi se il contenuto del menu a discesa contiene alcuni elementi focalizzabili, ad esempio input di moduli. Ruberanno la tua attenzione, onBlur saranno attivati ​​sul tuo contenitore a discesa e expanded saranno impostati su falso.
Kamagatos,

107

Ero bloccato sullo stesso problema. Sono un po 'in ritardo alla festa qui, ma per me questa è davvero un'ottima soluzione. Spero che possa essere d'aiuto a qualcun altro. Devi importare findDOMNodedareact-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
        });
    }
}

Approccio agli ami React (16.8+)

È possibile creare un hook riutilizzabile chiamato 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 };
}

Quindi nel componente si desidera aggiungere la funzionalità per eseguire le seguenti operazioni:

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

}

Trova un esempio di codesandbox qui.


1
@LeeHanKyeol Non del tutto: questa risposta richiama i gestori di eventi durante la fase di acquisizione della gestione degli eventi, mentre la risposta collegata richiama i gestori di eventi durante la fase a bolle.
stevejay,

3
Questa dovrebbe essere la risposta accettata. Ha funzionato perfettamente per i menu a discesa con posizionamento assoluto.
Lavastoviglie Con programmazione

1
ReactDOM.findDOMNodeed è obsoleto, dovrebbe utilizzare i refcallback: github.com/yannickcr/eslint-plugin-react/issues/…
dain

2
soluzione ottima e pulita
Karim,

1
Questo ha funzionato per me anche se ho dovuto hackerare il componente per ripristinare il valore visibile su "true" dopo 200 ms (altrimenti il ​​mio menu non viene più visualizzato). Grazie!
Lee Moe,

87

Dopo aver provato molti metodi qui, ho deciso di utilizzare github.com/Pomax/react-onclickoutside causa di quanto sia completo.

Ho installato il modulo tramite npm e l'ho importato nel mio componente:

import onClickOutside from 'react-onclickoutside'

Quindi, nella mia classe di componenti ho definito il handleClickOutsidemetodo:

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

E durante l'esportazione del mio componente l'ho avvolto in onClickOutside():

export default onClickOutside(NameOfComponent)

Questo è tutto.


2
Ci sono dei vantaggi concreti in questo rispetto all'approccio tabIndex/ onBlurproposto da Pablo ? Come funziona l'implementazione e in che modo il suo comportamento differisce onBlurdall'approccio?
Mark Amery,

4
È meglio utilizzare un componente dedicato quindi utilizzare un hack tabIndex. Lo votiamo!
Atul Yadav,

5
@MarkAmery - Ho inserito un commento tabIndex/onBlursull'approccio. Non funziona quando il menu a discesa è position:absolute, ad esempio un menu che passa sopra altri contenuti.
Beau Smith,

4
Un altro vantaggio dell'utilizzo di questo su tabindex è che la soluzione tabindex genera anche un evento sfocato se ti concentri su un elemento figlio
Jemar Jones,

1
@BeauSmith - Grazie mille! Mi hai salvato la giornata!
Mika,

39

Ho trovato una soluzione grazie a Ben Alpert su discuss.reactjs.org . L'approccio suggerito allega un gestore al documento ma si è rivelato problematico. Facendo clic su uno dei componenti nella mia struttura, si è verificato un rendering che ha rimosso l'elemento selezionato durante l'aggiornamento. Perché il rerender di React si verifica prima che venga chiamato il gestore del corpo del documento, l'elemento non è stato rilevato come "all'interno" dell'albero.

La soluzione a ciò era aggiungere il gestore sull'elemento radice dell'applicazione.

principale:

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

componente:

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
Questo non funziona più per me con React 0.14.7 - forse React ha cambiato qualcosa o forse ho fatto un errore durante l'adattamento del codice a tutte le modifiche a React. Sto invece usando github.com/Pomax/react-onclickoutside che funziona come un fascino.
Nicole,

1
Hmm. Non vedo alcun motivo per cui questo dovrebbe funzionare. Perché un gestore su un nodo DOM radice dell'app deve essere garantito per essere attivato prima che un rerender venga attivato da un altro gestore se uno sul non lo documentè?
Mark Amery,

1
Un puro punto UX: mousedownsarebbe probabilmente un gestore migliore di clickqui. Nella maggior parte delle applicazioni, il comportamento di chiusura menu-clic-esterno si verifica nel momento in cui si sposta il mouse verso il basso, non quando si rilascia. Provalo, ad esempio, con il flag di Stack Overflow o condividi i dialoghi o con uno dei menu a discesa dalla barra dei menu in alto del browser.
Mark Amery,

ReactDOM.findDOMNodee la stringa refsono obsoleti, devono usare i refcallback: github.com/yannickcr/eslint-plugin-react/issues/…
dain

questa è la soluzione più semplice e funziona perfettamente per me anche quando mi allego aldocument
Flion

37

Implementazione hook basata sull'eccellente discorso di Tanner Linsley a JSConf Hawaii 2020 :

useOuterClick API hook

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

Implementazione

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

useOuterClickfa uso di ref mutabili per creare un'API lean per il Client. È possibile impostare un callback senza doverlo memorizzare tramite useCallback. Il corpo di callback ha ancora accesso agli oggetti di scena e allo stato più recenti (nessun valore di chiusura non aggiornato).


Nota per gli utenti iOS

iOS tratta solo alcuni elementi come selezionabili. Per aggirare questo comportamento, scegliere un listener di clic esterno diverso da document- niente di inclusivo verso l'alto body. Ad esempio, potresti aggiungere un ascoltatore sulla radice di React divnell'esempio sopra ed espanderne l'altezza ( height: 100vho simile) per catturare tutti i clic esterni. Fonti: quirksmode.org e questa risposta


non funziona su dispositivi mobili così insuperabili
Omar,

@Omar ha appena testato Codesandbox nel mio post con Firefox 67.0.3 su un dispositivo Android - funziona benissimo. Potresti elaborare quale browser usi, qual è l'errore ecc.?
ford04,

Nessun errore ma non funziona su Chrome, iPhone 7+. Non rileva i rubinetti all'esterno. Negli strumenti di sviluppo di Chrome su dispositivo mobile funziona ma in un dispositivo iOS reale non funziona.
Omar,

1
@ ford04 grazie per averlo scoperto, mi sono imbattuto in un altro thread che suggeriva il seguente trucco. @media (hover: none) and (pointer: coarse) { body { cursor:pointer } }Aggiungendo questo nei miei stili globali, sembra risolto il problema.
Kostas Sarantopoulos,

1
@ ford04 sì, a proposito di questo thread c'è una query multimediale ancora più specifica @supports (-webkit-overflow-scrolling: touch)che prende di mira solo IOS anche se non so più quanto! L'ho appena testato e funziona.
Kostas Sarantopoulos,

31

Nessuna delle altre risposte qui ha funzionato per me. Stavo cercando di nascondere un popup sulla sfocatura, ma poiché i contenuti erano assolutamente posizionati, onBlur si attivava anche con un clic dei contenuti interni.

Ecco un approccio che ha funzionato per me:

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

Spero che sia di aiuto.


Molto bene! Grazie. Ma nel mio caso è stato un problema catturare "posto attuale" con posizione assoluta per il div, quindi uso "OnMouseLeave" per div con input e menu a discesa per disabilitare tutti i div quando il mouse lascia il div.
Alex,

1
Grazie! Aiutami molto
Ângelo Lucas,

1
Mi piace di più rispetto ai metodi associati document. Grazie.
kyw

Perché ciò funzioni è necessario assicurarsi che l'elemento cliccato (relatedTarget) sia focalizzabile. Vedere stackoverflow.com/questions/42764494/...
xabitrigo

21

[Aggiornamento] Soluzione con React ^ 16.8 utilizzando 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;

Soluzione con React ^ 16.3 :

CodeSandbox

import React, { Component } from "react";

class SampleComponent extends Component {
  state = {
    clickedOutside: false
  };

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

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

  myRef = React.createRef();

  handleClickOutside = e => {
    if (!this.myRef.current.contains(e.target)) {
      this.setState({ clickedOutside: true });
    }
  };

  handleClickInside = () => this.setState({ clickedOutside: false });

  render() {
    return (
      <button ref={this.myRef} onClick={this.handleClickInside}>
        {this.state.clickedOutside ? "Bye!" : "Hello!"}
      </button>
    );
  }
}

export default SampleComponent;

3
Questa è la soluzione ideale ora. Se stai ricevendo l'errore .contains is not a function, potrebbe essere perché stai passando il ref ref a un componente personalizzato piuttosto che a un vero elemento DOM come un<div>
willlma,

Per coloro che cercano di passare l' refelica a un componente personalizzato, ti consigliamo di dare un'occhiataReact.forwardRef
onoya

4

Ecco il mio approccio (demo - https://jsfiddle.net/agymay93/4/ ):

Ho creato un componente speciale chiamato WatchClickOutsidee può essere usato come (suppongoJSX sintassi):

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

Ecco il codice del WatchClickOutsidecomponente:

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

Ottima soluzione - per il mio uso, ho cambiato per ascoltare il documento anziché il corpo in caso di pagine con contenuto breve, e ho cambiato il riferimento da stringa a riferimento dom: jsfiddle.net/agymay93/9
Billy Moon

e utilizzato span invece di div in quanto è meno probabile che cambi layout, e aggiunta demo di gestione dei clic al di fuori del corpo: jsfiddle.net/agymay93/10
Billy Moon

La stringa refè obsoleta, dovrebbe usare i refcallbacks: reazionijs.org/docs/refs-and-the-dom.html
dain

4

Questo ha già molte risposte ma non si rivolgono e.stopPropagation()e impediscono di fare clic sui collegamenti di reazione all'esterno dell'elemento che si desidera chiudere.

A causa del fatto che React ha il proprio gestore di eventi artificiali, non è possibile utilizzare il documento come base per i listener di eventi. È necessario e.stopPropagation()prima che React utilizzi il documento stesso. Se document.querySelector('body')invece usi ad esempio . Sei in grado di impedire il clic dal link Reagisci. Di seguito è riportato un esempio di come implementare il clic all'esterno e la chiusura.
Questo utilizza ES6 e 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

Per coloro che necessitano di un posizionamento assoluto, una semplice opzione per cui ho optato è quella di aggiungere un componente wrapper disegnato per coprire l'intera pagina con uno sfondo trasparente. Quindi puoi aggiungere un onClick su questo elemento per chiudere il componente interno.

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

Come è ora se aggiungi un gestore di clic al contenuto, l'evento verrà anche propagato al div superiore e quindi attiverà il gestoreOutsideClick. Se questo non è il comportamento desiderato, è sufficiente interrompere la previsione dell'evento sul gestore.

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

`


Il problema con questo approccio è che non puoi avere alcun clic sul Contenuto: il div riceverà invece il clic.
rbonick,

Poiché il contenuto è nel div se non si interrompe la propagazione dell'evento, entrambi riceveranno il clic. Questo è spesso un comportamento desiderato ma se non si desidera che il clic venga propagato al div, è sufficiente interrompere eventPropagation sul contenuto sul gestore Click. Ho aggiornato la mia risposta per mostrare come.
Anthony Garant,

3

Estendere la risposta accettata fatta da Ben Bud , se si utilizzano componenti con stile, passare i riferimenti in questo modo ti darà un errore come "this.wrapperRef.contains non è una funzione".

La correzione suggerita, nei commenti, per avvolgere il componente in stile con un div e passare lì il riferimento, funziona. Detto questo, nel loro documenti spiegano già la ragione di ciò e il corretto uso dei riferimenti all'interno di componenti con stile:

Passare un ref ref a un componente con stile ti darà un'istanza del wrapper StyledComponent, ma non al nodo DOM sottostante. Ciò è dovuto al funzionamento degli ref. Non è possibile chiamare direttamente i metodi DOM, come focus, sui nostri wrapper. Per ottenere un riferimento al nodo DOM effettivo e impacchettato, passa invece il callback al puntello innerRef.

Così:

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

Quindi puoi accedervi direttamente dalla funzione "handleClickOutside":

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

Questo vale anche per l'approccio "onBlur":

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


2

La mia più grande preoccupazione con tutte le altre risposte è quella di filtrare gli eventi clic dalla radice / genitore verso il basso. Ho scoperto che il modo più semplice era semplicemente impostare un elemento fratello con posizione: fixed, uno z-index 1 dietro il menu a discesa e gestire l'evento click sull'elemento fisso all'interno dello stesso componente. Mantiene tutto centralizzato su un determinato componente.

Codice di esempio

#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 elegante. Funzionerebbe con più istanze? O ogni elemento fisso potrebbe potenzialmente bloccare il clic per gli altri?
Thijs Koerselman,

2
componentWillMount(){

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

handleClickOutside(event) {

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

L'evento path[0]è l'ultimo elemento su cui è stato fatto clic


3
Assicurati di rimuovere il listener di eventi quando il componente viene smontato per evitare problemi di gestione della memoria, ecc.
agm1984,

2

Ho usato questo modulo (non ho alcuna associazione con l'autore)

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

Ha fatto bene il lavoro.


Funziona in modo fantastico! Molto più semplice di molte altre risposte qui e, a differenza di almeno altre 3 soluzioni qui, dove per tutte ho ricevuto "Support for the experimental syntax 'classProperties' isn't currently enabled"questo risultato ha funzionato immediatamente. Per chiunque provi questa soluzione, ricorda di cancellare qualsiasi codice persistente che potresti aver copiato quando provi le altre soluzioni. ad esempio l'aggiunta di listener di eventi sembra essere in conflitto con questo.
Frikster,

2

L'ho fatto in parte seguendo questo e seguendo i documenti ufficiali di React sulla gestione dei ref che richiedono reazioni ^ 16.3. Questa è l'unica cosa che ha funzionato per me dopo aver provato alcuni degli altri suggerimenti qui ...

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

Un esempio con la strategia

Mi piacciono le soluzioni fornite che usano per fare la stessa cosa creando un wrapper attorno al componente.

Dato che si tratta più di un comportamento, ho pensato alla strategia e ho pensato a quanto segue.

Sono nuovo con React e ho bisogno di un po 'di aiuto per salvare un po' di scaldabagno nei casi d'uso

Per favore, rivedi e dimmi cosa ne pensi.

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

Esempio di utilizzo

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

Appunti

Ho pensato di conservare i componentganci del ciclo di vita passati e sostituirli con metodi simili a questo:

const baseDidMount = component.componentDidMount;

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

componentè il componente passato al costruttore di ClickOutsideBehavior.
Ciò rimuoverà l'abilitazione / disabilitazione del boilerplate dall'utente di questo comportamento, ma non sembra molto carino


1

Nel mio caso DROPDOWN la soluzione di Ben Bud ha funzionato bene, ma avevo un pulsante di attivazione / disattivazione separato con un gestore onClick. Quindi la logica del clic esterno è in conflitto con il pulsante onClick toggler. Ecco come l'ho risolto passando anche il riferimento del pulsante:

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

Se si desidera utilizzare un componente molto piccola (466 Byte gzip) che già esiste per questa funzionalità, allora è possibile controllare questa libreria reagire-outclick .

L'aspetto positivo della libreria è che ti consente anche di rilevare i clic all'esterno di un componente e all'interno di un altro. Supporta anche il rilevamento di altri tipi di eventi.


0

Se si desidera utilizzare un componente molto piccola (466 Byte gzip) che già esiste per questa funzionalità, allora è possibile controllare questa libreria reagire-outclick . Cattura eventi al di fuori di un componente di reazione o di un elemento jsx.

L'aspetto positivo della libreria è che ti consente anche di rilevare i clic all'esterno di un componente e all'interno di un altro. Supporta anche il rilevamento di altri tipi di eventi.


0

So che questa è una vecchia domanda, ma continuo a trovarlo e ho avuto molti problemi a capirlo in un formato semplice. Quindi se questo renderebbe la vita di qualcuno un po 'più semplice, usa OutsideClickHandler di airbnb. È il plug-in più semplice per eseguire questa attività senza scrivere il proprio codice.

Esempio:

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

puoi un modo semplice per risolvere il tuo problema, ti mostro:

....

const [dropDwonStatus , setDropDownStatus] = useState(false)

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

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

questo funziona per me, buona fortuna;)


-1

Ho trovato questo dall'articolo qui sotto:

render () {return ({this.node = node;}}> Attiva / disattiva Popover {this.state.popupVisible && (Sono un popover!)}); }}

Ecco un ottimo articolo su questo problema: "Gestire i clic al di fuori dei componenti di React" https://larsgraubner.com/handle-outside-clicks-react/


Benvenuto in Stack Overflow! Le risposte che sono solo un collegamento sono generalmente disapprovate: meta.stackoverflow.com/questions/251006/… . In futuro, includere un preventivo o un esempio di codice con le informazioni pertinenti alla domanda. Saluti!
Fred Stark,

-1

Aggiungi un onClickgestore al contenitore di livello superiore e incrementa un valore di stato ogni volta che l'utente fa clic all'interno. Passa quel valore al componente pertinente e ogni volta che il valore cambia puoi farlo funzionare.

In questo caso chiamiamo this.closeDropdown()ogni volta che il clickCountvalore cambia.

Il incrementClickCountmetodo viene attivato all'interno del .appcontenitore, ma non .dropdownperché utilizziamo event.stopPropagation()per evitare il gorgogliamento degli eventi.

Il tuo codice potrebbe apparire come questo:

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

Non sono sicuro di chi sta effettuando il downvoting ma questa soluzione è piuttosto pulita rispetto ai listener di eventi vincolanti / non vincolanti. Ho avuto zero problemi con questa implementazione.
mercoledì

-1

Per far funzionare la soluzione 'focus' per il menu a discesa con i listener di eventi, puoi aggiungerli con l' evento onMouseDown anziché onClick . In questo modo l'evento si attiva e successivamente il popup si chiude in questo modo:

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

E non dimenticare diimport ReactDOM from 'react-dom';
dipole_moment

-1

Ho fatto una soluzione per tutte le occasioni.

È necessario utilizzare un componente di ordine elevato per avvolgere il componente che si desidera ascoltare per i clic all'esterno di esso.

Questo esempio di componente ha un solo prop: "onClickedOutside" che riceve una funzione.

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

Gancio UseOnClickOutside: reagire 16.8+

Crea una funzione useOnOutsideClick per uso generale

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

Quindi utilizzare il gancio in qualsiasi componente funzionale.

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 alla demo

Parzialmente ispirato dalla risposta di @ pau1fitzgerald.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.