Reactコンポーネントの外側のクリックを検出


410

この記事で説明するように、クリックイベントがコンポーネントの外部で発生したかどうかを検出する方法を探しています。jQuerynearest()は、クリックイベントからのターゲットがその親の1つとしてdom要素を持っているかどうかを確認するために使用されます。一致がある場合、クリックイベントは子の1つに属しているため、コンポーネントの外部にあるとは見なされません。

したがって、私のコンポーネントでは、ウィンドウにクリックハンドラーをアタッチします。ハンドラーが起動したら、ターゲットをコンポーネントのdomの子と比較する必要があります。

クリックイベントには、イベントが移動したdomパスを保持しているように見える「path」などのプロパティが含まれています。何を比較するのか、どのように最適にトラバースするのかわからないので、誰かがすでにそれを巧妙なユーティリティ関数に入れている必要があると思います...いいえ?


ウィンドウではなく親にクリックハンドラーをアタッチできますか?
J.マークスティーブンス

親にクリックハンドラーをアタッチした場合、その要素またはその子の1つがクリックされたときにわかっていますが、クリックされた他のすべて場所を検出する必要があるため、ハンドラーをウィンドウにアタッチする必要があります。
Thijs Koerselman、2015

前回の回答後の記事を見ました。上のコンポーネントにclickStateを設定し、子供からのクリックアクションを渡すのはどうですか。次に、子供たちの小道具をチェックして開閉状態を管理します。
J.マークスティーブンス

一番上のコンポーネントは私のアプリです。しかし、リスニングコンポーネントは数レベルの深さで、domに厳密な位置はありません。アプリのすべてのコンポーネントにクリックハンドラーを追加することはできません。コンポーネントの1つが、その外部のどこかをクリックしたかどうかを知りたいという理由だけです。他のコンポーネントは、このロジックを認識しないでください。これは、ひどい依存関係とボイラープレートコードを作成するためです。
Thijs Koerselman、2015

5
私はあなたに非常に素晴らしいlibをお勧めしたいと思います。AirBnbによって作成:github.com/airbnb/react-outside-click-handler
Osoian Marcel

回答:


683

React 16.3以降での参照の使用が変更されました。

次のソリューションは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.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 2017

5
document.addEventListenerここではなく、Reactの合成イベントをどのように使用できますか?
Ralph David Abernathy 2017

9
@ polkovnikov.ph 1. contextコンストラクターの引数として使用する場合にのみ必要です。この場合は使用されていません。反応チームpropsがコンストラクターの引数として持つことを推奨する理由は、this.propsbefore beforeの使用super(props)が定義されておらず、エラーを引き起こす可能性があるためです。contextは引き続き内部ノードで使用できますcontext。それがの目的です。この方法では、プロップを使用する場合のように、コンポーネントからコンポーネントに渡す必要はありません。2.これは単なる文体の好みであり、この場合の議論を正当化するものではありません。
ベン・バド

7
次のエラーが表示されます:「this.wrapperRef.containsは関数ではありません」
Bogdan

5
@Bogdan、スタイル付きコンポーネントを使用すると同じエラーが発生しました。トップレベルでの<div>の使用を検討してください
user3040068

149

コンテナーにイベントをアタッチせずに私にとって最も効果的な解決策は次のとおりです。

特定のHTML要素は、「フォーカス」と呼ばれるもの、たとえば入力要素を持つことができます。これらの要素は、フォーカスを失うと、ぼかしイベントにも応答します。

要素にフォーカスを与える能力を与えるには、そのtabindex属性が-1以外に設定されていることを確認してください。通常のHTMLではtabindex属性を設定しますが、Reactでは使用する必要がありますtabIndex(大文字に注意I)。

JavaScriptを使用してそれを行うこともできます element.setAttribute('tabindex',0)

これは、カスタムドロップダウンメニューを作成するために使用していました。

var DropDownMenu = React.createClass({
    getInitialState: function(){
        return {
            expanded: false
        }
    },
    expand: function(){
        this.setState({expanded: true});
    },
    collapse: function(){
        this.setState({expanded: false});
    },
    render: function(){
        if(this.state.expanded){
            var dropdown = ...; //the dropdown content
        } else {
            var dropdown = undefined;
        }

        return (
            <div className="dropDownMenu" tabIndex="0" onBlur={ this.collapse } >
                <div className="currentValue" onClick={this.expand}>
                    {this.props.displayValue}
                </div>
                {dropdown}
            </div>
        );
    }
});

10
これはアクセシビリティに問題を引き起こさないでしょうか?フォーカスが出入りするときを検出するためだけに追加の要素をフォーカス可能にしていますが、相互作用はドロップダウン値にある必要がありますか?
Thijs Koerselman 16

2
これは非常に興味深いアプローチです。私の状況には問題なく機能しましたが、これがアクセシビリティにどのように影響するかを知りたいと思います。
クリノア2016

17
私はある程度「機能」しており、クールな小さなハックです。私の問題は、ドロップダウンコンテンツがdisplay:absoluteドロップダウンが親divのサイズに影響しないようにすることです。つまり、ドロップダウンの項目をクリックすると、onblurが起動します。
Dave

2
このアプローチで問題が発生しました。ドロップダウンコンテンツのどの要素も、onClickなどのイベントを起動しません。
アニマソラ2017年

8
ドロップダウンコンテンツに、たとえばフォーム入力などのフォーカス可能な要素が含まれている場合、他の問題が発生する可能性があります。フォーカスを奪い、onBlur ドロップダウンコンテナーでトリガーされ、expanded falseに設定されます。
カマガトス2017

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

React Hooksアプローチ(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
これは受け入れられる答えになるはずです。絶対配置のドロップダウンメニューに最適です。
dishwasherWithProgrammingSkill 2017

1
ReactDOM.findDOMNodeそして、推奨されていません、使用する必要がありref、コールバックを:github.com/yannickcr/eslint-plugin-react/issues/...
デイン

2
グレートでクリーンなソリューション
カリム

1
これは私にとってはうまくいきましたが、コンポーネントをハックして200ms後に可視を「true」にリセットする必要がありました(そうしないと、メニューが再び表示されることはありません)。ありがとうございました!
Lee Moe

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
Pabloによって提案されtabIndex/ onBlurアプローチに勝る具体的な利点はありますか?実装はどのように機能し、その動作はonBlurアプローチとどのように異なりますか?
Mark Amery 2017年

4
専用コンポーネントを使用してから、tabIndexハックを使用することをお勧めします。賛成投票!
Atul Yadav 2017年

5
@MarkAmery- tabIndex/onBlurアプローチについてコメントします。position:absoluteメニューが他のコンテンツの上にあるなど、ドロップダウンがの場合は機能しません。
ボー・スミス

4
これをtabindexよりも優れた方法で使用するもう1つの利点は、子要素に焦点を合わせると、tabindexソリューションがblurイベントも発生させることです
Jemar Jones

1
@BeauSmith-どうもありがとう!私の日を救った!
ミカ

38

Discussion.reactjs.orgでベン・アルパートのおかげで解決策を見つけました。提案されたアプローチは、ドキュメントにハンドラーを添付しますが、問題があることがわかりました。ツリー内のコンポーネントの1つをクリックすると、更新時にクリックされた要素を削除する再レンダリングが発生しました。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いない場合、別のハンドラーによって再レンダリングされる前に、そのハンドラーが確実に起動する必要があるのはなぜですか?
マークアメリー2017

1
純粋なUXポイント:mousedownおそらくclickここよりも優れたハンドラーでしょう。ほとんどのアプリケーションでは、離したときではなく、マウスを押した瞬間に、メニューの外側を閉じるという動作が発生します。たとえば、Stack Overflowのフラグ共有ダイアログ、またはブラウザのトップメニューバーのドロップダウンの1つで試してください。
Mark Amery 2017

ReactDOM.findDOMNode文字列ref廃止されましたが、使用する必要がありref、コールバックを:github.com/yannickcr/eslint-plugin-react/issues/...
デイン

これは最も簡単な解決策であるとに装着する場合でも、私のために完璧に動作しますdocument
Flion

37

JSConf Hawaii 2020でのTanner Linsleyの素晴らしい講演に基づくフックの実装:

useOuterClick フック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。たとえばdiv、上記の例のReactルートにリスナーを追加し、その高さ(height: 100vhまたは同様のもの)を拡張して、すべての外部クリックをキャッチできます。出典:quirksmode.orgおよびこの回答


モバイルで動作しないので、残念です
Omar

@Omarが私の投稿のCodesandboxをAndroidデバイスのFirefox 67.0.3でテストしたところ、問題なく動作しました。使用しているブラウザ、エラーの内容などについて詳しく教えてください。
ford04

エラーはありませんが、Chrome 7以降では動作しません。外のタップは検出しません。モバイルのChrome開発ツールでは機能しますが、実際のiOSデバイスでは機能しません。
オマー

1
@ ford04それを掘り下げてくれてありがとう、私は次のトリックを示唆している別のスレッドに偶然出会った。 @media (hover: none) and (pointer: coarse) { body { cursor:pointer } }これを私のグローバルスタイルに追加すると、問題が解決したようです。
Kostas Sarantopoulos

1
@ ford04ええ、このスレッドによると@supports (-webkit-overflow-scrolling: touch)、私はどれだけ知っているにもかかわらず、IOSのみをターゲットとするさらに具体的なメディアクエリがあります!私はそれをテストしただけで動作します。
Kostas Sarantopoulos

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

お役に立てれば。


とても良い!ありがとう。しかし私の場合、divの絶対位置で「現在の場所」をキャッチするのは問題でした。そのため、入力のあるdivに「OnMouseLeave」を使用し、ドロップダウンカレンダーを使用して、マウスがdivを離れたときにすべてのdivを無効にします。
Alex

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;

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をaなどの実際のDOM要素ではなくカスタムコンポーネントに渡していることが原因である可能性があります<div>
willlma

refpropをカスタムコンポーネントに渡そうとしている人は、見てみたいかもしれませんReact.forwardRef
小野屋

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

レイアウトを変更する可能性が低いため、divの代わりにスパンを使用し、本文の外側でクリックを処理するデモを追加しました:jsfiddle.net/agymay93/10
Billy Moon

文字列refは非推奨です。refコールバックを使用する必要があります:reactjs.org/docs/refs-and-the-dom.html
dain

4

これにはすでに多くの答えがありますが、それらは対処せずe.stopPropagation()、閉じたい要素の外側の反応リンクをクリックするのを防ぎます。

Reactには独自の人工イベントハンドラーがあるため、イベントリスナーのベースとしてdocumentを使用することはできません。e.stopPropagation()Reactはドキュメント自体を使用するため、この前に行う必要があります。document.querySelector('body')代わりに使用する場合など。Reactリンクからのクリックを防ぐことができます。以下は、外部クリックをクリックして閉じる方法の例です。
これはES6React 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 2017年

コンテンツはdivにあるため、イベントの伝播を停止しないと、両方がクリックを受け取ります。これは多くの場合望ましい動作ですが、クリックをdivに伝達したくない場合は、コンテンツのonClickハンドラーでeventPropagationを停止するだけです。回答を更新して、その方法を示します。
Anthony Garant 2017年

3

Ben Budが行った承認済みの回答を拡張するために、styled-componentsを使用している場合、refsをそのように渡すと、「this.wrapperRef.contains is not a function」などのエラーが発生します。

コメントで、スタイル付きコンポーネントをdivでラップし、そこにrefを渡すという修正案が機能します。そうは言っても、彼らのドキュメントでは、この理由と、styled-components内でのrefの適切な使用についてすでに説明しています。

refプロパティをスタイル付きコンポーネントに渡すと、StyledComponentラッパーのインスタンスが提供されますが、基になるDOMノードにはインスタンスが提供されません。これは、refがどのように機能するかによるものです。ラッパーでフォーカスなどのDOMメソッドを直接呼び出すことはできません。実際のラップされた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

他のすべての答えに対する私の最大の懸念は、ルート/親からクリックイベントをフィルタリングする必要があることです。最も簡単な方法は、兄弟要素に位置を設定することです:固定、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
うーん気の利いた。複数のインスタンスで動作しますか?または、各固定要素が他の要素のクリックをブロックする可能性がありますか?
Thijs Koerselman、2017年

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"これが箱から出してすぐに機能することを受け取りました。このソリューションを試す人は、他のソリューションを試すときにコピーした残留コードを必ず削除してください。たとえば、イベントリスナーの追加はこれと競合するようです。
Frikster

2

これと、React ^ 16.3を必要とする参照の処理に関するReactの公式ドキュメントに従って、これを部分的に行いました。ここで他のいくつかの提案を試した後、これは私にとってうまくいった唯一のものです...

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ケースでは、Ben Budのソリューションはうまく機能しましたが、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バイトgzip圧縮)を使用する場合は、このライブラリをチェックアウトできますreact-outclick

ライブラリの良いところは、コンポーネントの外側と内側のクリックを検出できることです。他のタイプのイベントの検出もサポートします。


0

この機能のためにすでに存在する小さなコンポーネント(466バイトgzip圧縮)を使用する場合は、このライブラリをチェックアウトできますreact-outclick。これは、reactコンポーネントまたはjsx要素の外部のイベントをキャプチャします。

ライブラリの良いところは、コンポーネントの外側と内側のクリックを検出できることです。他のタイプのイベントの検出もサポートします。


0

私はこれが古い質問であることを知っていますが、私はこれに遭遇し続け、これを単純な形式で理解するのに多くの問題を抱えていました。したがって、これが誰かの生活を少し簡単にする場合は、airbnbのOutsideClickHandlerを使用します。これは、独自のコードを記述せずにこのタスクを実行する最も簡単なプラグインです。

例:

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;}}> Toggle Popover {this.state.popupVisible &&(I'm a popover!)}); }}

この問題に関するすばらしい記事を以下に示します「Reactコンポーネントの外部でのクリックの処理」https://larsgraubner.com/handle-outside-clicks-react/


Stack Overflowへようこそ!単なるリンクである回答は、一般的に嫌われます: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

イベントリスナーを使用したドロップダウンで「フォーカス」ソリューションを機能させるには、onClickの代わりにonMouseDownイベントを使用してそれらを追加できます。そのようにしてイベントが発生し、その後ポップアップは次のように閉じます:

<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」というプロップが1つだけあります。

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フック-React 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の回答に部分的に触発されています。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.