Detectar clic fuera del componente React

Resuelto Thijs Koerselman asked hace 9 años • 57 respuestas

Estoy buscando una manera de detectar si ocurrió un evento de clic fuera de un componente, como se describe en este artículo . jQuery close() 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 hijos y, por lo tanto, no se considera fuera del componente.

Entonces, en mi componente, quiero adjuntar un controlador de clic al archivo window. Cuando el controlador dispara, necesito comparar el objetivo con los hijos dom de mi componente.

El evento de clic contiene propiedades como "ruta", que parece contener la ruta de dominio 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 avatar Sep 14 '15 01:09 Thijs Koerselman
Aceptado

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

Para verlo en acción:

  • Implementación de ganchos
  • Implementación de clase después de React 16.3
  • Implementación de clase antes de React 16.3

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

Implementación de clase:

Después del 16.3

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

Antes del 16.3

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.setWrapperRef = this.setWrapperRef.bind(this);
    this.handleClickOutside = this.handleClickOutside.bind(this);
  }

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

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

  /**
   * Set the wrapper ref
   */
  setWrapperRef(node) {
    this.wrapperRef = node;
  }

  /**
   * Alert if clicked on outside of element
   */
  handleClickOutside(event) {
    if (this.wrapperRef && !this.wrapperRef.contains(event.target)) {
      alert("You clicked outside of me!");
    }
  }

  render() {
    return <div ref={this.setWrapperRef}>{this.props.children}</div>;
  }
}

Ben Bud avatar Feb 14 '2017 19:02 Ben Bud

Estaba atrapado en el mismo tema. Llegué un poco tarde a la fiesta, pero para mí es una muy buena solución. Ojalá 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 de ganchos de reacción (16.8 +)

Puedes 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 que desea agregar la funcionalidad, haga lo siguiente:

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

Encuentre un ejemplo de codesandbox aquí.

Paul Fitzgerald avatar Jul 26 '2017 09:07 Paul Fitzgerald

Actualización 2021:

Ha pasado un tiempo desde que agregué esta respuesta y, dado que todavía parece generar cierto interés, pensé en actualizarla a una versión más actual de React. En 2021, así es como escribiría este componente:

import React, { useState } from "react";
import "./DropDown.css";

export function DropDown({ options, callback }) {
    const [selected, setSelected] = useState("");
    const [expanded, setExpanded] = useState(false);

    function expand() {
        setExpanded(true);
    }

    function close() {
        setExpanded(false);
    }

    function select(event) {
        const value = event.target.textContent;
        callback(value);
        close();
        setSelected(value);
    }

    return (
        <div className="dropdown" tabIndex={0} onFocus={expand} onBlur={close} >
            <div>{selected}</div>
            {expanded ? (
                <div className={"dropdown-options-list"}>
                    {options.map((O) => (
                        <div className={"dropdown-option"} onClick={select}>
                            {O}
                        </div>
                    ))}
                </div>
            ) : null}
        </div>
    );
}

Respuesta original (2016):

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 darle a cualquier elemento la capacidad de tener foco, simplemente asegúrese de que su atributo tabindex esté establecido en un valor distinto de -1. En HTML normal, eso sería configurando el tabindexatributo, pero en React tienes que usarlo tabIndex(nota la mayúscula I).

También puedes hacerlo vía JavaScript conelement.setAttribute('tabindex',0)

Esto es para lo que lo estaba usando, para crear un menú desplegable 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 avatar May 27 '2016 20:05 Pablo Barría Urenda