Detect click outside React component

后端 未结 30 1104
日久生厌
日久生厌 2020-11-22 13:54

I\'m looking for a way to detect if a click event happened outside of a component, as described in this article. jQuery closest() is used to see if the target from a click e

相关标签:
30条回答
  • 2020-11-22 14:17

    An example with Strategy

    I like the provided solutions that use to do the same thing by creating a wrapper around the component.

    Since this is more of a behavior I thought of Strategy and came up with the following.

    I'm new with React and I need a bit of help in order to save some boilerplate in the use cases

    Please review and tell me what you think.

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

    Sample Usage

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

    Notes

    I thought of storing the passed component lifecycle hooks and override them with methods simillar to this:

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

    component is the component passed to the constructor of ClickOutsideBehavior.
    This will remove the enable/disable boilerplate from the user of this behavior but it doesn't look very nice though

    0 讨论(0)
  • 2020-11-22 14:17

    If you want to use a tiny component (466 Byte gzipped) that already exists for this functionality then you can check out this library react-outclick.

    The good thing about the library is that it also lets you detect clicks outside of a component and inside of another. It also supports detecting other types of events.

    0 讨论(0)
  • 2020-11-22 14:18

    Here is the solution that best worked for me without attaching events to the container:

    Certain HTML elements can have what is known as "focus", for example input elements. Those elements will also respond to the blur event, when they lose that focus.

    To give any element the capacity to have focus, just make sure its tabindex attribute is set to anything other than -1. In regular HTML that would be by setting the tabindex attribute, but in React you have to use tabIndex (note the capital I).

    You can also do it via JavaScript with element.setAttribute('tabindex',0)

    This is what I was using it for, to make a custom DropDown menu.

    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>
            );
        }
    });
    
    0 讨论(0)
  • 2020-11-22 14:18
    import { useClickAway } from "react-use";
    
    useClickAway(ref, () => console.log('OUTSIDE CLICKED'));
    
    0 讨论(0)
  • 2020-11-22 14:19

    Hook implementation based on Tanner Linsley's excellent talk at JSConf Hawaii 2020:

    useOuterClick Hook API

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

    Implementation

    function useOuterClick(callback) {
      const callbackRef = useRef(); // initialize mutable callback ref
      const innerRef = useRef(); // returned to client, who sets the "border" element
    
      // update callback on each render, so second useEffect has most recent callback
      useEffect(() => { callbackRef.current = callback; });
      useEffect(() => {
        document.addEventListener("click", handleClick);
        return () => document.removeEventListener("click", handleClick);
        function handleClick(e) {
          if (innerRef.current && callbackRef.current && 
            !innerRef.current.contains(e.target)
          ) callbackRef.current(e);
        }
      }, []); // no dependencies -> stable click listener
          
      return innerRef; // convenience for client (doesn't need to init ref himself) 
    }
    

    Here is a working example:

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

    Explanation

    useOuterClick makes use of mutable refs to create a lean API for the Client. A callback can be set without having to memoize it via useCallback. The callback body still has access to the most recent props and state (no stale closure values).

    Note for iOS users

    iOS treats only certain elements as clickable. To circumvent this behavior, choose a different outer click listener than document - nothing upwards including body. E.g. you could add a listener on the React root div in above example and expand its height (height: 100vh or similar) to catch all outside clicks. Source: quirksmode.org

    0 讨论(0)
  • 2020-11-22 14:20

    If you want to use a tiny component (466 Byte gzipped) that already exists for this functionality then you can check out this library react-outclick. It captures events outside of a react component or jsx element.

    The good thing about the library is that it also lets you detect clicks outside of a component and inside of another. It also supports detecting other types of events.

    0 讨论(0)
提交回复
热议问题