Como ouvir eventos de clique fora de um componente

89

Quero fechar um menu suspenso quando ocorre um clique fora do componente suspenso.

Como faço isso?

Allan Hortle
fonte

Respostas:

58

No elemento que eu adicionei mousedowne mouseupassim:

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

Então no pai eu faço isso:

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

O showCalé um booleano que quando truemostra no meu caso um calendário e o falseesconde.

Robbert van Elk
fonte
isso vincula o clique especificamente a um mouse. Os eventos de clique podem ser gerados por eventos de toque e teclas de inserção também, às quais esta solução não será capaz de reagir, tornando-a inadequada para fins móveis e de acessibilidade = (
Mike 'Pomax' Kamermans
@ Mike'Pomax'Kamermans Agora você pode usar onTouchStart e onTouchEnd para celular. facebook.github.io/react/docs/events.html#touch-events
naoufal
3
Eles já existem há muito tempo, mas não vão funcionar bem com o Android, porque você precisa chamar preventDefault()os eventos imediatamente ou o comportamento nativo do Android entra em ação, interferindo no pré-processamento do React. Escrevi npmjs.com/package/react-onclickoutside desde então.
Mike 'Pomax' Kamermans
Eu amo isso! Recomendado. Remover o ouvinte de evento no mouse será útil. componentWillUnmount = () => window.removeEventListener ('mousedown', this.pageClick, false);
Juni Brosas de
73

Usando os métodos de ciclo de vida, adicione e remova ouvintes de eventos do 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);
    }
});

Verifique as linhas 48-54 deste componente: https://github.com/i-like-robots/react-tube-tracker/blob/91dc0129a1f6077bef57ea4ad9a860be0c600e9d/app/component/tube-tracker.jsx#L48-54

i_like_robots
fonte
Isso adiciona um ao documento, mas significa que quaisquer eventos de clique no componente também acionam o evento do documento. Isso causa seus próprios problemas, porque o alvo do ouvinte de documento é sempre algum div inferior no final de um componente, não o próprio componente.
Allan Hortle
Não tenho certeza se segui seu comentário, mas se você vincular ouvintes de evento ao documento, você pode filtrar quaisquer eventos de clique que borbulhem nele, seja combinando um seletor (delegação de evento padrão) ou por quaisquer outros requisitos arbitrários (EG era o elemento de destino não está dentro de outro elemento).
i_like_robots
3
Isso causa problemas como @AllanHortle aponta. stopPropagation em um evento react não impedirá que os manipuladores de eventos de documentos recebam o evento.
Matt Crinklaw-Vogt
4
Para todos os interessados, eu estava tendo problemas com o stopPropagation ao usar o documento. Se você anexar seu ouvinte de evento à janela, parece que ele resolveu o problema?
Titus
10
Conforme mencionado, this.getDOMNode () é Obsoleto. use ReactDOM em vez de assim: ReactDOM.findDOMNode (this) .contains (e.target)
Arne H. Bitubekk
17

Observe o destino do evento, se o evento estava diretamente no componente, ou nos filhos desse componente, então o clique estava dentro. Caso contrário, estava fora.

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

Se for usado em muitos componentes, é melhor com um mixin:

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

Veja o exemplo aqui: https://jsfiddle.net/0Lshs7mg/1/

ja
fonte
@ Mike'Pomax'Kamermans que foi corrigido, acho que esta resposta adiciona informações úteis, talvez seu comentário possa agora ser removido.
ja
você reverteu minhas alterações por um motivo errado. this.refs.componentestá se referindo ao elemento DOM a partir de 0,14 facebook.github.io/react/blog/2015/07/03/…
Gajus
@GajusKuizinas - não há problema em fazer essa alteração, uma vez que 0,14 é a versão mais recente (agora é beta).
ja
O que é dólar?
Ben Sinclair de
2
Eu odeio cutucar o jQuery com React, uma vez que são dois frameworks de visualização.
Abdennour TOUMI
11

Para o seu caso de uso específico, a resposta atualmente aceita é um pouco exagerada. Se você deseja ouvir quando um usuário clica em uma lista suspensa, simplesmente use um <select>componente como o elemento pai e anexe um onBlurmanipulador a ele.

A única desvantagem dessa abordagem é que ela assume que o usuário já manteve o foco no elemento e depende de um controle de formulário (que pode ou não ser o que você deseja se você levar em consideração que a tabchave também focaliza e desfoca os elementos ) - mas essas desvantagens são apenas um limite para casos de uso mais complicados, caso em que uma solução mais complicada pode ser necessária.

 var Dropdown = React.createClass({

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

   render: function() {
     return (
       <select onBlur={this.handleBlur}>
         ...
       </select>
     );
   }
 });
razorbeard
fonte
10
onBlurdiv também funciona com div se tiver tabIndexatributo
gorpacrate
Para mim esta foi a solução que achei mais simples e fácil de usar, estou usando em um elemento de botão e funciona muito bem. Obrigado!
Amir5000 de
5

Eu escrevi um manipulador de eventos genérico para eventos que se originam fora do componente, react-outside-event .

A implementação em si é simples:

  • Quando o componente é montado, um manipulador de eventos é anexado ao windowobjeto.
  • Quando ocorre um evento, o componente verifica se o evento se origina de dentro do componente. Caso contrário, ele será acionado onOutsideEventno componente de destino.
  • Quando o componente é desmontado, o manipulador de eventos é desconectado.
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 o componente, você precisa envolver a declaração da classe do componente de destino usando o componente de ordem superior e definir os eventos que deseja manipular:

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
fonte
4

Votei em uma das respostas, embora não tenha funcionado para mim. Isso acabou me levando a essa solução. Mudei ligeiramente a ordem das operações. Escuto mouseDown no alvo e mouseUp no alvo. Se algum deles retornar TRUE, não fechamos o modal. Assim que um clique é registrado, em qualquer lugar, esses dois booleanos {mouseDownOnModal, mouseUpOnModal} são redefinidos como falsos.

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

Isso tem a vantagem de que todo o código fica com o componente filho, e não com o pai. Isso significa que não há código padrão para copiar ao reutilizar esse componente.

Steve Zelaznik
fonte
3
  1. Crie uma camada fixa que se estende por toda a tela ( .backdrop).
  2. Tenha o elemento de destino ( .target) fora do .backdropelemento e com um índice de empilhamento maior ( z-index).

Então, qualquer clique no .backdropelemento será considerado "fora do .targetelemento".

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

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

Você pode usar refs para conseguir isso, algo como o seguinte deve funcionar.

Adicione o refao seu elemento:

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

Você pode então adicionar uma função para lidar com o clique fora do elemento, como:

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

Então, finalmente, adicione e remova os ouvintes de eventos em que serão montados e desmontados.

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
fonte
Modificado a sua resposta, que tinha possível erro em referência à chamada handleClickOutsideem document.addEventListener()adicionando thisreferência. Caso contrário, fornece Uncaught ReferenceError: handleClickOutside não está definido emcomponentWillMount()
Muhammad Hannan
1

Muito tarde para a festa, mas tive sucesso com a configuração de um evento de desfoque no elemento pai da lista suspensa com o código associado para fechar a lista suspensa e também anexando um ouvinte mousedown ao elemento pai que verifica se a lista suspensa está aberta ou não, e interromperá a propagação do evento se estiver aberto para que o evento de desfoque não seja acionado.

Uma vez que o evento de movimento do mouse borbulha, isso impedirá que qualquer movimento do mouse nas crianças cause um desfoque nos pais.

/* 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 () não deveria ser chamado tanto quanto eu posso raciocinar, mas o firefox não joga bem sem ele por qualquer motivo. Funciona no Chrome, Firefox e Safari.

Tanner Stults
fonte
0

Eu encontrei uma maneira mais simples de fazer isso.

Você só precisa adicionar onHide(this.closeFunction)o modal

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

Supondo que você tenha uma função para fechar o modal.

MR Murazza
fonte
-1

Use o excelente mixin reagir ao clicar fora :

npm install --save react-onclickoutside

E depois

var Component = React.createClass({
  mixins: [
    require('react-onclickoutside')
  ],
  handleClickOutside: function(evt) {
    // ...handling code goes here... 
  }
});
e18r
fonte
4
Os mix-ins para conhecimento estão obsoletos no React agora.
machineghost