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:
Aaryan Khandelwal 2023-08-19 18:50:12 +05:30 committed by GitHub
parent 64b5ba196f
commit cebc8bdc8d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 55 additions and 25 deletions

View File

@ -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]">

View File

@ -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">

View File

@ -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">

View File

@ -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);
}; };
}); });
}; };