fix(link-actions): dismiss the link chooser when an outside click stops propagation (#33679)

* fix(link-actions): dismiss the link chooser when an outside click stops propagation

* test(link-actions): wait for Radix to arm before the outside click
This commit is contained in:
Neil
2026-10-25 17:21:21 -07:00
committed by GitHub
parent 1b58be9ddc
commit 5cf4805682
2 changed files with 40 additions and 3 deletions
@@ -1,4 +1,4 @@
import { useMemo, useRef } from 'react'
import { useEffect, useEffectEvent, useMemo, useRef } from 'react'
import { Check, Copy, ExternalLink, Globe, Settings } from 'lucide-react'
import { toast } from 'sonner'
import { ShortcutKeyCombo } from '@/components/ShortcutKeyCombo'
@@ -64,6 +64,23 @@ export function LinkActionPopover<TRequest extends LinkActionRequest>({
const copyableDestination = request?.kind === 'url' ? request.destination : ''
const { copyText, status: copyStatus } = useClipboardTextCopyFeedback(copyableDestination)
const copyInFlightRef = useRef(false)
const contentRef = useRef<HTMLDivElement>(null)
const dismissOnOutsidePointerDown = useEffectEvent((event: PointerEvent): void => {
if (!request || (event.target instanceof Node && contentRef.current?.contains(event.target))) {
return
}
onPointerDownOutside?.(event)
onClose(request)
})
useEffect(() => {
if (!request) {
return
}
// Why: Radix defers outside dismissal to the click and skips it when the click's
// propagation is stopped, which hover cards and sidebar cards both do.
document.addEventListener('pointerdown', dismissOnOutsidePointerDown, true)
return () => document.removeEventListener('pointerdown', dismissOnOutsidePointerDown, true)
}, [request])
const virtualRef = useMemo(
() => ({
current: {
@@ -133,6 +150,7 @@ export function LinkActionPopover<TRequest extends LinkActionRequest>({
<PopoverAnchor virtualRef={virtualRef} />
{request ? (
<PopoverContent
ref={contentRef}
align="start"
side="top"
sideOffset={6}
@@ -143,7 +161,6 @@ export function LinkActionPopover<TRequest extends LinkActionRequest>({
onOpenAutoFocus={(event) => event.preventDefault()}
onCloseAutoFocus={(event) => event.preventDefault()}
onEscapeKeyDown={() => request.restoreFocus()}
onPointerDownOutside={onPointerDownOutside}
>
<div className="mb-0.5 flex items-center gap-1 overflow-hidden border-b border-border px-1.5 py-0.5 font-mono text-xs text-muted-foreground">
<span
@@ -1,6 +1,6 @@
// @vitest-environment happy-dom
import type { ReactNode } from 'react'
import { render, screen, fireEvent, cleanup } from '@testing-library/react'
import { act, render, screen, fireEvent, cleanup } from '@testing-library/react'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { WorktreeCardDetailsHover } from './WorktreeCardMeta'
import type { Worktree, WorkspaceAttachment } from '../../../../shared/worktree/types'
@@ -192,6 +192,26 @@ describe('workspace linked references in hover details', () => {
'true'
)
})
it('dismisses the link chooser when an outside click stops propagation', async () => {
Object.defineProperty(window, 'api', {
configurable: true,
value: { shell: { openUrl: vi.fn() } }
})
render(
<>
<Fixture onManage={vi.fn()} />
<button onClick={(event) => event.stopPropagation()}>Other card</button>
</>
)
fireEvent.click(screen.getByRole('button', { name: 'Hover workspace' }))
fireEvent.click(screen.getByRole('link', { name: 'Open MR !42' }))
// Why: Radix arms its outside-pointerdown listener a tick after opening.
await act(() => new Promise((resolve) => setTimeout(resolve, 0)))
const otherCard = screen.getByRole('button', { name: 'Other card' })
fireEvent.pointerDown(otherCard)
fireEvent.click(otherCard)
expect(screen.queryByRole('button', { name: /System Browser/ })).toBeNull()
})
it('opens link management from either section header and dismisses hover', () => {
const onManage = vi.fn()
const { container } = render(<Fixture onManage={onManage} />)