ตรวจหาการคลิกนอกองค์ประกอบการตอบโต้


410

ฉันกำลังมองหาวิธีการในการตรวจสอบหากมีเหตุการณ์เกิดขึ้นคลิกนอกขององค์ประกอบที่อธิบายไว้ในนี้บทความ jQuery nearby () ใช้เพื่อดูว่าเป้าหมายจากเหตุการณ์คลิกมีองค์ประกอบ dom เป็นหนึ่งในพาเรนต์หรือไม่ หากมีการแข่งขันเหตุการณ์คลิกเป็นของเด็ก ๆ คนหนึ่งและดังนั้นจึงไม่ถือว่าอยู่นอกองค์ประกอบ

ดังนั้นในองค์ประกอบของฉันฉันต้องการแนบตัวจัดการการคลิกกับหน้าต่าง เมื่อผู้ควบคุมยิงฉันต้องเปรียบเทียบเป้าหมายกับลูกโดมขององค์ประกอบของฉัน

เหตุการณ์คลิกมีคุณสมบัติเช่น "เส้นทาง" ซึ่งดูเหมือนว่าจะเก็บเส้นทาง dom ที่เหตุการณ์ได้เดินทาง ฉันไม่แน่ใจว่าจะเปรียบเทียบอะไรหรือจะสำรวจให้ดีที่สุดได้อย่างไรและฉันคิดว่าบางคนต้องใส่มันในฟังก์ชั่นยูทิลิตี้ที่ฉลาดอยู่แล้ว ... ไม่?


คุณสามารถแนบตัวจัดการการคลิกกับพาเรนต์แทนที่จะเป็นหน้าต่างได้หรือไม่?
J. Mark Stevens

หากคุณแนบตัวจัดการคลิกไปยังพาเรนต์ที่คุณรู้จักเมื่อองค์ประกอบนั้นหรือลูกของพวกเขาถูกคลิก แต่ฉันต้องตรวจสอบสถานที่อื่น ๆทั้งหมดที่ถูกคลิกดังนั้นตัวจัดการต้องแนบกับหน้าต่าง
Thijs Koerselman

ฉันดูบทความหลังจากคำตอบก่อนหน้านี้ วิธีการเกี่ยวกับการตั้ง clickState ในองค์ประกอบด้านบนและผ่านการกระทำคลิกจากเด็ก ๆ จากนั้นคุณจะตรวจสอบอุปกรณ์ประกอบฉากในเด็ก ๆ เพื่อจัดการสถานะเปิดปิด
J. Mark Stevens

องค์ประกอบด้านบนจะเป็นแอพของฉัน แต่องค์ประกอบการฟังนั้นลึกหลายระดับและไม่มีตำแหน่งที่เข้มงวดในโดม ฉันไม่สามารถเพิ่มตัวจัดการการคลิกไปที่ส่วนประกอบทั้งหมดในแอปของฉันได้เพราะหนึ่งในนั้นสนใจที่จะรู้ว่าคุณคลิกที่ด้านนอกหรือไม่ ส่วนประกอบอื่น ๆ ไม่ควรทราบถึงเหตุผลนี้เพราะจะสร้างการพึ่งพาที่น่ากลัวและรหัสสำเร็จรูป
Thijs Koerselman

5
ฉันอยากจะแนะนำให้คุณรู้จักกับ lib ที่ดีมาก สร้างโดย AirBnb: github.com/airbnb/react-outside-click-handler
Osoian Marcel

คำตอบ:


681

เปลี่ยนการใช้งานใน React 16.3+ แล้ว

โซลูชันต่อไปนี้ใช้ ES6 และปฏิบัติตามแนวทางปฏิบัติที่ดีที่สุดสำหรับการผูกรวมถึงการตั้งค่าการอ้างอิงผ่านวิธีการ

หากต้องการดูการทำงาน:

การใช้งานระดับ:

import React, { Component } from 'react';

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

        this.wrapperRef = React.createRef();
        this.setWrapperRef = this.setWrapperRef.bind(this);
        this.handleClickOutside = this.handleClickOutside.bind(this);
    }

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

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

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

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

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

การใช้งาน 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>;
}

3
คุณทดสอบสิ่งนี้อย่างไร คุณจะส่ง event.target ที่ถูกต้องไปยังฟังก์ชัน handleClickOutside ได้อย่างไร
csilk

5
คุณจะใช้เหตุการณ์สังเคราะห์ของ React แทนที่จะเป็นdocument.addEventListenerที่นี่ได้อย่างไร
Ralph David Abernathy

9
@ polkovnikov.ph 1. contextจำเป็นต้องใช้เป็นอาร์กิวเมนต์ใน Constructor เท่านั้นหากมีการใช้งาน มันไม่ได้ถูกใช้ในกรณีนี้ เหตุผลที่ทีมตอบโต้แนะนำให้มีpropsการโต้แย้งในตัวสร้างเนื่องจากการใช้this.propsก่อนการโทรsuper(props)จะไม่ได้กำหนดและสามารถนำไปสู่ข้อผิดพลาด ยังคงมีอยู่บนโหนดภายในที่เป็นวัตถุประสงค์ทั้งcontext contextวิธีนี้คุณไม่จำเป็นต้องผ่านมันลงมาจากส่วนประกอบหนึ่งไปยังอีกองค์ประกอบหนึ่งอย่างที่คุณทำกับอุปกรณ์ประกอบฉาก 2. นี่เป็นเพียงการตั้งค่าโวหารและไม่รับประกันการโต้วาทีในกรณีนี้
Ben Bud

7
ฉันได้รับข้อผิดพลาดต่อไปนี้: "this.wrapperRef.contain ไม่ใช่ฟังก์ชั่น"
Bogdan

5
@ Bogdan ฉันได้รับข้อผิดพลาดเดียวกันเมื่อใช้องค์ประกอบที่มีสไตล์ ลองใช้ <div> ที่ระดับบนสุด
user3040068

149

นี่คือทางออกที่ดีที่สุดสำหรับฉันโดยไม่ต้องแนบกิจกรรมเข้ากับคอนเทนเนอร์:

องค์ประกอบ HTML บางอย่างสามารถมีสิ่งที่เรียกว่า " โฟกัส " เช่นองค์ประกอบอินพุต องค์ประกอบเหล่านั้นจะตอบสนองต่อเหตุการณ์เบลอเมื่อพวกเขาสูญเสียโฟกัส

เพื่อให้องค์ประกอบความสามารถในการมีโฟกัสให้ตรวจสอบให้แน่ใจว่าแอตทริบิวต์ tabindex นั้นถูกตั้งค่าเป็นอย่างอื่นที่ไม่ใช่ -1 ใน HTML ปกติที่จะมีการตั้งค่าtabindexคุณลักษณะ แต่ในการตอบสนองคุณต้องใช้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

2
นี่เป็นวิธีที่น่าสนใจมาก ทำงานอย่างสมบูรณ์แบบสำหรับสถานการณ์ของฉัน แต่ฉันสนใจที่จะเรียนรู้ว่าสิ่งนี้อาจส่งผลกระทบต่อการเข้าถึงได้อย่างไร
klinore

17
ฉันมีสิ่งนี้ "ทำงาน" เท่าที่มันแฮ็คตัวเล็ก ๆ ปัญหาของฉันคือเนื้อหาแบบเลื่อนลงของฉันคือdisplay:absoluteเพื่อให้แบบเลื่อนลงจะไม่ส่งผลกระทบต่อขนาดของ div parent ซึ่งหมายความว่าเมื่อฉันคลิกที่รายการในเมนูแบบเลื่อนลงไฟ onblur จะเริ่มทำงาน
เดฟ

2
ฉันมีปัญหากับวิธีการนี้องค์ประกอบใด ๆ ในเนื้อหาแบบเลื่อนลงจะไม่เริ่มต้นเหตุการณ์เช่น onClick
AnimaSola

8
คุณอาจประสบปัญหาอื่น ๆ บางอย่างหากเนื้อหาแบบเลื่อนลงมีองค์ประกอบที่สามารถโฟกัสได้เช่นการป้อนข้อมูลในแบบฟอร์ม พวกเขาจะขโมยโฟกัสของคุณonBlur จะถูกเรียกใช้บนคอนเทนเนอร์ดรอปดาวน์ของคุณและexpanded จะถูกตั้งค่าเป็นเท็จ
Kamagatos

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

}

ค้นหาตัวอย่างcodeandboxที่นี่


1
@LeeHanKyeol ไม่ใช่ทั้งหมด - คำตอบนี้จะเรียกใช้ตัวจัดการเหตุการณ์ระหว่างขั้นตอนการจับภาพของการจัดการเหตุการณ์ในขณะที่คำตอบที่เชื่อมโยงจะเรียกตัวจัดการเหตุการณ์ในช่วงฟอง
stevejay

3
นี่ควรเป็นคำตอบที่ยอมรับได้ ทำงานอย่างสมบูรณ์แบบสำหรับเมนูแบบเลื่อนลงด้วยการจัดตำแหน่งที่แน่นอน
เครื่องล้างจานด้วยโปรแกรม

1
ReactDOM.findDOMNodeและเลิกใช้แล้วควรใช้การrefเรียกกลับ: github.com/yannickcr/eslint-plugin-react/issues/…
dain

2
ทางออกที่ดีและสะอาด
Karim

1
สิ่งนี้ใช้ได้สำหรับฉันแม้ว่าฉันจะต้องแฮ็กส่วนประกอบของคุณเพื่อรีเซ็ตการมองเห็นกลับเป็น 'จริง' หลังจาก 200 มิลลิวินาที (ไม่เช่นนั้นเมนูของฉันจะไม่แสดงอีกเลย) ขอบคุณ!
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
มีข้อได้เปรียบที่เป็นรูปธรรมในเรื่องนี้มากกว่าtabIndex/ onBlurแนวทางที่เสนอโดย Pabloหรือไม่? การปรับใช้ทำงานอย่างไรและพฤติกรรมของมันแตกต่างจากonBlurวิธีการอย่างไร
Mark Amery

4
เป็นการดีกว่าสำหรับผู้ใช้ส่วนประกอบเฉพาะจากนั้นใช้การแฮ็บ tabIndex การประกาศขึ้น!
Atul Yadav

5
@ MarkAmery - ฉันใส่ความคิดเห็นเกี่ยวกับtabIndex/onBlurวิธีการ มันไม่ทำงานเมื่อมีการเลื่อนลงposition:absoluteเช่นเมนูที่อยู่เหนือเนื้อหาอื่น ๆ
Beau Smith

4
ข้อดีอีกอย่างของการใช้สิ่งนี้เหนือ tabindex ก็คือวิธีการแก้ปัญหา tabindex นั้นยังทำให้เกิดเหตุการณ์เบลอถ้าคุณมุ่งเน้นไปที่องค์ประกอบย่อย
Jemar Jones

1
@BeauSmith - ขอบคุณมาก! บันทึกวันของฉัน!
มิกะ

38

ผมพบว่าวิธีการแก้ปัญหาต้องขอบคุณเบนอัลเพิร์บนdiscuss.reactjs.org วิธีการที่แนะนำแนบตัวจัดการกับเอกสาร แต่กลายเป็นปัญหา การคลิกที่หนึ่งในองค์ประกอบในทรีของฉันทำให้เกิดการแสดงซ้ำซึ่งลบองค์ประกอบที่ถูกคลิกออกไป เพราะผู้กระทำจาก React เกิดขึ้นก่อนที่จะเรียกใช้ตัวจัดการเอกสารเอกสารองค์ประกอบจึงไม่ถูกตรวจพบว่าเป็น "ข้างใน" ต้นไม้

วิธีแก้ไขปัญหานี้คือการเพิ่มตัวจัดการในองค์ประกอบรูทแอพพลิเคชัน

หลัก:

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

ส่วนประกอบ:

import { Component, PropTypes } from 'react';
import ReactDOM from 'react-dom';

export default class ClickListener extends Component {

  static propTypes = {
    children: PropTypes.node.isRequired,
    onClickOutside: PropTypes.func.isRequired
  }

  componentDidMount () {
    window.__myapp_container.addEventListener('click', this.handleDocumentClick)
  }

  componentWillUnmount () {
    window.__myapp_container.removeEventListener('click', this.handleDocumentClick)
  }

  /* using fat arrow to bind to instance */
  handleDocumentClick = (evt) => {
    const area = ReactDOM.findDOMNode(this.refs.area);

    if (!area.contains(evt.target)) {
      this.props.onClickOutside(evt)
    }
  }

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

8
สิ่งนี้ไม่ทำงานสำหรับฉันอีกต่อไปกับ React 0.14.7 - อาจจะ React เปลี่ยนแปลงบางอย่างหรือฉันอาจเกิดข้อผิดพลาดเมื่อปรับใช้รหัสกับการเปลี่ยนแปลงทั้งหมดของ React ฉันใช้github.com/Pomax/react-onclickoutsideแทนซึ่งใช้งานได้อย่างมีเสน่ห์
Nicole

1
อืมมม ฉันไม่เห็นเหตุผลว่าทำไมสิ่งนี้ถึงได้ผล ทำไมต้องจัดการบนรากโหนด DOM ของแอปได้รับการรับประกันที่จะยิงก่อน rerender ที่เกิดจากการจัดการอื่นถ้าหนึ่งในdocumentไม่?
Mark Amery

1
จุด UX ที่บริสุทธิ์: mousedownอาจเป็นตัวจัดการที่ดีกว่าclickที่นี่ ในแอปพลิเคชันส่วนใหญ่พฤติกรรมการปิดเมนูด้วยการคลิกภายนอกจะเกิดขึ้นทันทีที่คุณวางเมาส์ไม่ใช่เมื่อคุณปล่อย ตัวอย่างเช่นลองด้วยการตั้งค่าสถานะหรือแชร์การสนทนาของ Stack Overflow หรือหนึ่งในรายการแบบเลื่อนลงจากแถบเมนูด้านบนของเบราว์เซอร์
Mark Amery

ReactDOM.findDOMNodeและสตริงrefเลิกใช้แล้วควรใช้การrefเรียกกลับ: github.com/yannickcr/eslint-plugin-react/issues/…
เขียนเมื่อ

นี่เป็นทางออกที่ง่ายที่สุดและทำงานได้อย่างสมบูรณ์แบบสำหรับฉันแม้เมื่อติดกับdocument
Flion

37

การติดตั้ง Hook ตามการพูดคุยที่ยอดเยี่ยมของ Tanner Linsley ที่ JSConf Hawaii 2020 :

useOuterClick Hook API

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

การดำเนินงาน

/*
  Custom Hook
*/
function useOuterClick(callback) {
  const innerRef = useRef();
  const callbackRef = useRef();

  // set current callback in ref, before second useEffect uses it
  useEffect(() => { // useEffect wrapper to be safe for concurrent mode
    callbackRef.current = callback;
  });

  useEffect(() => {
    document.addEventListener("click", handleClick);
    return () => document.removeEventListener("click", handleClick);

    // read most recent callback and innerRef dom node from refs
    function handleClick(e) {
      if (
        innerRef.current && 
        callbackRef.current &&
        !innerRef.current.contains(e.target)
      ) {
        callbackRef.current(e);
      }
    }
  }, []); // no need for callback + innerRef dep
  
  return innerRef; // return ref; client can omit `useRef`
}

/*
  Usage 
*/
const Client = () => {
  const [counter, setCounter] = useState(0);
  const innerRef = useOuterClick(e => {
    // counter state is up-to-date, when handler is called
    alert(`Clicked outside! Increment counter to ${counter + 1}`);
    setCounter(c => c + 1);
  });
  return (
    <div>
      <p>Click outside!</p>
      <div id="container" ref={innerRef}>
        Inside, counter: {counter}
      </div>
    </div>
  );
};

ReactDOM.render(<Client />, document.getElementById("root"));
#container { border: 1px solid red; padding: 20px; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.12.0/umd/react.production.min.js" integrity="sha256-Ef0vObdWpkMAnxp39TYSLVS/vVUokDE8CDFnx7tjY6U=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.12.0/umd/react-dom.production.min.js" integrity="sha256-p2yuFdE8hNZsQ31Qk+s8N+Me2fL5cc6NKXOC0U9uGww=" crossorigin="anonymous"></script>
<script> var {useRef, useEffect, useCallback, useState} = React</script>
<div id="root"></div>

useOuterClickทำให้การใช้refs ไม่แน่นอนในการสร้าง API Clientลีนสำหรับ โทรกลับสามารถตั้งค่าได้โดยไม่ต้อง memoize useCallbackผ่านทาง ตัวเรียกกลับยังคงสามารถเข้าถึงอุปกรณ์ประกอบฉากและสถานะล่าสุด (ไม่มีค่าการปิดที่เก่า)


หมายเหตุสำหรับผู้ใช้ iOS

iOS ถือว่าองค์ประกอบบางอย่างเท่านั้นที่คลิกได้ เพื่อหลีกเลี่ยงปัญหานี้เลือกเป็นผู้ฟังที่ด้านนอกคลิกที่แตกต่างdocument- bodyไม่มีอะไรขึ้นรวมทั้ง เช่นคุณสามารถเพิ่มผู้ฟังบนรูท React divในตัวอย่างด้านบนและขยายความสูงของมัน ( height: 100vhหรือคล้ายกัน) เพื่อรับการคลิกภายนอกทั้งหมด แหล่งที่มา: quirksmode.orgและคำตอบนี้


ไม่สามารถใช้งานบนมือถือได้โดยไม่คาดฝัน
Omar

@ Omar เพิ่งทดสอบ Codesandbox ในโพสต์ของฉันด้วย Firefox 67.0.3 บนอุปกรณ์ Android - ใช้งานได้ดี คุณช่วยอธิบายเพิ่มเติมเกี่ยวกับเบราว์เซอร์ใดที่คุณใช้อยู่ข้อผิดพลาดคืออะไร?
ford04

ไม่มีข้อผิดพลาด แต่ใช้ไม่ได้กับ chrome, iphone 7+ ไม่พบก๊อกน้ำด้านนอก ในเครื่องมือ dev Chrome บนมือถือมันใช้งานได้ แต่ในอุปกรณ์ iOS จริงมันไม่ทำงาน
โอมาร์

1
@ ford04 ขอบคุณที่ขุดออกมาฉันสะดุดกับหัวข้ออื่นที่แนะนำเคล็ดลับต่อไปนี้ @media (hover: none) and (pointer: coarse) { body { cursor:pointer } }การเพิ่มสิ่งนี้ในสไตล์สากลของฉันดูเหมือนจะแก้ไขปัญหาได้แล้ว
Kostas Sarantopoulos

1
@ ford04 ใช่แล้ว btw ตามเธรดนี้มีข้อความค้นหาสื่อเฉพาะเจาะจงมากขึ้น@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 ดังนั้นฉันจึงใช้ "OnMouseLeave" สำหรับ div ด้วยอินพุตและวางปฏิทินเพื่อปิดการใช้งาน div ทั้งหมดเมื่อเมาส์ออกจาก div
Alex

1
ขอบคุณ! ช่วยฉันมาก
Ângelo Lucas

1
documentผมชอบแบบนี้ดีกว่าวิธีการเหล่านั้นแนบกับ ขอบคุณ
kyw

เพื่อให้มันใช้งานได้คุณต้องแน่ใจว่าองค์ประกอบที่คลิก (relatedTarget) นั้นสามารถโฟกัสได้ ดูstackoverflow.com/questions/42764494/…
xabitrigo

21

[อัพเดท]วิธีแก้ปัญหาด้วยReact ^ 16.8โดยใช้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;

โซลูชันที่ตอบสนอง ^ 16.3 :

CodeSandbox

import React, { Component } from "react";

class SampleComponent extends Component {
  state = {
    clickedOutside: false
  };

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

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

  myRef = React.createRef();

  handleClickOutside = e => {
    if (!this.myRef.current.contains(e.target)) {
      this.setState({ clickedOutside: true });
    }
  };

  handleClickInside = () => this.setState({ clickedOutside: false });

  render() {
    return (
      <button ref={this.myRef} onClick={this.handleClickInside}>
        {this.state.clickedOutside ? "Bye!" : "Hello!"}
      </button>
    );
  }
}

export default SampleComponent;

3
นี่คือทางออกสู่ปัจจุบัน หากคุณได้รับข้อผิดพลาด.contains is not a functionอาจเป็นเพราะคุณกำลังส่งเสาอ้างอิงไปยังองค์ประกอบที่กำหนดเองแทนที่จะเป็นองค์ประกอบ DOM จริงเช่น<div>
วิลล์มา

สำหรับผู้ที่พยายามส่งrefเสาไปยังองค์ประกอบที่กำหนดเองคุณอาจต้องการดูReact.forwardRef
onoya

4

นี่คือวิธีการของฉัน (สาธิต - https://jsfiddle.net/agymay93/4/ ):

ฉันได้สร้างส่วนประกอบพิเศษที่เรียกว่าWatchClickOutsideและสามารถใช้เช่น (ฉันถือว่าJSXไวยากรณ์):

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

นี่คือรหัสของWatchClickOutsideส่วนประกอบ:

import React, { Component } from 'react';

export default class WatchClickOutside extends Component {
  constructor(props) {
    super(props);
    this.handleClick = this.handleClick.bind(this);
  }

  componentWillMount() {
    document.body.addEventListener('click', this.handleClick);
  }

  componentWillUnmount() {
    // remember to remove all events to avoid memory leaks
    document.body.removeEventListener('click', this.handleClick);
  }

  handleClick(event) {
    const {container} = this.refs; // get container that we'll wait to be clicked outside
    const {onClickOutside} = this.props; // get click outside callback
    const {target} = event; // get direct click event target

    // if there is no proper callback - no point of checking
    if (typeof onClickOutside !== 'function') {
      return;
    }

    // if target is container - container was not clicked outside
    // if container contains clicked target - click was not outside of it
    if (target !== container && !container.contains(target)) {
      onClickOutside(event); // clicked outside - fire callback
    }
  }

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

ทางออกที่ดี - สำหรับการใช้งานของฉันฉันเปลี่ยนไปฟังเอกสารแทนเนื้อหาในกรณีที่มีเนื้อหาสั้น ๆ และเปลี่ยนการอ้างอิงจากสตริงเป็นการอ้างอิง dom: jsfiddle.net/agymay93/9
Billy Moon

และใช้ span แทน div เนื่องจากมีโอกาสน้อยที่จะเปลี่ยนเค้าโครงและเพิ่มการสาธิตการจัดการการคลิกนอกร่างกาย: jsfiddle.net/agymay93/10
Billy Moon

สตริงrefไม่ได้รับการสนับสนุนควรใช้การrefเรียกกลับ: reactjs.org/docs/refs-and-the-dom.html
dain

4

นี่มีคำตอบอยู่แล้วหลายข้อ แต่ไม่ได้กล่าวถึงe.stopPropagation()และป้องกันการคลิกลิงก์ตอบโต้ภายนอกองค์ประกอบที่คุณต้องการปิด

เนื่องจากความจริงที่ว่า React มีตัวจัดการเหตุการณ์เทียมของตัวเองคุณจึงไม่สามารถใช้เอกสารเป็นฐานสำหรับผู้ฟังเหตุการณ์ได้ คุณต้องทำe.stopPropagation()สิ่งนี้ก่อนเพราะ React ใช้เอกสารเอง ถ้าคุณใช้ตัวอย่างเช่นdocument.querySelector('body')แทน คุณสามารถป้องกันการคลิกจากลิงค์ตอบสนอง ต่อไปนี้เป็นตัวอย่างของวิธีที่ฉันใช้การคลิกภายนอกและปิด
นี้ใช้ES6และตอบสนอง 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

สำหรับผู้ที่ต้องการตำแหน่งแน่นอนตัวเลือกง่าย ๆ ที่ฉันเลือกใช้คือการเพิ่มองค์ประกอบ wrapper ที่มีสไตล์ให้ครอบคลุมทั้งหน้าด้วยพื้นหลังโปร่งใส จากนั้นคุณสามารถเพิ่ม onClick ในองค์ประกอบนี้เพื่อปิดองค์ประกอบภายในของคุณ

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

เนื่องจากตอนนี้ถ้าคุณเพิ่มตัวจัดการการคลิกบนเนื้อหาเหตุการณ์จะถูกเผยแพร่ไปยัง div ด้านบนและทำให้ทริกเกอร์ handlerOutsideClick หากนี่ไม่ใช่พฤติกรรมที่คุณต้องการให้หยุดการสร้างเหตุการณ์ในตัวจัดการของคุณ

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

`


ปัญหาของวิธีนี้คือคุณไม่สามารถคลิกใด ๆ ในเนื้อหา - div จะได้รับการคลิกแทน
rbonick

เนื่องจากเนื้อหาอยู่ใน div หากคุณไม่หยุดการเผยแพร่เหตุการณ์ทั้งคู่จะได้รับการคลิก นี่เป็นพฤติกรรมที่ต้องการ แต่ถ้าคุณไม่ต้องการให้มีการแพร่กระจายการคลิกไปที่ div เพียงแค่หยุด eventPropagation บนเนื้อหาของคุณบนตัวจัดการคลิก ฉันได้อัปเดตคำตอบเพื่อแสดงว่า
Anthony Garant

3

หากต้องการขยายคำตอบที่ได้รับการยอมรับจากBen Budหากคุณใช้ส่วนประกอบที่มีสไตล์การผ่านการอ้างอิงด้วยวิธีนั้นจะทำให้คุณมีข้อผิดพลาดเช่น "this.wrapperRef.contain ไม่ใช่ฟังก์ชัน"

การแก้ไขที่แนะนำในความคิดเห็นเพื่อห่อองค์ประกอบที่มีสไตล์ด้วย div และผ่านการอ้างอิงนั้นทำงานได้ ต้องบอกว่าในเอกสารของพวกเขาแล้วอธิบายเหตุผลสำหรับการนี้และการใช้งานที่เหมาะสมของการอ้างอิงภายในองค์ประกอบสไตล์:

การส่ง prop prop ไปยังคอมโพเนนต์ที่มีสไตล์จะให้อินสแตนซ์ของ wrapper StyledComponent แต่ไม่ใช่ไปยังโหนด DOM ต้นแบบ นี่เป็นเพราะการทำงานของผู้อ้างอิง เป็นไปไม่ได้ที่จะโทรหาวิธีการ DOM เช่นโฟกัสบน wrappers ของเราโดยตรง ในการรับการอ้างอิงไปยังโหนด DOM ที่แท้จริงที่ถูกล้อมรอบให้ส่งการเรียกกลับไปที่ innerRef prop แทน

ชอบมาก

<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-index 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

2
componentWillMount(){

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

handleClickOutside(event) {

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

กิจกรรมpath[0]เป็นรายการสุดท้ายที่คลิก


3
ตรวจสอบให้แน่ใจว่าคุณลบ listener เหตุการณ์เมื่อส่วนประกอบ unmounts เพื่อป้องกันปัญหาการจัดการหน่วยความจำ ฯลฯ
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

ฉันทำสิ่งนี้บางส่วนโดยทำตามสิ่งนี้และทำตามเอกสารตอบโต้อย่างเป็นทางการเกี่ยวกับการจัดการอ้างอิงที่ต้องมีปฏิกิริยา ^ 16.3 นี่เป็นสิ่งเดียวที่ทำงานให้ฉันหลังจากลองคำแนะนำอื่น ๆ ที่นี่ ...

class App extends Component {
  constructor(props) {
    super(props);
    this.inputRef = React.createRef();
  }
  componentWillMount() {
    document.addEventListener("mousedown", this.handleClick, false);
  }
  componentWillUnmount() {
    document.removeEventListener("mousedown", this.handleClick, false);
  }
  handleClick = e => {
    if (this.inputRef.current === e.target) {
      return;
    }
    this.handleclickOutside();
  };
handleClickOutside(){
...***code to handle what to do when clicked outside***...
}
render(){
return(
<div>
...***code for what's outside***...
<span ref={this.inputRef}>
...***code for what's "inside"***...
</span>
...***code for what's outside***
)}}

1

ตัวอย่างกับกลยุทธ์

ฉันชอบโซลูชันที่มีให้ซึ่งใช้ในการทำสิ่งเดียวกันโดยการสร้างเสื้อคลุมรอบองค์ประกอบ

เนื่องจากนี่เป็นพฤติกรรมที่มากกว่าฉันคิดถึงกลยุทธ์และทำสิ่งต่อไปนี้

ฉันใหม่กับ React และฉันต้องการความช่วยเหลือเล็กน้อยเพื่อบันทึกแผ่นเหล็กบางส่วนในกรณีการใช้งาน

โปรดตรวจสอบและบอกสิ่งที่คุณคิด

ClickOutsideBehavior

import ReactDOM from 'react-dom';

export default class ClickOutsideBehavior {

  constructor({component, appContainer, onClickOutside}) {

    // Can I extend the passed component's lifecycle events from here?
    this.component = component;
    this.appContainer = appContainer;
    this.onClickOutside = onClickOutside;
  }

  enable() {

    this.appContainer.addEventListener('click', this.handleDocumentClick);
  }

  disable() {

    this.appContainer.removeEventListener('click', this.handleDocumentClick);
  }

  handleDocumentClick = (event) => {

    const area = ReactDOM.findDOMNode(this.component);

    if (!area.contains(event.target)) {
        this.onClickOutside(event)
    }
  }
}

ตัวอย่างการใช้งาน

import React, {Component} from 'react';
import {APP_CONTAINER} from '../const';
import ClickOutsideBehavior from '../ClickOutsideBehavior';

export default class AddCardControl extends Component {

  constructor() {
    super();

    this.state = {
      toggledOn: false,
      text: ''
    };

    this.clickOutsideStrategy = new ClickOutsideBehavior({
      component: this,
      appContainer: APP_CONTAINER,
      onClickOutside: () => this.toggleState(false)
    });
  }

  componentDidMount () {

    this.setState({toggledOn: !!this.props.toggledOn});
    this.clickOutsideStrategy.enable();
  }

  componentWillUnmount () {
    this.clickOutsideStrategy.disable();
  }

  toggleState(isOn) {

    this.setState({toggledOn: isOn});
  }

  render() {...}
}

หมายเหตุ

ฉันคิดถึงการจัดเก็บcomponenthooks lifecycle ที่ผ่านไปและแทนที่พวกเขาด้วย method simillar ในสิ่งนี้:

const baseDidMount = component.componentDidMount;

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

componentClickOutsideBehaviorเป็นส่วนประกอบที่ผ่านมาคอนสตรัคของ
สิ่งนี้จะลบการเปิดใช้งาน / ปิดใช้งาน boilerplate จากผู้ใช้ของพฤติกรรมนี้ แต่มันก็ดูไม่ค่อยดีนัก


1

ในกรณี DROPDOWN ของฉันโซลูชันของBen Budทำงานได้ดี แต่ฉันมีปุ่มสลับแยกต่างหากที่มีตัวจัดการ onClick ตรรกะการคลิกภายนอกขัดแย้งกับปุ่มบนคลิก toggler นี่คือวิธีที่ฉันแก้ไขมันโดยผ่านการอ้างอิงของปุ่มเช่นกัน:

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 Byte gzipped) ที่มีอยู่แล้วสำหรับฟังก์ชั่นนี้คุณสามารถตรวจสอบไลบรารี่ที่ตอบสนองต่อไลบรารี่นี้ได้

ข้อดีของไลบรารี่คือมันช่วยให้คุณตรวจจับการคลิกนอกองค์ประกอบและภายในอีกอัน นอกจากนี้ยังรองรับการตรวจจับเหตุการณ์ประเภทอื่น ๆ


0

หากคุณต้องการใช้ส่วนประกอบเล็ก ๆ (466 Byte gzipped) ที่มีอยู่แล้วสำหรับฟังก์ชั่นนี้คุณสามารถตรวจสอบไลบรารี่ที่ตอบสนองต่อไลบรารี่นี้ได้ มันจับเหตุการณ์นอกองค์ประกอบปฏิกิริยาหรือองค์ประกอบ jsx

ข้อดีของไลบรารี่คือมันช่วยให้คุณตรวจจับการคลิกนอกองค์ประกอบและภายในอีกอัน นอกจากนี้ยังรองรับการตรวจจับเหตุการณ์ประเภทอื่น ๆ


0

ฉันรู้ว่านี่เป็นคำถามเก่า แต่ฉันก็เจอปัญหานี้และฉันมีปัญหามากมายในการหาสิ่งนี้ในรูปแบบที่เรียบง่าย ดังนั้นถ้าสิ่งนี้จะทำให้ชีวิตของใคร ๆ ง่ายขึ้นให้ใช้ OutsideClickHandler โดย airbnb มันเป็นปลั๊กอินที่ง่ายที่สุดในการทำงานนี้โดยไม่ต้องเขียนโค้ดของคุณเอง

ตัวอย่าง:

hideresults(){
   this.setState({show:false})
}
render(){
 return(
 <div><div onClick={() => this.setState({show:true})}>SHOW</div> {(this.state.show)? <OutsideClickHandler onOutsideClick={() => 
  {this.hideresults()}} > <div className="insideclick"></div> </OutsideClickHandler> :null}</div>
 )
}


0

คุณเป็นวิธีง่ายๆในการแก้ปัญหาของคุณฉันแสดงให้คุณเห็น:

....

const [dropDwonStatus , setDropDownStatus] = useState(false)

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

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

มันใช้งานได้สำหรับฉันโชคดี;)


-1

ฉันพบสิ่งนี้จากบทความด้านล่าง:

render () {return ({this.node = node;}}> สลับ Popover {this.state.popupVisible && (ฉันเป็น Popover!)}); }}

นี่เป็นบทความที่ยอดเยี่ยมเกี่ยวกับปัญหานี้: "จัดการการคลิกนอกส่วนประกอบ React" https://larsgraubner.com/handle-outside-clicks-react/


ยินดีต้อนรับสู่ Stack Overflow! คำตอบที่เป็นเพียงการเชื่อมโยงจะขมวดคิ้วโดยทั่วไปเมื่อmeta.stackoverflow.com/questions/251006/... ในอนาคตให้ใส่ตัวอย่างการเสนอราคาหรือรหัสพร้อมข้อมูลที่เกี่ยวข้องกับคำถาม ไชโย!
Fred Stark

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

ไม่แน่ใจว่ามีใคร downvoting แต่วิธีนี้ค่อนข้างสะอาดเมื่อเทียบกับการผูก / ไม่ผูกพันฟังเหตุการณ์ ฉันไม่มีปัญหาเกี่ยวกับการใช้งานนี้
wdm

-1

ที่จะทำให้'โฟกัส'การทำงานของโซลูชั่นสำหรับการเลื่อนลงกับผู้ฟังเหตุการณ์ที่คุณสามารถเพิ่มพวกเขาด้วยOnMouseDownเหตุการณ์แทนonClick ด้วยวิธีนี้เหตุการณ์จะเริ่มขึ้นและหลังจากนั้นป๊อปอัปจะปิดลงดังนี้:

<TogglePopupButton
                    onClick = { this.toggleDropup }
                    tabIndex = '0'
                    onBlur = { this.closeDropup }
                />
                { this.state.isOpenedDropup &&
                <ul className = { dropupList }>
                    { this.props.listItems.map((item, i) => (
                        <li
                            key = { i }
                            onMouseDown = { item.eventHandler }
                        >
                            { item.itemName}
                        </li>
                    ))}
                </ul>
                }

-1
import ReactDOM from 'react-dom' ;

class SomeComponent {

  constructor(props) {
    // First, add this to your constructor
    this.handleClickOutside = this.handleClickOutside.bind(this);
  }

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

  // Unbind event on unmount to prevent leaks
  componentWillUnmount() {
    window.removeEventListener('mousedown', this.handleClickOutside, false);
  }

  handleClickOutside(event) {
    if(!ReactDOM.findDOMNode(this).contains(event.path[0])){
       console.log("OUTSIDE");
    }
  }
}

และอย่าลืมimport ReactDOM from 'react-dom';
dipole_moment

-1

ฉันทำทางออกสำหรับทุกโอกาส

คุณควรใช้ High Order Component เพื่อตัดองค์ประกอบที่คุณต้องการฟังการคลิกที่อยู่ด้านนอก

ตัวอย่างองค์ประกอบนี้มีเพียงหนึ่งเสา: "onClickedOutside" ที่ได้รับฟังก์ชั่น

ClickedOutside.js
import React, { Component } from "react";

export default class ClickedOutside extends Component {
  componentDidMount() {
    document.addEventListener("mousedown", this.handleClickOutside);
  }

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

  handleClickOutside = event => {
    // IF exists the Ref of the wrapped component AND his dom children doesnt have the clicked component 
    if (this.wrapperRef && !this.wrapperRef.contains(event.target)) {
      // A props callback for the ClikedClickedOutside
      this.props.onClickedOutside();
    }
  };

  render() {
    // In this piece of code I'm trying to get to the first not functional component
    // Because it wouldn't work if use a functional component (like <Fade/> from react-reveal)
    let firstNotFunctionalComponent = this.props.children;
    while (typeof firstNotFunctionalComponent.type === "function") {
      firstNotFunctionalComponent = firstNotFunctionalComponent.props.children;
    }

    // Here I'm cloning the element because I have to pass a new prop, the "reference" 
    const children = React.cloneElement(firstNotFunctionalComponent, {
      ref: node => {
        this.wrapperRef = node;
      },
      // Keeping all the old props with the new element
      ...firstNotFunctionalComponent.props
    });

    return <React.Fragment>{children}</React.Fragment>;
  }
}

-1

UseOnClickOutside Hook - ตอบสนอง 16.8 +

สร้างฟังก์ชั่นการใช้งานทั่วไป OnOutsideClick

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.