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


412

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

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

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


Не могли бы вы прикрепить обработчик кликов к родителю, а не к окну?
Дж. Марк Стивенс

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

Я посмотрел на статью после предыдущего ответа. Как насчет установки clickState в верхнем компоненте и передачи действий click от детей. Затем вы проверяли бы реквизиты у детей, чтобы управлять состоянием открытия и закрытия.
Дж. Марк Стивенс

Верхним компонентом будет мое приложение. Но компонент прослушивания имеет несколько уровней глубины и не имеет строгой позиции в доме. Я не могу добавить обработчики кликов ко всем компонентам в моем приложении только потому, что одному из них интересно знать, нажали ли вы где-нибудь за его пределами. Другие компоненты не должны знать об этой логике, потому что это создаст ужасные зависимости и шаблонный код.
Тийс Коерсельман

5
Я хотел бы порекомендовать вам очень хорошую библиотеку. Создано AirBnb: github.com/airbnb/react-outside-click-handler
Осоян Марсель

Ответы:


686

Использование ссылок в React 16.3+ изменено.

Следующее решение использует 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.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,
};

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

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
Как вы это проверяете? Как отправить действительный event.target в функцию handleClickOutside?
csilk

5
Как вы можете использовать синтетические события React вместо document.addEventListenerздесь?
Ральф Дэвид Абернати

10
@ polkovnikov.ph 1. contextнеобходим только в качестве аргумента в конструкторе, если он используется. Это не используется в этом случае. Причина, по которой группа реагирования рекомендует использовать propsв качестве аргумента конструктор, заключается в том, что использование this.propsперед вызовом super(props)будет неопределенным и может привести к ошибкам. contextвсе еще доступен на внутренних узлах, вот и вся цель context. Таким образом, вам не нужно передавать его от компонента к компоненту, как это происходит с реквизитом. 2. Это всего лишь стилистическое предпочтение, и в данном случае оно не требует обсуждения.
Бен Буд

7
Я получаю следующее сообщение об ошибке: «this.wrapperRef.contains не является функцией»
Богдан

5
@ Богдан, я получал ту же ошибку при использовании styled-компонента. Подумайте об использовании <div> на верхнем уровне
user3040068

151

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

Некоторые элементы 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>
        );
    }
});

10
Не создаст ли это проблемы с доступностью? Так как вы делаете дополнительный элемент фокусируемым, просто чтобы определить, когда он входит / выходит из фокуса, но взаимодействие должно быть на выпадающих значениях нет?
Тийс Коерсельман

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

17
У меня это "работает" до такой степени, это крутой маленький взломать. Моя проблема в том, что мой выпадающий контент display:absoluteтаков, что выпадающий не повлияет на размер родительского div. Это означает, что когда я щелкаю по элементу в раскрывающемся списке, срабатывает onblur.
Дэйв

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

8
Вы можете столкнуться с некоторыми другими проблемами, если выпадающий контент содержит некоторые фокусируемые элементы, такие как, например, ввод данных формы. Они украдут ваш фокус, onBlur сработают на вашем выпадающем контейнере и expanded будут установлены в false.
Камагатос

107

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

}

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


1
@LeeHanKyeol Не совсем - этот ответ вызывает обработчики событий во время фазы захвата обработки событий, тогда как ответ, связанный с, вызывает обработчики событий во время пузырьковой фазы.
Stevejay

3
Это должен быть принятый ответ. Идеально подходит для выпадающих меню с абсолютным позиционированием.
посудомоечная машина с программированиемSkill

1
ReactDOM.findDOMNodeи является устаревшим, следует использовать refобратные вызовы: github.com/yannickcr/eslint-plugin-react/issues/...
Дейн

2
отличное и чистое решение
Карим

1
Это сработало для меня, хотя мне пришлось взломать ваш компонент, чтобы сбросить видимый обратно на «истину» через 200 мс (в противном случае мое меню больше не отображается). Спасибо!
Ли Мо

87

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

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

import onClickOutside from 'react-onclickoutside'

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

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

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

export default onClickOutside(NameOfComponent)

Вот и все.


2
Есть ли какие-то конкретные преимущества по сравнению с tabIndex/ onBlurподходом, предложенным Пабло ? Как работает реализация и как ее поведение отличается от onBlurподхода?
Марк Амери

4
Лучше использовать выделенный компонент, чем использовать взломать tabIndex. Upvoting это!
Атул Ядав

5
@MarkAmery - я комментирую tabIndex/onBlurподход. Он не работает, когда выпадающий список position:absolute, например, меню над другим содержимым.
Бо Смит

4
Другое преимущество использования этого по сравнению с tabindex состоит в том, что решение tabindex также запускает событие размытия, если вы фокусируете внимание на дочернем элементе
Джемар Джонс,

1
@BeauSmith - Большое спасибо! Спас мой день!
Мика

39

Я нашел решение, благодаря Бену Альперту, на сайте обсуждения.reactjs.org . Предложенный подход прикрепляет обработчик к документу, но это оказалось проблематичным. Нажатие на один из компонентов в моем дереве привело к повторному отображению, при котором элемент обновления, по которому щелкнули, удалялся при обновлении. Поскольку повторное отображение из 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них не запущен ?
Марк Амери

1
Чистая точка UX: mousedownвероятно, будет лучшим обработчиком, чем clickздесь. В большинстве приложений поведение «закрытие меню за щелчком» происходит в тот момент, когда вы нажимаете кнопку мыши, а не при отпускании. Попробуйте, например, с помощью флагов Stack Overflow или общих диалоговых окон или с одним из выпадающих меню в верхней строке меню вашего браузера.
Марк Амери

ReactDOM.findDOMNodeи строка refустарели, следует использовать refобратные вызовы: github.com/yannickcr/eslint-plugin-react/issues/…
dain

это самое простое решение, и оно отлично работает для меня, даже когда я присоединяюсь кdocument
Flion

37

Реализация хуков на основе прекрасного выступления Таннера Линсли на JSConf Hawaii 2020 :

useOuterClick Hook API

const Client = () => {
  const innerRef = useOuterClick(ev => { /* what you want do on outer click */ });
  return <div ref={innerRef}> Inside </div> 
};

Реализация

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

useOuterClickиспользует изменяемые ссылки для создания скудного API для Client. Обратный вызов может быть установлен без необходимости запоминать его через useCallback. Тело обратного вызова по-прежнему имеет доступ к самым последним реквизитам и состоянию (без устаревших значений закрытия).


Примечание для пользователей iOS

iOS обрабатывает только определенные элементы как кликабельные. Чтобы обойти это поведение, выберите другого внешнего слушателя щелчка, чем document- ничего выше, включая body. Например, вы можете добавить слушателя в корень React divв приведенном выше примере и увеличить его высоту ( height: 100vhили аналогичную), чтобы ловить все внешние клики. Источники: quirksmode.org и этот ответ


не работает на мобильном телефоне, так что незаживай
Омар

@Omar только что протестировал Codesandbox в моем посте с Firefox 67.0.3 на устройстве Android - работает все отлично. Не могли бы вы уточнить, какой браузер вы используете, что за ошибка и т. Д.?
ford04

Нет ошибок, но не работает на Chrome, Iphone 7+. Он не обнаруживает краны снаружи. В Chrome Dev Tools на мобильном устройстве это работает, но на реальном устройстве IOS это не работает.
Омар

1
@ ford04 спасибо, что выкопали это, я наткнулся на другую ветку, которая предлагает следующий трюк. @media (hover: none) and (pointer: coarse) { body { cursor:pointer } }Добавление этого в мои глобальные стили, кажется, решило проблему.
Костас Сарантопулос

1
@ ford04 да, кстати, согласно этой теме, есть еще более специфический медиа-запрос, @supports (-webkit-overflow-scrolling: touch)который нацелен только на IOS, хотя я не знаю больше, сколько! Я только что проверил, и это работает.
Костас Сарантопулос

31

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

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


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

1
Спасибо! Помоги мне очень.
Анджело Лукас

1
Мне нравится это лучше, чем те методы, которые прилагаются к document. Спасибо.
kyw

Чтобы это работало, вам нужно убедиться, что выбранный элемент (relatedTarget) является фокусируемым. См stackoverflow.com/questions/42764494/...
xabitrigo

21

[Update] Решение с 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, это может быть связано с тем, что вы передаете <div>
опорную ссылку

Для тех, кто пытается передать refопору пользовательскому компоненту, вы можете взглянуть наReact.forwardRef
onoya

4

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

Отличное решение - для моего использования я переключился на прослушивание документа вместо тела в случае страниц с коротким содержимым и изменил ссылку с ссылки на строку на ссылку: jsfiddle.net/agymay93/9
Билли Мун

и использовал span вместо div, так как менее вероятно, что он изменит макет, и добавил демонстрацию обработки кликов за пределами тела: jsfiddle.net/agymay93/10
Billy Moon

Строка refявляется устаревшим, следует использовать refобратные вызовы: reactjs.org/docs/refs-and-the-dom.html
Дейн

4

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

3

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

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

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

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

`


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

Поскольку содержимое находится в div, если вы не остановите распространение события, оба получат щелчок. Это часто желаемое поведение, но если вы не хотите, чтобы щелчок передавался в div, просто остановите eventPropagation в вашем обработчике контента onClick. Я обновил свой ответ, чтобы показать как.
Энтони Гарант

3

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

Моя самая большая проблема со всеми остальными ответами - это необходимость фильтровать события кликов от корня / родителя вниз. Я обнаружил, что проще всего было просто установить элемент-брат с позицией: 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
Хм, отличный. Будет ли это работать с несколькими экземплярами? Или каждый фиксированный элемент потенциально может заблокировать щелчок для других?
Тийс Коерсельман

2
componentWillMount(){

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

handleClickOutside(event) {

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

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


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

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

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


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

2

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

1

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

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

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

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


1

В моем случае DROPDOWN решение Бен Бада работало хорошо, но у меня была отдельная кнопка переключения с обработчиком onClick. Таким образом, логика внешнего нажатия противоречила переключателю кнопки onClick. Вот как я решил это, передав ссылку на кнопку:

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

Если вы хотите использовать крошечный компонент (466 байт в сжатом виде), который уже существует для этой функции, вы можете проверить эту реакцию библиотеки .

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


0

Если вы хотите использовать крошечный компонент (466 байт в сжатом виде), который уже существует для этой функции, вы можете проверить эту реакцию библиотеки . Он захватывает события вне реагирующего компонента или элемента jsx.

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


0

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

Пример:

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

Вы можете простой способ решить вашу проблему, я покажу вам:

....

const [dropDwonStatus , setDropDownStatus] = useState(false)

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

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

это работает для меня, удачи;)


-1

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

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

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


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

-1

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

В этом случае мы звоним this.closeDropdown()всякий раз, когда 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>
        );
    }
}

Не уверен, кто голосует, но это решение довольно чистое по сравнению с прослушивателями событий привязки / отмены привязки. У меня не было проблем с этой реализацией.
WDM

-1

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

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

И не забудьтеimport ReactDOM from 'react-dom';
dipole_moment

-1

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

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

В этом примере компонента есть только одна опора: «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>;
  }
}

-1

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.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.