Detectar clic fuera del componente Reaccionar

411

Estoy buscando una manera de detectar si ocurrió un evento de clic fuera de un componente, como se describe en este artículo . jQueryarest () se usa para ver si el objetivo de un evento de clic tiene el elemento dom como uno de sus padres. Si hay una coincidencia, el evento de clic pertenece a uno de los elementos secundarios y, por lo tanto, no se considera que esté fuera del componente.

Entonces, en mi componente, quiero adjuntar un controlador de clic a la ventana. Cuando se dispara el controlador, necesito comparar el objetivo con los hijos de dom de mi componente.

El evento de clic contiene propiedades como "ruta" que parece contener la ruta dom que ha recorrido el evento. No estoy seguro de qué comparar o cómo recorrerlo mejor, y creo que alguien ya debe haber puesto eso en una función de utilidad inteligente ... ¿No?

Thijs Koerselman
fuente
¿Podría adjuntar el controlador de clics al padre en lugar de a la ventana?
J. Mark Stevens
Si adjuntas un controlador de clics al padre, sabes cuándo se hace clic en ese elemento o uno de sus hijos, pero necesito detectar todos los demás lugares en los que se hace clic, por lo que el controlador debe estar conectado a la ventana.
Thijs Koerselman
Miré el artículo después de la respuesta anterior. ¿Qué hay de configurar un clickState en el componente superior y pasar acciones de clic de los niños? Luego, verificaría los accesorios en los niños para administrar el estado de apertura y cierre.
J. Mark Stevens
El componente superior sería mi aplicación. Pero el componente de escucha tiene varios niveles de profundidad y no tiene una posición estricta en el dom. No puedo agregar controladores de clics a todos los componentes de mi aplicación solo porque uno de ellos está interesado en saber si hizo clic en algún lugar fuera de él. Otros componentes no deberían ser conscientes de esta lógica porque eso crearía terribles dependencias y código repetitivo.
Thijs Koerselman
55
Me gustaría recomendarle una muy buena lib. creado por AirBnb: github.com/airbnb/react-outside-click-handler
Osoian Marcel

Respuestas:

684

El uso de referencias en React 16.3+ cambió.

La siguiente solución utiliza ES6 y sigue las mejores prácticas para vincular y configurar la referencia mediante un método.

Para verlo en acción:

Implementación de clase:

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

Implementación de ganchos:

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>;
}
Ben Bud
fuente
3
¿Cómo pruebas esto sin embargo? ¿Cómo se envía un event.target válido a la función handleClickOutside?
csilk
55
¿Cómo puedes usar los eventos sintéticos de React, en lugar del document.addEventListeneraquí?
Ralph David Abernathy
10
@ polkovnikov.ph 1. contextsolo es necesario como argumento en constructor si se usa. No se está utilizando en este caso. La razón por la cual el equipo de reacción recomienda tener propsun argumento en el constructor es porque el uso de this.propsantes de llamar super(props)no estará definido y puede provocar errores. contexttodavía está disponible en los nodos internos, ese es el propósito de context. De esta manera, no tiene que pasarlo de un componente a otro como lo hace con los accesorios. 2. Esto es solo una preferencia estilística, y no garantiza debatir en este caso.
Ben Bud
77
Me sale el siguiente error: "this.wrapperRef.contains no es una función"
Bogdan
55
@Bogdan, recibí el mismo error al usar un componente con estilo. Considere usar un <div> en el nivel superior
usuario 3040068
151

Aquí está la solución que mejor funcionó para mí sin adjuntar eventos al contenedor:

Ciertos elementos HTML pueden tener lo que se conoce como " foco ", por ejemplo, elementos de entrada. Esos elementos también responderán al evento de desenfoque , cuando pierdan ese enfoque.

Para dar a cualquier elemento la capacidad de tener foco, solo asegúrese de que su atributo tabindex esté configurado en algo que no sea -1. En HTML normal, eso sería mediante la configuración del tabindexatributo, pero en React debe usar tabIndex(tenga en cuenta la mayúscula I).

También puedes hacerlo a través de JavaScript con element.setAttribute('tabindex',0)

Esto es para lo que lo estaba usando, para hacer un menú DropDown personalizado.

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>
        );
    }
});
Pablo Barría Urenda
fuente
10
¿No crearía esto problemas de accesibilidad? Dado que puede enfocar un elemento adicional solo para detectar cuándo está entrando / saliendo, pero la interacción debe estar en los valores desplegables, ¿no?
Thijs Koerselman
2
Este es un enfoque muy interesante. Funcionó perfectamente para mi situación, pero me interesaría saber cómo esto podría afectar la accesibilidad.
klinore
17
Tengo este "trabajo" hasta cierto punto, es un pequeño truco genial. Mi problema es que mi contenido desplegable es display:absolutepara que el menú desplegable no afecte el tamaño del div principal. Esto significa que cuando hago clic en un elemento en el menú desplegable, se activa el desenfoque.
Dave
2
Tuve problemas con este enfoque, cualquier elemento en el contenido desplegable no dispara eventos como onClick.
AnimaSola
8
Es posible que tenga otros problemas si el contenido desplegable contiene algunos elementos enfocables, como las entradas de formulario, por ejemplo. Robarán su enfoque, onBlur se activarán en su contenedor desplegable y expanded se establecerán en falso.
Kamagatos
107

Estaba atrapado en el mismo problema. Llego un poco tarde a la fiesta aquí, pero para mí esta es una muy buena solución. Esperemos que sea de ayuda para alguien más. Necesitas importar findDOMNodedesdereact-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
        });
    }
}

Enfoque Reaccionar ganchos (16.8 +)

Puede crear un gancho reutilizable llamado 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 };
}

Luego, en el componente, desea agregar la funcionalidad para hacer lo siguiente:

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

}

Encuentre un ejemplo de codeandbox aquí.

Paul Fitzgerald
fuente
1
@LeeHanKyeol No del todo: esta respuesta invoca los controladores de eventos durante la fase de captura del manejo de eventos, mientras que la respuesta vinculada a invoca los controladores de eventos durante la fase de burbuja.
stevejay
3
Esta debería ser la respuesta aceptada. Funcionó perfectamente para menús desplegables con posicionamiento absoluto.
lavaplatosConProgrammingSkill
1
ReactDOM.findDOMNodey está en desuso, debe usar refdevoluciones de llamada: github.com/yannickcr/eslint-plugin-react/issues/…
fecha
2
gran y limpia solución
Karim
1
Esto funcionó para mí, aunque tuve que hackear su componente para restablecer lo visible nuevamente a 'verdadero' después de 200 ms (de lo contrario, mi menú nunca vuelve a aparecer). ¡Gracias!
Lee Moe
87

Después de probar muchos métodos aquí, decidí usar github.com/Pomax/react-onclickoutside por lo completo que es.

Instalé el módulo a través de npm y lo importé a mi componente:

import onClickOutside from 'react-onclickoutside'

Luego, en mi clase de componente, definí el handleClickOutsidemétodo:

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

Y al exportar mi componente lo envolví en onClickOutside():

export default onClickOutside(NameOfComponent)

Eso es.

Beau Smith
fuente
2
¿Hay ventajas concretas a este sobre el tabIndex/ onBlurenfoque propuesto por Pablo ? ¿Cómo funciona la implementación y cómo difiere su comportamiento del onBlurenfoque?
Mark Amery
44
Es mejor usar un componente dedicado que usar un hack tabIndex. ¡Votarlo!
Atul Yadav el
55
@ MarkAmery - Puse un comentario sobre el tabIndex/onBlurenfoque. No funciona cuando el menú desplegable es position:absolute, como un menú que se cierne sobre otro contenido.
Beau Smith
44
Otra ventaja de usar esto sobre tabindex es que la solución tabindex también dispara un evento borroso si te enfocas en un elemento hijo
Jemar Jones
1
@BeauSmith - ¡Muchas gracias! Me salvó el día!
Mika
39

Encontré una solución gracias a Ben Alpert en discusion.reactjs.org . El enfoque sugerido adjunta un controlador al documento, pero resultó ser problemático. Al hacer clic en uno de los componentes de mi árbol, se produjo una reproducción que eliminó el elemento seleccionado en la actualización. Debido a que la reproducción desde React ocurre antes de que se llame al controlador del cuerpo del documento, el elemento no se detectó como "dentro" del árbol.

La solución a esto fue agregar el controlador en el elemento raíz de la aplicación.

principal:

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>
    )
  }
}
Thijs Koerselman
fuente
8
Esto ya no me funciona con React 0.14.7: tal vez React cambió algo, o tal vez cometí un error al adaptar el código a todos los cambios en React. En cambio, estoy usando github.com/Pomax/react-onclickoutside que funciona de maravilla .
Nicole
1
Hmm No veo ninguna razón por la que esto debería funcionar. ¿Por qué se debe garantizar que un controlador en el nodo DOM raíz de la aplicación se active antes de que otro controlador active una nueva reproducción si documentno está en uno ?
Mark Amery
1
Un punto puro de experiencia de usuario: mousedownprobablemente sería un mejor controlador que clickaquí. En la mayoría de las aplicaciones, el comportamiento de cerrar el menú haciendo clic fuera ocurre en el momento en que presiona el mouse, no cuando lo suelta. Pruébelo, por ejemplo, con la bandera de Stack Overflow o comparta diálogos o con uno de los menús desplegables de la barra de menú superior de su navegador.
Mark Amery
ReactDOM.findDOMNodey la cadena refestán en desuso, deben usar refdevoluciones de llamada: github.com/yannickcr/eslint-plugin-react/issues/…
fecha
Esta es la solución más simple y funciona perfectamente para mí, incluso cuando se conecta a ladocument
Flion
37

Implementación de gancho basada en la excelente charla de Tanner Linsley en JSConf Hawaii 2020 :

useOuterClick API de gancho

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

Implementación

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

useOuterClickutiliza referencias mutables para crear una API magra para Client. Se puede configurar una devolución de llamada sin tener que memorizarla a través de useCallback. El cuerpo de devolución de llamada todavía tiene acceso a los accesorios y el estado más recientes (sin valores de cierre obsoletos).


Nota para usuarios de iOS

iOS trata solo ciertos elementos como clicables. Para eludir este comportamiento, elija un oyente de clic externo diferente al que documentno incluye nada hacia arriba body. Por ejemplo, podría agregar un oyente en la raíz React diven el ejemplo anterior y expandir su altura ( height: 100vho similar) para capturar todos los clics externos. Fuentes: quirksmode.org y esta respuesta

ford04
fuente
no funciona en el móvil tan imprevisto
Omar
@Omar acaba de probar el Codesandbox en mi publicación con Firefox 67.0.3 en un dispositivo Android, funciona bien. ¿Podría explicar qué navegador usa, cuál es el error, etc.?
ford04
Sin errores, pero no funciona en Chrome, iPhone 7+. No detecta los grifos afuera. En las herramientas de desarrollo de Chrome en dispositivos móviles funciona, pero en un dispositivo iOS real no funciona.
Omar
1
@ Ford04 gracias por desenterrar eso, me topé con otro hilo que sugería el siguiente truco. @media (hover: none) and (pointer: coarse) { body { cursor:pointer } }Agregando esto en mis estilos globales, parece que solucionó el problema.
Kostas Sarantopoulos
1
@ ford04 sí, por cierto, de acuerdo con este hilo, hay una consulta de medios aún más específica @supports (-webkit-overflow-scrolling: touch)que se dirige solo a iOS, ¡aunque no sé cuánto! Lo acabo de probar y funciona.
Kostas Sarantopoulos
31

Ninguna de las otras respuestas aquí funcionó para mí. Estaba tratando de ocultar una ventana emergente en el desenfoque, pero como el contenido estaba en una posición absoluta, el onBlur también se activaba incluso con el clic de los contenidos internos.

Aquí hay un enfoque que funcionó para mí:

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

Espero que esto ayude.

Niyaz
fuente
¡Muy bien! Gracias. Pero en mi caso fue un problema atrapar el "lugar actual" con posición absoluta para el div, así que uso "OnMouseLeave" para div con entrada y calendario desplegable para deshabilitar todo div cuando el mouse sale del div.
Alex
1
¡Gracias! Ayúdame mucho.
Ángel Lucas
1
Me gusta más que los métodos a los que se adjunta document. Gracias.
kyw
Para que esto funcione, debe asegurarse de que el elemento seleccionado (relatedTarget) sea enfocable. Ver stackoverflow.com/questions/42764494/…
xabitrigo
21

[Actualización] Solución con React ^ 16.8 usando ganchos

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;

Solución 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;
onoya
fuente
3
Esta es la solución de acceso ahora. Si obtiene el error .contains is not a function, puede deberse a que está pasando el accesorio de referencia a un componente personalizado en lugar de un elemento DOM real como un<div>
willlma
Para aquellos que intentan pasar la refutilería a un componente personalizado, es posible que quieran echar un vistazoReact.forwardRef
onoya
4

Aquí está mi enfoque (demo - https://jsfiddle.net/agymay93/4/ ):

He creado un componente especial llamado WatchClickOutsidey se puede usar como (supongo que la JSXsintaxis):

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

Aquí está el código 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>
    );
  }
}
pie6k
fuente
Gran solución: para mi uso, cambié para escuchar el documento en lugar del cuerpo en el caso de páginas con contenido corto, y cambié la referencia de la cadena a la referencia dom: jsfiddle.net/agymay93/9
Billy Moon
y usé span en lugar de div ya que es menos probable que cambie el diseño, y agregué una demostración de manejo de clics fuera del cuerpo: jsfiddle.net/agymay93/10
Billy Moon
La cadena refestá en desuso, debe usar refdevoluciones de llamada: reactjs.org/docs/refs-and-the-dom.html
desde el
4

Esto ya tiene muchas respuestas, pero no abordan e.stopPropagation()y evitan hacer clic en los enlaces de reacción fuera del elemento que desea cerrar.

Debido al hecho de que React tiene su propio controlador de eventos artificial, no puede usar el documento como base para los oyentes de eventos. Debe hacerlo e.stopPropagation()antes de esto, ya que React usa el documento en sí. Si usa por ejemplo en su document.querySelector('body')lugar. Puede evitar el clic desde el enlace Reaccionar. El siguiente es un ejemplo de cómo implemento hacer clic afuera y cerrar.
Esto usa ES6 y 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;
Richard Herries
fuente
3

Para aquellos que necesitan un posicionamiento absoluto, una opción simple por la que opté es agregar un componente de envoltura diseñado para cubrir toda la página con un fondo transparente. Luego puede agregar un onClick en este elemento para cerrar su componente interno.

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

Como sucede ahora si agrega un controlador de clic en el contenido, el evento también se propagará al div superior y, por lo tanto, activará el controladorOutsideClick. Si este no es su comportamiento deseado, simplemente detenga la proyección del evento en su controlador.

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

``

Anthony Garant
fuente
El problema con este enfoque es que no puede hacer clic en el Contenido; el div recibirá el clic en su lugar.
rbonick 01 de
Dado que el contenido está en el div si no detiene la propagación del evento, ambos recibirán el clic. Este suele ser un comportamiento deseado, pero si no desea que el clic se propague al div, simplemente detenga la propagación de eventos en su controlador de contenido onClick. He actualizado mi respuesta para mostrar cómo.
Anthony Garant
3

Para ampliar la respuesta aceptada hecha por Ben Bud , si está utilizando componentes con estilo, pasar referencias de esa manera le dará un error como "this.wrapperRef.contains no es una función".

La solución sugerida, en los comentarios, para envolver el componente con estilo con un div y pasar la referencia allí, funciona. Dicho esto, en sus documentos ya explican la razón de esto y el uso adecuado de las referencias dentro de los componentes con estilo:

Pasar un accesorio de referencia a un componente con estilo le dará una instancia del contenedor StyledComponent, pero no al nodo DOM subyacente. Esto se debe a cómo funcionan los árbitros. No es posible llamar a métodos DOM, como focus, directamente en nuestros contenedores. Para obtener una referencia al nodo DOM real envuelto, pase la devolución de llamada al accesorio innerRef.

Al igual que:

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

Luego puede acceder directamente desde la función "handleClickOutside":

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

Esto también se aplica para el enfoque "onBlur":

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

Mi mayor preocupación con todas las otras respuestas es tener que filtrar los eventos de clic desde la raíz / padre hacia abajo. Encontré que la forma más fácil era simplemente establecer un elemento hermano con posición: fijo, un índice z 1 detrás del menú desplegable y manejar el evento de clic en el elemento fijo dentro del mismo componente. Mantiene todo centralizado en un componente dado.

Código de ejemplo

#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;
    }
}
metroninja
fuente
1
Hmm ingenioso. ¿Funcionaría con varias instancias? ¿O cada elemento fijo podría bloquear el clic para los demás?
Thijs Koerselman
2
componentWillMount(){

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

handleClickOutside(event) {

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

El evento path[0]es el último elemento en el que se hizo clic

Ivan Sanchez
fuente
3
Asegúrese de retirar el detector de eventos cuando se desmonta los componentes para evitar los problemas de gestión de memoria, etc
agm1984
2

Usé este módulo (no tengo ninguna asociación con el 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>
    );
  }
}

Hizo el trabajo bien.

ErichBSchulz
fuente
¡Esto funciona fantásticamente! Mucho más simple que muchas otras respuestas aquí y, a diferencia de al menos otras 3 soluciones aquí, donde para todas ellas recibí "Support for the experimental syntax 'classProperties' isn't currently enabled"esto simplemente funcionó de inmediato. Para cualquiera que pruebe esta solución, recuerde borrar cualquier código persistente que haya copiado al probar las otras soluciones. por ejemplo, agregar oyentes de eventos parece estar en conflicto con esto.
Frikster
2

Hice esto en parte siguiendo esto y siguiendo los documentos oficiales de React sobre el manejo de referencias que requieren reaccionar ^ 16.3. Esto es lo único que me funcionó después de probar algunas de las otras sugerencias aquí ...

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***
)}}
RawCode
fuente
1

Un ejemplo con estrategia

Me gustan las soluciones proporcionadas que hacen lo mismo creando un contenedor alrededor del componente.

Como se trata más de un comportamiento, pensé en Estrategia y se me ocurrió lo siguiente.

Soy nuevo con React y necesito un poco de ayuda para ahorrar algo de repetitivo en los casos de uso.

Por favor revisa y dime lo que piensas.

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

Uso de muestra

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

Notas

Pensé en almacenar los componentganchos del ciclo de vida pasados y anularlos con métodos similares a esto:

const baseDidMount = component.componentDidMount;

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

componentes el componente pasado al constructor de ClickOutsideBehavior.
Esto eliminará la placa de activación / desactivación del usuario de este comportamiento, pero no se ve muy bien.

kidroca
fuente
1

En mi caso DROPDOWN, la solución de Ben Bud funcionó bien, pero tenía un botón de alternar separado con un controlador onClick. Por lo tanto, la lógica de clic externa entró en conflicto con el botón onClick toggler. Así es como lo resolví pasando también la referencia del botón:

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>
  );
}
Ilyas Assainov
fuente
0

Si desea utilizar un componente pequeño (466 Bytes comprimido) que ya existe para esta funcionalidad, puede consultar esta biblioteca react-outclick .

Lo bueno de la biblioteca es que también le permite detectar clics fuera de un componente y dentro de otro. También es compatible con la detección de otros tipos de eventos.

Tushar Sharma
fuente
0

Si desea utilizar un componente pequeño (466 Bytes comprimido) que ya existe para esta funcionalidad, puede consultar esta biblioteca react-outclick . Captura eventos fuera de un componente de reacción o elemento jsx.

Lo bueno de la biblioteca es que también le permite detectar clics fuera de un componente y dentro de otro. También es compatible con la detección de otros tipos de eventos.

Tushar Sharma
fuente
0

Sé que esta es una vieja pregunta, pero sigo encontrando esto y tuve muchos problemas para resolver esto en un formato simple. Entonces, si esto facilitaría la vida de cualquiera, use OutsideClickHandler de airbnb. Es el complemento más simple para realizar esta tarea sin escribir su propio código.

Ejemplo:

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>
 )
}
Sam Nikaein
fuente
0
import { useClickAway } from "react-use";

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

Puedes encontrar una manera simple de resolver tu problema, te muestro:

....

const [dropDwonStatus , setDropDownStatus] = useState(false)

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

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

esto funciona para mí, buena suerte;)

Moshtaba Darzi
fuente
-1

Encontré esto en el artículo a continuación:

render () {return ({this.node = node;}}> Toggle Popover {this.state.popupVisible && (I'm a popover!)}); }}

Aquí hay un gran artículo sobre este tema: "Manejar clics fuera de React components" https://larsgraubner.com/handle-outside-clicks-react/

Amanda Koster
fuente
¡Bienvenido a Stack Overflow! Las respuestas que son solo un enlace generalmente están mal vistas: meta.stackoverflow.com/questions/251006/… . En el futuro, incluya una cita o un ejemplo de código con la información relevante para la pregunta. ¡Salud!
Fred Stark
-1

Agregue un onClickcontrolador a su contenedor de nivel superior e incremente un valor de estado cada vez que el usuario haga clic dentro. Pase ese valor al componente relevante y siempre que el valor cambie, puede trabajar.

En este caso, estamos llamando this.closeDropdown()cada vez clickCountque cambia el valor.

El incrementClickCountmétodo se activa dentro del .appcontenedor, pero no .dropdownporque usamos event.stopPropagation()para evitar el burbujeo de eventos.

Su código puede terminar pareciéndose a esto:

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>
        );
    }
}
wdm
fuente
No estoy seguro de quién está votando, pero esta solución es bastante limpia en comparación con los oyentes de eventos vinculantes / desvinculadores. No he tenido ningún problema con esta implementación.
wdm
-1

Para que la solución 'focus' funcione para el menú desplegable con oyentes de eventos, puede agregarlos con el evento onMouseDown en lugar de onClick . De esa manera, el evento se disparará y después de eso, la ventana emergente se cerrará así:

<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>
                }
idtmc
fuente
-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");
    }
  }
}
momento bipolar
fuente
Y no olvidesimport ReactDOM from 'react-dom';
dipole_moment
-1

Hice una solución para todas las ocasiones.

Debe usar un Componente de orden superior para ajustar el componente que desea escuchar para los clics que se encuentran fuera de él.

Este ejemplo de componente tiene solo un accesorio: "onClickedOutside" que recibe una función.

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>;
  }
}
Higor Lorenzon
fuente
-1

UseOnClickOutside Hook - Reacciona 16.8 +

Crear una función general useOnOutsideClick

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

Luego use el gancho en cualquier componente funcional.

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

};

Enlace a demo

Parcialmente inspirado por la respuesta de @ pau1fitzgerald.

Ben Carp
fuente