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

230

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

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

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

  • 0
    Не могли бы вы прикрепить обработчик кликов к родителю, а не к окну?
  • 0
    Если вы прикрепляете обработчик кликов к родительскому элементу, то знаете, что этот элемент или один из его дочерних элементов щелкаются, но мне нужно обнаружить все другие места, по которым щелкают, поэтому обработчик должен быть присоединен к окну.
Показать ещё 3 комментария
Теги:
dom

22 ответа

365
Лучший ответ

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

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

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,
};
  • 2
    Как вы это проверяете? Как отправить действительный event.target в функцию handleClickOutside?
  • 2
    В вашем тесте вы могли визуализировать <OutsideAlerter> рядом с <div>. Смоделируйте щелчок мышью на <div> и прослушайте вызываемый метод оповещения, используя что-то вроде шутки или шпионов-синонов.
Показать ещё 30 комментариев
117

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

Некоторые элементы 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>
        );
    }
});
  • 8
    Не создаст ли это проблемы с доступностью? Поскольку вы делаете дополнительный элемент фокусируемым, просто чтобы определить, когда он входит / выходит из фокуса, но взаимодействие должно быть на выпадающих значениях нет?
  • 0
    Доступность не была проблемой этого конкретного виджета. Я не эксперт по этому вопросу, но я считаю, что любые проблемы доступности можно было бы решить с помощью более сложной версии этого подхода. Спасибо за указание на это.
Показать ещё 12 комментариев
82

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

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

import onClickOutside from 'react-onclickoutside'

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

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

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

export default onClickOutside(NameOfComponent)

Что это.

  • 6
    Приятно видеть, что для этого есть твердый компонент. Имеет смысл использовать это сейчас вместо ручного подхода.
  • 2
    Есть ли в этом какие-то конкретные преимущества по сравнению с подходом tabIndex / onBlur предложенным Пабло ? Как работает реализация и как ее поведение отличается от подхода onBlur ?
Показать ещё 6 комментариев
53

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

}

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

  • 0
    Отличное решение, но технически это дубликат stackoverflow.com/a/42234988/4286919
  • 1
    @LeeHanKyeol Не совсем - этот ответ вызывает обработчики событий во время фазы захвата обработки событий, тогда как ответ, связанный с, вызывает обработчики событий во время пузырьковой фазы.
Показать ещё 5 комментариев
37

Я нашел решение благодаря Бен Альперту на 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>
    )
  }
}
  • 8
    Это больше не работает для меня с React 0.14.7 - возможно, React что-то изменил, или я допустил ошибку при адаптации кода ко всем изменениям в React. Вместо этого я использую github.com/Pomax/react-onclickoutside, который работает как шарм.
  • 1
    Хм. Я не вижу причин, почему это должно работать. Почему обработчик на корневом DOM-узле приложения должен гарантированно сработать перед повторной обработкой, инициированной другим обработчиком, если таковой в document нет?
Показать ещё 3 комментария
25

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

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

  • 0
    Отлично! Благодарю. Но в моем случае было проблемой отловить «текущее место» с абсолютной позицией для div, поэтому я использую «OnMouseLeave» для div с вводом и выпадающим календарем, чтобы просто отключить все div, когда мышь покидает div.
  • 1
    Спасибо! Помоги мне очень.
Показать ещё 1 комментарий
11

[Обновление] Решение с 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;
  • 3
    Это самое подходящее решение сейчас. Если вы получаете сообщение об ошибке .contains is not a function , это может быть связано с тем, что вы передаете ref prop пользовательскому компоненту, а не реальному элементу DOM, например <div>
4

Вот мой подход (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>
    );
  }
}
  • 0
    Отличное решение - для моего использования я переключился на прослушивание документа вместо тела в случае страниц с коротким контентом и изменил ссылку со строки на ссылку на домен : jsfiddle.net/agymay93/9
  • 0
    и использовал span вместо div, так как менее вероятно, что он изменит макет, и добавил демонстрацию обработки кликов за пределами тела: jsfiddle.net/agymay93/10
Показать ещё 1 комментарий
2

Решение с крючками React (16,8 +)

Codesandbox:

"Внутренний" компонент

function OutsideClickComp() {
  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>
  );
}

Таможенный Крюк

function useOuterClickNotifier(onOuterClick, innerRef) {
  useLayoutEffect(
    () => {
      window.addEventListener("click", handleClick);
      // called for each previous layout effect
      return () => document.removeEventListener("click", handleClick);

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

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


Вот мое решение: мне пришлось реализовать какое-то всплывающее меню, которое закрывается автоматически при нажатии вне контейнера меню. Используя хуки, IMO дает следующие преимущества:

  • побочный эффект/логика регистрации внешних кликов может быть полностью удалена и упростит "внутренний" компонент
  • повторное использование useOuterClickNotifier везде
  • нет дополнительного компонента оболочки (заменяется логикой хука)

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

Отличия от других предлагаемых крюковых решений

Чтобы гарантировать, что прослушиватель внешнего клика события запускается после того, как React полностью обработал свои изменения и избежал проблем с параллелизмом (в моем случае useLayoutEffect), вместо useEffect и window.addEventListener используется window.addEventListener вместо document.addEventListener.

Фон: useEffect запускается асинхронно после фазы рендеринга, тогда как useLayoutEffect вызывается синхронно после всех мутаций DOM (поведение жизненного цикла useEffect отличается от componentDidMount и componentDidUpdate !). Что касается событий DOM, React регистрирует одного слушателя в document. Зарегистрировав наш пользовательский прослушиватель событий для внешних щелчков в window один уровень выше, этот слушатель гарантированно запустится после обработки реагирующего события в фазе пузырьков DOM. Таким образом, мы можем обеспечить следующую последовательность процессов:

react event handler -> react render -> outer click listener outside react scope

Смотрите также тайминги эффекта и useLayoutEffect.

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

2

На это уже есть много ответов, но они не обращаются к 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;
2

Чтобы расширить принятый ответ, сделанный Беном Бадом, если вы используете 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 }}
        />
    )
}
2

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

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

Это хорошо сработало.

  • 0
    Это работает фантастически! Гораздо проще, чем многие другие ответы здесь, и в отличие от, по крайней мере, трех других решений здесь, где для всех них я получил "Support for the experimental syntax 'classProperties' isn't currently enabled" это просто работало сразу ». Для любого, кто пробует это решение, не забудьте стереть какой-либо устаревший код, который вы, возможно, скопировали при попытке попробовать другие решения. например, добавление прослушивателей событий кажется конфликтующим с этим.
2
componentWillMount(){

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

handleClickOutside(event) {

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

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

  • 3
    Обязательно удалите прослушиватель событий при размонтировании компонента, чтобы предотвратить проблемы с управлением памятью и т. Д.
2

Для тех, кто нуждается в абсолютном позиционировании, простым вариантом, который я выбрал, является добавление компонента-оболочки, который предназначен для покрытия всей страницы прозрачным фоном. Затем вы можете добавить 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();
                                        }}/>

`

  • 0
    Проблема с этим подходом заключается в том, что у вас не может быть никаких кликов по Контенту - вместо этого div получит клик.
  • 0
    Поскольку содержимое находится в div, если вы не остановите распространение события, оба получат щелчок. Это часто желаемое поведение, но если вы не хотите, чтобы щелчок передавался в div, просто остановите eventPropagation в вашем обработчике контента onClick. Я обновил свой ответ, чтобы показать как.
1

Я сделал это частично, следуя этому и следуя официальным документам 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***
)}}
1

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

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

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

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

1

Мое самое большое беспокойство во всех остальных ответах - это отфильтровать события щелчка от корня/родителя вниз. Я нашел, что самый простой способ - просто установить элемент 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;
    }
}
  • 1
    Хм, отличный. Будет ли это работать с несколькими экземплярами? Или каждый фиксированный элемент потенциально может заблокировать щелчок для других?
0
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");
    }
  }
}
  • 0
    И не забывайте import ReactDOM from 'react-dom';
0

Чтобы заставить решение "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>
                }
0

Добавьте обработчик 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>
        );
    }
}
0

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

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

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

  • 0
    Добро пожаловать в стек переполнения! Ответы, которые являются просто ссылкой, обычно не одобряются: meta.stackoverflow.com/questions/251006/… . В будущем включите цитату или пример кода с информацией, относящейся к вопросу. Ура!
-8

Вы можете просто установить обработчик двойного щелчка на теле, а другой - на этот элемент. В обработчике этого элемента просто верните 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>
  • 0
    Да, это бы сработало, но предотвращение распространения в этом контексте неприемлемо. Прочитайте статью, которую я связал для получения дополнительной информации.
  • 0
    Является ли пример, который я добавил к своему ответу, тем, что вы ищете?
Показать ещё 3 комментария

Ещё вопросы

Сообщество Overcoder
Наверх
Меню