diff --git a/frontend/app/element/dropdown.stories.tsx b/frontend/app/element/dropdown.stories.tsx index 413775d5..cc79cc9e 100644 --- a/frontend/app/element/dropdown.stories.tsx +++ b/frontend/app/element/dropdown.stories.tsx @@ -64,27 +64,27 @@ export const Test: Story = { }, args: { items: [ - { label: "Option 1", onClick: () => console.log("Clicked Option 1") }, + { label: "Option 1", onClick: () => null }, { label: "Option 2", onClick: () => console.log("Clicked Option 2"), subItems: [ - { label: "Option 2 -> 1", onClick: () => console.log("Clicked Sub-option 1") }, - { label: "Option 2 -> 2", onClick: () => console.log("Clicked Sub-option 2") }, + { label: "Option 2 -> 1", onClick: () => null }, + { label: "Option 2 -> 2", onClick: () => null }, ], }, { label: "Option 3", onClick: () => console.log("Clicked Option 3"), subItems: [ - { label: "Option 3 -> 1", onClick: () => console.log("Clicked Sub-option 1") }, - { label: "Option 3 -> 2", onClick: () => console.log("Clicked Sub-option 2") }, + { label: "Option 3 -> 1", onClick: () => null }, + { label: "Option 3 -> 2", onClick: () => null }, { label: "Option 3 -> 3", onClick: () => console.log("Clicked Option 3"), subItems: [ - { label: "Option 3 -> 3 -> 1", onClick: () => console.log("Clicked Sub-option 1") }, - { label: "Option 3 -> 3 -> 2", onClick: () => console.log("Clicked Sub-option 2") }, + { label: "Option 3 -> 3 -> 1", onClick: () => null }, + { label: "Option 3 -> 3 -> 2", onClick: () => null }, ], }, ], diff --git a/frontend/app/element/dropdown.tsx b/frontend/app/element/dropdown.tsx index 7328699c..e59113b7 100644 --- a/frontend/app/element/dropdown.tsx +++ b/frontend/app/element/dropdown.tsx @@ -6,6 +6,64 @@ import ReactDOM from "react-dom"; import "./dropdown.less"; +const SubMenu = ({ + subItems, + parentKey, + parentRef, + subMenuPosition, + visibleSubMenus, + handleMouseEnterItem, + subMenuRefs, +}: { + subItems: DropdownItem[]; + parentKey: string; + parentRef: React.RefObject; + subMenuPosition: any; + visibleSubMenus: any; + handleMouseEnterItem: any; + subMenuRefs: any; +}) => { + return ( +
+ {subItems.map((item, idx) => { + const newKey = `${parentKey}-${idx}`; // Full hierarchical key + console.log("newKey===============", newKey, visibleSubMenus[newKey]); + console.log("visibleSubMenus************", visibleSubMenus); + return ( +
handleMouseEnterItem(event, parentKey, idx, item)} + > + {item.label} + {item.subItems && } + {visibleSubMenus[newKey]?.visible && item.subItems && ( + + )} +
+ ); + })} +
+ ); +}; + type DropdownItem = { label: string; onClick?: () => void; @@ -20,22 +78,27 @@ interface DropdownProps { } const Dropdown = memo(({ items, anchorRef, boundaryRef, className }: DropdownProps) => { - const [visibleSubMenus, setVisibleSubMenus] = useState<{ [key: number]: any }>({}); // Track visibility of each submenu (nested object) - const [subMenuPosition, setSubMenuPosition] = useState<{ [key: number]: { top: number; left: number } }>({}); + const [visibleSubMenus, setVisibleSubMenus] = useState<{ [key: string]: any }>({}); // Track visibility of each submenu + const [subMenuPosition, setSubMenuPosition] = useState<{ + [key: string]: { top: number; left: number; label: string }; + }>({}); const [position, setPosition] = useState({ top: 0, left: 0 }); const dropdownRef = useRef(null); - const subMenuRefs = useRef>>([]); // Array of refs for each submenu + const subMenuRefs = useRef<{ [key: string]: React.RefObject }>({}); // Store refs using flat structure const effectiveBoundaryRef: React.RefObject = boundaryRef ?? { current: document.documentElement }; const width = useWidth(effectiveBoundaryRef); const height = useHeight(effectiveBoundaryRef); - // Add ref for each submenu item dynamically - if (subMenuRefs.current.length !== items.length) { - subMenuRefs.current = Array(items.length) - .fill(null) - .map((_, i) => subMenuRefs.current[i] || React.createRef()); - } + // console.log("visibleSubMenus.............", visibleSubMenus); + + // Add ref for each submenu dynamically + items.forEach((_, idx) => { + const key = `${idx}`; + if (!subMenuRefs.current[key]) { + subMenuRefs.current[key] = React.createRef(); + } + }); useLayoutEffect(() => { if (anchorRef.current && dropdownRef.current) { @@ -64,15 +127,20 @@ const Dropdown = memo(({ items, anchorRef, boundaryRef, className }: DropdownPro } }, [width, height]); + // Position submenus based on available space const handleSubMenuPosition = ( - parentIndex: number, - index: number, + key: string, itemRect: DOMRect, - parentRef: React.RefObject | React.RefObject + parentRef: React.RefObject, + label: string ) => { - const subMenuRef = subMenuRefs.current[index].current; - const parentMenuRef = parentRef.current; - if (subMenuRef && parentMenuRef) { + // Delay the position calculation to allow the subMenuRef to be populated + setTimeout(() => { + const subMenuRef = subMenuRefs.current[key]?.current; + if (!subMenuRef) { + return; // Avoid proceeding if the ref is still null + } + const boundaryRect = effectiveBoundaryRef.current?.getBoundingClientRect() || { top: 0, left: 0, @@ -80,170 +148,108 @@ const Dropdown = memo(({ items, anchorRef, boundaryRef, className }: DropdownPro right: window.innerWidth, }; - // Position to the right of the hovered item - let left = itemRect.width - 5; + const submenuWidth = subMenuRef.offsetWidth; + const submenuHeight = subMenuRef.offsetHeight; - // Adjust to the left if overflowing right boundary - if (left + subMenuRef.offsetWidth > boundaryRect.right) { - left = itemRect.left - subMenuRef.offsetWidth; // Align left if overflow + let left = itemRect.width; // Default position to the right of the hovered item + let top = submenuHeight - itemRect.height; + + // Adjust to the left if overflowing the right boundary + if (left + submenuWidth > window.innerWidth) { + left = itemRect.left - submenuWidth; } - // Calculate top based on parent's position - const parentRect = parentMenuRef.getBoundingClientRect(); - const top = itemRect.top - parentRect.top; + // Adjust if the submenu overflows the bottom boundary + if (top + submenuHeight > window.innerHeight) { + top = window.innerHeight - submenuHeight - 10; + } + // Set the submenu position setSubMenuPosition((prev) => ({ ...prev, - [parentIndex]: { - ...prev[parentIndex], - [index]: { top, left }, - }, + [key]: { top, left, label }, })); - } + }, 0); // Delay by 50 milliseconds to ensure the submenu has rendered }; - // Recursive function to update visibility for multi-level submenus - const updateVisibility = (currentState: any, parentIndex: number | null, index: number, item: DropdownItem) => { - // Recursively clone the current state - const updatedState = { ...currentState }; + // Handle submenu visibility updates + const updateVisibility = (currentState: any, key: string, item: DropdownItem) => { + const updatedState = Object.keys(currentState).reduce((acc, k) => { + acc[k] = { ...currentState[k], visible: false }; + return acc; + }, {} as any); - // Handle the case where we need to update a nested level (parentIndex is not null) - if (parentIndex !== null) { - // Ensure that the parentIndex exists in the state - if (!updatedState[parentIndex]) { - updatedState[parentIndex] = {}; - } - - // Reset visibility for all nested submenus at this level - for (let key in updatedState[parentIndex]) { - if (updatedState[parentIndex][key]?.visible !== undefined) { - updatedState[parentIndex][key].visible = false; - } - } - - // Update the submenu at the correct level - updatedState[parentIndex] = updateVisibility( - updatedState[parentIndex], // Pass the state for the current level - null, // Stop recursion when we reach the correct level - index, - item - ); - } else { - // We're at the correct level (root or no parent), so set all siblings' visibility to false - for (let key in updatedState) { - if (updatedState[key]?.visible !== undefined) { - updatedState[key].visible = false; - } - } - // Set the current index submenu to visible - updatedState[index] = { visible: true, label: item.label }; - } + updatedState[key] = { visible: true, label: item.label }; return updatedState; }; const handleMouseEnterItem = ( event: React.MouseEvent, - parentIndex: number | null, + parentKey: string | null, index: number, - item: DropdownItem, - parentRef: React.RefObject | React.RefObject, - reason?: string + item: DropdownItem ) => { event.stopPropagation(); + + const key = parentKey ? `${parentKey}-${index}` : `${index}`; // Full hierarchical key + setVisibleSubMenus((prev) => { - return updateVisibility(prev, parentIndex, index, item); + // Preserve the current hierarchy visibility and only update the current item + const updatedState = { ...prev }; + + // Ensure the current submenu is visible + updatedState[key] = { visible: true, label: item.label }; + + return updatedState; }); - if (subMenuRefs.current[index].current) { - const itemRect = subMenuRefs.current[index].current!.parentElement?.getBoundingClientRect(); - if (itemRect) { - handleSubMenuPosition(parentIndex ?? index, index, itemRect, parentRef); - } - } + const itemRect = event.currentTarget.getBoundingClientRect(); + handleSubMenuPosition(key, itemRect, dropdownRef, item.label); }; - const handleMouseLeaveItem = (parentIndex: number | null, index: number) => { + // Hide submenu on mouse leave + const handleMouseLeaveItem = (key: string) => { setTimeout(() => { setVisibleSubMenus((prev) => ({ ...prev, - [parentIndex ?? index]: { - ...prev[parentIndex ?? index], - [index]: { visible: false, label: prev[parentIndex ?? index][index].label }, // Maintain label - }, - })); // Hide the specific submenu + [key]: { ...prev[key], visible: false }, + })); }, 200); }; - // Recursive renderSubMenu to handle multiple nested submenus - const renderSubMenu = ( - subItems: DropdownItem[], - parentIndex: number | null, - index: number, - parentRef: React.RefObject - ) => { - return ( -
- {subItems.map((item, idx) => ( -
- handleMouseEnterItem( - event, - index, - idx, - item, - subMenuRefs.current[index], - "submenu item hovered" - ) - } - onClick={item.onClick} - > - {item.label} - {item.subItems && } - {visibleSubMenus[index]?.[idx]?.visible && - item.subItems && - renderSubMenu(item.subItems, index, idx, subMenuRefs.current[index])} -
- ))} -
- ); - }; - - if (!anchorRef?.current) { - return null; - } - + // Render the main dropdown and submenus return ReactDOM.createPortal(
- {items.map((item, index) => ( -
- handleMouseEnterItem(event, null, index, item, dropdownRef, "root menu item hovered") - } - onClick={item.onClick} - > - {item.label} - {item.subItems && } - {visibleSubMenus[index]?.visible && - item.subItems && - renderSubMenu(item.subItems, null, index, dropdownRef)} -
- ))} + {items.map((item, index) => { + const key = `${index}`; + return ( +
handleMouseEnterItem(event, null, index, item)} + > + {item.label} + {item.subItems && } + {visibleSubMenus[key]?.visible && item.subItems && ( + + )} +
+ ); + })}
, document.body );