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,