authorgravatar for jakeleventhal@me.comJake Leventhal <jakeleventhal@me.com> 2026-10-06 19:21:17-04:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-06 20:21:17-03:00
logd8d037eae1b77a77f372fd1afd2662a31ebe37c8
tree0fa7a62a2d7ee5aa3a64deaa0ace75d1f7432979
parent13aabf3d7d0ec13e902234e86d3df27149c0e4a9
signature Signed by PGP key B5690EEEBB952194

fix(web): open pull request row actions on right-click (#16612)


1 files changed, 29 insertions(+), 3 deletions(-)

apps/web/src/components/pullRequest/ThreadPullRequestsPanel.tsx+29-3
...@@ -11,7 +11,7 @@ import {...@@ -11,7 +11,7 @@ import {
11 MoreHorizontalIcon,11 MoreHorizontalIcon,
12 PlusIcon,12 PlusIcon,
13} from "lucide-react";13} from "lucide-react";
14import { useCallback, useMemo } from "react";14import { useCallback, useMemo, useState } from "react";
1515
16import { writeTextToClipboard } from "~/hooks/useCopyToClipboard";16import { writeTextToClipboard } from "~/hooks/useCopyToClipboard";
17import { useOpenPrLink } from "~/lib/openPullRequestLink";17import { useOpenPrLink } from "~/lib/openPullRequestLink";
...@@ -84,6 +84,15 @@ function LinkRow({...@@ -84,6 +84,15 @@ function LinkRow({
84 onSetWatching: ((link: ThreadPullRequestLink, watching: boolean) => void) | null;84 onSetWatching: ((link: ThreadPullRequestLink, watching: boolean) => void) | null;
85}) {85}) {
86 const openPrLink = useOpenPrLink(threadRef);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 const { link, depth, stack } = line;96 const { link, depth, stack } = line;
88 const snapshot = link.snapshot;97 const snapshot = link.snapshot;
89 const open = snapshot === null || snapshot.state === "open";98 const open = snapshot === null || snapshot.state === "open";
...@@ -91,6 +100,12 @@ function LinkRow({...@@ -91,6 +100,12 @@ function LinkRow({
91 return (100 return (
92 <div101 <div
93 className={cn(PULL_REQUEST_ROW_CLASS, "relative hover:bg-accent/60")}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 // Each layer steps in under the one it targets. The step is capped: beyond a few layers109 // Each layer steps in under the one it targets. The step is capped: beyond a few layers
95 // the indent only says "still in the stack", which the connector line already does, and110 // the indent only says "still in the stack", which the connector line already does, and
96 // a sixteen-layer stack would otherwise stair-step off the right edge.111 // a sixteen-layer stack would otherwise stair-step off the right edge.
...@@ -221,7 +236,13 @@ function LinkRow({...@@ -221,7 +236,13 @@ function LinkRow({
221 )}236 )}
222 >237 >
223 <span aria-hidden className="absolute inset-0 bg-accent/60" />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 <MenuTrigger246 <MenuTrigger
226 render={247 render={
227 <Button248 <Button
...@@ -234,7 +255,12 @@ function LinkRow({...@@ -234,7 +255,12 @@ function LinkRow({
234 </Button>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 <MenuItem onClick={() => void writeTextToClipboard(link.url, "link")}>264 <MenuItem onClick={() => void writeTextToClipboard(link.url, "link")}>
239 <LinkIcon className="size-3.5" />265 <LinkIcon className="size-3.5" />
240 Copy link266 Copy link