Обнаружение щелчка снаружи компонента React

Я ищу способ определить, произошло ли событие клика вне компонента, как описано в этой статье . jQuery closeest() используется, чтобы увидеть, имеет ли цель из события click элемент dom как один из его родителей. Если есть совпадение, событие click принадлежит одному из дочерних элементов и, следовательно, не считается вне его.

Итак, в моем компоненте я хочу привязать обработчик кликов к окну. Когда срабатывает обработчик, мне нужно сравнить цель с дочерними элементами моего компонента.

Событие клика содержит свойства, такие как "путь", который, как представляется, содержит путь dom, по которому прошло событие. Я не уверен, что сравнивать или как лучше всего пройти его, и я думаю, что кто-то, должно быть, уже положил это на умную функцию полезности... Нет?

Ответ 1

Следующее решение использует ES6 и следует рекомендациям для привязки, а также для установки ссылки с помощью метода.

Чтобы увидеть это в действии:

Реализация класса:

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

OutsideAlerter.propTypes = {
  children: PropTypes.element.isRequired,
};

Реализация крючков:

import React, { useRef, useEffect } from "react";

/**
 * Hook that alerts clicks outside of the passed ref
 */
function useOutsideAlerter(ref) {
  /**
   * Alert if clicked on outside of element
   */
  function handleClickOutside(event) {
    if (ref.current && !ref.current.contains(event.target)) {
      alert("You clicked outside of me!");
    }
  }

  useEffect(() => {
    // Bind the event listener
    document.addEventListener("mousedown", handleClickOutside);
    return () => {
      // Unbind the event listener on clean up
      document.removeEventListener("mousedown", handleClickOutside);
    };
  });
}

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

Ответ 2

Вот решение, которое лучше всего сработало для меня без добавления событий в контейнер:

Некоторые элементы HTML могут иметь то, что известно как " фокус", например элементы ввода. Эти элементы также будут реагировать на событие размытия, когда они потеряют этот фокус.

Чтобы дать любому элементу возможность сосредоточиться, просто убедитесь, что для его атрибута tabindex установлено значение, отличное от -1. В обычном HTML, который был бы, установив атрибут tabindex, но в React вы должны использовать tabIndex (обратите внимание на капитал I).

Вы также можете сделать это с помощью JavaScript с помощью element.setAttribute('tabindex',0)

Это то, для чего я его использовал, для создания пользовательского меню DropDown.

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

Ответ 3

Попробовав много методов здесь, я решил использовать github.com/Pomax/react-onclickoutside из-за того, насколько он завершен.

Я установил модуль через npm и импортировал его в свой компонент:

import onClickOutside from 'react-onclickoutside'

Затем в моем классе компонента я определил метод handleClickOutside:

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

И при экспорте моего компонента я завернул его в onClickOutside():

export default onClickOutside(NameOfComponent)

Что это.

Ответ 4

Я застрял на том же вопросе. Я немного опоздал на вечеринку, но для меня это действительно хорошее решение. Надеюсь, это поможет кому-то еще. Вам нужно импортировать findDOMNode из react-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
        });
    }
}

Подход Реакции Крюков (16.8 +)

Вы можете создать многоразовый хук с именем 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 };
}

Затем в компонент вы хотите добавить функциональность, чтобы сделать следующее:

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

}

Найдите пример кода и коробки здесь.

Ответ 5

Я нашел решение благодаря Бен Альперту на discuss.reactjs.org. Предлагаемый подход придает обработчик документа, но это оказалось проблематичным. Нажав на один из компонентов в моем дереве, вы получили редердер, который удалил элемент щелчка при обновлении. Поскольку reerender из React происходит до вызова обработчика тела документа, элемент не был обнаружен как "внутри" дерева.

Решением этого было добавить обработчик в корневой элемент приложения.

главная:

window.__myapp_container = document.getElementById('app')
React.render(<App/>, window.__myapp_container)

компонент:

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

Ответ 6

Ни один из других ответов здесь не работал. Я пытался скрыть всплывающее окно, но поскольку содержимое было абсолютно позиционировано, onBlur стрелял даже по щелчку внутреннего содержимого.

Вот такой подход, который работал у меня:

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

Надеюсь, что это поможет.

Ответ 7

Многоразовое решение с React Hooks (16,8 +)

Codesandbox

Создать внешний щелчок для уведомления Hook:

function useOuterClickNotifier(onOuterClick, innerRef) {
  useEffect(
    () => {
      // only add listener, if the element exists
      if (innerRef.current) {
        document.addEventListener("click", handleClick);
      }

      // unmount previous first in case inputs have changed
      return () => document.removeEventListener("click", handleClick);

      function handleClick(e) {
        innerRef.current && !innerRef.current.contains(e.target) && onOuterClick(e);
      }
    },
    [onOuterClick, innerRef] // invoke again, if inputs have changed
  );
}

Используйте Хук в любом компоненте, подобном следующему:

const InnerComp = () => {
  const innerRef = useRef(null);
  useOuterClickNotifier(
    // if you want to optimize performance a bit,
    // don't provide an anonymous function here
    // See link down under (*1)
    e => alert("clicked outside of this component!"),
    innerRef
  );
  return (
    <div ref={innerRef}>
      inside component
    </div>
  );
}

* 1 Оптимизация производительности за счет пропуска эффектов

Затем, в зависимости от вашего варианта использования, вы можете сделать что-нибудь во внешнем обратном вызове клика, который здесь для простоты обозначен alert("clicked outside of this component!"). Например. установить некоторое состояние с помощью useState Hook или вызвать данный обратный вызов.


Преимущества перед классовым решением:

  • Внешнюю логику побочных эффектов можно заключить в капсулу (разделение интересов) и упростить использование компонента.
  • useOuterClickNotifier Хук можно повторно использовать во всех компонентах без необходимости использования компонента-обертки/реквизита рендеринга, как в решении для класса (Ссылка).
  • В долгосрочной перспективе команда React ожидает, что хуки будут основным способом написания компонентов React (Ссылка).

Недостатки:

  • Вы не можете использовать useOuterClickNotifier внутри других компонентов класса (ссылка), см. также здесь.

Дополнительная информация: причуды IOS с интерактивными элементами (влияет на mousedown, click)

IOS рассматривает только определенные элементы как кликабельные - больше информации об этом в quirksmode, SO answer и здесь. Чтобы обойти это поведение, выберите другой элемент прослушивания внешнего щелчка (ничего выше, включая body). Например. вместо этого вы можете зарегистрировать щелчки для реагирующего корня <div id="root"></div> и увеличить его высоту до всего окна просмотра (посмотрите коды IOS). Или даже лучше и лучше реагировать: полностью избегать глобальных переменных и передавать явный элемент в useOuterClickNotifier Hook, который можно использовать для регистрации внешних кликов.

Спасибо @Omar за подсказку и @Kostas Sarantopoulos за дополнительные альтернативы с медиа-запросами CSS (см. комментарии).

Надеюсь, это поможет.

Ответ 8

[Обновление] Решение с React ^ 16.8 с использованием хуков

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;

Решение с 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;

Ответ 9

Вот мой подход (демо - https://jsfiddle.net/agymay93/4/):

Я создал специальный компонент под названием WatchClickOutside и его можно использовать следующим образом (я предполагаю синтаксис JSX):

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

Вот код компонента WatchClickOutside:

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

Ответ 10

На это уже есть много ответов, но они не обращаются к e.stopPropagation() и не позволяют щелкать по ссылкам реакции вне элемента, который вы хотите закрыть.

Из-за того, что в React имеется собственный искусственный обработчик событий, вы не можете использовать документ в качестве базы для прослушивателей событий. Перед этим вам нужно использовать e.stopPropagation() поскольку React использует сам документ. Если вы используете, например, document.querySelector('body') вместо этого. Вы можете предотвратить переход по ссылке Реакт. Ниже приведен пример того, как я реализую щелчок снаружи и близко.
Это использует ES6 и 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;

Ответ 11

Чтобы расширить принятый ответ, сделанный Беном Бадом, если вы используете styled-компоненты, передача ссылок таким образом приведет к ошибке, такой как "this.wrapperRef.contains не является функцией".

Предлагаемое исправление, в комментариях, чтобы обернуть стилизованный компонент с помощью div и передать туда ссылку, работает. Сказав это, в своих документах они уже объясняют причину этого и правильное использование ссылок внутри styled-components:

Передача ref prop к стилевому компоненту даст вам экземпляр оболочки StyledComponent, но не к базовому узлу DOM. Это связано с тем, как работают рефери. Невозможно напрямую вызывать методы DOM, такие как focus, для наших оболочек. Чтобы получить ссылку на фактический, обернутый DOM-узел, вместо этого передайте обратный вызов в службу innerRef.

Вот так:

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

Затем вы можете получить к нему доступ непосредственно в функции handleClickOutside:

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

Это также относится к подходу "onBlur":

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

Ответ 12

Для тех, кто нуждается в абсолютном позиционировании, простым вариантом, который я выбрал, является добавление компонента-оболочки, который предназначен для покрытия всей страницы прозрачным фоном. Затем вы можете добавить onClick в этот элемент, чтобы закрыть свой внутренний компонент.

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

Как сейчас, если вы добавите обработчик кликов по контенту, событие также будет распространено на верхний div и, следовательно, будет запускать обработчикOutsideClick. Если это не ваше желаемое поведение, просто остановите прогенцию событий на вашем обработчике.

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

`

Ответ 13

Мое самое большое беспокойство во всех остальных ответах - это отфильтровать события щелчка от корня/родителя вниз. Я нашел, что самый простой способ - просто установить элемент sibling с позицией: fixed, z-index 1 за выпадающим списком и обработать событие click на фиксированном элементе внутри одного и того же компонента. Все централизовано для данного компонента.

Пример кода

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

Ответ 14

componentWillMount(){

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

handleClickOutside(event) {

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

Событие path[0] - последний элемент, нажатый

Ответ 15

Я использовал этот модуль (я не связан с автором)

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

Ответ 16

Я сделал это частично, следуя этому и следуя официальным документам React по обработке ссылок, которые требуют реакции ^ 16.3. Это единственное, что сработало для меня, попробовав некоторые другие предложения здесь...

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 outside***...
<span ref={this.inputRef}>
...***code for what "inside"***...
</span>
...***code for what outside***
)}}

Ответ 17

Пример со стратегией

Мне нравятся предоставленные решения, которые используют одно и то же, создавая оболочку вокруг компонента.

Поскольку это больше похоже на поведение, которое я думал о стратегии, и придумал следующее.

Я новичок в React, и мне нужна помощь, чтобы сохранить какой-либо шаблон в используемых случаях.

Пожалуйста, просмотрите и скажите мне, что вы думаете.

ClickOutsideBehavior

import ReactDOM from 'react-dom';

export default class ClickOutsideBehavior {

  constructor({component, appContainer, onClickOutside}) {

    // Can I extend the passed component 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)
    }
  }
}

Использование образца

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

Примечания

Я подумал о сохранении пройденных перехватчиков жизненного цикла component и переопределить их с помощью методов, аналогичных этому:

const baseDidMount = component.componentDidMount;

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

component - это компонент, переданный конструктору ClickOutsideBehavior.
Это приведет к удалению шаблона enable/disable от пользователя этого поведения, но это выглядит не очень хорошо, хотя

Ответ 18

Я нашел это из статьи ниже:

render() {return ({this.node = node;}}> Переключить Popover {this.state.popupVisible && (Я поповер!)}); }}

Вот отличная статья об этой проблеме: "Обрабатывать клики за пределами компонентов React" https://larsgraubner.com/handle-outside-clicks-react/

Ответ 19

Добавьте обработчик onClick в контейнер верхнего уровня и увеличивайте значение состояния всякий раз, когда пользователь щелкает внутри. Передайте это значение соответствующему компоненту, и всякий раз, когда значение изменяется, вы можете выполнять работу.

В этом случае мы вызываем this.closeDropdown() всякий раз, когда clickCount значение clickCount.

Метод incrementClickCount запускается в контейнере .app но не в .dropdown потому что мы используем event.stopPropagation() для предотвращения появления событий.

Ваш код может выглядеть примерно так:

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

Ответ 20

Чтобы заставить решение "focus" работать с раскрывающимися списками прослушивателей событий, вы можете добавить их с событием onMouseDown вместо onClick. Таким образом, событие сработает, и после этого всплывающее окно закроется так:

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

Ответ 21

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

Ответ 22

Я принял решение на все случаи жизни.

Вы должны использовать Компонент Высокого Порядка, чтобы обернуть компонент, который вы хотели бы прослушивать для кликов за его пределами.

В этом примере компонента есть только одна опора: "onClickedOutside", которая получает функцию.

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

Ответ 23

UseOnClickOutside Hook - Реакция 16.8 +

Создать общую функцию 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, []);

Тогда используйте крючок в любом функциональном компоненте.

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

};

Ссылка на демо

Частично вдохновлен ответом @pau1fitzgerald.

Ответ 24

Ни один из приведенных выше ответов не помог мне, поэтому вот что я сделал в итоге:

импортировать React, {Component} из'act ';

/**
 * Component that alerts if you click outside of it
 */
export default class OutsideAlerter extends Component {
  constructor(props) {
    super(props);

    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 (!event.path || !event.path.filter(item => item.className=='classOfAComponent').length) {
      alert('You clicked outside of me!');
    }
  }

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

OutsideAlerter.propTypes = {
  children: PropTypes.element.isRequired,
};

Ответ 25

Вы можете просто установить обработчик двойного щелчка на теле, а другой - на этот элемент. В обработчике этого элемента просто верните false, чтобы предотвратить распространение события. Поэтому, когда двойной щелчок происходит, если он находится на элементе, он будет пойман и не будет распространяться на обработчик на теле. В противном случае он будет пойман обработчиком на теле.

Обновление: если вы действительно не хотите предотвращать распространение событий, вам просто нужно использовать ближайший, чтобы проверить, произошел ли щелчок по вашему элементу или одному из его детей:

<html>
<head>
<script src="https://code.jquery.com/jquery-2.1.4.min.js"></script>
<script>
$(document).on('click', function(event) {
    if (!$(event.target).closest('#div3').length) {
    alert("outside");
    }
});
</script>
</head>
<body>
    <div style="background-color:blue;width:100px;height:100px;" id="div1"></div>
    <div style="background-color:red;width:100px;height:100px;" id="div2"></div>
    <div style="background-color:green;width:100px;height:100px;" id="div3"></div>
    <div style="background-color:yellow;width:100px;height:100px;" id="div4"></div>
    <div style="background-color:grey;width:100px;height:100px;" id="div5"></div>
</body>
</html>

Обновление: без jQuery:

<html>
<head>
<script>
function findClosest (element, fn) {
  if (!element) return undefined;
  return fn(element) ? element : findClosest(element.parentElement, fn);
}
document.addEventListener("click", function(event) {
    var target = findClosest(event.target, function(el) {
        return el.id == 'div3'
    });
    if (!target) {
        alert("outside");
    }
}, false);
</script>
</head>
<body>
    <div style="background-color:blue;width:100px;height:100px;" id="div1"></div>
    <div style="background-color:red;width:100px;height:100px;" id="div2"></div>
    <div style="background-color:green;width:100px;height:100px;" id="div3">
        <div style="background-color:pink;width:50px;height:50px;" id="div6"></div>
    </div>
    <div style="background-color:yellow;width:100px;height:100px;" id="div4"></div>
    <div style="background-color:grey;width:100px;height:100px;" id="div5"></div>
</body>
</html>