一个下拉组件,在单击时会显示下拉菜单,在下拉区域之外单击时会关闭下拉菜单.

现在有一个我花了很长时间试图解决的问题:

仅使用一个下拉列表组件,在空白区域中单击即可成功关闭下拉列表.

但是,如果有两个或三个下拉列表组件,则在空白区域中单击不会关闭下拉列表.

我的预期结果是:多个下拉组件,点击空白区域应该会关闭下拉菜单.

当前的问题是:对于单个下拉组件,单击空白区域会将其关闭;然而,对于多个下拉组件,单击空白区域并不会关闭它们.

如何解决这个问题?

现场演示:

https://stackblitz.com/edit/vitejs-vite-cizh35?file=src%2FApp.tsx


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

type DropdownProps = {
  className?: string;
  handleSelectLanguage?: (language: string) => void;
  text: string;
  items: string[];
};

export const Dropdown: React.FC<DropdownProps> = (props) => {
  const [open, setOpen] = useState(false);
  const [text, setText] = useState(props.text);

  const ref = useRef<HTMLDivElement>(null);

  const handleClick = () => {
    setOpen(!open);
  };

  useEffect(() => {
    setText(props.text);
  }, [props.text]);

  useEffect(() => {
    console.log('open1: ', open);

    function handleOutsideClick(e: MouseEvent) {
      e.stopImmediatePropagation();

      console.log('open2: ', open);

      const [target] = e.composedPath();

      console.log('target:', !ref.current?.contains(target as Node), open);

      if (!ref.current?.contains(target as Node) && open) {
        console.log('2');
        setOpen(false);
      }
    }

    document.body.addEventListener('click', handleOutsideClick, false);

    return () => {
      document.body.removeEventListener('click', handleOutsideClick, false);
    };
  }, [open]);

  return (
    <>
      <div className={`dropdown relative ${props.className}`} ref={ref}>
        <button
          className="w-full flex justify-center items-center rounded text-sm px-2 py-1 hover:bg-[#e3e3e3] border"
          style={{ display: 'flex' }}
          onClick={handleClick}
        >
          <span>{text}</span>
          <span className="test relative top-[1px] ml-1">
            <svg
              // stroke="currentColor"
              viewBox="0 0 12 12"
              fill="currentColor"
              xmlns="http://www.w3.org/2000/svg"
              className="w-3 h-3"
            >
              <path d="M8.77814 4.00916C8.58288 3.8139 8.26629 3.8139 8.07103 4.00916L5.89096 6.18923L3.77338 4.07164C3.57811 3.87638 3.26153 3.87638 3.06627 4.07164L2.71272 4.42519C2.51745 4.62046 2.51745 4.93704 2.71272 5.1323L5.54114 7.96073C5.7364 8.15599 6.05299 8.15599 6.24825 7.96073L6.6018 7.60718C6.61932 7.58966 6.63527 7.57116 6.64965 7.55186L9.13169 5.06982C9.32695 4.87456 9.32695 4.55797 9.13169 4.36271L8.77814 4.00916Z" />
            </svg>
          </span>
        </button>
        {open && (
          <ul
            className={`dropdown-ul z-2 absolute mt-1 w-full rounded bg-[#efefef] ring-1 ring-gray-300 max-h-52 overflow-y-auto`}
          >
            {props.items.map((item, index) => {
              return (
                <li
                  key={index}
                  className="relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-xs outline-none hover:bg-[#e3e3e3]"
                  onClick={() => {
                    setOpen(false);
                    setText(item);
                    console.log('item: ', item);
                    props.handleSelectLanguage &&
                      props.handleSelectLanguage(item);
                  }}
                >
                  {item}
                </li>
              );
            })}
          </ul>
        )}
      </div>
    </>
  );
};



无法解决此问题,我使用了第三方库.

https://tailwindui.com/components/application-ui/elements/dropdowns

推荐答案

加载组件时,按以下顺序 for each 下拉菜单(一、二、三)添加三个事件侦听器至document.body:

- one (`handleOutsideClick` added by dropdown one)
- two (`handleOutsideClick` added by dropdown two)
- three (`handleOutsideClick` added by dropdown three)

现在,如果您决定打开DropDown One,例如,通过单击它,您将其open状态从true更新为false,导致您的组件重新呈现.组件完成重新渲染后,它将:

  • useEffect()中为DropDown 1调用Cleanup函数,为one删除添加到document.body中的事件侦听器
  • open开始调用useEffect()回调,状态改变,将Click事件侦听器添加回document.body以获取DropDown 1.

现在,添加到document.body的点击事件的顺序已经改变,因为我们删除了DropDown One的点击事件侦听器(one),并将其添加回(one'),因此它看起来如下所示:

- two
- three
- one'

这意味着当您try 并在DropDown 1之外单击以"关闭"它时,将首先调用DropDown 2(two)的回调,因为它现在是添加到document.body的第一个回调.DropDown twos回调只知道它的open状态(当前为false),这就是为什么您会看到false被记录.这里重要的部分是,在调用two之后不会调用threeone'的回调,因为在调用two时调用e.stopImmediatePropagation(),这会阻止这些后续事件处理程序被调用.相反,要解决这个问题,删除您的e.stopImmediatePropagation()调用,因为您确实希望调用这些回调,以便它们可以正确地更新其关联的DropDown open状态,以便在需要时关闭DropDown.

function handleOutsideClick(e: MouseEvent) {
  e.stopImmediatePropagation();

您会注意到,如果您点击一个下拉列表,然后点击另一个,前一个将关闭.如果您不想要这种行为,可以在handleClick()中调用stopPropagation();,这会阻止添加到document.body的任何回调被调用(因此可能会关闭一个下拉列表),因为Click事件不会冒泡到body:

const handleClick: React.MouseEventHandler<HTMLButtonElement> = (e) => {
  e.stopPropagation();
  setOpen(open => !open);
};

Reactjs相关问答推荐

App.js中的H2组件不会动态呈现.这可能是什么问题?

FireBase未与Reaction应用程序连接

useState导致对函数的无休止调用

根据处于react 状态的数组的名称键,将新对象添加到嵌套的对象数组中

StrictMode+SetInterval=双重渲染(即使使用useEffect清理)

太多的重新渲染 - React 中的无限循环.如何使用React.effect并使用fetch?

找不到名称setCometChat

console.logs 没有显示在浏览器控制台上,但显示在我的终端上

如何在 React Hook Form 中使用嵌套对象处理错误验证消息

对于 ref 上的可见 prop 或 open 方法来react 组件,哪种方式更好?

React 状态不更新

将水平滚动视图添加到底部导航选项卡

使用 react-markdown 组件时 Tailwind CSS 的问题

意外的标记.您可能需要一个合适的加载器来处理这种文件类型.书?.img? /*#__PURE__*/React.createElement("img",

如何通过 id 从 useSprings 数组中删除元素

将 ref 赋予功能组件以使用内部功能

将路径数组传递给Route会引发错误

谁能根据经验提供有关 useEffect 挂钩的更多信息

调用 Jest 和服务方法的问题

getAttribute 函数并不总是检索属性值