fix(web): only anchor presses dismiss the hover card
The card is a React child of the wrapper, so capture-phase presses on it reached the wrapper's dismissal handler: the first pointerdown of a text selection closed the card, contradicting its JSDoc contract. Restrict the immediate close to presses outside the card, keeping it mounted under a held press (and the browser click with it), and align onPointerLeave's grace arming with Menu (only while open). Pin both new behaviors in hover-card.spec and update the bilingual Agent Note.
This commit is contained in:
@@ -109,11 +109,17 @@ export function HoverCard({ anchor, content, openDelayMs = 500, disabled = false
|
||||
}}
|
||||
onPointerLeave={() => {
|
||||
clearTimer()
|
||||
armClose()
|
||||
// Leaving a closed card schedules a no-op close; only arm while
|
||||
// open, matching Menu's shape.
|
||||
if (open) armClose()
|
||||
}}
|
||||
// Any press inside the anchor (row click, menu trigger) dismisses the
|
||||
// A press inside the anchor (row click, menu trigger) dismisses the
|
||||
// card immediately, without waiting for the owner to flip `disabled`.
|
||||
onPointerDownCapture={() => {
|
||||
// Capture presses reach this handler from the card too — it is a React
|
||||
// child of the wrapper — but a press there starts a selection, so the
|
||||
// card must stay mounted under it (and the browser's click with it).
|
||||
onPointerDownCapture={(e) => {
|
||||
if (cardRef.current?.contains(e.target as Node)) return
|
||||
clearTimer()
|
||||
cancelClose()
|
||||
setOpen(false)
|
||||
|
||||
@@ -108,6 +108,26 @@ describe('HoverCard', () => {
|
||||
expect(screen.queryByText('card body')).toBeNull()
|
||||
})
|
||||
|
||||
it('a press on the card starts a selection instead of dismissing it', () => {
|
||||
// The card is a React child of the wrapper, so capture-phase presses on
|
||||
// it reach the wrapper's dismissal handler too; they must not close it,
|
||||
// or the first pointerdown of a text-selection drag would kill the card.
|
||||
const { wrapper } = mount()
|
||||
fireEvent.pointerEnter(wrapper)
|
||||
act(() => { vi.advanceTimersByTime(500) })
|
||||
fireEvent.pointerDown(screen.getByText('card body'))
|
||||
// Still mounted after a grace's worth of time: no close was armed either.
|
||||
act(() => { vi.advanceTimersByTime(POINTER_GRACE_MS) })
|
||||
expect(screen.getByText('card body')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('a press while closed leaves the card closed', () => {
|
||||
mount()
|
||||
fireEvent.pointerDown(screen.getByText('row'))
|
||||
act(() => { vi.advanceTimersByTime(1000) })
|
||||
expect(screen.queryByText('card body')).toBeNull()
|
||||
})
|
||||
|
||||
it('disabled suppresses opening entirely', () => {
|
||||
const { wrapper } = mount({ disabled: true })
|
||||
fireEvent.pointerEnter(wrapper)
|
||||
|
||||
Reference in New Issue
Block a user