[javascript] React 컴포넌트 외부의 클릭 감지

기사 에서 설명하는 것처럼 클릭 이벤트가 구성 요소 외부에서 발생했는지 감지하는 방법을 찾고 있습니다. jQuery closest ()는 click 이벤트의 대상에 부모 요소 중 하나 인 dom 요소가 있는지 확인하는 데 사용됩니다. 일치하는 경우 click 이벤트는 하위 중 하나에 속하므로 구성 요소 외부에있는 것으로 간주되지 않습니다.

그래서 내 구성 요소에서 클릭 핸들러를 창에 연결하고 싶습니다. 처리기가 시작되면 대상을 내 구성 요소의 dom 자식과 비교해야합니다.

click 이벤트에는 이벤트가 이동 한 dom 경로를 보유하는 것으로 보이는 “path”와 같은 속성이 포함되어 있습니다. 나는 무엇을 비교할 것인지 또는 어떻게 그것을 가장 잘 횡단해야하는지 잘 모르겠습니다.



답변

React 16.3+의 Refs 사용법이 변경되었습니다.

다음 솔루션은 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,
};

후크 구현 :

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


답변

컨테이너에 이벤트를 첨부하지 않고 나에게 가장 적합한 솔루션은 다음과 같습니다.

특정 HTML 요소는 입력 요소 와 같이 ” 초점 ” 이라고하는 것을 가질 수 있습니다 . 이러한 요소는 초점을 잃을 때 흐림 이벤트에 반응합니다 .

요소에 초점을 둘 수있는 용량을 제공하려면 tabindex 속성이 -1 이외의 값으로 설정되어 있는지 확인하십시오. 일반 HTML에서는 tabindex속성 을 설정 하지만 React에서는 tabIndex(대문자 사용 I) 을 사용해야 합니다.

당신은 또한 자바 스크립트를 통해 그것을 할 수 있습니다 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>
        );
    }
});


답변

나는 같은 문제에 갇혀 있었다. 나는 파티에 조금 늦었지만 나에게는 이것이 정말 좋은 해결책이다. 잘만되면 그것은 다른 누군가에게 도움이 될 것입니다. 에서 가져와야합니다 findDOMNode.react-dom

import ReactDOM from 'react-dom';
// ... ✂

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

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

handleClickOutside = event => {
    const domNode = ReactDOM.findDOMNode(this);

    if (!domNode || !domNode.contains(event.target)) {
        this.setState({
            visible: false
        });
    }
}

반응 고리 접근 (16.8 +)

라는 재사용 가능한 후크를 만들 수 있습니다 useComponentVisible.

import { useState, useEffect, useRef } from 'react';

export default function useComponentVisible(initialIsVisible) {
    const [isComponentVisible, setIsComponentVisible] = useState(initialIsVisible);
    const ref = useRef(null);

    const handleClickOutside = (event) => {
        if (ref.current && !ref.current.contains(event.target)) {
            setIsComponentVisible(false);
        }
    };

    useEffect(() => {
        document.addEventListener('click', handleClickOutside, true);
        return () => {
            document.removeEventListener('click', handleClickOutside, true);
        };
    });

    return { ref, isComponentVisible, setIsComponentVisible };
}

그런 다음 구성 요소에서 기능을 추가하여 다음을 수행하십시오.

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

}

codesandbox 예제를 찾으 십시오.


답변

여기에서 많은 방법을 시도한 후 github.com/Pomax/react-onclickoutside 를 사용하기로 결정했습니다 .

npm을 통해 모듈을 설치하고 내 구성 요소로 가져 왔습니다.

import onClickOutside from 'react-onclickoutside'

그런 다음 컴포넌트 클래스에서 handleClickOutside메소드를 정의했습니다 .

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

그리고 내 구성 요소를 내보낼 때 다음과 같이 포장했습니다 onClickOutside().

export default onClickOutside(NameOfComponent)

그게 다야.


답변

나는 벤 알퍼트에 대한 해결책 덕분에 발견 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>
    )
  }
}


답변

JSConf Hawaii 2020에서 Tanner Linsley의 탁월한 강연을 기반으로 한 후크 구현 :

useOuterClick 후크 API

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

이행

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

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

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

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

  return innerRef; // return ref; client can omit `useRef`
}

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

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

useOuterClick에 대한 린 API를 생성하기 위해 가변 참조 를 사용합니다 Client. 를 통해 메모하지 않아도 콜백을 설정할 수 있습니다 useCallback. 콜백 본문은 여전히 ​​최신 소품과 상태에 액세스 할 수 있습니다 (부실 폐쇄 값 없음).


iOS 사용자를위한 참고 사항

iOS는 특정 요소 만 클릭 가능한 것으로 취급합니다. 이 동작을 피하려면을 document포함하여 위쪽 이외의 다른 외부 클릭 리스너를 선택하십시오 body. 예를 div들어 위 예제에서 React 루트 에 리스너를 추가 하고 높이 ( height: 100vh또는 유사한)를 확장하여 모든 외부 클릭을 잡을 수 있습니다. 출처 : quirksmode.org이 답변


답변

여기에 다른 답변들 중 어느 것도 나를 위해 일하지 않았습니다. 블러시 팝업을 숨기려고했지만 내용이 절대적으로 배치되었으므로 내부 내용을 클릭해도 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.
    }
},

도움이 되었기를 바랍니다.