Deteksi klik di luar komponen Bereaksi

410

Saya mencari cara untuk mendeteksi apakah peristiwa klik terjadi di luar komponen, seperti yang dijelaskan dalam artikel ini . jQuery terdekat () digunakan untuk melihat apakah target dari peristiwa klik memiliki elemen dom sebagai salah satu orang tuanya. Jika ada kecocokan, acara klik milik salah satu anak dan karenanya tidak dianggap berada di luar komponen.

Jadi dalam komponen saya, saya ingin melampirkan pengendali klik ke jendela. Ketika pawang menyala saya harus membandingkan target dengan anak-anak dom dari komponen saya.

Acara klik berisi properti seperti "jalur" yang tampaknya menampung jalur dom yang telah dilalui acara. Saya tidak yakin harus membandingkan apa atau bagaimana cara melewatinya, dan saya pikir seseorang pasti sudah memasukkannya ke dalam fungsi utilitas pintar ... Tidak?

Thijs Koerselman
sumber
Bisakah Anda melampirkan pengendali klik ke induk daripada jendela?
J. Mark Stevens
Jika Anda melampirkan pengendali klik ke induk, Anda tahu kapan elemen atau salah satu dari anak-anak mereka diklik, tetapi saya perlu mendeteksi semua tempat lain yang diklik, sehingga pengendali harus dilampirkan ke jendela.
Thijs Koerselman
Saya melihat artikel setelah tanggapan sebelumnya. Bagaimana dengan mengatur clickState di komponen atas dan melewati tindakan klik dari anak-anak. Kemudian Anda akan memeriksa alat peraga pada anak-anak untuk mengelola buka tutup keadaan.
J. Mark Stevens
Komponen teratas adalah aplikasi saya. Tetapi komponen mendengarkan memiliki beberapa level dan tidak memiliki posisi yang ketat dalam dom. Saya tidak mungkin menambahkan penangan klik ke semua komponen di aplikasi saya hanya karena salah satu dari mereka tertarik untuk mengetahui apakah Anda mengklik di suatu tempat di luarnya. Komponen lain seharusnya tidak menyadari logika ini karena itu akan membuat dependensi yang mengerikan dan kode boilerplate.
Thijs Koerselman
5
Saya ingin merekomendasikan Anda lib yang sangat bagus. dibuat oleh AirBnb: github.com/airbnb/react-outside-click-handler
Osoian Marcel

Jawaban:

684

Referensi penggunaan di React 16.3+ berubah.

Solusi berikut menggunakan ES6 dan mengikuti praktik terbaik untuk mengikat serta mengatur referensi melalui metode.

Untuk melihatnya dalam aksi:

Implementasi Kelas:

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

Implementasi Hooks:

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>;
}
Ben Bud
sumber
3
Bagaimana Anda menguji ini? Bagaimana Anda mengirim event.target yang valid ke fungsi handleClickOutside?
csilk
5
Bagaimana Anda bisa menggunakan acara sintetik React, alih-alih di document.addEventListenersini?
Ralph David Abernathy
10
@ polkovnikov.ph 1. contexthanya diperlukan sebagai argumen dalam konstruktor jika digunakan. Itu tidak digunakan dalam kasus ini. Alasan tim reaksi merekomendasikan untuk memiliki propsargumen dalam konstruktor adalah karena penggunaan this.propssebelum panggilan super(props)akan tidak ditentukan dan dapat menyebabkan kesalahan. contextmasih tersedia pada node batin, itulah tujuan keseluruhan dari context. Dengan cara ini Anda tidak harus meneruskannya dari komponen ke komponen seperti yang Anda lakukan dengan alat peraga. 2. Ini hanya preferensi gaya, dan tidak menjamin berdebat dalam kasus ini.
Ben Bud
7
Saya mendapatkan kesalahan berikut: "this.wrapperRef.contains bukan fungsi"
Bogdan
5
@ Bogdan, saya mendapatkan kesalahan yang sama ketika menggunakan komponen-gaya. Pertimbangkan untuk menggunakan <div> di tingkat atas
user3040068
150

Inilah solusi yang paling berhasil bagi saya tanpa melampirkan acara pada wadah:

Elemen HTML tertentu dapat memiliki apa yang dikenal sebagai " fokus ", misalnya elemen input. Elemen-elemen itu juga akan merespons peristiwa blur , ketika mereka kehilangan fokus itu.

Untuk memberikan elemen apa pun kapasitas untuk fokus, pastikan atribut tabindex-nya disetel ke selain -1. Dalam HTML biasa itu akan dengan menetapkan tabindexatribut, tetapi dalam Bereaksi Anda harus menggunakan tabIndex(perhatikan modal I).

Anda juga dapat melakukannya melalui JavaScript dengan element.setAttribute('tabindex',0)

Ini adalah tujuan saya menggunakannya, untuk membuat menu DropDown khusus.

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>
        );
    }
});
Pablo Barría Urenda
sumber
10
Bukankah ini akan menimbulkan masalah dengan aksesibilitas? Karena Anda membuat elemen ekstra yang dapat difokuskan hanya untuk mendeteksi kapan itu masuk / keluar fokus, tetapi interaksi harus pada nilai-nilai dropdown tidak?
Thijs Koerselman
2
Ini adalah pendekatan yang sangat menarik. Berfungsi sempurna untuk situasi saya, tetapi saya tertarik untuk mempelajari bagaimana ini dapat mempengaruhi aksesibilitas.
klinore
17
Saya punya ini "berfungsi" sampai batas tertentu, ini sedikit hack keren. Masalah saya adalah bahwa konten drop down saya display:absolutesehingga drop down tidak akan mempengaruhi ukuran div induk. Ini berarti ketika saya mengklik item di dropdown, onblur menyala.
Dave
2
Saya punya masalah dengan pendekatan ini, elemen apa pun di konten dropdown tidak memecat acara seperti onClick.
AnimaSola
8
Anda mungkin menghadapi beberapa masalah lain jika konten tarik-turun berisi beberapa elemen yang dapat difokuskan seperti input formulir misalnya. Mereka akan mencuri fokus Anda, onBlur akan dipicu pada wadah dropdown Anda dan expanded akan disetel ke false.
Kamagatos
107

Saya terjebak pada masalah yang sama. Saya agak terlambat ke pesta di sini, tetapi bagi saya ini adalah solusi yang sangat bagus. Semoga ini bisa membantu orang lain. Anda perlu mengimpor findDOMNodedarireact-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
        });
    }
}

Pendekatan React Hooks (16.8 +)

Anda dapat membuat pengait yang dapat digunakan kembali bernama 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 };
}

Kemudian di komponen Anda ingin menambahkan fungsionalitas untuk melakukan hal berikut:

const DropDown = () => {
    const { ref, isComponentVisible } = useComponentVisible(true);
    return (
       <div ref={ref}>
          {isComponentVisible && (<p>Dropdown Component</p>)}
       </div>
    );

}

Temukan contoh kode dan kotak di sini.

Paul Fitzgerald
sumber
1
@LeeHanKyeol Tidak sepenuhnya - jawaban ini memanggil penangan acara selama fase penangkapan penanganan acara, sedangkan jawaban yang terkait dengan memanggil penangan acara selama fase gelembung.
stevejay
3
Ini harus menjadi jawaban yang diterima. Bekerja dengan sempurna untuk menu dropdown dengan posisi absolut.
dishwasherWithProgrammingSkill
1
ReactDOM.findDOMNodedan sudah tidak digunakan lagi, harus menggunakan refcallback: github.com/yannickcr/eslint-plugin-react/issues/…
dain
2
solusi hebat dan bersih
Karim
1
Ini bekerja untuk saya walaupun saya harus meretas komponen Anda untuk mengatur ulang yang terlihat kembali ke 'true' setelah 200 ms (jika tidak, menu saya tidak akan pernah muncul lagi). Terima kasih!
Lee Moe
87

Setelah mencoba banyak metode di sini, saya memutuskan untuk menggunakan github.com/Pomax/react-onclickoutside karena seberapa lengkapnya.

Saya menginstal modul melalui npm dan mengimpornya ke komponen saya:

import onClickOutside from 'react-onclickoutside'

Kemudian, di kelas komponen saya, saya mendefinisikan handleClickOutsidemetode:

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

Dan ketika mengekspor komponen saya, saya membungkusnya onClickOutside():

export default onClickOutside(NameOfComponent)

Itu dia.

Beau Smith
sumber
2
Apakah ada keuntungan konkret untuk ini selama tabIndex/ onBlurpendekatan yang diusulkan oleh Pablo ? Bagaimana cara kerja implementasi, dan bagaimana perilakunya berbeda dari onBlurpendekatan?
Mark Amery
4
Lebih baik bagi pengguna komponen khusus kemudian menggunakan hack tabIndex. Menebusnya!
Atul Yadav
5
@MarkAmery - Saya memberi komentar pada tabIndex/onBlurpendekatan. Ini tidak berfungsi ketika dropdown adalah position:absolute, seperti menu melayang di atas konten lain.
Beau Smith
4
Keuntungan lain menggunakan tabindex lebih dari ini adalah bahwa solusi tabindex juga menembakkan peristiwa blur jika Anda fokus pada elemen anak
Jemar Jones
1
@BeauSmith - Terima kasih banyak! Selamatkan hari saya!
Mika
39

Saya menemukan solusi terima kasih kepada Ben Alpert di mendiskusikan.reactjs.org . Pendekatan yang disarankan melampirkan handler ke dokumen tetapi ternyata bermasalah. Mengklik pada salah satu komponen di pohon saya menghasilkan rerender yang menghapus elemen yang diklik pada pembaruan. Karena rerender dari Bereaksi terjadi sebelum penangan dokumen tubuh dipanggil, elemen tidak terdeteksi sebagai "di dalam" pohon.

Solusi untuk ini adalah menambahkan handler pada elemen root aplikasi.

utama:

window.__myapp_container = document.getElementById('app')
React.render(<App/>, window.__myapp_container)

komponen:

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>
    )
  }
}
Thijs Koerselman
sumber
8
Ini tidak berfungsi lagi untuk saya dengan React 0.14.7 - mungkin React mengubah sesuatu, atau mungkin saya membuat kesalahan ketika mengadaptasi kode ke semua perubahan untuk React. Saya malah menggunakan github.com/Pomax/react-onclickoutside yang berfungsi seperti mantra.
Nicole
1
Hmm. Saya tidak melihat alasan mengapa ini harus berhasil. Mengapa penangan pada simpul DOM aplikasi harus dijamin untuk dinyalakan sebelum rerender dipicu oleh penangan yang lain jika ada yang documenttidak?
Mark Amery
1
Titik UX murni: mousedownmungkin akan menjadi penangan yang lebih baik daripada di clicksini. Pada sebagian besar aplikasi, perilaku menu-dekat-dengan-mengklik-luar terjadi saat Anda menekan mouse, bukan saat Anda melepaskannya. Cobalah, misalnya, dengan flag Stack Overflow atau bagikan dialog atau dengan salah satu dropdown dari bilah menu teratas browser Anda.
Mark Amery
ReactDOM.findDOMNodedan string refsudah tidak digunakan lagi, harus menggunakan refcallback: github.com/yannickcr/eslint-plugin-react/issues/…
dain
ini adalah solusi paling sederhana dan berfungsi dengan baik untuk saya bahkan ketika melampirkan kedocument
Flion
37

Implementasi Hook berdasarkan pembicaraan luar biasa Tanner Linsley di JSConf Hawaii 2020 :

useOuterClick API kait

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

Penerapan

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

useOuterClickmemanfaatkan ref yang bisa berubah untuk membuat lean API untuk Client. Panggilan balik dapat diatur tanpa harus memonya melalui useCallback. Badan panggilan balik masih memiliki akses ke properti dan status terkini (tidak ada nilai penutupan basi).


Catatan untuk pengguna iOS

iOS hanya memperlakukan elemen tertentu sebagai dapat diklik. Untuk menghindari perilaku ini, pilih pendengar klik luar berbeda dari document- tidak termasuk ke atas body. Misalnya, Anda dapat menambahkan pendengar pada React root divdi contoh di atas dan perluas ketinggiannya ( height: 100vhatau yang serupa) untuk menangkap semua klik di luar. Sumber: quirksmode.org dan jawaban ini

ford04
sumber
tidak bekerja di ponsel jadi tidak sukarela
Omar
@Omar baru saja menguji Codesandbox di postingan saya dengan Firefox 67.0.3 pada perangkat android - semuanya baik-baik saja. Bisakah Anda menguraikan browser apa yang Anda gunakan, apa kesalahannya dll?
ford04
Tidak ada kesalahan tetapi tidak berfungsi pada chrome, iphone 7+. Itu tidak mendeteksi keran di luar. Dalam perangkat dev chrome di ponsel berfungsi tetapi dalam perangkat iOS nyata itu tidak berfungsi.
Omar
1
@ ford04 terima kasih atas penggaliannya, saya menemukan utas lainnya yang menyarankan trik berikut. @media (hover: none) and (pointer: coarse) { body { cursor:pointer } }Menambahkan ini dalam gaya global saya, sepertinya itu memperbaiki masalah.
Kostas Sarantopoulos
1
@ ford04 yeah, btw menurut utas ini ada permintaan media yang lebih spesifik @supports (-webkit-overflow-scrolling: touch)yang hanya menargetkan iOS meskipun saya tidak tahu berapa banyak! Saya baru saja mengujinya dan berfungsi.
Kostas Sarantopoulos
31

Tidak ada jawaban lain di sini yang berfungsi untuk saya. Saya mencoba untuk menyembunyikan popup pada blur, tetapi karena isinya benar-benar diposisikan, onBlur menembak bahkan pada klik isi dalam juga.

Berikut adalah pendekatan yang berhasil bagi saya:

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

Semoga ini membantu.

Niyaz
sumber
Baik sekali! Terima kasih. Tetapi dalam kasus saya itu adalah masalah untuk menangkap "tempat saat ini" dengan posisi absolut untuk div, jadi saya menggunakan "OnMouseLeave" untuk div dengan input dan drop down calendar untuk hanya menonaktifkan semua div ketika mouse meninggalkan div.
Alex
1
Terima kasih! Bantu saya banyak.
Ângelo Lucas
1
Saya suka ini lebih baik daripada metode yang dilampirkan document. Terima kasih.
kyw
Agar ini berfungsi, Anda perlu memastikan bahwa elemen yang diklik (relatedTarget) dapat fokus. Lihat stackoverflow.com/questions/42764494/…
xabitrigo
21

[Perbarui] Solusi dengan React ^ 16.8 menggunakan Hooks

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;

Solusi dengan Bereaksi ^ 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;
onoya
sumber
3
Ini adalah solusi masuk ke sekarang. Jika Anda mendapatkan kesalahan .contains is not a function, itu mungkin karena Anda meneruskan prop <div>
referensi
Bagi mereka yang mencoba mengirimkan refprop ke komponen kustom, Anda mungkin ingin melihatReact.forwardRef
onoya
4

Inilah pendekatan saya (demo - https://jsfiddle.net/agymay93/4/ ):

Saya telah membuat komponen khusus yang disebut WatchClickOutsidedan dapat digunakan seperti (saya berasumsi JSXsintaks):

<WatchClickOutside onClickOutside={this.handleClose}>
  <SomeDropdownEtc>
</WatchClickOutside>

Berikut ini adalah kode WatchClickOutsidekomponen:

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>
    );
  }
}
pie6k
sumber
Solusi hebat - untuk saya gunakan, saya mengubah untuk mendengarkan dokumen alih-alih isi dalam kasus halaman dengan konten pendek, dan mengubah referensi dari string ke dom: jsfiddle.net/agymay93/9
Billy Moon
dan menggunakan rentang bukannya div karena lebih kecil kemungkinannya untuk mengubah tata letak, dan menambahkan demo menangani klik di luar tubuh: jsfiddle.net/agymay93/10
Billy Moon
String refsudah ditinggalkan, harus menggunakan refcallback: reactjs.org/docs/refs-and-the-dom.html
dain
4

Ini sudah memiliki banyak jawaban tetapi mereka tidak membahas e.stopPropagation()dan mencegah mengklik tautan reaksi di luar elemen yang ingin Anda tutup.

Karena Bereaksi memiliki event handler buatannya sendiri, Anda tidak dapat menggunakan dokumen sebagai dasar untuk pendengar acara. Anda perlu e.stopPropagation()sebelum ini karena Bereaksi menggunakan dokumen itu sendiri. Jika Anda menggunakan misalnya document.querySelector('body')sebagai gantinya. Anda dapat mencegah klik dari tautan Bereaksi. Berikut ini adalah contoh bagaimana saya menerapkan klik di luar dan dekat.
Ini menggunakan ES6 dan Bereaksi 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;
Richard Herries
sumber
3

Bagi mereka yang membutuhkan penentuan posisi absolut, opsi sederhana yang saya pilih adalah menambahkan komponen pembungkus yang ditata untuk menutupi seluruh halaman dengan latar belakang transparan. Kemudian Anda bisa menambahkan onClick pada elemen ini untuk menutup komponen di dalam Anda.

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

Seperti sekarang jika Anda menambahkan handler klik pada konten, acara tersebut juga akan disebarkan ke div atas dan karenanya memicu handlerOutsideClick. Jika ini bukan perilaku yang Anda inginkan, cukup hentikan acara progasi pada handler Anda.

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

`

Anthony Garant
sumber
Masalah dengan pendekatan ini adalah bahwa Anda tidak dapat memiliki klik pada Konten - div akan menerima klik sebagai gantinya.
rbonick
Karena konten ada di div jika Anda tidak menghentikan propagasi acara, keduanya akan menerima klik. Ini sering merupakan perilaku yang diinginkan tetapi jika Anda tidak ingin klik disebarkan ke div, cukup hentikan eventPropagation pada konten Anda diClick handler. Saya telah memperbarui jawaban saya untuk menunjukkan caranya.
Anthony Garant
3

Untuk memperluas jawaban yang diterima dibuat oleh Ben Bud , jika Anda menggunakan komponen-komponen yang ditata, memberikan referensi seperti itu akan memberi Anda kesalahan seperti "this.wrapperRef.contains bukan fungsi".

Perbaikan yang disarankan, dalam komentar, untuk membungkus komponen gaya dengan div dan lulus referensi di sana, bekerja. Karena itu, dalam dokumen mereka, mereka sudah menjelaskan alasan untuk ini dan penggunaan referensi yang tepat dalam komponen yang ditata:

Melewati penyangga ref ke komponen yang ditata akan memberi Anda instance dari pembungkus StyledComponent, tetapi tidak ke simpul DOM yang mendasarinya. Ini karena cara kerja referensi. Tidak mungkin memanggil metode DOM, seperti fokus, pada pembungkus kami secara langsung. Untuk mendapatkan referensi ke simpul DOM yang sebenarnya, dibungkus, berikan callback ke prop innerRef.

Seperti itu:

<StyledDiv innerRef={el => { this.el = el }} />

Kemudian Anda dapat mengaksesnya langsung di dalam fungsi "handleClickOutside":

handleClickOutside = e => {
    if (this.el && !this.el.contains(e.target)) {
        console.log('clicked outside')
    }
}

Ini juga berlaku untuk pendekatan "onBlur":

componentDidMount(){
    this.el.focus()
}
blurHandler = () => {
    console.log('clicked outside')
}
render(){
    return(
        <StyledDiv
            onBlur={this.blurHandler}
            tabIndex="0"
            innerRef={el => { this.el = el }}
        />
    )
}
Flatlinediver
sumber
2

Perhatian terbesar saya dengan semua jawaban lain adalah harus memfilter acara klik dari root / induk ke bawah. Saya menemukan cara termudah adalah dengan hanya mengatur elemen saudara dengan posisi: tetap, indeks-z 1 di belakang dropdown dan menangani acara klik pada elemen tetap di dalam komponen yang sama. Menjaga agar semuanya terpusat pada komponen tertentu.

Kode contoh

#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;
    }
}
metroninja
sumber
1
Hmm bagus. Apakah ini akan berfungsi dengan banyak instance? Atau apakah setiap elemen tetap berpotensi memblokir klik untuk yang lain?
Thijs Koerselman
2
componentWillMount(){

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

handleClickOutside(event) {

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

Acara path[0]adalah item terakhir yang diklik

Ivan Sanchez
sumber
3
Pastikan Anda menghapus pendengar acara ketika komponen dilepas untuk mencegah masalah manajemen memori, dll.
agm1984
2

Saya menggunakan modul ini (saya tidak memiliki hubungan dengan penulis)

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

Itu melakukan pekerjaan dengan baik.

ErichBSchulz
sumber
Ini bekerja dengan luar biasa! Jauh lebih sederhana daripada banyak jawaban lain di sini dan tidak seperti setidaknya 3 solusi lain di sini di mana bagi mereka semua saya menerima "Support for the experimental syntax 'classProperties' isn't currently enabled"ini hanya bekerja langsung di luar kotak. Bagi siapa pun yang mencoba solusi ini, ingatlah untuk menghapus kode yang tersisa yang mungkin telah Anda salin saat mencoba solusi lain. mis. menambahkan acara, pendengar tampaknya bertentangan dengan ini.
Frikster
2

Saya melakukan ini sebagian dengan mengikuti ini dan dengan mengikuti dokumen resmi Bereaksi tentang penanganan referensi yang memerlukan reaksi ^ 16.3. Ini adalah satu-satunya hal yang berhasil bagi saya setelah mencoba beberapa saran lainnya di sini ...

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***
)}}
RawCode
sumber
1

Contoh dengan Strategi

Saya suka solusi yang disediakan yang digunakan untuk melakukan hal yang sama dengan membuat pembungkus di sekitar komponen.

Karena ini lebih merupakan perilaku saya memikirkan Strategi dan muncul sebagai berikut.

Saya baru dengan Bereaksi dan saya perlu sedikit bantuan untuk menghemat beberapa boilerplate dalam kasus penggunaan

Harap tinjau dan beri tahu saya pendapat Anda.

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

Contoh Penggunaan

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() {...}
}

Catatan

Saya berpikir untuk menyimpan componentkait siklus hidup yang telah berlalu dan menimpanya dengan metode yang serupa dengan ini:

const baseDidMount = component.componentDidMount;

component.componentDidMount = () => {
  this.enable();
  baseDidMount.call(component)
}

componentadalah komponen yang diteruskan ke konstruktor ClickOutsideBehavior.
Ini akan menghapus mengaktifkan / menonaktifkan boilerplate dari pengguna perilaku ini tetapi tidak terlihat sangat bagus

kidroca
sumber
1

Dalam kasus DROPDOWN saya , solusi Ben Bud bekerja dengan baik, tetapi saya memiliki tombol sakelar terpisah dengan pengendali onClick. Jadi logika mengklik luar bertentangan dengan tombol onClick toggler. Inilah cara saya menyelesaikannya dengan mengirimkan ref tombol juga:

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>
  );
}
Ilyas Assainov
sumber
0

Jika Anda ingin menggunakan komponen kecil (466 Byte gzipped) yang sudah ada untuk fungsi ini, maka Anda dapat melihat pustaka ini reaksi-outclick .

Hal yang baik tentang pustaka adalah ia juga memungkinkan Anda mendeteksi klik di luar komponen dan di dalam komponen lainnya. Ini juga mendukung mendeteksi jenis acara lainnya.

Tushar Sharma
sumber
0

Jika Anda ingin menggunakan komponen kecil (466 Byte gzipped) yang sudah ada untuk fungsi ini, maka Anda dapat melihat pustaka ini reaksi-outclick . Itu menangkap peristiwa di luar komponen reaksi atau elemen JSX.

Hal yang baik tentang pustaka adalah ia juga memungkinkan Anda mendeteksi klik di luar komponen dan di dalam komponen lainnya. Ini juga mendukung mendeteksi jenis acara lainnya.

Tushar Sharma
sumber
0

Saya tahu ini adalah pertanyaan lama, tetapi saya terus menemukan ini dan saya memiliki banyak kesulitan dalam mencari tahu ini dalam format sederhana. Jadi jika ini akan membuat hidup siapa pun sedikit lebih mudah, gunakan OutsideClickHandler dengan airbnb. Ini adalah plugin paling sederhana untuk menyelesaikan tugas ini tanpa menulis kode Anda sendiri.

Contoh:

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>
 )
}
Sam Nikaein
sumber
0
import { useClickAway } from "react-use";

useClickAway(ref, () => console.log('OUTSIDE CLICKED'));
Melounek
sumber
0

Anda dapat Anda cara sederhana untuk menyelesaikan masalah Anda, saya tunjukkan:

....

const [dropDwonStatus , setDropDownStatus] = useState(false)

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

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

ini bekerja untuk saya, semoga berhasil;)

Moshtaba Darzi
sumber
-1

Saya menemukan ini dari artikel di bawah ini:

render () {return ({this.node = node;}}> Toggle Popover {this.state.popupVisible && (Saya popover!)}); }}

Berikut ini adalah artikel yang bagus tentang masalah ini: "Menangani klik di luar komponen Bereaksi" https://larsgraubner.com/handle-outside-clicks-react/

Amanda Koster
sumber
Selamat Datang di Stack Overflow! Jawaban yang hanya berupa tautan pada umumnya disukai: meta.stackoverflow.com/questions/251006/… . Di masa depan, sertakan contoh kutipan atau kode dengan informasi yang relevan dengan pertanyaan tersebut. Bersulang!
Fred Stark
-1

Tambahkan onClickpawang ke wadah tingkat atas Anda dan tambahkan nilai status setiap kali pengguna mengklik di dalam. Berikan nilai itu ke komponen yang relevan dan kapan pun nilai berubah, Anda dapat melakukan pekerjaan.

Dalam hal ini kami menelepon this.closeDropdown()setiap kali clickCountnilainya berubah.

The incrementClickCountMetode kebakaran dalam .appwadah tetapi tidak .dropdownkarena kami menggunakan event.stopPropagation()untuk mencegah acara menggelegak.

Kode Anda mungkin terlihat seperti ini:

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
sumber
Tidak yakin siapa yang downvoting tetapi solusi ini cukup bersih dibandingkan dengan pendengar acara yang mengikat / tidak mengikat. Saya tidak memiliki masalah dengan implementasi ini.
wdm
-1

Untuk membuat solusi 'fokus' berfungsi untuk dropdown dengan pendengar acara, Anda dapat menambahkannya dengan acara onMouseDown alih-alih onClick . Dengan begitu acara akan terbuka dan setelah itu popup akan ditutup seperti ini:

<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>
                }
idtmc
sumber
-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");
    }
  }
}
momen dipol
sumber
Dan jangan lupa untukimport ReactDOM from 'react-dom';
dipole_moment
-1

Saya membuat solusi untuk semua kesempatan.

Anda harus menggunakan Komponen Orde Tinggi untuk membungkus komponen yang ingin Anda dengarkan untuk klik di luarnya.

Contoh komponen ini hanya memiliki satu penyangga: "onClickedOutside" yang menerima fungsi.

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>;
  }
}
Higor Lorenzon
sumber
-1

UseOnClickOutside Hook - React 16.8 +

Buat fungsi useOnOutsideClick secara umum

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, []);

Kemudian gunakan pengait di komponen fungsional apa pun.

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

};

Tautan ke demo

Sebagian terinspirasi oleh jawaban @ pau1fitzgerald.

Ben Carp
sumber