简体   繁体   English

使用钩子检测 React 组件外的点击

[英]Detect click outside React component using hooks

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 could put in a hook and share across components to save me writing the same logic in every component?随着钩子的引入,我是否可以将其放入钩子中并在组件之间共享以节省我在每个组件中编写相同的逻辑?

I have implemented it once in a component as follows.我已经在一个组件中实现了一次,如下所示。

const Dropdown = () => {
    const [isDropdownVisible, setIsDropdownVisible] = useState(false);   
    const wrapperRef = useRef<HTMLDivElement>(null);

    const handleHideDropdown = (event: KeyboardEvent) => {
        if (event.key === 'Escape') {
            setIsDropdownVisible(false);
        }
    };

    const handleClickOutside = (event: Event) => {
        if (
            wrapperRef.current &&
            !wrapperRef.current.contains(event.target as Node)
        ) {
            setIsDropdownVisible(false);
        }
    };

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

    return(
       <DropdownWrapper ref={wrapperRef}>
         <p>Dropdown</p>
       </DropdownWrapper>
    );
}

This is possible.这个有可能。

You can create a reusable hook called useComponentVisible您可以创建一个名为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.在此处查找代码沙盒示例。

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.首先,我希望我的下拉菜单在 ESCAPE 键事件和鼠标点击外关闭。 To avoid creating a useEffect per event, I ended with a helper function:为了避免为每个事件创建 useEffect,我以一个辅助函数结束:

//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 钩子带来了所有想要的功能:

//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 .此解决方案的功劳是针对先前的答案here, this entry in medium 和this article

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM