From 3c60757c133b97128b697996637a2e3b534cf760 Mon Sep 17 00:00:00 2001 From: Red Adaya Date: Tue, 24 Sep 2024 13:05:26 +0800 Subject: [PATCH] hide if click outside the menus --- frontend/app/element/dropdown.less | 1 + frontend/app/element/dropdown.stories.tsx | 14 +++++++++- frontend/app/element/dropdown.tsx | 31 +++++++++++++++++++++-- 3 files changed, 43 insertions(+), 3 deletions(-) diff --git a/frontend/app/element/dropdown.less b/frontend/app/element/dropdown.less index 1cdc91a9..f256b706 100644 --- a/frontend/app/element/dropdown.less +++ b/frontend/app/element/dropdown.less @@ -26,6 +26,7 @@ line-height: normal; letter-spacing: -0.12px; width: 100%; + border-radius: 2px; &:hover, &.active { diff --git a/frontend/app/element/dropdown.stories.tsx b/frontend/app/element/dropdown.stories.tsx index 77fa35f8..461067e1 100644 --- a/frontend/app/element/dropdown.stories.tsx +++ b/frontend/app/element/dropdown.stories.tsx @@ -1,4 +1,5 @@ import type { Meta, StoryObj } from "@storybook/react"; +import { fn } from "@storybook/test"; import { useRef, useState } from "react"; import { Dropdown } from "./dropdown"; @@ -10,6 +11,7 @@ const meta = { anchorRef: undefined, boundaryRef: undefined, className: "", + setVisibility: fn(), }, argTypes: { items: { @@ -18,6 +20,9 @@ const meta = { anchorRef: { description: "Element to attach the dropdown", }, + setVisibility: { + description: "Visibility event handler", + }, boundaryRef: { description: "Component that defines the boundaries of the dropdown", }, @@ -82,7 +87,14 @@ export const Test: Story = { Anchor Element - {isDropdownVisible && } + {isDropdownVisible && ( + setIsDropdownVisible(visible)} + anchorRef={anchorRef} + boundaryRef={boundaryRef} + /> + )} ); }, diff --git a/frontend/app/element/dropdown.tsx b/frontend/app/element/dropdown.tsx index fd9ad2d7..47fab13d 100644 --- a/frontend/app/element/dropdown.tsx +++ b/frontend/app/element/dropdown.tsx @@ -1,7 +1,7 @@ import { useHeight } from "@/app/hook/useHeight"; import { useWidth } from "@/app/hook/useWidth"; import clsx from "clsx"; -import React, { memo, useLayoutEffect, useRef, useState } from "react"; +import React, { memo, useEffect, useLayoutEffect, useRef, useState } from "react"; import ReactDOM from "react-dom"; import "./dropdown.less"; @@ -92,8 +92,8 @@ const SubMenu = memo( type DropdownItem = { label: string; - onClick?: (e) => void; subItems?: DropdownItem[]; + onClick?: (e) => void; }; const Dropdown = memo( @@ -102,11 +102,13 @@ const Dropdown = memo( anchorRef, boundaryRef, className, + setVisibility, }: { items: DropdownItem[]; anchorRef: React.RefObject; boundaryRef?: React.RefObject; className?: string; + setVisibility: (_: boolean) => void; }) => { const [visibleSubMenus, setVisibleSubMenus] = useState<{ [key: string]: any }>({}); const [hoveredItems, setHoveredItems] = useState([]); // Track hovered items and ancestors @@ -159,6 +161,31 @@ const Dropdown = memo( } }, [width, height]); + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + const isClickInsideDropdown = dropdownRef.current && dropdownRef.current.contains(event.target as Node); + const isClickInsideAnchor = anchorRef.current && anchorRef.current.contains(event.target as Node); + + // Check if the click is inside any of the submenus + const isClickInsideSubMenus = Object.keys(subMenuRefs.current).some( + (key) => + subMenuRefs.current[key]?.current && + subMenuRefs.current[key]?.current.contains(event.target as Node) + ); + + // If the click is outside the dropdown, anchor, and all submenus, hide the dropdown + if (!isClickInsideDropdown && !isClickInsideAnchor && !isClickInsideSubMenus) { + setVisibility(false); + } + }; + + document.addEventListener("mousedown", handleClickOutside); + + return () => { + document.removeEventListener("mousedown", handleClickOutside); + }; + }, [dropdownRef, anchorRef, subMenuRefs]); + // Position submenus based on available space and scroll position const handleSubMenuPosition = ( key: string,