| ... | ... | @@ -11,7 +11,7 @@ import { |
| 11 | 11 | MoreHorizontalIcon, |
| 12 | 12 | PlusIcon, |
| 13 | 13 | } from "lucide-react"; |
| 14 | | import { useCallback, useMemo } from "react"; |
| 14 | import { useCallback, useMemo, useState } from "react"; |
| 15 | 15 | |
| 16 | 16 | import { writeTextToClipboard } from "~/hooks/useCopyToClipboard"; |
| 17 | 17 | import { useOpenPrLink } from "~/lib/openPullRequestLink"; |
| ... | ... | @@ -84,6 +84,15 @@ function LinkRow({ |
| 84 | 84 | onSetWatching: ((link: ThreadPullRequestLink, watching: boolean) => void) | null; |
| 85 | 85 | }) { |
| 86 | 86 | const openPrLink = useOpenPrLink(threadRef); |
| 87 | const [menuOpen, setMenuOpen] = useState(false); |
| 88 | const [menuPosition, setMenuPosition] = useState<{ x: number; y: number } | null>(null); |
| 89 | const menuAnchor = useMemo( |
| 90 | () => |
| 91 | menuPosition |
| 92 | ? { getBoundingClientRect: () => new DOMRect(menuPosition.x, menuPosition.y, 0, 0) } |
| 93 | : undefined, |
| 94 | [menuPosition], |
| 95 | ); |
| 87 | 96 | const { link, depth, stack } = line; |
| 88 | 97 | const snapshot = link.snapshot; |
| 89 | 98 | const open = snapshot === null || snapshot.state === "open"; |
| ... | ... | @@ -91,6 +100,12 @@ function LinkRow({ |
| 91 | 100 | return ( |
| 92 | 101 | <div |
| 93 | 102 | className={cn(PULL_REQUEST_ROW_CLASS, "relative hover:bg-accent/60")} |
| 103 | onContextMenu={(event) => { |
| 104 | event.preventDefault(); |
| 105 | event.stopPropagation(); |
| 106 | setMenuPosition({ x: event.clientX, y: event.clientY }); |
| 107 | setMenuOpen(true); |
| 108 | }} |
| 94 | 109 | // Each layer steps in under the one it targets. The step is capped: beyond a few layers |
| 95 | 110 | // the indent only says "still in the stack", which the connector line already does, and |
| 96 | 111 | // a sixteen-layer stack would otherwise stair-step off the right edge. |
| ... | ... | @@ -221,7 +236,13 @@ function LinkRow({ |
| 221 | 236 | )} |
| 222 | 237 | > |
| 223 | 238 | <span aria-hidden className="absolute inset-0 bg-accent/60" /> |
| 224 | | <Menu> |
| 239 | <Menu |
| 240 | open={menuOpen} |
| 241 | onOpenChange={(open) => { |
| 242 | setMenuOpen(open); |
| 243 | if (!open) setMenuPosition(null); |
| 244 | }} |
| 245 | > |
| 225 | 246 | <MenuTrigger |
| 226 | 247 | render={ |
| 227 | 248 | <Button |
| ... | ... | @@ -234,7 +255,12 @@ function LinkRow({ |
| 234 | 255 | </Button> |
| 235 | 256 | } |
| 236 | 257 | /> |
| 237 | | <MenuPopup align="end" side="bottom"> |
| 258 | <MenuPopup |
| 259 | anchor={menuAnchor} |
| 260 | align={menuPosition ? "start" : "end"} |
| 261 | side="bottom" |
| 262 | sideOffset={menuPosition ? 0 : 4} |
| 263 | > |
| 238 | 264 | <MenuItem onClick={() => void writeTextToClipboard(link.url, "link")}> |
| 239 | 265 | <LinkIcon className="size-3.5" /> |
| 240 | 266 | Copy link |