mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
fix: context menu dynamic positioning, multiple context menus opening issue (#1913)
* fix: context menu positioning * fix: close already opened context menu on outside click
This commit is contained in:
parent
64b5ba196f
commit
cebc8bdc8d
@ -86,7 +86,8 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
// context menu
|
// context menu
|
||||||
const [contextMenu, setContextMenu] = useState(false);
|
const [contextMenu, setContextMenu] = useState(false);
|
||||||
const [contextMenuPosition, setContextMenuPosition] = useState({ x: 0, y: 0 });
|
const [contextMenuPosition, setContextMenuPosition] = useState<React.MouseEvent | null>(null);
|
||||||
|
|
||||||
const [isMenuActive, setIsMenuActive] = useState(false);
|
const [isMenuActive, setIsMenuActive] = useState(false);
|
||||||
const [isDropdownActive, setIsDropdownActive] = useState(false);
|
const [isDropdownActive, setIsDropdownActive] = useState(false);
|
||||||
|
|
||||||
@ -201,7 +202,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ContextMenu
|
<ContextMenu
|
||||||
position={contextMenuPosition}
|
clickEvent={contextMenuPosition}
|
||||||
title="Quick actions"
|
title="Quick actions"
|
||||||
isOpen={contextMenu}
|
isOpen={contextMenu}
|
||||||
setIsOpen={setContextMenu}
|
setIsOpen={setContextMenu}
|
||||||
@ -243,7 +244,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
|||||||
onContextMenu={(e) => {
|
onContextMenu={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setContextMenu(true);
|
setContextMenu(true);
|
||||||
setContextMenuPosition({ x: e.pageX, y: e.pageY });
|
setContextMenuPosition(e);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex flex-col justify-between gap-1.5 group/card relative select-none px-3.5 py-3 h-[118px]">
|
<div className="flex flex-col justify-between gap-1.5 group/card relative select-none px-3.5 py-3 h-[118px]">
|
||||||
|
@ -33,7 +33,7 @@ import {
|
|||||||
} from "@heroicons/react/24/outline";
|
} from "@heroicons/react/24/outline";
|
||||||
import { LayerDiagonalIcon } from "components/icons";
|
import { LayerDiagonalIcon } from "components/icons";
|
||||||
// helpers
|
// helpers
|
||||||
import { copyTextToClipboard, truncateText } from "helpers/string.helper";
|
import { copyTextToClipboard } from "helpers/string.helper";
|
||||||
import { handleIssuesMutation } from "constants/issue";
|
import { handleIssuesMutation } from "constants/issue";
|
||||||
// types
|
// types
|
||||||
import { ICurrentUserResponse, IIssue, IIssueViewProps, ISubIssueResponse, UserAuth } from "types";
|
import { ICurrentUserResponse, IIssue, IIssueViewProps, ISubIssueResponse, UserAuth } from "types";
|
||||||
@ -71,7 +71,7 @@ export const SingleListIssue: React.FC<Props> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
// context menu
|
// context menu
|
||||||
const [contextMenu, setContextMenu] = useState(false);
|
const [contextMenu, setContextMenu] = useState(false);
|
||||||
const [contextMenuPosition, setContextMenuPosition] = useState({ x: 0, y: 0 });
|
const [contextMenuPosition, setContextMenuPosition] = useState<React.MouseEvent | null>(null);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
||||||
@ -167,7 +167,7 @@ export const SingleListIssue: React.FC<Props> = ({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ContextMenu
|
<ContextMenu
|
||||||
position={contextMenuPosition}
|
clickEvent={contextMenuPosition}
|
||||||
title="Quick actions"
|
title="Quick actions"
|
||||||
isOpen={contextMenu}
|
isOpen={contextMenu}
|
||||||
setIsOpen={setContextMenu}
|
setIsOpen={setContextMenu}
|
||||||
@ -199,7 +199,7 @@ export const SingleListIssue: React.FC<Props> = ({
|
|||||||
onContextMenu={(e) => {
|
onContextMenu={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setContextMenu(true);
|
setContextMenu(true);
|
||||||
setContextMenuPosition({ x: e.pageX, y: e.pageY });
|
setContextMenuPosition(e);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex-grow cursor-pointer min-w-[200px] whitespace-nowrap overflow-hidden overflow-ellipsis">
|
<div className="flex-grow cursor-pointer min-w-[200px] whitespace-nowrap overflow-hidden overflow-ellipsis">
|
||||||
|
@ -1,47 +1,76 @@
|
|||||||
import React, { useEffect } from "react";
|
import React, { useEffect, useRef } from "react";
|
||||||
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
|
// hooks
|
||||||
|
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
position: {
|
clickEvent: React.MouseEvent | null;
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
};
|
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
title?: string | JSX.Element;
|
title?: string | JSX.Element;
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ContextMenu = ({ position, children, title, isOpen, setIsOpen }: Props) => {
|
const ContextMenu = ({ clickEvent, children, title, isOpen, setIsOpen }: Props) => {
|
||||||
|
const contextMenuRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
// Close the context menu when clicked outside
|
||||||
|
useOutsideClickDetector(contextMenuRef, () => {
|
||||||
|
if (isOpen) setIsOpen(false);
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const hideContextMenu = () => {
|
const hideContextMenu = () => {
|
||||||
if (isOpen) setIsOpen(false);
|
if (isOpen) setIsOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener("click", hideContextMenu);
|
const escapeKeyEvent = (e: KeyboardEvent) => {
|
||||||
window.addEventListener("keydown", (e: KeyboardEvent) => {
|
|
||||||
if (e.key === "Escape") hideContextMenu();
|
if (e.key === "Escape") hideContextMenu();
|
||||||
});
|
};
|
||||||
|
|
||||||
|
window.addEventListener("click", hideContextMenu);
|
||||||
|
window.addEventListener("keydown", escapeKeyEvent);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener("click", hideContextMenu);
|
window.removeEventListener("click", hideContextMenu);
|
||||||
window.removeEventListener("keydown", hideContextMenu);
|
window.removeEventListener("keydown", escapeKeyEvent);
|
||||||
};
|
};
|
||||||
}, [isOpen, setIsOpen]);
|
}, [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 (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`fixed z-20 h-full w-full ${
|
className={`fixed z-50 top-0 left-0 h-full w-full ${
|
||||||
isOpen ? "pointer-events-auto opacity-100" : "pointer-events-none opacity-0"
|
isOpen ? "pointer-events-auto opacity-100" : "pointer-events-none opacity-0"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`fixed z-20 flex min-w-[8rem] flex-col items-stretch gap-1 rounded-md border border-custom-border-200 bg-custom-background-90 p-2 text-xs shadow-lg`}
|
ref={contextMenuRef}
|
||||||
style={{
|
className={`fixed z-50 flex min-w-[8rem] flex-col items-stretch gap-1 rounded-md border border-custom-border-200 bg-custom-background-90 p-2 text-xs shadow-lg`}
|
||||||
left: `${position.x}px`,
|
|
||||||
top: `${position.y}px`,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{title && (
|
{title && (
|
||||||
<h4 className="border-b border-custom-border-200 px-1 py-1 pb-2 text-[0.8rem] font-medium">
|
<h4 className="border-b border-custom-border-200 px-1 py-1 pb-2 text-[0.8rem] font-medium">
|
||||||
|
@ -8,10 +8,10 @@ const useOutsideClickDetector = (ref: React.RefObject<HTMLElement>, callback: ()
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.addEventListener("click", handleClick);
|
document.addEventListener("mousedown", handleClick);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener("click", handleClick);
|
document.removeEventListener("mousedown", handleClick);
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user