Cómo escuchar eventos de clic que están fuera de un componente

89

Quiero cerrar un menú desplegable cuando se produce un clic fuera del componente desplegable.

¿Cómo puedo hacer eso?

Allan Hortle
fuente

Respuestas:

59

En el elemento que he agregado mousedowny mouseupasí:

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

Luego, en el padre hago esto:

componentDidMount: function () {
    window.addEventListener('mousedown', this.pageClick, false);
},

pageClick: function (e) {
  if (this.mouseIsDownOnCalendar) {
      return;
  }

  this.setState({
      showCal: false
  });
},

mouseDownHandler: function () {
    this.mouseIsDownOnCalendar = true;
},

mouseUpHandler: function () {
    this.mouseIsDownOnCalendar = false;
}

El showCales un booleano que cuando truemuestra en mi caso un calendario y lo falseoculta.

Robbert van Elk
fuente
Sin embargo, esto vincula el clic específicamente a un mouse. Los eventos de clic se pueden generar mediante eventos táctiles e ingresar teclas también, a los que esta solución no podrá reaccionar, lo que la hace inadecuada para fines móviles y de accesibilidad = (
Mike 'Pomax' Kamermans
@ Mike'Pomax'Kamermans Ahora puede usar onTouchStart y onTouchEnd para dispositivos móviles. facebook.github.io/react/docs/events.html#touch-events
naoufal
3
Esos han existido durante mucho tiempo, pero no funcionarán bien con Android, porque debe llamar preventDefault()a los eventos de inmediato o se activa el comportamiento nativo de Android, con lo que el preprocesamiento de React interfiere. Escribí npmjs.com/package/react-onclickoutside desde entonces.
Mike 'Pomax' Kamermans
¡Me encanta! Elogiado. Será útil eliminar el detector de eventos en el mousedown. componentWillUnmount = () => window.removeEventListener ('mousedown', this.pageClick, false);
Juni Brosas
73

Usando los métodos de ciclo de vida, agregue y elimine detectores de eventos al documento.

React.createClass({
    handleClick: function (e) {
        if (this.getDOMNode().contains(e.target)) {
            return;
        }
    },

    componentWillMount: function () {
        document.addEventListener('click', this.handleClick, false);
    },

    componentWillUnmount: function () {
        document.removeEventListener('click', this.handleClick, false);
    }
});

Consulte las líneas 48-54 de este componente: https://github.com/i-like-robots/react-tube-tracker/blob/91dc0129a1f6077bef57ea4ad9a860be0c600e9d/app/component/tube-tracker.jsx#L48-54

i_like_robots
fuente
Esto agrega uno al documento, pero eso significa que cualquier evento de clic en el componente también activa el evento de documento. Esto causa sus propios problemas porque el objetivo del oyente del documento es siempre un div bajo al final de un componente, no el componente en sí.
Allan Hortle
No estoy muy seguro de seguir su comentario, pero si vincula los oyentes de eventos al documento, puede filtrar cualquier evento de clic que aparezca, ya sea haciendo coincidir un selector (delegación de eventos estándar) o por cualquier otro requisito arbitrario (EG fue el elemento de destino no está dentro de otro elemento).
i_like_robots
3
Esto causa problemas como señala @AllanHortle. stopPropagation en un evento de reacción no evitará que los controladores de eventos del documento reciban el evento.
Matt Crinklaw-Vogt
4
Para cualquier persona interesada, estaba teniendo problemas con stopPropagation al usar el documento. Si adjunta su detector de eventos a la ventana, ¿parece solucionar este problema?
Titus
10
Como se mencionó, this.getDOMNode () es obsoleto. use ReactDOM en su lugar de esta manera: ReactDOM.findDOMNode (this) .contains (e.target)
Arne H. Bitubekk
17

Mire el destino del evento, si el evento estaba directamente en el componente o en los hijos de ese componente, entonces el clic estaba dentro. De lo contrario, estaba afuera.

React.createClass({
    clickDocument: function(e) {
        var component = React.findDOMNode(this.refs.component);
        if (e.target == component || $(component).has(e.target).length) {
            // Inside of the component.
        } else {
            // Outside of the component.
        }

    },
    componentDidMount: function() {
        $(document).bind('click', this.clickDocument);
    },
    componentWillUnmount: function() {
        $(document).unbind('click', this.clickDocument);
    },
    render: function() {
        return (
            <div ref='component'>
                ...
            </div> 
        )
    }
});

Si se va a utilizar en muchos componentes, es mejor con una mezcla:

var ClickMixin = {
    _clickDocument: function (e) {
        var component = React.findDOMNode(this.refs.component);
        if (e.target == component || $(component).has(e.target).length) {
            this.clickInside(e);
        } else {
            this.clickOutside(e);
        }
    },
    componentDidMount: function () {
        $(document).bind('click', this._clickDocument);
    },
    componentWillUnmount: function () {
        $(document).unbind('click', this._clickDocument);
    },
}

Vea el ejemplo aquí: https://jsfiddle.net/0Lshs7mg/1/

ja
fuente
@ Mike'Pomax'Kamermans que se ha solucionado, creo que esta respuesta agrega información útil, tal vez su comentario ahora se pueda eliminar.
ja
ha revertido mis cambios por una razón incorrecta. this.refs.componentse refiere al elemento DOM a partir de 0.14 facebook.github.io/react/blog/2015/07/03/…
Gajus
@GajusKuizinas: está bien hacer ese cambio una vez que 0.14 sea la última versión (ahora es beta).
ja
¿Qué es el dólar?
pronebird
2
Odio tocar jQuery con React ya que son el marco de dos vistas.
Abdennour TOUMI
11

Para su caso de uso específico, la respuesta actualmente aceptada es un poco sobre-diseñada. Si desea escuchar cuando un usuario hace clic fuera de una lista desplegable, simplemente use un <select>componente como elemento principal y adjunte un onBlurcontrolador.

Los únicos inconvenientes de este enfoque es que asume que el usuario ya ha mantenido el foco en el elemento, y se basa en un control de formulario (que puede ser o no lo que desea si tiene en cuenta que la tabclave también enfoca y desenfoca elementos ) - pero estos inconvenientes son solo un límite para casos de uso más complicados, en cuyo caso podría ser necesaria una solución más complicada.

 var Dropdown = React.createClass({

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

   render: function() {
     return (
       <select onBlur={this.handleBlur}>
         ...
       </select>
     );
   }
 });
barba de afeitar
fuente
10
onBlurdiv también funciona con div si tiene tabIndexatributo
gorpacrate
Para mí, esta fue la solución que encontré más simple y fácil de usar, la estoy usando en un elemento de botón y funciona a la perfección. ¡Gracias!
Amir5000
5

He escrito un controlador de eventos genérico para eventos que se originan fuera del componente, react-outside-event .

La implementación en sí es simple:

  • Cuando se monta el componente, se adjunta un controlador de eventos al windowobjeto.
  • Cuando ocurre un evento, el componente verifica si el evento se origina dentro del componente. Si no es así, se activa onOutsideEventen el componente de destino.
  • Cuando se desmonta el componente, se desconecta el controlador de eventos.
import React from 'react';
import ReactDOM from 'react-dom';

/**
 * @param {ReactClass} Target The component that defines `onOutsideEvent` handler.
 * @param {String[]} supportedEvents A list of valid DOM event names. Default: ['mousedown'].
 * @return {ReactClass}
 */
export default (Target, supportedEvents = ['mousedown']) => {
    return class ReactOutsideEvent extends React.Component {
        componentDidMount = () => {
            if (!this.refs.target.onOutsideEvent) {
                throw new Error('Component does not defined "onOutsideEvent" method.');
            }

            supportedEvents.forEach((eventName) => {
                window.addEventListener(eventName, this.handleEvent, false);
            });
        };

        componentWillUnmount = () => {
            supportedEvents.forEach((eventName) => {
                window.removeEventListener(eventName, this.handleEvent, false);
            });
        };

        handleEvent = (event) => {
            let target,
                targetElement,
                isInside,
                isOutside;

            target = this.refs.target;
            targetElement = ReactDOM.findDOMNode(target);
            isInside = targetElement.contains(event.target) || targetElement === event.target;
            isOutside = !isInside;



            if (isOutside) {
                target.onOutsideEvent(event);
            }
        };

        render() {
            return <Target ref='target' {... this.props} />;
        }
    }
};

Para usar el componente, necesita ajustar la declaración de clase del componente de destino usando el componente de orden superior y definir los eventos que desea manejar:

import React from 'react';
import ReactDOM from 'react-dom';
import ReactOutsideEvent from 'react-outside-event';

class Player extends React.Component {
    onOutsideEvent = (event) => {
        if (event.type === 'mousedown') {

        } else if (event.type === 'mouseup') {

        }
    }

    render () {
        return <div>Hello, World!</div>;
    }
}

export default ReactOutsideEvent(Player, ['mousedown', 'mouseup']);
Gajus
fuente
4

Voté una de las respuestas a pesar de que no funcionó para mí. Terminó llevándome a esta solución. Cambié ligeramente el orden de las operaciones. Escucho mouseDown en el objetivo y mouseUp en el objetivo. Si alguno de ellos devuelve TRUE, no cerramos el modal. Tan pronto como se registra un clic, en cualquier lugar, esos dos valores booleanos {mouseDownOnModal, mouseUpOnModal} se vuelven a establecer en falso.

componentDidMount() {
    document.addEventListener('click', this._handlePageClick);
},

componentWillUnmount() {
    document.removeEventListener('click', this._handlePageClick);
},

_handlePageClick(e) {
    var wasDown = this.mouseDownOnModal;
    var wasUp = this.mouseUpOnModal;
    this.mouseDownOnModal = false;
    this.mouseUpOnModal = false;
    if (!wasDown && !wasUp)
        this.close();
},

_handleMouseDown() {
    this.mouseDownOnModal = true;
},

_handleMouseUp() {
    this.mouseUpOnModal = true;
},

render() {
    return (
        <Modal onMouseDown={this._handleMouseDown} >
               onMouseUp={this._handleMouseUp}
            {/* other_content_here */}
        </Modal>
    );
}

Esto tiene la ventaja de que todo el código recae en el componente hijo y no en el padre. Significa que no hay un código repetitivo para copiar al reutilizar este componente.

Steve Zelaznik
fuente
3
  1. Cree una capa fija que abarque toda la pantalla ( .backdrop).
  2. Tener el elemento de destino ( .target) fuera del .backdropelemento y con un índice de apilamiento mayor ( z-index).

Entonces, cualquier clic en el .backdropelemento se considerará "fuera del .targetelemento".

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

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

Puede usar refs para lograr esto, algo como lo siguiente debería funcionar.

Agregue el refa su elemento:

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

Luego puede agregar una función para manejar el clic fuera del elemento de esta manera:

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

Luego, finalmente, agregue y elimine los detectores de eventos que se montarán y desmontarán.

componentWillMount() {
  document.addEventListener('click', this.handleClickOutside, false);  // assuming that you already did .bind(this) in constructor
}

componentWillUnmount() {
  document.removeEventListener('click', this.handleClickOutside, false);  // assuming that you already did .bind(this) in constructor
}
Chris Borrowdale
fuente
Modificado su respuesta, que tenía la posibilidad de error en referencia a llamar handleClickOutsideen document.addEventListener()añadiendo thisreferencia. De lo contrario, da un error de referencia no detectado: handleClickOutside no está definido encomponentWillMount()
Muhammad Hannan
1

Súper tarde para la fiesta, pero tuve éxito al configurar un evento de desenfoque en el elemento principal del menú desplegable con el código asociado para cerrar el menú desplegable, y también adjuntar un oyente del mousedown al elemento principal que verifica si el menú desplegable está abierto o no, y detendrá la propagación del evento si está abierto para que no se active el evento de desenfoque.

Dado que el evento mousedown aparece, esto evitará que cualquier mousedown en los niños cause un desenfoque en el padre.

/* Some react component */
...

showFoo = () => this.setState({ showFoo: true });

hideFoo = () => this.setState({ showFoo: false });

clicked = e => {
    if (!this.state.showFoo) {
        this.showFoo();
        return;
    }
    e.preventDefault()
    e.stopPropagation()
}

render() {
    return (
        <div 
            onFocus={this.showFoo}
            onBlur={this.hideFoo}
            onMouseDown={this.clicked}
        >
            {this.state.showFoo ? <FooComponent /> : null}
        </div>
    )
}

...

e.preventDefault () no debería tener que ser llamado hasta donde puedo razonar, pero firefox no funciona bien sin él por cualquier razón. Funciona en Chrome, Firefox y Safari.

Tanner Stults
fuente
0

Encontré una forma más sencilla de hacerlo.

Solo necesitas agregar onHide(this.closeFunction)el modal

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

Suponiendo que tiene una función para cerrar el modal.

MR Murazza
fuente
-1

Utilice el excelente mixin react-onclickoutside :

npm install --save react-onclickoutside

Y entonces

var Component = React.createClass({
  mixins: [
    require('react-onclickoutside')
  ],
  handleClickOutside: function(evt) {
    // ...handling code goes here... 
  }
});
e18r
fuente
4
FYI mix-ins están obsoletos en React ahora.
machineghost