Виявити клацання поза компонентом React


410

Я шукаю спосіб виявити, чи відбулася подія клацання поза компонентом, як описано в цій статті . jQuery najbliže () використовується для того, щоб побачити, чи має ціль із події клацання елемент dom як один із батьків. Якщо є відповідність, подія клацання належить одному з дітей і, таким чином, не вважається поза компонентом.

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

Подія клацання містить такі властивості, як "шлях", який, здається, утримує шлях дому, який пройшла подія. Я не впевнений, що порівнювати чи як найкраще це перейти, і я думаю, що хтось, мабуть, уже поставив це в розумну функцію утиліти ... Ні?


Чи можете ви прикріпити обробник кліків до батьківського, а не до вікна?
Дж. Марк Стівенс

Якщо ви приєднаєте обробник кліків до батьків, ви знаєте, коли натиснутий цей елемент або одного з їхніх дітей, але мені потрібно виявити всі інші місця, які натискаються, тому обробник потрібно прикріпити до вікна.
Thijs Koerselman

Я подивився статтю після попередньої відповіді. Як щодо встановлення clickState у верхньому компоненті та передачі дій кліку від дітей. Тоді ви перевірите реквізит у дітей для управління відкритим закритим станом.
Дж. Марк Стівенс

Головним компонентом буде мій додаток. Але компонент прослуховування глибокий на кілька рівнів і не має суворого положення в домі. Я не можу додати оброблювач кліків до всіх компонентів у моєму додатку лише тому, що один із них зацікавлений дізнатися, чи натиснув ви десь поза ним. Інші компоненти не повинні знати про цю логіку, оскільки це створило б жахливі залежності та код котла.
Thijs Koerselman

5
Я хотів би порекомендувати вам дуже приємну лайку. створено AirBnb: github.com/airbnb/react-outside-click-handler
Осоян Марсель

Відповіді:


681

Наступне рішення використовує ES6 та дотримується кращих практик прив’язки, а також встановлення ref через метод.

Щоб побачити це в дії:

Реалізація класу:

import React, { Component } from 'react';

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

    this.setWrapperRef = this.setWrapperRef.bind(this);
    this.handleClickOutside = this.handleClickOutside.bind(this);
  }

  componentDidMount() {
    document.addEventListener('mousedown', this.handleClickOutside);
  }

  componentWillUnmount() {
    document.removeEventListener('mousedown', this.handleClickOutside);
  }

  /**
   * Set the wrapper ref
   */
  setWrapperRef(node) {
    this.wrapperRef = node;
  }

  /**
   * Alert if clicked on outside of element
   */
  handleClickOutside(event) {
    if (this.wrapperRef && !this.wrapperRef.contains(event.target)) {
      alert('You clicked outside of me!');
    }
  }

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

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

Впровадження гачків:

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

/**
 * Hook that alerts clicks outside of the passed ref
 */
function useOutsideAlerter(ref) {
  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тут?
Ральф Девід Абернаті

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

7
Я отримую таку помилку: "this.wrapperRef.contains не є функцією"
Богдан

5
@Bogdan, я отримував таку ж помилку при використанні стильового компонента. Подумайте про використання <div> на вищому рівні
user3040068

149

Ось рішення, яке найкраще працювало для мене, не додаючи події до контейнера:

Деякі елементи 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
Чи не створило б це проблеми з доступністю? Оскільки ви робите додатковий елемент, орієнтований лише на виявлення, коли він фокусується на вході / виході, але взаємодія має бути на спадному значенні ні?
Thijs Koerselman

2
Це дуже цікавий підхід. Відмінно працював для моєї ситуації, але мені було б цікаво дізнатися, як це може вплинути на доступність.
кліноре

17
У мене це "працює" певною мірою, його крутий маленький хакер. Моя проблема полягає в тому, що мій вміст, display:absoluteщо випадає, таким чином, щоб спадне меню не впливало на розмір батьківського діва. Це означає, що при натисканні на елемент у спадному меню вимкнення onblur.
Дейв

2
У мене виникли проблеми з таким підходом, будь-який елемент вмісту, що випадає, не запускає події, такі як onClick.
AnimaSola

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

106

Я застряг у тому ж питанні. Я тут трохи запізнююсь на вечірку, але для мене це справді хороше рішення. Сподіваємось, це допоможе комусь іншому. Вам потрібно імпортувати 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
Це має бути прийнятою відповіддю. Відмінно працював для випадаючих меню з абсолютним позиціонуванням.
посудомийна

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. Оголошення!
Атул Ядав

5
@MarkAmery - я коментую tabIndex/onBlurпідхід. Він не працює, коли випадає меню position:absolute, наприклад, наведення курсору на інший вміст.
Бо Сміт

4
Ще одна перевага використання цього способу над tabindex полягає в тому, що рішення tabindex також спричинить розмиття, якщо зосередитись на дочірньому елементі
Джемар Джонс,

1
@BeauSmith - Дякую! Врятував мій день!
Міка

38

Я знайшов рішення завдяки Бену Альперту на дискусії.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тут. У більшості програм поведінка закритого меню, натиснувши назовні, відбувається з моменту, коли ви натискаєте кнопку миші, а не при відпуску. Спробуйте, наприклад, з прапором Стек Overflow або поділитись діалогами або з однією зі спадних панелей у верхній панелі меню браузера.
Марк Амері

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

це найпростіше рішення і прекрасно працює для мене, навіть коли я приєднуюся доdocument
Фліон

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. Наприклад, ви можете додати слухача в корінь Reactdiv у наведеному вище прикладі та збільшити його висоту ( height: 100vhабо подібну) для отримання всіх зовнішніх кліків. Джерела: quirksmode.org та ця відповідь


не працює так мобільно на мобільних пристроях
Омар

@Omar щойно тестував Codesandbox у своєму дописі з Firefox 67.0.3 на пристрої Android - працює все добре. Чи можете ви розробити, який браузер ви використовуєте, що таке помилка тощо?
ford04

Немає помилок, але не працює на chrome, iphone 7+. Він не виявляє крани зовні. У інструментах хромованого розробника на мобільних пристроях він працює, але в реальному пристрої ios він не працює.
Омар

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

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

30

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

Сподіваюсь, це допомагає.


Дуже добре! Дякую. Але в моєму випадку було проблемою зловити "поточне місце" з абсолютною позицією для діва, тому я використовую "OnMouseLeave" для div з введенням і спадним календарем, щоб просто відключити всі div, коли миша залишає діл.
Олексій

1
Дякую! Допоможіть мені багато.
Венло Лукас

1
Мені це подобається краще, ніж ті методи, що стосуються document. Дякую.
kyw

Для цього вам потрібно переконатися в тому, що клацнув елемент (relatedTarget) орієнтований. Див stackoverflow.com/questions/42764494 / ...
xabitrigo

21

[Оновлення] Рішення з 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;

Розв’язання з Реакцією ^ 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, це може бути через те, що ви передаєте додаткову підтримку користувальницькому компоненту, а не справжньому елементу DOM, наприклад,<div>
willlma

Для тих, хто намагається передати 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>
    );
  }
}

Прекрасне рішення - для мого використання я змінив для прослуховування документа замість корпусу у випадку сторінок з коротким вмістом і змінив ref з рядка на dom посилання: jsfiddle.net/agymay93/9
Біллі Мун

і використовував проміжок замість діва, оскільки менше шансів змінити макет, а також додав демо для обробки кліків поза тілом: jsfiddle.net/agymay93/10
Біллі Мун

Рядок refзастарілий, він повинен використовувати refзворотні дзвінки: reactjs.org/docs/refs-and-the-dom.html
dain

4

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

Через те, що React має власний обробник штучних подій, ви не можете використовувати документ як базу для слухачів подій. Потрібно зробити e.stopPropagation()це перед тим, як React використовує сам документ. Якщо ви використовуєте, наприклад, document.querySelector('body')замість цього. Ви можете запобігти натискання посилання "React". Далі наводиться приклад того, як я реалізую клацання зовні та закриття.
Для цього використовується 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>

Як це зараз, якщо ви додасте обробник кліків до вмісту, подія також буде передана у верхній розділ і, таким чином, запускатиме обробникOutsideClick. Якщо це не ваша бажана поведінка, просто припиніть розгляд подій на своєму обробнику.

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

`


Проблема такого підходу полягає в тому, що ви не можете мати жодного клацання на Вміст - div натомість отримає клік.
rbonick

Оскільки вміст знаходиться у діві, якщо ви не зупините розповсюдження події, обидва отримають клацання. Це часто бажана поведінка, але якщо ви не хочете, щоб клацання поширювалося на div, просто зупиніть подіюPropagation на вашому вмісті onClick обробника. Я оновив свою відповідь, щоб показати, як.
Ентоні Гарант

3

Продовжити прийняту відповідь, зроблену Бен Будом , якщо ви використовуєте стильові компоненти, передача посилань таким чином призведе до помилки типу "this.wrapperRef.contains - це не функція".

Запропонований виправлення, в коментарях, обернути стильовий компонент дівом та передати туди посилання, працює. Сказавши, що в своїх документах вони вже пояснюють причину цього і правильне використання рефлексу в стильових компонентах:

Якщо передати повторну підтримку стильовому компоненту, ви отримаєте екземпляр обгортки StyledComponent, але не базовий вузол DOM. Це пов’язано з тим, як працюють реф. Неможливо викликати методи DOM, як-от фокус, безпосередньо на наших обгортках. Щоб отримати посилання на фактичний, загорнутий вузол DOM, передайте зворотний виклик замість опори InternalRef.

Так:

<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

Моя найбільша турбота щодо всіх інших відповідей полягає в тому, щоб фільтрувати події клацання від кореня / батьківського вниз. Я знайшов найпростіший спосіб - просто встановити елемент-близнюк з позицією: фіксований, z-індекс 1 за спадною панеллю та обробляти подію клацання на нерухомому елементі всередині того ж компонента. Зберігає все, що централізоване на заданий компонент.

Приклад коду

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

Це прекрасно зробило роботу.


Це працює фантастично! Тут набагато простіше, ніж на багато інших відповідей, і на відміну від принаймні трьох інших рішень тут, де для всіх із них я отримав "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 байт gzipped), який вже існує для цієї функціональності, то ви можете перевірити реакцію цієї бібліотеки .

Хороша річ у бібліотеці полягає в тому, що вона також дозволяє виявляти клацання за межами компонента і всередині іншого. Він також підтримує виявлення інших типів подій.


0

Якщо ви хочете використати крихітний компонент (466 байт gzipped), який вже існує для цієї функціональності, то ви можете перевірити реакцію цієї бібліотеки . Він фіксує події за межами реагуючого компонента або елемента 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

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

….

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

Щоб рішення «фокусування» працювало на випадаючі програми із слухачами подій, ви можете додати їх до події 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.