简体   繁体   English

React,在打开事件后立即单击外部事件触发,防止显示模式

[英]React, click outside event fire right after the open event, preventing the modal from being displayed

I have a problem.我有个问题。 I added an onClick event to an element.我向一个元素添加了一个onClick事件。 Its event handler's work is to change a state.它的事件处理程序的工作是改变一个状态。 After that state changed, I display a popup.在该状态更改后,我会显示一个弹出窗口。 I get access to that popup using useRef hook.我可以使用useRef挂钩访问该弹出窗口。 Then I add a click event to the document and its event handler work is to check that user clicked outside the popup or not.然后我向document添加一个click事件,它的事件处理程序的工作是检查用户是否在弹出窗口之外点击。

But the problem is here: when user clicks on the element, immediately the added event handler to the document will be executed!但问题就在这里:当用户点击元素时,添加到document的事件处理程序将立即被执行! But how?但是怎么做? look at these steps and you'll understand my point better:看看这些步骤,你会更好地理解我的观点:

user clicked on the show popup button --> onClick event handler executed --> the state changed --> an other event added to the document(for outside click purpose) --> immediately the event handler of click event on document executed (all these happen when you click on show popup button just once!!).用户单击显示弹出按钮-> onClick 事件处理程序已执行--> 状态已更改--> 添加到文档中的另一个事件(用于外部单击目的)--> 立即执行文档上单击事件的事件处理程序(当您单击一次显示弹出按钮时,所有这些都会发生!!)。

Option popup component:选项弹出组件:

import { useRef } from "react";
import useAxis from "../../hooks/useAxis";
import useOutSideClick from "../../hooks/useOutSideClick";

const Options = (props) => {
  const { children, setShowOptions, showOptions } = props;
  const boxRef = useRef();

  const { childrens, offset } = useAxis(children, {
    /* add an onClick to this components children(it has just one child and it is the open 
    popup button)*/
    onClick: () => {
      console.log("test1");
      //show the options popup
      setShowOptions(!showOptions);
    },
  });
  
  //close the popup if user clicked outside the popup
  useOutSideClick(boxRef, () => {
    console.log("test2");
    //close the popup
    setShowOptions((prevState) => !prevState);
  }, showOptions);

  return (
    <>
      {showOptions && (
        <div
          ref={boxRef}
          className="absolute rounded-[20px] bg-[#0d0d0d] border border-[#1e1e1e] border-solid w-[250px] overflow-y-auto h-[250px]"
          style={{ left: offset.x + 25 + "px", top: offset.y + "px" }}
        ></div>
      )}
      {childrens}
    </>
  );
};

export default Options;

useOutSideClick custom hook: useOutSideClick 自定义钩子:

import { useEffect } from "react";

//a custom hook to detect that user clicked
const useOutSideClick = (ref, outSideClickHandler, condition = true) => {
  useEffect(() => {
    if (condition) {
      const handleClickOutside = (event) => {
        console.log("hellloooo");
        //if ref.current doesnt contain event.target it means that user clicked outside
        if (ref.current && !ref.current.contains(event.target)) {
          outSideClickHandler();
        }
      };

      document.addEventListener("click", handleClickOutside);
      return () => {
        document.removeEventListener("click", handleClickOutside);
      };
    }
  }, [ref, condition]);
};

export default useOutSideClick;

useAxis custom hook: useAxis 自定义钩子:

import React, { useEffect, useRef, useState } from "react";

const useAxis = (children, events) => {
  const childRef = useRef();
  const [offset, setOffset] = useState({
    x: "",
    y: "",
  });

  useEffect(() => {
    Object.keys(events).forEach((event) => {
      let eventHandler;
      let callBack = events[event];
      if (event === "onClick" || event === "onMouseEnter") {
        eventHandler = (e) => {
          callBack();
        };
        events[event] = eventHandler;
      }
    });
  }, [JSON.stringify(events)]);

  //setting mouse enter and leave event for the components children
  const childrens = React.Children.map(children, (child) => {
    return React.cloneElement(child, {
      ...events,
      ref: childRef,
    });
  });

  //initialize the position of child component at the first render
  useEffect(() => {
    setOffset(() => {
      return {
        x: childRef.current.offsetLeft,
        y: childRef.current.offsetTop,
      };
    });
  }, []);

  return { childrens, offset };
};

export default useAxis;

the button(actually the element) component:按钮(实际上是元素)组件:

//basic styles for icons
const iconsStyles = "w-[24px] h-[24px] transition duration-300 cursor-pointer";

    const NavBar = () => {
      const [showOptions, setShowOptions] = useState(false);
      return (
          <Options
            setShowOptions={setShowOptions}
            showOptions={showOptions}
          >
            //onClick will be applied to this div only
            <div
            >
              <TooltipContainer tooltipText="options">
                <div>
                  <DotsHorizontalIcon
                    className={`${iconsStyles} ${
                      showOptions
                        ? "text-[#fafafa]"
                        : "text-[#828282] hover:text-[#fafafa]"
                    }`}
                  />
                </div>
              </TooltipContainer>
            </span>
          </Options>
       //there are some other items that theres no need to be here
      );
    };
    
    export default NavBar;

You can see my codes and the part of my app that you need to see in this CodeSandbox link .您可以在此 CodeSandbox 链接中查看我的代码和您需要查看的应用程序部分。 so what should I do to avoid executing the event handler(for outside click purpose) immediately after the event added to the document by clicking on open popup button for the first time?那么我应该怎么做才能避免在第一次单击打开弹出按钮将事件添加到document后立即执行事件处理程序(用于外部单击目的)?

I believe the event of the on click bubbles up, while the state is already toggled, and the ref is initialized, therefore at some point reaching the documents, whose listener is now registered and then called.我相信 on click 事件会冒泡,而状态已经切换,并且 ref 已初始化,因此在某个时候到达文档,其侦听器现在已注册然后调用。 to stop this behaviour you need to call e.stopPropagation() in useAxis for the click event.要停止此行为,您需要在useAxis中为单击事件调用e.stopPropagation() However the signed up listener to the button is not the one you would expect .然而,注册的按钮监听器不是您所期望的 It will sign up the listener you passed to the useAxis hook, instead of the modified one in useAxis itself.它将注册您传递给useAxis挂钩的侦听器,而不是在useAxis本身中注册修改过的侦听器。 To circumvent this, put the code from inside the useEffect in useAxis simply in the render function.为了避免这种情况,只需将useAxis中的代码放在useEffect中,只需在渲染函数中即可。 If you then call e.stopPropagation() on the event, it should work.如果您随后在事件上调用e.stopPropagation() ,它应该可以工作。 Here the final partial code in useAxis :这是useAxis中的最终部分代码:

  // now correctly updates events in time before cloning the children
  Object.keys(events).forEach((event) => {
    let eventHandler;
    let callBack = events[event];
    if (event === "onClick" || event === "onMouseEnter") {
      eventHandler = (e) => {
        // stop the propagation of the event
        e.stopPropagation();
        callBack();
      };
      events[event] = eventHandler;
      console.log("inner events", events);
    }
  });

  //setting updated mouse enter and leave event for the components children
  const childrens = React.Children.map(children, (child) => {
    console.log(events);
    return React.cloneElement(child, {
      ...events,
      ref: childRef
    });
  });

Problem问题

You are returning the below JSX inside Options.jsx .您将在Options.jsx中返回以下 JSX。 Where childrens contains the button that opens the modal.其中childrens包含打开模式的按钮。 If you look at your JSX, this button will not be rendered as a child of the div with boxRef , which is why this if (ref.current && !ref.current.contains(event.target)) inside useOutSideClick will always pass, so it closes the modal right after.如果你查看你的 JSX,这个按钮不会被渲染为带有boxRefdiv的子元素,这就是为什么 useOutSideClick 内部的 if (ref.current && ! useOutSideClick if (ref.current && !ref.current.contains(event.target))总是会通过的原因,所以它会立即关闭模态。

return (
    <>
      {showOptions && (
        <div
          ref={boxRef}
          className="absolute rounded-[20px] bg-[#0d0d0d] border border-[#1e1e1e] border-solid w-[250px] overflow-y-auto h-[250px]"
          style={{ left: offset.x + 25 + "px", top: offset.y + "px" }}
        ></div>
      )}
      {childrens}
    </>
  );

Solution解决方案

A quick solution is to change the above return with the below one.一个快速的解决方案是用下面的返回值来改变上面的return值。 Notice the ref is being added to a outer div , so the button for opening the modal is rendered as its child.请注意ref被添加到外部div ,因此用于打开模式的按钮呈现为其子项。

 return (
    <div ref={boxRef}>
      {showOptions && (
        <div
          className="absolute rounded-[20px] bg-[#0d0d0d] border border-[#1e1e1e] border-solid w-[250px] overflow-y-auto h-[250px]"
          style={{ left: offset.x + 25 + "px", top: offset.y + "px" }}
        ></div>
      )}
      {childrens}
    </div>
  );

Working CodeSandbox here . 在这里工作 CodeSandbox。

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

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