Detect click outside React component using hooks

后端 未结 2 853
醉梦人生
醉梦人生 2021-01-02 11:33

I am finding that I am reusing behaviour across an app that when a user clicks outside an element I can hide it.

With the introduction of hooks is this something I

相关标签:
2条回答
  • 2021-01-02 12:15

    This is possible.

    You can create a reusable hook called useComponentVisible

    import { useState, useEffect, useRef } from 'react';
    
    export default function useComponentVisible(initialIsVisible) {
        const [isComponentVisible, setIsComponentVisible] = useState(initialIsVisible);
        const ref = useRef<HTMLDivElement>(null);
    
        const handleHideDropdown = (event: KeyboardEvent) => {
            if (event.key === 'Escape') {
                setIsComponentVisible(false);
            }
        };
    
        const handleClickOutside = (event: Event) => {
            if (ref.current && !ref.current.contains(event.target as Node)) {
                setIsComponentVisible(false);
            }
        };
    
        useEffect(() => {
            document.addEventListener('keydown', handleHideDropdown, true);
            document.addEventListener('click', handleClickOutside, true);
            return () => {
                document.removeEventListener('keydown', handleHideDropdown, true);
                document.removeEventListener('click', handleClickOutside, true);
            };
        });
    
        return { ref, isComponentVisible, setIsComponentVisible };
    }
    

    Then in the component you wish to add the functionality to do the following:

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

    Find a codesandbox example here.

    0 讨论(0)
  • 2021-01-02 12:16

    Well, after struggling this for a bit, I have come to the next workarround, IN ADITION to what Paul Fitzgerald did, and having in count that my answer includes transitions too

    First, I want my dropdown to be closed on ESCAPE key event and mouse click outside. To avoid creating a useEffect per event, I ended with a helper function:

    //useDocumentEvent.js
    
    import { useEffect } from 'react'
    export const useDocumentEvent = (events) => {
      useEffect(
        () => {
          events.forEach((event) => {
            document.addEventListener(event.type, event.callback)
          })
          return () =>
            events.forEach((event) => {
              document.removeEventListener(event.type, event.callback)
            })
        },
        [events]
      )
    }
    

    After that, useDropdown hook that brings all the desired functionality:

    //useDropdown.js
    
    import { useCallback, useState, useRef } from 'react'
    import { useDocumentEvent } from './useDocumentEvent'
    
    /**
     * Functions which performs a click outside event listener
     * @param {*} initialState initialState of the dropdown
     * @param {*} onAfterClose some extra function call to do after closing dropdown
     */
    export const useDropdown = (initialState = false, onAfterClose = null) => {
      const ref = useRef(null)
      const [isOpen, setIsOpen] = useState(initialState)
    
      const handleClickOutside = useCallback(
        (event) => {
          if (ref.current && ref.current.contains(event.target)) {
            return
          }
          setIsOpen(false)
          onAfterClose && onAfterClose()
        },
        [ref, onAfterClose]
      )
    
      const handleHideDropdown = useCallback(
        (event) => {
          if (event.key === 'Escape') {
            setIsOpen(false)
            onAfterClose && onAfterClose()
          }
        },
        [onAfterClose]
      )
    
      useDocumentEvent([
        { type: 'click', callback: handleClickOutside },
        { type: 'keydown', callback: handleHideDropdown },
      ])
    
      return [ref, isOpen, setIsOpen]
    }
    

    Finally, to use this(it has some emotion styling):

    //Dropdown.js
    import React, { useState, useEffect } from 'react'
    import styled from '@emotion/styled'
    
    import { COLOR } from 'constants/styles'
    import { useDropdown } from 'hooks/useDropdown'
    import { Button } from 'components/Button'
    
    const Dropdown = ({ children, closeText, openText, ...rest }) => {
      const [dropdownRef, isOpen, setIsOpen] = useDropdown()
      const [inner, setInner] = useState(false)
      const [disabled, setDisabled] = useState(false)
      const timeout = 150
      useEffect(() => {
        if (isOpen) {
          setInner(true)
        } else {
          setDisabled(true)
          setTimeout(() => {
            setDisabled(false)
            setInner(false)
          }, timeout + 10)
        }
      }, [isOpen])
      return (
        <div style={{ position: 'relative' }} ref={dropdownRef}>
          <Button onClick={() => setIsOpen(!isOpen)} disabled={disabled}>
            {isOpen ? closeText || 'Close' : openText || 'Open'}
          </Button>
          <DropdownContainer timeout={timeout} isVisible={isOpen} {...rest}>
            {inner && children}
          </DropdownContainer>
        </div>
      )
    }
    
    const DropdownContainer = styled.div(
      {
        position: 'absolute',
        backgroundColor: COLOR.light,
        color: COLOR.dark,
        borderRadius: '2px',
        width: 400,
        boxShadow: '0px 0px 2px 0px rgba(0,0,0,0.5)',
        zIndex: 1,
        overflow: 'hidden',
        right: 0,
      },
      (props) => ({
        transition: props.isVisible
          ? `all 700ms ease-in-out`
          : `all ${props.timeout}ms ease-in-out`,
        maxHeight: props.isVisible ? props.maxHeight || 300 : 0,
      })
    )
    
    export { Dropdown }
    

    And, to use it, simply:

    //.... your code
    <Dropdown>
      <Whatever.Needs.To.Be.Rendered />
    </Dropdown>
    //... more code
    

    Credits to this solution are for previous answer here, this entry in medium and this article.

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