diff --git a/src/browser/components/home.tsx b/src/browser/components/home.tsx index d3a01cc..35f8297 100644 --- a/src/browser/components/home.tsx +++ b/src/browser/components/home.tsx @@ -48,6 +48,7 @@ import { DialogTitle, } from "../ui/dialog"; import { Button } from "../ui/button"; +import { BlockLink } from "../ui/block-link"; import { cn } from "../cn"; import { Skeleton } from "../ui/skeleton"; import { UserHoverCard } from "../ui/user-hover-card"; @@ -1962,8 +1963,17 @@ function PRListItem({ pr, onSelect }: PRListItemProps) { return pr.updated_at ? pr.updated_at > baseline : false; }, [repoInfo, pr.updated_at, pr.viewerLastReviewAt, pr.isReadByViewer]); - const handleClick = () => { + const href = repoInfo + ? `/${repoInfo.owner}/${repoInfo.repo}/pull/${pr.number}` + : undefined; + + const handleLinkClick = (e: React.MouseEvent) => { + // Let the browser handle modifier clicks natively (new tab/window). + if (e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) { + return; + } if (repoInfo) { + e.preventDefault(); onSelect(repoInfo.owner, repoInfo.repo, pr.number, pr.title); } }; @@ -2294,10 +2304,7 @@ function PRListItem({ pr, onSelect }: PRListItemProps) { }; return ( - + ); } diff --git a/src/browser/ui/block-link.tsx b/src/browser/ui/block-link.tsx new file mode 100644 index 0000000..b60963d --- /dev/null +++ b/src/browser/ui/block-link.tsx @@ -0,0 +1,80 @@ +import * as React from "react"; +import { Slot as SlotPrimitive } from "radix-ui"; + +// A ref that BlockLink.Root uses to know where its BlockLink.Link is. Root +// clicks are re-dispatched onto this element so the browser handles them as +// native activation. +type BlockLinkContextValue = { + linkRef: React.RefObject; +}; + +const BlockLinkContext = React.createContext( + null +); + +type RootProps = Omit, "onClick"> & { + onClick?: React.MouseEventHandler; + children: React.ReactNode; + asChild?: boolean; +}; + +function Root({ onClick, children, asChild, ...props }: RootProps) { + const linkRef = React.useRef(null); + + const handleClick = React.useCallback( + (e: React.MouseEvent) => { + onClick?.(e); + if (e.defaultPrevented) return; + const link = linkRef.current; + if (link && !link.contains(e.target as Node)) { + link.click(); + } + }, + [onClick] + ); + + const Comp = asChild ? SlotPrimitive.Slot : "div"; + + return ( + + + {children} + + + ); +} + +type LinkOwnProps = { + asChild?: boolean; + children: React.ReactNode; +}; + +type LinkProps = LinkOwnProps & + Omit, keyof LinkOwnProps>; + +function Link({ asChild, children, onClick, ...props }: LinkProps) { + const context = React.useContext(BlockLinkContext); + const localRef = React.useRef(null); + + const setRef = React.useCallback( + (el: HTMLElement | null) => { + localRef.current = el; + if (context) context.linkRef.current = el; + }, + [context] + ); + + const handleClick = (e: React.MouseEvent) => { + e.stopPropagation(); + onClick?.(e as React.MouseEvent); + }; + + const Comp = asChild ? SlotPrimitive.Slot : "a"; + return ( + + {children} + + ); +} + +export const BlockLink = { Root, Link };