React bileşeninin dışındaki tıklamayı tespit et


410

Bu makalede açıklandığı gibi, bir bileşenin dışında bir tıklama olayı olup olmadığını tespit etmek için bir yol arıyorum . jQuery closest (), bir tıklama etkinliğinden gelen hedefin üst öğelerinden biri olarak dom öğesine sahip olup olmadığını görmek için kullanılır. Bir eşleşme varsa, tıklama etkinliği çocuklardan birine aittir ve bu nedenle bileşenin dışında sayılmaz.

Bileşenime pencereye bir tıklama işleyicisi eklemek istiyorum. İşleyici ateşlediğinde, hedefi bileşenimin dom çocukları ile karşılaştırmam gerekiyor.

Click olayı, etkinliğin gittiği dom yolunu elinde tutan "yol" gibi özellikler içerir. Ne karşılaştırmak ya da en iyi nasıl geçmek için emin değilim ve sanırım birisi zaten bir akıllı yarar işlevine koymuş olmalı ... Hayır?


Tıklama işleyicisini pencereden ziyade üst öğeye ekleyebilir misiniz?
J. Mark Stevens

Bu öğeye veya alt öğelerinden birine tıklandığında bildiğiniz üst öğeye bir tıklama işleyicisi eklerseniz, ancak tıklatılan diğer tüm yerleri algılamam gerekir , bu nedenle işleyicinin pencereye eklenmesi gerekir.
Thijs Koerselman

Önceki yanıttan sonra makaleye baktım. Üst bileşende bir clickState ayarlama ve çocuklardan tıklama işlemlerini geçirme hakkında. Sonra açık yakın durumunu yönetmek için çocuklarda sahne kontrol ediyorum.
J. Mark Stevens

En üstteki bileşen benim uygulamam olacak. Ancak, dinleme bileşeni birkaç seviye derinliğinde ve dom'da katı bir konuma sahip değil. Uygulamamdaki tüm bileşenlere tıklama işleyicileri ekleyemiyorum, çünkü bunlardan biri bunun dışında bir yere tıklayıp tıklamadığınızı bilmek istiyor. Diğer bileşenler bu mantığın farkında olmamalıdır, çünkü bu korkunç bağımlılıklar ve kaynak plakası yaratacaktır.
Thijs Koerselman

5
Sana çok hoş bir lib tavsiye ederim. AirBnb tarafından oluşturuldu: github.com/airbnb/react-outside-click-handler
Osoian Marcel

Yanıtlar:


683

React 16.3+ sürümünde başvuru kullanımı değişti.

Aşağıdaki çözüm ES6'yı kullanır ve ref için bir yöntemle ayarlamanın yanı sıra ciltleme için en iyi uygulamaları izler.

Çalışırken görmek için:

Sınıf Uygulaması:

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

Kanca Uygulaması:

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
Bunu nasıl test edersin? HandleClickOutside işlevine nasıl geçerli bir event.target gönderirsiniz?
csilk

5
document.addEventListenerBurada yerine React'in sentetik olaylarını nasıl kullanabilirsiniz ?
Ralph David Abernathy

9
@ polkovnikov.ph 1. contextsadece yapıcıda kullanılırsa argüman olarak gereklidir. Bu durumda kullanılmaz. Reaksiyon ekibinin propsyapıcıda bir argüman olarak önerilmesini önermesinin nedeni, çağrıdan this.propsönce kullanımının super(props)tanımsız olması ve hatalara yol açabilmesidir. contextiç düğümlerde hala kullanılabilir, yani tüm amacı budur context. Bu şekilde, sahne malzemelerinde olduğu gibi bileşenden bileşene geçirmeniz gerekmez. 2. Bu sadece stilistik bir tercihtir ve bu durumda tartışmayı garanti etmez.
Ben Bud

7
Şu hatayı alıyorum: "this.wrapperRef.contains bir işlev değil"
Bogdan

5
@Bogdan, tarz bir bileşen kullanırken aynı hatayı alıyordum. En üst düzeyde bir <div> kullanmayı düşünün
user3040068

149

Kapsayıcıya olaylar eklemeden benim için en uygun çözüm:

Bazı HTML öğeleri "olarak bilinen olabilir odak örneği giriş elemanları,". Bu öğeler, bu odağı kaybettiklerinde bulanıklaştırma olayına da yanıt verecektir .

Herhangi bir öğeye odaklanma kapasitesi vermek için tabindex özelliğinin -1 dışında bir değere ayarlandığından emin olun. Normal HTML'de, tabindexözniteliği ayarlayarak olur , ancak React'ta kullanmak zorundasınız tabIndex(büyük harf not edin I).

Ayrıca JavaScript ile element.setAttribute('tabindex',0)

Özel bir DropDown menüsü yapmak için bunu kullanıyordum.

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
Bu, erişilebilirlikle ilgili sorunlar yaratmaz mı? Fazladan bir öğeyi sadece ne zaman içeri / dışarı odaklandığını algılamak için odaklanabilir hale getirdiğiniz için, etkileşim etkileşim açılır değerlerinde olmalı hayır?
Thijs Koerselman

2
Bu çok ilginç bir yaklaşım. Durumum için mükemmel çalıştı, ancak bunun erişilebilirliği nasıl etkileyebileceğini öğrenmek isterim.
klinore

17
Ben bir ölçüde bu "çalışma" var, onun serin küçük bir kesmek. Benim sorunum, açılır içeriğimin display:absoluteaçılır listenin üst div'ın boyutunu etkilememesi. Bu, açılır menüdeki bir öğeyi tıkladığımda onblur tetiklenir.
Dave

2
Bu yaklaşımla ilgili sorunlar yaşadım, açılır içerikteki herhangi bir öğe onClick gibi etkinlikleri tetiklemiyor.
AnimaSola

8
Açılır içerik, örneğin form girişleri gibi bazı odaklanabilir öğeler içeriyorsa başka sorunlarla karşılaşabilirsiniz. Odağınızı çalacak, onBlur açılır kutunuzda tetiklenecek ve expanded false olarak ayarlanacaklar.
Kamagatos

106

Aynı konuya takıldım. Buradaki partiye biraz geç kaldım, ama benim için bu gerçekten iyi bir çözüm. Umarım bir başkasına yardım eder. findDOMNodeBuradan içe aktarmanız gerekiyorreact-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
        });
    }
}

Reaksiyon Kancaları Yaklaşımı (16.8 +)

Yeniden adlandırılan bir kanca oluşturabilirsiniz 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 };
}

Ardından bileşende aşağıdakileri yapmak için işlevsellik eklemek istiyorsunuz:

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

}

Burada bir codeandbox örneği bulun .


1
@LeeHanKyeol Tamamen değil - bu cevap olay işleme yakalama aşamasında olay işleyicileri çağırırken, kabarcık aşaması sırasında olay işleyicileri çağırır.
stevejay

3
Bu kabul edilen cevap olmalı. Mutlak konumlandırma içeren açılır menüler için mükemmel çalıştı.
dishwasherWithProgrammingSkill

1
ReactDOM.findDOMNodeve uygun bulunmamaktadır, kullanması gereken refgeri aramalar: github.com/yannickcr/eslint-plugin-react/issues/...
dain

2
harika ve temiz bir çözüm
Karim

1
200ms sonra görünür geri 'true' sıfırlamak için bileşen kesmek zorunda olmasına rağmen bu benim için çalıştı (aksi takdirde benim menü bir daha asla göstermez). Teşekkür ederim!
Lee Moe

87

Burada birçok yöntem denedikten sonra github.com/Pomax/react-onclickoutside kullanmaya karar verdim , ne kadar eksiksiz olduğu .

Modülü npm ile kurdum ve bileşenime aktardım:

import onClickOutside from 'react-onclickoutside'

Sonra, bileşen sınıfımda handleClickOutside yöntemi :

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

Ve bileşenimi dışa aktarırken onClickOutside() :

export default onClickOutside(NameOfComponent)

Bu kadar.


2
Pablo'nun önerdiği tabIndex/ onBluryaklaşımına göre bunun somut avantajları var mı? Uygulama nasıl çalışır ve davranışı onBluryaklaşımdan nasıl farklıdır ?
Mark Amery

4
Bir tabIndex kesmek kullanarak özel bir bileşen kullanıcı daha iyidir. Olumlu oy!
Atul Yadav

5
@MarkAmery - tabIndex/onBlurYaklaşım hakkında bir yorum yaptım . Açılır durumdayken, position:absolutediğer içeriklerin üzerine fareyle gelen bir menü gibi çalışmaz .
Beau Smith

4
Bunu tabindex üzerinde kullanmanın bir diğer avantajı, bir alt öğeye odaklanırsanız tabindex çözümünün de bir bulanıklık olayı
başlatmasıdır

1
@BeauSmith - Çok teşekkürler! Günümü kurtardım!
Mika

38

Discuss.reactjs.org'daki Ben Alpert sayesinde bir çözüm buldum . Önerilen yaklaşım, belgeye bir işleyici ekler, ancak bu sorunlu olduğu ortaya çıktı. Ağacımdaki bileşenlerden birini tıklattığınızda, güncelleştirme sırasında tıklanan öğeyi kaldıran bir yeniden oluşturmayla sonuçlandı. Çünkü React'tan gelen kişi olur önce ifadesi, belge gövdesi işleyicisi çağrılmadan için, öğe ağacın "içinde" olarak algılanmadı.

Bunun çözümü, işleyiciyi uygulama kök öğesine eklemekti.

ana:

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

bileşen:

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
Bu benim için artık React 0.14.7 ile çalışmıyor - belki React bir şey değiştirdi, ya da belki kodu React'teki tüm değişikliklere uyarlarken bir hata yaptım. Bunun yerine bir cazibe gibi çalışan github.com/Pomax/react-onclickoutside kullanıyorum .
Nicole

1
Hmm. Bunun çalışması için hiçbir neden göremiyorum. Bir uygulamanın kök DOM düğümündeki bir işleyicinin, başka bir işleyici tarafından tetiklenmeyen bir yeniden oluşturucudan önce neden tetiklenmeyeceği garanti edilmelidir document?
Mark Amery

1
Saf bir UX noktası: mousedownmuhtemelen clickburadakinden daha iyi bir işleyici olacaktır . Çoğu uygulamada, menüyü tıklatarak-dışa-kapat davranışı, bıraktığınızda değil, farenizi aşağı doğru hareket ettiğiniz anda gerçekleşir. Örneğin, Stack Overflow'un bayrağıyla veya paylaşım diyaloglarıyla veya tarayıcınızın üst menü çubuğundaki açılır listelerden biriyle deneyin .
Mark Amery

ReactDOM.findDOMNodeve dize refkaldırılıyor, kullanması gereken refgeri aramalar: github.com/yannickcr/eslint-plugin-react/issues/...
Dain

Bu en basit çözüm ve hatta takarken bile benim için mükemmel çalışıyordocument
Flion

37

Tanner Linsley'in JSConf Hawaii 2020'deki mükemmel konuşmasına dayanan kanca uygulaması :

useOuterClick Hook API'sı

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

uygulama

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

useOuterClickmarkaları kullanımı değişken hakemlerimizle için yalın API oluşturulması Client. Bir geri çağırma, üzerinden hatırlamak zorunda kalmadan ayarlanabilir useCallback. Geri arama gövdesi hala en yeni sahne ve durumlara erişime sahiptir (eski kapanma değerleri yoktur).


İOS kullanıcıları için not

iOS yalnızca belirli öğelere tıklanabilir olarak davranır. Bu davranışı atlatmak için, dışında bir dış tıklama dinleyicisi seçin document- yukarı doğru hiçbir şey body. Örneğin div, yukarıdaki örnekte React köküne bir dinleyici ekleyebilir ve height: 100vhtüm dış tıklamaları yakalamak için yüksekliğini ( veya benzerini) genişletebilirsiniz . Kaynaklar: quirksmode.org ve bu cevap


cepte çalışmaz o kadar da unutulmaz
Omar

@Omar sadece bir android cihaz üzerinde Firefox 67.0.3 ile benim yazı Codesandbox test - hepsi iyi çalışıyor. Hangi tarayıcıyı kullandığınızı, hatanın ne olduğunu vs. açıklayabilir misiniz?
ford04

Hata yok ama krom, iphone 7+ üzerinde çalışmıyor. Dışarıdaki muslukları algılamaz. Mobil cihazlardaki krom geliştirme araçlarında çalışır, ancak gerçek bir ios cihazında çalışmaz.
Omar

1
@ ford04 kazma için teşekkür ederim, ben aşağıdaki hile öneren başka bir iş parçacığına tökezledi . @media (hover: none) and (pointer: coarse) { body { cursor:pointer } }Bunu global stillerime eklemek, sorunu düzeltti gibi görünüyor.
Kostas Sarantopoulos

1
@ ford04 evet, btw bu konuya göre @supports (-webkit-overflow-scrolling: touch)ben daha ne kadar bilmiyorum bile sadece IOS hedefler daha da özel bir medya sorgusu var! Sadece test ettim ve işe yarıyor.
Kostas Sarantopoulos

30

Buradaki diğer cevapların hiçbiri benim için işe yaramadı. Bulanıklık üzerinde bir pop-up gizlemeye çalışıyordum, ama içeriği kesinlikle konumlandırılmış olduğundan, onBlur bile iç içeriklerin tıklandığında ateş ediyordu.

İşte benim için işe yarayan bir yaklaşım:

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

Bu yardımcı olur umarım.


Çok iyi! Teşekkürler. Ama benim durumumda div için mutlak konumu ile "geçerli yer" yakalamak için bir sorun oldu, bu yüzden fare ile div bırakın tüm div devre dışı bırakmak için giriş ve açılan takvim ile div için "OnMouseLeave" kullanın.
Alex

1
Teşekkürler! Bana çok yardım et.
Angelo Lucas

1
Bunu ekli olan yöntemlerden daha iyi seviyorum document. Teşekkürler.
kyw

Bunun çalışması için, tıklanan öğenin (relatedTarget) odaklanabilir olduğundan emin olmanız gerekir. Görmek Stackoverflow.com/questions/42764494/…
xabitrigo

21

[Güncelleme] ile çözüm ^ 16.8 tepki kullanılarak Kancalar

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;

Reaksiyon ^ 16.3 ile çözüm :

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
Bu artık çözüm. Hatayı .contains is not a function<div>
alıyorsanız

refReact.forwardRef
Prop'u

4

İşte yaklaşımım (demo - https://jsfiddle.net/agymay93/4/ ):

Ben denilen özel bir bileşen oluşturdum WatchClickOutsideve gibi kullanılabilir ( JSXsözdizimi varsayalım ):

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

İşte WatchClickOutsidebileşen kodu :

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

Harika bir çözüm - kullanımım için, kısa içerikli sayfalarda gövde yerine dokümanı dinlemeye ve dize yerine dom referansına değiştirdim: jsfiddle.net/agymay93/9
Billy Moon

ve div yerine span yerine span kullandı ve tıklamaları gövde dışına taşıma demosu eklendi: jsfiddle.net/agymay93/10
Billy Moon

Dize refkullanımdan kaldırılmıştır, kullanması gereken refgeri aramalar: reactjs.org/docs/refs-and-the-dom.html
Dain

4

Bunun zaten birçok yanıtı var, ancak e.stopPropagation()kapatmak istediğiniz öğenin dışındaki reaksiyon bağlantılarına tıklamaktan ve bunları önlemekten kaçınmıyorlar.

React'in kendi yapay olay işleyicisine sahip olması nedeniyle, olay dinleyicileri için temel olarak belge kullanamazsınız. e.stopPropagation()React belgenin kendisini kullandığından , bundan önce yapmanız gerekir . document.querySelector('body')Bunun yerine örneğin kullanırsanız. Tepki bağlantısından tıklamayı engelleyebilirsiniz. Aşağıda, dış ve yakın tıklamayı nasıl uyguladığımın bir örneği verilmiştir. ES6 ve React 16.3
kullanır .

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

Mutlak konumlandırmaya ihtiyaç duyanlar için, seçtiğim basit bir seçenek, tüm sayfayı şeffaf bir arka planla kaplayacak şekilde tasarlanmış bir sargı bileşeni eklemektir. Ardından, iç bileşeninizi kapatmak için bu öğeye bir onClick ekleyebilirsiniz.

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

İçeriğe bir tıklama işleyicisi eklerseniz şu an olduğu gibi, etkinlik üst div'a da yayılacak ve bu nedenle handlerOutsideClick'i tetikleyecektir. İstediğiniz davranış bu değilse, işleyicinizdeki olay ilerlemesini durdurmanız yeterlidir.

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

'


Bu yaklaşımla ilgili sorun, İçeriğe herhangi bir tıklama alamamanızdır; bunun yerine div, tıklamayı alır.
rbonick

İçerik div'de olduğundan, olay yayılımını durdurmazsanız her ikisi de tıklamayı alır. Bu genellikle istenen bir davranıştır, ancak tıklamanın div öğesine yayılmasını istemiyorsanız, Click işleyicisindeki içeriğinizdeki eventPropagation'ı durdurmanız yeterlidir. Nasıl olduğunu göstermek için cevabımı güncelledim.
Anthony Garant

3

Ben Bud'ın kabul ettiği cevabı uzatmak stilize edilmiş bileşenler kullanıyorsanız, ref'leri bu şekilde iletmek size "this.wrapperRef.contains bir işlev değildir" gibi bir hata verecektir.

Tavsiye edilen düzeltme, yorumlarda, stil edilen bileşeni bir div ile sarmak ve ref'yi oradan geçirmek için çalışır. Bununla birlikte, belgelerinde bunun nedenini ve stil bileşenlerinde ref'lerin uygun kullanımını zaten açıklamışlardır:

Stillendirilmiş bir bileşene ref prop iletilmesi size StyledComponent paketleyicisinin bir örneğini verir, ancak alttaki DOM düğümüne değil. Bunun nedeni ref'lerin çalışma şeklidir. Odak gibi DOM yöntemlerini doğrudan sarmalayıcılarımıza çağırmak mümkün değildir. Gerçek, sarılmış DOM düğümüne bir başvuru almak için, geri çağrıyı bunun yerine innerRef prop'e iletin.

Şöyle ki:

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

Daha sonra doğrudan "handleClickOutside" işlevi içinden erişebilirsiniz:

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

Bu, "onBlur" yaklaşımı için de geçerlidir:

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


2

Diğer tüm yanıtlarla ilgili en büyük endişem, tıklama olaylarını kökten / üst öğeden aşağıya filtrelemek. En kolay yolun sadece konumlu bir kardeş öğesi ayarlamak olduğunu tespit ettik: sabit, açılır listenin arkasında bir z-endeksi 1 ve aynı bileşenin içindeki sabit elemandaki tıklama olayını işlemek. Her şeyi belirli bir bileşende merkezileştirir.

Örnek kod

#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
Hmm şık. Birden çok örnekle çalışır mı? Yoksa her bir sabit eleman diğerleri için tıklamayı engeller mi?
Thijs Koerselman

2
componentWillMount(){

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

handleClickOutside(event) {

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

Etkinlik path[0], tıklanan son öğedir


3
Bellek yönetimi sorunlarını vb. Önlemek için bileşen bağlantısını
kestiğinde

2

Bu modülü kullandım (yazarla hiçbir ilişkim yok)

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

İşi güzel yaptı.


Bu harika çalışıyor! Buradaki diğer birçok cevaptan çok daha basit ve burada en az 3 diğer çözümün aksine, hepsi için aldığım "Support for the experimental syntax 'classProperties' isn't currently enabled"bu sadece kutunun dışında çalıştı. Bu çözümü deneyen herkes için, diğer çözümleri denerken kopyalamış olabileceğiniz kalan kodu silmeyi unutmayın. örneğin olay dinleyicileri eklemek bununla çelişiyor gibi görünüyor.
Frikster

2

Kısmen uygulayarak bunu bu ve ^ 16.3 tepki gerektiren ref taşıma resmi belgeler tepki izleyerek. Buradaki diğer önerileri denedikten sonra benim için işe yarayan tek şey bu ...

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

Strateji ile bir örnek

Bileşenin etrafında bir sarıcı oluşturarak aynı şeyi yapmak için kullanılan çözümleri seviyorum.

Bu daha çok bir davranış olduğundan, Stratejiyi düşündüm ve aşağıdakileri buldum.

React ile yeniyim ve kullanım durumlarında bazı kazanları kaydetmek için biraz yardıma ihtiyacım var

Lütfen bana ne düşündüğünü söyle.

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

Örnek Kullanımı

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

notlar

Geçilen componentyaşam döngüsü kancalarını saklamayı ve buna benzeyen yöntemlerle geçersiz kılmayı düşündüm :

const baseDidMount = component.componentDidMount;

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

component'nin yapıcısına iletilen bileşendir ClickOutsideBehavior.
Bu, bu davranışın kullanıcıdan etkinleştirme / devre dışı bırakma plakasını kaldıracak, ancak çok hoş görünmüyor


1

DROPDOWN durumumda Ben Bud'ın çözümü iyi çalıştı, ancak bir onClick işleyicisine sahip ayrı bir geçiş düğmem vardı. Bu nedenle, dış tıklama mantığı, tıklatma düğmesindeki düğmeyle çakıştı. Ben de düğmenin ref de geçirerek nasıl çözüldü:

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

Bu işlevsellik için zaten var olan küçük bir bileşen (466 Byte gzip) kullanmak istiyorsanız, bu kütüphane tepki tepkisini kontrol edebilirsiniz .

Kütüphane ile ilgili iyi bir şey, bir bileşenin dışında ve bir başkasının içindeki tıklamaları tespit etmenize izin vermesidir. Ayrıca diğer olay türlerinin algılanmasını destekler.


0

Bu işlevsellik için zaten var olan küçük bir bileşen (466 Byte gzip) kullanmak istiyorsanız, bu kütüphane tepki tepkisini kontrol edebilirsiniz . Bir reaksiyon bileşeninin veya jsx öğesinin dışındaki olayları yakalar.

Kütüphane ile ilgili iyi bir şey, bir bileşenin dışında ve bir başkasının içindeki tıklamaları tespit etmenize izin vermesidir. Ayrıca diğer olay türlerinin algılanmasını destekler.


0

Bunun eski bir soru olduğunu biliyorum, ama bununla karşılaşmaya devam ediyorum ve bunu basit bir biçimde anlamakta çok zorlandım. Bu, anyonların hayatını biraz daha kolay hale getirirse, airbnb tarafından OutsideClickHandler kullanın. Kendi kodunuzu yazmadan bu görevi gerçekleştirmek için en basit eklentidir.

Misal:

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

size sorunu çözmek için basit bir yol olabilir, ben size göstermek:

....

const [dropDwonStatus , setDropDownStatus] = useState(false)

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

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

bu benim için çalışıyor, iyi şanslar;)


-1

Bunu aşağıdaki makaleden buldum:

render () {return ({this.node = node;}}> Popover'ı aç / kapat {this.state.popupVisible && (Ben bir popover'ım!)}); }}

Bu sorunla ilgili harika bir makale: "React bileşenleri dışındaki tıklamaları işleme" https://larsgraubner.com/handle-outside-clicks-react/


Stack Overflow'a hoş geldiniz! Sadece bir bağlantı olan cevaplar genellikle şu kişiden hoşlanır : meta.stackoverflow.com/questions/251006/… . Gelecekte, soruyla ilgili bilgileri içeren bir alıntı veya kod örneği ekleyin. Şerefe!
Fred Stark

-1

onClickÜst düzey kapsayıcınıza bir işleyici ekleyin ve kullanıcı her tıkladığında bir durum değerini artırın. Bu değeri ilgili bileşene iletin ve değer her değiştiğinde çalışabilirsiniz.

Bu durumda biz diyoruz this.closeDropdown()zaman clickCountdeğeri değiştirir.

incrementClickCountİçinde yöntem yangınları .appkap ama .dropdownkullandığımız çünkü event.stopPropagation()olay baloncuklanmasını önlemek için.

Kodunuz şu şekilde görünebilir:

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

Kimin aşağıya düştüğünden emin değilim ancak bu çözüm, ciltleme / bağlama olayı dinleyicilerine kıyasla oldukça temiz. Bu uygulamada sıfır sorun yaşadım.
wdm

-1

Yapmak için 'odak' olay dinleyicileri ile açılan çözüm çalışmaları sizinle ekleyebilirsiniz onMouseDown yerine olay onClick . Bu şekilde etkinlik tetiklenir ve bundan sonra pop-up şöyle kapanır:

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

Ve unutmayınimport ReactDOM from 'react-dom';
dipole_moment

-1

Tüm durumlar için bir çözüm hazırladım.

Dışındaki tıklamaları dinlemek istediğiniz bileşeni sarmak için Yüksek Sipariş Bileşeni kullanmalısınız.

Bu bileşen örneğinde yalnızca bir pervane vardır: "onClickedOutside" bir işlev alır.

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 Kanca - Tepki 16.8 +

Genel bir useOnOutsideClick işlevi oluşturma

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

Daha sonra kancayı herhangi bir fonksiyonel bileşende kullanın.

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

};

Demo bağlantısı

Kısmen ilham @ pau1fitzgerald cevap.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.