Detectar clique fora do componente React


410

Estou procurando uma maneira de detectar se um evento de clique ocorreu fora de um componente, conforme descrito neste artigo . O jQuery closest () é usado para verificar se o destino de um evento click possui o elemento dom como um de seus pais. Se houver uma correspondência, o evento click pertence a um dos filhos e, portanto, não é considerado como estando fora do componente.

Então, no meu componente, quero anexar um manipulador de cliques à janela. Quando o manipulador é acionado, preciso comparar o alvo com os filhos do meu componente.

O evento click contém propriedades como "path", que parece conter o caminho dom que o evento percorreu. Não sei ao certo o que comparar ou a melhor forma de percorrê-lo, e acho que alguém já deve ter colocado isso em uma função inteligente de utilitário ... Não?


Você poderia anexar o manipulador de cliques ao pai e não à janela?
J. Mark Stevens

Se você anexar um manipulador de cliques ao pai, você saberá quando esse elemento ou um de seus filhos for clicado, mas preciso detectar todos os outros locais clicados, para que o manipulador precise ser anexado à janela.
Thijs Koerselman 13/09/2015

Eu olhei para o artigo após a resposta anterior. Que tal definir um clickState no componente superior e transmitir ações de clique das crianças. Depois, você verificaria os adereços das crianças para gerenciar o estado aberto.
J. Mark Stevens

O componente principal seria meu aplicativo. Mas o componente de escuta tem vários níveis de profundidade e não possui uma posição estrita no domínio. Não posso adicionar manipuladores de cliques a todos os componentes do meu aplicativo apenas porque um deles está interessado em saber se você clicou em algum lugar fora dele. Outros componentes não devem estar cientes dessa lógica, pois isso criaria terríveis dependências e código padrão.
Thijs Koerselman 13/09/2015

5
Eu gostaria de recomendar uma lib muito boa. criado por Airbnb: github.com/airbnb/react-outside-click-handler
Osoian Marcel

Respostas:


683

O uso de referências no React 16.3+ foi alterado.

A solução a seguir usa o ES6 e segue as práticas recomendadas para ligação, além de definir a ref por meio de um método.

Para vê-lo em ação:

Implementação de 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,
};

Implementação 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>;
}

3
Como você testou isso? Como você envia um event.target válido para a função handleClickOutside?
csilk

5
Como você pode usar os eventos sintéticos do React, em vez do document.addEventListeneraqui?
Ralph David Abernathy

9
@ polkovnikov.ph 1. contextsó é necessário como argumento no construtor se for usado. Não está sendo usado neste caso. A razão pela qual a equipe de reação recomenda ter propscomo argumento o construtor é porque o uso de this.propsantes da chamada super(props)será indefinido e pode levar a erros. contextainda está disponível nos nós internos, esse é o objetivo de context. Dessa forma, você não precisa transmiti-lo de componente para componente, como faz com os adereços. 2. Essa é apenas uma preferência estilística e não garante debate neste caso.
Ben Bud

7
Eu recebo o seguinte erro: "this.wrapperRef.contains não é uma função"
Bogdan

5
@ Bogdan, eu estava recebendo o mesmo erro ao usar um componente com estilo. Considere usar um <div> no nível superior
user3040068

149

Aqui está a solução que melhor funcionou para mim sem anexar eventos ao contêiner:

Certos elementos HTML podem ter o que é conhecido como " foco ", por exemplo, elementos de entrada. Esses elementos também responderão ao evento de desfoque , quando perderem o foco.

Para dar a qualquer elemento a capacidade de ter foco, verifique se o atributo tabindex está definido como algo diferente de -1. No HTML comum, isso seria definido pelo tabindexatributo, mas no React você deve usar tabIndex(observe a capital I).

Você também pode fazer isso via JavaScript com element.setAttribute('tabindex',0)

Era para isso que eu o estava usando, para criar um menu 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>
        );
    }
});

10
Isso não criaria problemas de acessibilidade? Como você torna um elemento extra focável apenas para detectar quando está entrando / saindo do foco, mas a interação deve estar nos valores suspensos, não?
Thijs Koerselman

2
Esta é uma abordagem muito interessante. Funcionou perfeitamente para minha situação, mas eu estaria interessado em saber como isso pode afetar a acessibilidade.
Klinore # 1/16

17
Eu tenho esse "trabalho" até certo ponto, é um pequeno truque legal. Meu problema é que meu conteúdo suspenso é display:absolutepara que a lista suspensa não afete o tamanho da div principal. Isso significa que, quando clico em um item no menu suspenso, o onblur é acionado.
Dave

2
Eu tive problemas com essa abordagem, qualquer elemento no conteúdo suspenso não dispara eventos como o onClick.
AnimaSola

8
Você pode enfrentar outros problemas se o conteúdo da lista suspensa contiver alguns elementos focáveis, como entradas de formulário, por exemplo. Eles roubam seu foco, onBlur serão acionados no contêiner suspenso e expanded serão configurados como false.
Kamagatos

106

Eu estava preso no mesmo problema. Estou um pouco atrasado para a festa aqui, mas para mim esta é uma solução muito boa. Espero que ajude outra pessoa. Você precisa importar findDOMNodedereact-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
        });
    }
}

Abordagem de reagir ganchos (16,8 +)

Você pode criar um gancho reutilizável chamado 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 };
}

Em seguida, no componente que você deseja adicionar a funcionalidade, faça o seguinte:

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

}

Encontre um exemplo de codesandbox aqui.


11
@LeeHanKyeol Não totalmente - esta resposta chama os manipuladores de eventos durante a fase de captura da manipulação de eventos, enquanto a resposta vinculada a chama os manipuladores de eventos durante a fase de bolha.
stevejay

3
Essa deve ser a resposta aceita. Funcionou perfeitamente para menus suspensos com posicionamento absoluto.
dishwasherWithProgrammingSkill

11
ReactDOM.findDOMNodee está obsoleto, deve usar refretornos de chamada: github.com/yannickcr/eslint-plugin-react/issues/…
dain

2
grande e solução limpa
Karim

11
Isso funcionou para mim, embora eu tenha que hackear seu componente para redefinir o visível de volta para 'true' após 200ms (caso contrário, meu menu nunca será exibido novamente). Obrigado!
Lee Moe

87

Depois de experimentar muitos métodos aqui, decidi usar o github.com/Pomax/react-onclickoutside por causa de sua completude.

Instalei o módulo via npm e o importei para o meu componente:

import onClickOutside from 'react-onclickoutside'

Então, na minha classe de componente, defini o handleClickOutsidemétodo:

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

E ao exportar meu componente, envolvi-o em onClickOutside():

export default onClickOutside(NameOfComponent)

É isso aí.


2
Há vantagens concretas nisso em relação à abordagem tabIndex/ onBlurproposta por Pablo ? Como a implementação funciona e como seu comportamento difere da onBlurabordagem?
Mark Amery

4
É melhor usar um componente dedicado, em seguida, usando um hack tabIndex. Voto positivo!
Atul Yadav

5
@ MarkAmery - coloquei um comentário sobre a tabIndex/onBlurabordagem. Não funciona quando o menu suspenso estiver position:absolute, como um menu pairando sobre outro conteúdo.
Beau Smith

4
Outra vantagem de usar este tabindex mais é que a solução tabindex também disparar um evento de borrão se você se concentrar em um elemento filho
Jemar Jones

11
@BeauSmith - Muito obrigado! Salvou o meu dia!
Mika

38

Encontrei uma solução graças a Ben Alpert em discuss.reactjs.org . A abordagem sugerida anexa um manipulador ao documento, mas isso acabou sendo problemático. Clicar em um dos componentes da minha árvore resultou em um novo renderizador, que removeu o elemento clicado na atualização. Como o renderizador do React acontece antes da chamada do manipulador do corpo do documento, o elemento não foi detectado como "dentro" da árvore.

A solução para isso foi adicionar o manipulador no elemento raiz do aplicativo.

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

8
Isso não funciona mais para mim com o React 0.14.7 - talvez o React tenha alterado algo ou talvez tenha cometido um erro ao adaptar o código a todas as alterações no React. Estou usando o github.com/Pomax/react-onclickoutside, que funciona como um encanto.
5196 Nicole

11
Hmm. Não vejo nenhuma razão para que isso funcione. Por que um manipulador no nó DOM raiz do aplicativo deve ser garantido para disparar antes que um novo renderizador seja acionado por outro manipulador, se um deles documentnão estiver?
Mark Amery

11
Um ponto UX puro: mousedownprovavelmente seria um manipulador melhor do que clickaqui. Na maioria dos aplicativos, o comportamento de fechar o menu clicando-fora ocorre no momento em que você passa o mouse, não quando você o libera. Experimente, por exemplo, com o sinalizador ou compartilhamento do Stack Overflow diálogos ou com um dos menus suspensos da barra de menus superior do navegador.
Mark Amery

ReactDOM.findDOMNodee string refestão obsoletos, deve usar refretornos de chamada: github.com/yannickcr/eslint-plugin-react/issues/…
dain

esta é a solução mais simples e funciona perfeitamente para mim, mesmo quando colocar àdocument
Flion

37

Implementação de hook baseada na excelente palestra de Tanner Linsley no 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> 
};

Implementação

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

useOuterClickfaz uso de referências mutáveis para criar uma API lean para o Client. É possível definir um retorno de chamada sem precisar memorizá-lo via useCallback. O corpo de retorno de chamada ainda tem acesso aos adereços e estado mais recentes (sem valores de fechamento obsoletos).


Nota para usuários do iOS

O iOS trata apenas determinados elementos como clicáveis. Para contornar esse comportamento, escolha um ouvinte de clique externo diferente de document- nada mais, inclusive body. Por exemplo, você pode adicionar um ouvinte na raiz do React divno exemplo acima e expandir sua altura ( height: 100vhou similar) para capturar todos os cliques externos. Fontes: quirksmode.org e esta resposta


não funciona no celular assim unforuntate
Omar

O @Omar acabou de testar o Codesandbox no meu post com o Firefox 67.0.3 em um dispositivo Android - funciona bem. Você poderia elaborar qual navegador usa, qual é o erro, etc.?
ford04

Sem erros, mas não funciona no chrome, iphone 7+. Não detecta as torneiras do lado de fora. Nas ferramentas de desenvolvimento do Chrome para dispositivos móveis, ele funciona, mas em um dispositivo ios real não está funcionando.
Omar

11
@ ford04 obrigado por desenterrar isso, deparei-me com outro tópico que sugeria o seguinte truque. @media (hover: none) and (pointer: coarse) { body { cursor:pointer } }Adicionando isso em meus estilos globais, parece que resolveu o problema.
Kostas Sarantopoulos

11
@ ford04 sim, btw de acordo com este tópico, existe uma consulta de mídia ainda mais específica @supports (-webkit-overflow-scrolling: touch)que visa apenas o IOS, embora eu não saiba mais quanto! Acabei de testar e funciona.
Kostas Sarantopoulos

30

Nenhuma das outras respostas aqui funcionou para mim. Eu estava tentando ocultar um pop-up no desfoque, mas como o conteúdo estava absolutamente posicionado, o onBlur estava disparando mesmo com o clique do conteúdo interno.

Aqui está uma abordagem que funcionou para mim:

// 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 isto ajude.


Muito bom! Obrigado. Mas no meu caso, foi um problema capturar "local atual" com posição absoluta para a div, então eu uso "OnMouseLeave" para div com entrada e calendário suspenso para desativar apenas todas as div quando o mouse sai da div.
Alex

11
Obrigado! Me ajude bastante.
Ângelo Lucas

11
Eu gosto disso melhor do que os métodos associados document. Obrigado.
KYW

Para que isso funcione, você precisa garantir que o elemento clicado (relatedTarget) seja focalizável. Veja stackoverflow.com/questions/42764494/…
xabitrigo

21

[Update] Solução com 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;

Solução com 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
Esta é a solução preferida agora. Se você está recebendo o erro .contains is not a function, pode ser porque você está passando o prop ref para um componente personalizado em vez de um elemento DOM real como um<div>
willlma

Para aqueles que tentam passar refprop a um componente personalizado, você pode querer ter um olhar paraReact.forwardRef
Onoya

4

Aqui está minha abordagem (demo - https://jsfiddle.net/agymay93/4/ ):

Eu criei um componente especial chamado WatchClickOutsidee pode ser usado como (eu assumo JSXsintaxe):

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

Aqui está o código do 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>
    );
  }
}

Ótima solução - para meu uso, mudei para escutar documento em vez de corpo, no caso de páginas com conteúdo curto, e mudei ref de string para dom: jsfiddle.net/agymay93/9
Billy Moon

e span usado em vez de div como é menos provável que a disposição da mudança, e demonstração adicional de manipulação de cliques fora do corpo: jsfiddle.net/agymay93/10
Billy Lua

Corda refestá obsoleta, deve usar refretornos de chamada: reactjs.org/docs/refs-and-the-dom.html
Dain

4

Isso já tem muitas respostas, mas elas não abordam e.stopPropagation() e impedem o clique nos links de reação fora do elemento que você deseja fechar.

Devido ao fato de o React possuir seu próprio manipulador de eventos artificiais, você não pode usar o documento como base para os ouvintes de eventos. Você precisa e.stopPropagation()antes disso, pois o React usa o próprio documento. Se você usar, por exemplo, em document.querySelector('body')vez disso. Você pode impedir o clique no link React. A seguir, é apresentado um exemplo de como eu implemento o clique fora e o fecho.
Isso usa 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

Para aqueles que precisam de posicionamento absoluto, uma opção simples que eu optei é adicionar um componente de invólucro com estilo para cobrir a página inteira com um plano de fundo transparente. Em seguida, você pode adicionar um onClick nesse elemento para fechar seu componente interno.

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

Como agora, se você adicionar um manipulador de cliques no conteúdo, o evento também será propagado para a div superior e, portanto, acionará o manipuladorOutsideClick. Se esse não é o seu comportamento desejado, basta interromper o andamento do evento no seu manipulador.

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

`


O problema dessa abordagem é que você não pode ter nenhum clique no conteúdo - a div receberá o clique.
rbonick

Como o conteúdo está na div, se você não parar a propagação do evento, os dois receberão o clique. Geralmente, esse é um comportamento desejado, mas se você não deseja que o clique seja propagado para a div, basta parar a eventPropagation no seu conteúdo no manipulador de cliques. Eu atualizei minha resposta para mostrar como.
Anthony Garant

3

Para estender a resposta aceita de Ben Bud , se você estiver usando componentes estilizados, passar refs dessa maneira resultará em um erro como "this.wrapperRef.contains não é uma função".

A correção sugerida, nos comentários, para envolver o componente estilizado com uma div e passar a referência para lá, funciona. Dito isto, em seus documentos eles já explicam a razão disso e o uso adequado de referências dentro de componentes denominados:

Passar um objeto ref para um componente estilizado fornecerá uma instância do wrapper StyledComponent, mas não para o nó DOM subjacente. Isto é devido a como os árbitros funcionam. Não é possível chamar métodos DOM, como foco, diretamente em nossos wrappers. Para obter uma ref para o nó DOM real, agrupado, passe o retorno de chamada para o prop innerRef.

Igual a:

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

Então você pode acessá-lo diretamente na função "handleClickOutside":

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

Isso também se aplica à abordagem "onBlur":

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


2

Minha maior preocupação com todas as outras respostas é ter que filtrar eventos de clique da raiz / pai para baixo. Achei que a maneira mais fácil era simplesmente definir um elemento irmão com a posição: fixed, um z-index 1 atrás da lista suspensa e manipular o evento click no elemento fixo dentro do mesmo componente. Mantém tudo centralizado em um determinado componente.

Código de exemplo

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

11
Hummm bacana. Funcionaria com várias instâncias? Ou cada elemento fixo poderia bloquear o clique dos outros?
Thijs Koerselman

2
componentWillMount(){

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

handleClickOutside(event) {

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

Evento path[0]é o último item clicado


3
Certifique-se de remover o ouvinte de eventos quando as desmontagens de componentes para evitar problemas de gerenciamento de memória, etc.
agm1984

2

Eu usei este módulo (não tenho associação com o 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>
    );
  }
}

Ele fez o trabalho muito bem.


Isso funciona de maneira fantástica! Muito mais simples do que muitas outras respostas aqui e, ao contrário de pelo menos três outras soluções aqui, onde para todas elas eu recebi "Support for the experimental syntax 'classProperties' isn't currently enabled"isso funcionou diretamente da caixa. Para quem estiver tentando esta solução, lembre-se de apagar qualquer código remanescente que você possa ter copiado ao tentar as outras soluções. por exemplo, adicionar ouvintes de eventos parece entrar em conflito com isso.
Frikster 18/10/19

2

Eu fiz isso em parte, seguindo isto e seguindo os documentos oficiais do React sobre o manuseio de refs, o que requer react ^ 16.3. Esta é a única coisa que funcionou para mim depois de tentar algumas das outras sugestões aqui ...

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

Um exemplo com estratégia

Gosto das soluções fornecidas que fazem a mesma coisa criando um wrapper em torno do componente.

Como esse é mais um comportamento, pensei em Estratégia e criei o seguinte.

Eu sou novo no React e preciso de um pouco de ajuda para salvar alguns clichês nos casos de uso

Por favor, revise e me diga o que acha.

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 da amostra

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

Pensei em armazenar os componentganchos do ciclo de vida passados e substituí-los por métodos semelhantes a isso:

const baseDidMount = component.componentDidMount;

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

componenté o componente passado ao construtor de ClickOutsideBehavior.
Isso removerá o clichê de habilitação / desabilitação do usuário desse comportamento, mas não parece muito bom


1

No meu caso DROPDOWN, a solução do Ben Bud funcionou bem, mas eu tinha um botão de alternância separado com um manipulador onClick. Portanto, a lógica do clique externo entra em conflito com o botão onClick toggler. Aqui está como eu o resolvi passando a referência do botão também:

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 você deseja usar um componente minúsculo (466 Byte gzipped) que já existe para essa funcionalidade, consulte esta biblioteca react-outclick .

O bom da biblioteca é que ela também permite detectar cliques fora de um componente e dentro de outro. Ele também suporta a detecção de outros tipos de eventos.


0

Se você deseja usar um pequeno componente (466 Byte gzipped) que já existe para essa funcionalidade, consulte esta biblioteca react-outclick . Ele captura eventos fora de um componente de reação ou elemento jsx.

O bom da biblioteca é que ela também permite detectar cliques fora de um componente e dentro de outro. Ele também suporta a detecção de outros tipos de eventos.


0

Sei que essa é uma pergunta antiga, mas continuo me deparando com isso e tive muitos problemas para descobrir isso em um formato simples. Portanto, se isso facilitar a vida de alguém, use o OutsideClickHandler da airbnb. É o plug-in mais simples para realizar essa tarefa sem escrever seu próprio código.

Exemplo:

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

você pode uma maneira simples de resolver seu problema, eu mostro a você:

....

const [dropDwonStatus , setDropDownStatus] = useState(false)

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

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

isso funciona para mim, boa sorte;)


-1

Encontrei isso no artigo abaixo:

render () {return ({this.node = node;}}> alternar popover {this.state.popupVisible && (eu sou popover!)}); }}

Aqui está um ótimo artigo sobre esse problema: "Manipular cliques fora dos componentes do React" https://larsgraubner.com/handle-outside-clicks-react/


Bem-vindo ao Stack Overflow! As respostas que são apenas um link geralmente são desaprovadas: meta.stackoverflow.com/questions/251006/… . No futuro, inclua uma citação ou exemplo de código com as informações relevantes para a pergunta. Felicidades!
Fred Stark

-1

Adicione um onClickmanipulador ao seu contêiner de nível superior e aumente um valor de estado sempre que o usuário clicar nele. Passe esse valor para o componente relevante e sempre que o valor mudar, você poderá trabalhar.

Nesse caso, estamos ligando this.closeDropdown()sempre que o clickCountvalor muda.

O incrementClickCountmétodo é acionado dentro do .appcontêiner, mas não .dropdownporque usamos event.stopPropagation()para impedir que o evento borbulhe.

Seu código pode acabar parecido com isto:

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

Não tenho certeza de quem está com voto negativo, mas esta solução é bastante limpa em comparação com os ouvintes de eventos de ligação / desassociação. Eu tive zero problemas com esta implementação.
wdm 27/01

-1

Para que a solução 'focus' funcione para o menu suspenso com ouvintes de eventos, você pode adicioná-los com o evento onMouseDown em vez de onClick . Dessa forma, o evento será acionado e depois o pop-up será fechado da seguinte maneira:

<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 não se esqueça deimport ReactDOM from 'react-dom';
dipole_moment

-1

Eu fiz uma solução para todas as ocasiões.

Você deve usar um componente de alta ordem para agrupar o componente que deseja ouvir cliques fora dele.

Este exemplo de componente possui apenas um suporte: "onClickedOutside" que recebe uma função.

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

UseOnClickOutside Hook - Reagir 16.8 +

Criar uma função useOnOutsideClick geral

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

Em seguida, use o gancho em qualquer 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>
  );

};

Link para demonstração

Parcialmente inspirado pela resposta @ pau1fitzgerald.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.