Обнаружение компонента Click outside React

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

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

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

20 ответов


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

чтобы увидеть его в действии: код песочницы демо

import React, { Component } from 'react';
import PropTypes from 'prop-types';

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

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

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

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

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

это то, для чего я его использовал, чтобы сделать пользовательское выпадающее меню.

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

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

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

import onClickOutside from 'react-onclickoutside'

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

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

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

export default onClickOutside(NameOfComponent)

вот именно.


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

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

main:

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

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

ни один из других ответов здесь работал для меня. Я пытался скрыть всплывающее окно на blur, но поскольку содержимое было абсолютно расположено, 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.
    }
},

надеюсь, что это помогает.


вот мое решение с реагировать 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;

вот мой подход (demo -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>
    );
  }
}

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

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

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

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

`


моя самая большая проблема со всеми другими ответами - фильтровать события щелчка от корня / родителя вниз. Я нашел самый простой способ-просто установить элемент sibling с position: 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;
    }
}

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

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

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

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

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

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

Пример Использования

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.
Это удалит шаблон включения/отключения от пользователя этого поведения, но это не выглядит очень хорошо, хотя


componentWillMount(){

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

handleClickOutside(event) {

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

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


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

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

Он хорошо справился с работой.


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

рендер() { возвращаться ( { этот.узел = Узел; }} > Тумблер Пирог {этот.государство.popupVisible && ( Я пирог! )} ); } }

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


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

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

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

ваш код может выглядеть что-то вроде этого:--9-->

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

Я сделал это частично по следующим этой и следуя официальным документам React по обработке ссылок, которые требуют 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's outside***...
<span ref={this.inputRef}>
...***code for what's "inside"***...
</span>
...***code for what's outside***
)}}

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

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

передача 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 }}
        />
    )
}

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

из-за того, что React имеет собственный искусственный обработчик событий, вы не можете использовать документ в качестве базы для прослушивателей событий. Вам нужно e.stopPropagation() перед этим as React использует сам документ. Если вы используете, например . Вы можете предотвратить щелчок по ссылке React. Ниже приведен пример того, как я реализуйте щелчок снаружи и закройте.
Это использует ЕС6 и реагировать 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;

сделать 'фокус' решение Работа для выпадающего списка с прослушивателями событий вы можете добавить их с 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>
                }

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

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

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