From cebc8bdc8d480a8815e4339009bdf1e816c393c4 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Sat, 19 Aug 2023 18:50:12 +0530 Subject: [PATCH] fix: context menu dynamic positioning, multiple context menus opening issue (#1913) * fix: context menu positioning * fix: close already opened context menu on outside click --- .../core/views/board-view/single-issue.tsx | 7 ++- .../core/views/list-view/single-issue.tsx | 8 +-- .../components/ui/dropdowns/context-menu.tsx | 61 ++++++++++++++----- apps/app/hooks/use-outside-click-detector.tsx | 4 +- 4 files changed, 55 insertions(+), 25 deletions(-) diff --git a/apps/app/components/core/views/board-view/single-issue.tsx b/apps/app/components/core/views/board-view/single-issue.tsx index 4a1f25fe0..b676e809c 100644 --- a/apps/app/components/core/views/board-view/single-issue.tsx +++ b/apps/app/components/core/views/board-view/single-issue.tsx @@ -86,7 +86,8 @@ export const SingleBoardIssue: React.FC = ({ }) => { // context menu const [contextMenu, setContextMenu] = useState(false); - const [contextMenuPosition, setContextMenuPosition] = useState({ x: 0, y: 0 }); + const [contextMenuPosition, setContextMenuPosition] = useState(null); + const [isMenuActive, setIsMenuActive] = useState(false); const [isDropdownActive, setIsDropdownActive] = useState(false); @@ -201,7 +202,7 @@ export const SingleBoardIssue: React.FC = ({ return ( <> = ({ onContextMenu={(e) => { e.preventDefault(); setContextMenu(true); - setContextMenuPosition({ x: e.pageX, y: e.pageY }); + setContextMenuPosition(e); }} >
diff --git a/apps/app/components/core/views/list-view/single-issue.tsx b/apps/app/components/core/views/list-view/single-issue.tsx index 7d1cea37e..eafe74612 100644 --- a/apps/app/components/core/views/list-view/single-issue.tsx +++ b/apps/app/components/core/views/list-view/single-issue.tsx @@ -33,7 +33,7 @@ import { } from "@heroicons/react/24/outline"; import { LayerDiagonalIcon } from "components/icons"; // helpers -import { copyTextToClipboard, truncateText } from "helpers/string.helper"; +import { copyTextToClipboard } from "helpers/string.helper"; import { handleIssuesMutation } from "constants/issue"; // types import { ICurrentUserResponse, IIssue, IIssueViewProps, ISubIssueResponse, UserAuth } from "types"; @@ -71,7 +71,7 @@ export const SingleListIssue: React.FC = ({ }) => { // context menu const [contextMenu, setContextMenu] = useState(false); - const [contextMenuPosition, setContextMenuPosition] = useState({ x: 0, y: 0 }); + const [contextMenuPosition, setContextMenuPosition] = useState(null); const router = useRouter(); const { workspaceSlug, projectId, cycleId, moduleId } = router.query; @@ -167,7 +167,7 @@ export const SingleListIssue: React.FC = ({ return ( <> = ({ onContextMenu={(e) => { e.preventDefault(); setContextMenu(true); - setContextMenuPosition({ x: e.pageX, y: e.pageY }); + setContextMenuPosition(e); }} >
diff --git a/apps/app/components/ui/dropdowns/context-menu.tsx b/apps/app/components/ui/dropdowns/context-menu.tsx index d7ecb4de7..78df25ec9 100644 --- a/apps/app/components/ui/dropdowns/context-menu.tsx +++ b/apps/app/components/ui/dropdowns/context-menu.tsx @@ -1,47 +1,76 @@ -import React, { useEffect } from "react"; +import React, { useEffect, useRef } from "react"; import Link from "next/link"; +// hooks +import useOutsideClickDetector from "hooks/use-outside-click-detector"; + type Props = { - position: { - x: number; - y: number; - }; + clickEvent: React.MouseEvent | null; children: React.ReactNode; title?: string | JSX.Element; isOpen: boolean; setIsOpen: React.Dispatch>; }; -const ContextMenu = ({ position, children, title, isOpen, setIsOpen }: Props) => { +const ContextMenu = ({ clickEvent, children, title, isOpen, setIsOpen }: Props) => { + const contextMenuRef = useRef(null); + + // Close the context menu when clicked outside + useOutsideClickDetector(contextMenuRef, () => { + if (isOpen) setIsOpen(false); + }); + useEffect(() => { const hideContextMenu = () => { if (isOpen) setIsOpen(false); }; - window.addEventListener("click", hideContextMenu); - window.addEventListener("keydown", (e: KeyboardEvent) => { + const escapeKeyEvent = (e: KeyboardEvent) => { if (e.key === "Escape") hideContextMenu(); - }); + }; + + window.addEventListener("click", hideContextMenu); + window.addEventListener("keydown", escapeKeyEvent); return () => { window.removeEventListener("click", hideContextMenu); - window.removeEventListener("keydown", hideContextMenu); + window.removeEventListener("keydown", escapeKeyEvent); }; }, [isOpen, setIsOpen]); + useEffect(() => { + const contextMenu = contextMenuRef.current; + + if (contextMenu && isOpen) { + const contextMenuWidth = contextMenu.clientWidth; + const contextMenuHeight = contextMenu.clientHeight; + + const clickX = clickEvent?.pageX || 0; + const clickY = clickEvent?.pageY || 0; + + let top = clickY; + // check if there's enough space at the bottom, otherwise show at the top + if (clickY + contextMenuHeight > window.innerHeight) top = clickY - contextMenuHeight; + + // check if there's enough space on the right, otherwise show on the left + let left = clickX; + if (clickX + contextMenuWidth > window.innerWidth) left = clickX - contextMenuWidth; + + contextMenu.style.top = `${top}px`; + contextMenu.style.left = `${left}px`; + } + }, [clickEvent, isOpen]); + return (
{title && (

diff --git a/apps/app/hooks/use-outside-click-detector.tsx b/apps/app/hooks/use-outside-click-detector.tsx index f20666f8c..5331d11c8 100644 --- a/apps/app/hooks/use-outside-click-detector.tsx +++ b/apps/app/hooks/use-outside-click-detector.tsx @@ -8,10 +8,10 @@ const useOutsideClickDetector = (ref: React.RefObject, callback: () }; useEffect(() => { - document.addEventListener("click", handleClick); + document.addEventListener("mousedown", handleClick); return () => { - document.removeEventListener("click", handleClick); + document.removeEventListener("mousedown", handleClick); }; }); };