}) {
const id = useId()
@@ -220,8 +222,24 @@ export function Tooltip({ label, shortcutKeys, side = 'right', align = 'center',
'aria-describedby': openOnClick && visible
? [children.props['aria-describedby'], id].filter(Boolean).join(' ')
: children.props['aria-describedby'],
- onMouseEnter: (e) => { children.props.onMouseEnter?.(e); triggers.current.hover = true; showAfterDelay(delayMs) },
- onMouseLeave: (e) => { children.props.onMouseLeave?.(e); triggers.current.hover = false; cancelShow(); if (!pinned) withdraw() },
+ onPointerEnter: (e) => {
+ children.props.onPointerEnter?.(e)
+ if (e.pointerType === 'touch') return
+ // iOS can emit mouse entry when a tapped control replaces its icon.
+ if (e.pointerType === 'mouse' && !window.matchMedia('(any-hover: hover)').matches) return
+ triggers.current.hover = true
+ showAfterDelay(delayMs)
+ },
+ onPointerLeave: (e) => {
+ children.props.onPointerLeave?.(e)
+ triggers.current.hover = false
+ cancelShow()
+ if (!pinned) withdraw()
+ },
+ onPointerDown: (e) => {
+ children.props.onPointerDown?.(e)
+ if (e.pointerType === 'touch' && !pinned) dismiss()
+ },
// Activating an ordinary action dismisses the bubble: it often changes
// what the anchor now does (pin → unpin), and the click leaves the
// anchor focused, which would otherwise pin the relabelled bubble up.
diff --git a/packages/client/ui-primitives/src/WebBlock.module.css b/packages/client/ui-primitives/src/WebBlock.module.css
index 3f6aa72743..c4b1c6feb4 100644
--- a/packages/client/ui-primitives/src/WebBlock.module.css
+++ b/packages/client/ui-primitives/src/WebBlock.module.css
@@ -69,12 +69,18 @@
margin-right: 5px;
}
-.sourceLink:hover,
.sourceLink:focus-visible {
text-decoration: underline dotted;
text-underline-offset: 3px;
}
+@media (hover: hover) {
+ .sourceLink:hover {
+ text-decoration: underline dotted;
+ text-underline-offset: 3px;
+ }
+}
+
.snippet {
margin-top: 2px;
color: var(--dsw-alias-label-secondary);
@@ -117,12 +123,18 @@
text-decoration: none;
}
-.fetchUrl:hover,
.fetchUrl:focus-visible {
text-decoration: underline dotted;
text-underline-offset: 3px;
}
+@media (hover: hover) {
+ .fetchUrl:hover {
+ text-decoration: underline dotted;
+ text-underline-offset: 3px;
+ }
+}
+
.fetchMeta {
display: flex;
align-items: baseline;
diff --git a/packages/client/ui-primitives/src/markdown/JsonBlock.module.css b/packages/client/ui-primitives/src/markdown/JsonBlock.module.css
index 78717a6238..7253169934 100644
--- a/packages/client/ui-primitives/src/markdown/JsonBlock.module.css
+++ b/packages/client/ui-primitives/src/markdown/JsonBlock.module.css
@@ -13,8 +13,10 @@
border-radius: var(--dsw-radius-sm);
}
-.toggle:hover {
- background: var(--dsw-alias-interactive-bg-hover);
+@media (hover: hover) {
+ .toggle:hover {
+ background: var(--dsw-alias-interactive-bg-hover);
+ }
}
.body {
diff --git a/packages/client/ui-primitives/src/markdown/MarkdownText.module.css b/packages/client/ui-primitives/src/markdown/MarkdownText.module.css
index d54374106a..fc6a907ad1 100644
--- a/packages/client/ui-primitives/src/markdown/MarkdownText.module.css
+++ b/packages/client/ui-primitives/src/markdown/MarkdownText.module.css
@@ -74,13 +74,20 @@
margin-right: -3px;
}
-.markdown a:hover,
.markdown a:focus {
outline: none;
text-decoration: underline dotted var(--dsw-alias-link);
text-underline-offset: 3px;
}
+@media (hover: hover) {
+ .markdown a:hover {
+ outline: none;
+ text-decoration: underline dotted var(--dsw-alias-link);
+ text-underline-offset: 3px;
+ }
+}
+
.markdown a:focus-visible,
.fileMention:focus-visible {
box-shadow: 0 0 0 2px var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary));
@@ -195,8 +202,8 @@
overscroll-behavior-x: contain;
}
-/* Wide tables reveal their horizontal bar on hover (or keyboard focus)
- instead of keeping it painted. Chromium never repaints state-conditioned
+/* Hover-capable inputs reveal a wide table's bar on hover or keyboard focus.
+ Other inputs keep native horizontal scrolling. Chromium never repaints state-conditioned
scrollbar STYLES (neither hover-conditioned `::-webkit-scrollbar*` rules
nor a :hover `scrollbar-color` change reaches the painted bar), so the
toggle is `overflow-x` itself — a layout change repaints reliably. The
@@ -205,17 +212,23 @@
Other engines and overlay scrollbars may reserve a different height.
Wheel and trackpad scrolling need the pointer over the table, which is
already the hover that re-enables scrolling. */
-.tableScroll:global(.md-table-wide) {
- overflow-x: hidden;
- padding-bottom: var(--dsh-scrollbar-width, 5px);
-}
-
-.tableScroll:global(.md-table-wide):hover,
.tableScroll:global(.md-table-wide):focus-visible {
overflow-x: scroll;
padding-bottom: 0;
}
+@media (hover: hover) {
+ .tableScroll:global(.md-table-wide) {
+ overflow-x: hidden;
+ padding-bottom: var(--dsh-scrollbar-width, 5px);
+ }
+
+ .tableScroll:global(.md-table-wide):hover {
+ overflow-x: scroll;
+ padding-bottom: 0;
+ }
+}
+
/* Chromium keyboard-focuses scrollable containers by default; the ring uses
the sheet's link focus color. */
.tableScroll:focus-visible {
@@ -321,13 +334,20 @@
text-align: inherit;
}
-.fileMention:hover,
.fileMention:focus {
outline: none;
text-decoration: underline dotted var(--dsw-alias-link);
text-underline-offset: 3px;
}
+@media (hover: hover) {
+ .fileMention:hover {
+ outline: none;
+ text-decoration: underline dotted var(--dsw-alias-link);
+ text-underline-offset: 3px;
+ }
+}
+
.compact {
max-width: 100%;
font-size: var(--dsh-content-font-size-secondary, 13px);
@@ -374,13 +394,18 @@
text-decoration: underline dotted;
}
-.compact a:hover,
.compact a:focus,
-.compact .fileMention:hover,
.compact .fileMention:focus {
text-decoration-color: currentColor;
}
+@media (hover: hover) {
+ .compact a:hover,
+ .compact .fileMention:hover {
+ text-decoration-color: currentColor;
+ }
+}
+
.compact :not(pre) > code {
display: inline;
font: inherit;
diff --git a/packages/client/ui-primitives/src/markdown/render.tsx b/packages/client/ui-primitives/src/markdown/render.tsx
index c4ff4a2400..1d6b28b7bf 100644
--- a/packages/client/ui-primitives/src/markdown/render.tsx
+++ b/packages/client/ui-primitives/src/markdown/render.tsx
@@ -490,8 +490,8 @@ function renderTable(node: Md.Table, key: Key, context: MarkdownRenderContext):
// column and wrap instead (deepsuite chat TableWrapper parity).
const wide = columns >= 4 && context.inBlockquote !== true
return (
- // Wide tables rest with overflow-x hidden (the hover-revealed bar in
- // MarkdownText.module.css), which drops Chromium's implicit scroller
+ // On hover-capable inputs, wide tables rest with overflow-x hidden (the
+ // hover-revealed bar in MarkdownText.module.css), dropping Chromium's implicit scroller
// focusability — the explicit tabindex keeps them keyboard-reachable,
// and :focus-visible restores scrolling.
{
it('shows hover tooltips for copying and wrapping and updates the wrapping action', () => {
render()
const copy = screen.getByRole('button', { name: 'Copy' })
- fireEvent.mouseEnter(copy)
+ fireEvent.pointerEnter(copy)
expect(screen.getByRole('tooltip').textContent).toBe('Copy')
- fireEvent.mouseLeave(copy)
+ fireEvent.pointerLeave(copy)
const wrap = screen.getByRole('button', { name: 'Wrap lines' })
- fireEvent.mouseEnter(wrap)
+ fireEvent.pointerEnter(wrap)
expect(screen.getByRole('tooltip').textContent).toBe('Do not wrap lines')
fireEvent.click(wrap)
expect(screen.queryByRole('tooltip')).toBeNull()
- fireEvent.mouseEnter(wrap)
+ fireEvent.pointerEnter(wrap)
expect(screen.getByRole('tooltip').textContent).toBe('Wrap lines')
})
diff --git a/packages/client/ui-primitives/tests/hover-card.client.spec.tsx b/packages/client/ui-primitives/tests/hover-card.client.spec.tsx
index 50be572916..423a99ffb1 100644
--- a/packages/client/ui-primitives/tests/hover-card.client.spec.tsx
+++ b/packages/client/ui-primitives/tests/hover-card.client.spec.tsx
@@ -5,7 +5,10 @@ import { HoverCard, Tooltip } from '@deepseek-ai/dsh-client-ui-primitives'
import { POINTER_GRACE_MS } from '../src/pointer-grace.ts'
afterEach(cleanup)
-beforeEach(() => { vi.useFakeTimers() })
+beforeEach(() => {
+ vi.useFakeTimers()
+ vi.stubGlobal('matchMedia', (query: string) => ({ matches: query === '(any-hover: hover)' }))
+})
afterEach(() => { vi.useRealTimers(); vi.unstubAllGlobals(); vi.restoreAllMocks() })
/** Anchor wrapper rect: the card positions from this (jsdom rects are all-zero by default). */
@@ -54,6 +57,28 @@ function installClipboard(writeText: (text: string) => Promise): () => voi
}
describe('HoverCard', () => {
+ it.each([false, true])('opens mouse previews only when a pointer can hover: %s', (canHover) => {
+ vi.stubGlobal('matchMedia', () => ({ matches: canHover }))
+ const { wrapper } = mount()
+ const event = new Event('pointerover', { bubbles: true })
+ Object.defineProperty(event, 'pointerType', { value: 'mouse' })
+ fireEvent(wrapper, event)
+ act(() => { vi.advanceTimersByTime(1000) })
+ expect(screen.queryByText('card body') !== null).toBe(canHover)
+ })
+
+ it.each(['compact', 'preview'] as const)('does not open a %s card when a touch enters the anchor', (variant) => {
+ const { wrapper } = mount({ variant })
+ const event = new Event('pointerover', { bubbles: true })
+ Object.defineProperty(event, 'pointerType', { value: 'touch' })
+ fireEvent(wrapper, event)
+ act(() => { vi.advanceTimersByTime(1000) })
+ expect(screen.queryByText('card body')).toBeNull()
+ fireEvent.pointerEnter(wrapper)
+ act(() => { vi.advanceTimersByTime(500) })
+ expect(screen.getByText('card body')).toBeTruthy()
+ })
+
it.each(['disabled', 'unmounted'] as const)('keeps its preview mounted while a nested tooltip shows and is then %s', (released) => {
const markup = (state: 'active' | 'disabled' | 'unmounted') => (
{
const view = render(markup('active'))
fireEvent.pointerEnter(screen.getByText('row').parentElement as HTMLElement)
act(() => { vi.advanceTimersByTime(500) })
- fireEvent.mouseEnter(screen.getByRole('button', { name: 'action' }))
+ fireEvent.pointerEnter(screen.getByRole('button', { name: 'action' }))
expect(screen.getByRole('tooltip').textContent).toBe('Create')
const card = screen.getByText('card body')
expect(card.isConnected).toBe(true)
@@ -88,7 +113,7 @@ describe('HoverCard', () => {
fireEvent.pointerEnter(screen.getByText('row').parentElement as HTMLElement)
act(() => { vi.advanceTimersByTime(500) })
const card = screen.getByText('card body')
- fireEvent.mouseEnter(screen.getByRole('button', { name: 'card action' }))
+ fireEvent.pointerEnter(screen.getByRole('button', { name: 'card action' }))
expect(card.isConnected).toBe(true)
expect(screen.getByRole('tooltip').textContent).toBe('Details')
act(() => { vi.advanceTimersByTime(1000) })
@@ -626,6 +651,7 @@ describe('HoverCard', () => {
})
it('opens inline previews only for keyboard focus and closes when focus leaves the anchor', () => {
+ vi.stubGlobal('matchMedia', () => ({ matches: false }))
const anchor = <>>
const view = render(thumbnail} />)
const first = screen.getByRole('button', { name: 'first' })
diff --git a/packages/client/ui-primitives/tests/tooltip.client.spec.tsx b/packages/client/ui-primitives/tests/tooltip.client.spec.tsx
index 83f6ca6e09..e4b7891e30 100644
--- a/packages/client/ui-primitives/tests/tooltip.client.spec.tsx
+++ b/packages/client/ui-primitives/tests/tooltip.client.spec.tsx
@@ -31,6 +31,8 @@ beforeEach(() => {
automaticResize = true
observers.length = 0
vi.stubGlobal('ResizeObserver', TooltipResizeObserver)
+ vi.stubGlobal('matchMedia', vi.fn(() => ({ matches: true })))
+ fireEvent.keyDown(document.body, { key: 'Tab' })
})
afterEach(() => {
cleanup()
@@ -39,6 +41,103 @@ afterEach(() => {
})
describe('Tooltip', () => {
+ describe('touch interaction', () => {
+ beforeEach(() => { vi.useFakeTimers() })
+ afterEach(() => { cleanup(); vi.useRealTimers() })
+
+ it('ignores emulated mouse entry on touch-only devices while preserving keyboard focus and connected mice', () => {
+ const media = vi.fn(() => ({ matches: false }))
+ vi.stubGlobal('matchMedia', media)
+ const view = render()
+ const anchor = screen.getByText('anchor')
+ fireEvent.pointerDown(anchor, { pointerType: 'touch' })
+ fireEvent.click(anchor)
+ for (const label of ['Copied', 'Copy']) {
+ view.rerender()
+ fireEvent.pointerEnter(anchor, { pointerType: 'mouse' })
+ expect(screen.queryByRole('tooltip')).toBeNull()
+ }
+ expect(media).toHaveBeenCalledWith('(any-hover: hover)')
+ fireEvent.keyDown(document.body, { key: 'Tab' })
+ fireEvent.focus(anchor)
+ expect(screen.getByRole('tooltip').textContent).toBe('Copy')
+ fireEvent.blur(anchor)
+ media.mockReturnValue({ matches: true })
+ fireEvent.pointerEnter(anchor, { pointerType: 'mouse' })
+ expect(screen.getByRole('tooltip').textContent).toBe('Copy')
+ })
+
+ it('executes an action once without showing a tooltip for touch or compatibility mouse events', () => {
+ const clicked = vi.fn()
+ const label = vi.fn(() => 'Copy')
+ render()
+ const anchor = screen.getByText('anchor')
+ fireEvent.pointerEnter(anchor, { pointerType: 'touch' })
+ fireEvent.pointerDown(anchor, { pointerType: 'touch' })
+ fireEvent.mouseEnter(anchor)
+ fireEvent.focus(anchor)
+ act(() => { vi.advanceTimersByTime(500) })
+ expect(label).not.toHaveBeenCalled()
+ expect(screen.queryByRole('tooltip')).toBeNull()
+ fireEvent.pointerUp(anchor, { pointerType: 'touch' })
+ fireEvent.pointerLeave(anchor, { pointerType: 'touch' })
+ fireEvent.click(anchor)
+ fireEvent.mouseLeave(anchor)
+ fireEvent.mouseEnter(anchor)
+ act(() => { vi.advanceTimersByTime(500) })
+ expect(clicked).toHaveBeenCalledOnce()
+ expect(label).not.toHaveBeenCalled()
+ expect(screen.queryByRole('tooltip')).toBeNull()
+ })
+
+ it.each([0, 500])('cancels an existing mouse hover on touch with a %i ms delay', (delayMs) => {
+ render()
+ const anchor = screen.getByText('anchor')
+ fireEvent.pointerEnter(anchor, { pointerType: 'mouse' })
+ expect(screen.queryByRole('tooltip') !== null).toBe(delayMs === 0)
+ fireEvent.pointerDown(anchor, { pointerType: 'touch' })
+ act(() => { vi.advanceTimersByTime(500) })
+ expect(screen.queryByRole('tooltip')).toBeNull()
+
+ fireEvent.pointerLeave(anchor, { pointerType: 'touch' })
+ fireEvent.pointerEnter(anchor, { pointerType: 'mouse' })
+ act(() => { vi.advanceTimersByTime(500) })
+ expect(screen.getByRole('tooltip').textContent).toBe('Copy')
+ fireEvent.pointerLeave(anchor, { pointerType: 'mouse' })
+ fireEvent.keyDown(document.body, { key: 'Tab' })
+ fireEvent.focus(anchor)
+ expect(screen.getByRole('tooltip').textContent).toBe('Copy')
+ })
+
+ it('opens an explanation only on tap and keeps it readable until another tap or outside press', () => {
+ render()
+ const anchor = screen.getByText('anchor')
+ const tap = () => {
+ fireEvent.pointerEnter(anchor, { pointerType: 'touch' })
+ fireEvent.pointerDown(anchor, { pointerType: 'touch' })
+ fireEvent.focus(anchor)
+ fireEvent.pointerUp(anchor, { pointerType: 'touch' })
+ fireEvent.pointerLeave(anchor, { pointerType: 'touch' })
+ fireEvent.click(anchor)
+ }
+ fireEvent.pointerEnter(anchor, { pointerType: 'touch' })
+ act(() => { vi.advanceTimersByTime(500) })
+ expect(screen.queryByRole('tooltip')).toBeNull()
+ tap()
+ const bubble = screen.getByRole('tooltip')
+ expect(bubble.textContent).toBe('Plugin information')
+ expect(anchor.getAttribute('aria-describedby')).toBe(bubble.id)
+ fireEvent.blur(anchor)
+ fireEvent.pointerDown(bubble, { pointerType: 'touch' })
+ expect(screen.getByRole('tooltip')).toBe(bubble)
+ tap()
+ expect(screen.queryByRole('tooltip')).toBeNull()
+ tap()
+ fireEvent.pointerDown(document.body, { pointerType: 'touch' })
+ expect(screen.queryByRole('tooltip')).toBeNull()
+ })
+ })
+
it('updates independent keycaps and the accessible combination while visible', () => {
const view = render()
fireEvent.focus(screen.getByText('anchor'))
@@ -56,7 +155,7 @@ describe('Tooltip', () => {
const measured = vi.spyOn(Element.prototype, 'getBoundingClientRect')
const view = render()
const anchor = screen.getByText('anchor')
- fireEvent.mouseEnter(anchor)
+ fireEvent.pointerEnter(anchor)
const bubble = view.container.querySelector('[role="tooltip"]')!
expect(measured.mock.contexts).toEqual([anchor])
expect(bubble.style.visibility).toBe('hidden')
@@ -115,7 +214,7 @@ describe('Tooltip', () => {
if (element === null) fireEvent(window, new Event('resize'))
}
const view = render()
- fireEvent.mouseEnter(screen.getByText('anchor'))
+ fireEvent.pointerEnter(screen.getByText('anchor'))
expect(screen.getByRole('tooltip')).toBeTruthy()
view.unmount()
expect(screen.queryByRole('tooltip')).toBeNull()
@@ -133,7 +232,7 @@ describe('Tooltip', () => {
,
)
expect(label).not.toHaveBeenCalled()
- fireEvent.mouseEnter(screen.getByText('anchor'))
+ fireEvent.pointerEnter(screen.getByText('anchor'))
act(() => { vi.advanceTimersByTime(499) })
expect(label).not.toHaveBeenCalled()
act(() => { vi.advanceTimersByTime(1) })
@@ -153,16 +252,16 @@ describe('Tooltip', () => {
,
)
const anchor = screen.getByText('anchor')
- fireEvent.mouseEnter(anchor)
+ fireEvent.pointerEnter(anchor)
act(() => { vi.advanceTimersByTime(499) })
expect(screen.queryByRole('tooltip')).toBeNull()
- fireEvent.mouseLeave(anchor)
+ fireEvent.pointerLeave(anchor)
act(() => { vi.advanceTimersByTime(1) })
expect(screen.queryByRole('tooltip')).toBeNull()
- fireEvent.mouseEnter(anchor)
+ fireEvent.pointerEnter(anchor)
act(() => { vi.advanceTimersByTime(500) })
expect(screen.getByRole('tooltip').textContent).toBe('Timing details')
- fireEvent.mouseLeave(anchor)
+ fireEvent.pointerLeave(anchor)
fireEvent.focus(anchor)
expect(screen.getByRole('tooltip').textContent).toBe('Timing details')
} finally {
@@ -193,7 +292,7 @@ describe('Tooltip', () => {
expect(screen.getByRole('tooltip').textContent).toBe('Refresh')
})
- it.each(['blur', 'click', 'mouseLeave', 'disabled', 'unmount'] as const)(
+ it.each(['blur', 'click', 'pointerLeave', 'disabled', 'unmount'] as const)(
'cancels a pending focus bubble on %s', (cancel) => {
const view = render()
const anchor = screen.getByText('anchor')
@@ -222,7 +321,7 @@ describe('Tooltip', () => {
render()
const anchor = screen.getByText('anchor')
const initialTimers = vi.getTimerCount()
- fireEvent.mouseEnter(anchor)
+ fireEvent.pointerEnter(anchor)
act(() => { vi.advanceTimersByTime(499) })
expect(screen.queryByRole('tooltip')).toBeNull()
fireEvent.focus(anchor)
@@ -256,13 +355,13 @@ describe('Tooltip', () => {
,
)
const anchor = screen.getByText('anchor')
- fireEvent.mouseEnter(anchor)
+ fireEvent.pointerEnter(anchor)
const bubble = screen.getByRole('tooltip')
expect(bubble.textContent).toBe('Open sidebar')
expect(bubble.getAttribute('data-side')).toBe('right')
expect(bubble.style.left).toBe('12px')
expect(bubble.style.top).toBe('12px')
- fireEvent.mouseLeave(anchor)
+ fireEvent.pointerLeave(anchor)
expect(screen.queryByRole('tooltip')).toBeNull()
})
@@ -296,7 +395,7 @@ describe('Tooltip', () => {
,
)
- fireEvent.mouseEnter(screen.getByText('anchor'))
+ fireEvent.pointerEnter(screen.getByText('anchor'))
const bubble = screen.getByRole('tooltip')
expect(bubble.getAttribute('data-align')).toBe('end')
expect(bubble.style.left).toBe('200px')
@@ -311,7 +410,7 @@ describe('Tooltip', () => {
,
)
- fireEvent.mouseEnter(screen.getByText('anchor'))
+ fireEvent.pointerEnter(screen.getByText('anchor'))
expect(screen.getByRole('tooltip').style.maxWidth).toBe('min(360px, calc(100vw - 24px))')
})
@@ -325,7 +424,7 @@ describe('Tooltip', () => {
,
)
- fireEvent.mouseEnter(screen.getByText('anchor'))
+ fireEvent.pointerEnter(screen.getByText('anchor'))
// pos.x = 1000 (anchor center); measured right edge 1100 overflows the
// 1024 viewport's 12px safe margin (limit 1012) by 88, so the clamp
// shifts left to 912.
@@ -345,7 +444,7 @@ describe('Tooltip', () => {
,
)
- fireEvent.mouseEnter(screen.getByText('anchor'))
+ fireEvent.pointerEnter(screen.getByText('anchor'))
expect(screen.getByRole('tooltip').style.left).toBe('912px')
view.rerender(
@@ -377,7 +476,7 @@ describe('Tooltip', () => {
,
)
- fireEvent.mouseEnter(screen.getByText('anchor'))
+ fireEvent.pointerEnter(screen.getByText('anchor'))
// pos.x = 30 (anchor center); measured left edge -20 underflows the
// 12px safe margin by 32, so the clamp shifts right to 62.
expect(screen.getByRole('tooltip').style.left).toBe('62px')
@@ -401,7 +500,7 @@ describe('Tooltip', () => {
,
)
- fireEvent.mouseEnter(screen.getByText('anchor'))
+ fireEvent.pointerEnter(screen.getByText('anchor'))
const bubble = screen.getByRole('tooltip')
// There is room above, so the requested side stands: the bubble's own
// top sits at the anchor's top less the 8px gutter.
@@ -423,7 +522,7 @@ describe('Tooltip', () => {
,
)
- fireEvent.mouseEnter(screen.getByText('anchor'))
+ fireEvent.pointerEnter(screen.getByText('anchor'))
const bubble = screen.getByRole('tooltip')
expect(bubble.getAttribute('data-side')).toBe('top')
expect(bubble.style.top).toBe('592px')
@@ -440,7 +539,7 @@ describe('Tooltip', () => {
,
)
- fireEvent.mouseEnter(screen.getByText('anchor'))
+ fireEvent.pointerEnter(screen.getByText('anchor'))
const bubble = screen.getByRole('tooltip')
expect(bubble.getAttribute('data-side')).toBe('bottom')
expect(bubble.style.top).toBe('48px')
@@ -454,7 +553,7 @@ describe('Tooltip', () => {
const spy = placed(anchorBottom - 20, anchorBottom, 20)
try {
const view = render()
- fireEvent.mouseEnter(screen.getByText('anchor'))
+ fireEvent.pointerEnter(screen.getByText('anchor'))
const bubble = screen.getByRole('tooltip')
expect(bubble.getAttribute('data-side')).toBe(side)
expect(bubble.style.top).toBe(`${side === 'bottom' ? anchorBottom + 4 : anchorBottom - 24}px`)
@@ -474,7 +573,7 @@ describe('Tooltip', () => {
,
)
- fireEvent.mouseEnter(screen.getByText('anchor'))
+ fireEvent.pointerEnter(screen.getByText('anchor'))
expect(screen.getByRole('tooltip').getAttribute('data-side')).toBe('right')
expect(screen.getByRole('tooltip').style.left).toBe('210px')
} finally {
@@ -499,7 +598,7 @@ describe('Tooltip', () => {
const bubble = screen.getByRole('tooltip')
expect(bubble.textContent).toBe('Plugin information')
expect(anchor.getAttribute('aria-describedby')).toBe(`existing-description ${bubble.id}`)
- fireEvent.mouseLeave(anchor)
+ fireEvent.pointerLeave(anchor)
fireEvent.blur(anchor)
expect(screen.getByRole('tooltip')).toBe(bubble)
fireEvent.pointerDown(bubble)
@@ -519,9 +618,9 @@ describe('Tooltip', () => {
)
const anchor = screen.getByText('anchor')
- fireEvent.mouseEnter(anchor)
+ fireEvent.pointerEnter(anchor)
expect(screen.getByRole('tooltip').textContent).toBe('Plugin information')
- fireEvent.mouseLeave(anchor)
+ fireEvent.pointerLeave(anchor)
expect(screen.queryByRole('tooltip')).toBeNull()
fireEvent.keyDown(anchor, { key: 'Tab' })
fireEvent.focus(anchor)
@@ -544,11 +643,11 @@ describe('Tooltip', () => {
try {
render()
const anchor = screen.getByText('anchor')
- fireEvent.mouseEnter(anchor)
+ fireEvent.pointerEnter(anchor)
expect(screen.queryByRole('tooltip')).toBeNull()
fireEvent.click(anchor)
expect(screen.getByRole('tooltip')).toBeTruthy()
- fireEvent.mouseLeave(anchor)
+ fireEvent.pointerLeave(anchor)
expect(screen.getByRole('tooltip')).toBeTruthy()
fireEvent.keyDown(anchor, { key: 'Escape' })
act(() => { vi.advanceTimersByTime(300) })
@@ -571,7 +670,7 @@ describe('Tooltip', () => {
fireEvent.click(anchor)
expect(screen.getByRole('tooltip')).toBeTruthy()
view.rerender()
- fireEvent.mouseLeave(anchor)
+ fireEvent.pointerLeave(anchor)
expect(screen.queryByRole('tooltip')).toBeNull()
view.rerender()
fireEvent.click(anchor)
@@ -588,15 +687,15 @@ describe('Tooltip', () => {
const anchor = screen.getByText('anchor')
// Pointer click: browsers focus the button first, then deliver the click.
fireEvent.focus(anchor)
- fireEvent.mouseEnter(anchor)
+ fireEvent.pointerEnter(anchor)
expect(screen.getByRole('tooltip')).toBeTruthy()
fireEvent.click(anchor)
expect(screen.queryByRole('tooltip')).toBeNull()
// The retained focus alone must not resurrect it on mouse leave.
- fireEvent.mouseLeave(anchor)
+ fireEvent.pointerLeave(anchor)
expect(screen.queryByRole('tooltip')).toBeNull()
// A fresh hover shows the (possibly relabelled) bubble again.
- fireEvent.mouseEnter(anchor)
+ fireEvent.pointerEnter(anchor)
expect(screen.getByRole('tooltip')).toBeTruthy()
})
@@ -620,24 +719,27 @@ describe('Tooltip', () => {
})
it('chains the anchor\'s own handlers ahead of the tooltip\'s', () => {
- const onMouseEnter = vi.fn()
- const onMouseLeave = vi.fn()
+ const onPointerEnter = vi.fn()
+ const onPointerLeave = vi.fn()
+ const onPointerDown = vi.fn()
const onClick = vi.fn()
const onFocus = vi.fn()
const onBlur = vi.fn()
render(
-
+
,
)
const anchor = screen.getByText('anchor')
- fireEvent.mouseEnter(anchor)
- fireEvent.mouseLeave(anchor)
+ fireEvent.pointerEnter(anchor)
+ fireEvent.pointerDown(anchor)
+ fireEvent.pointerLeave(anchor)
fireEvent.click(anchor)
fireEvent.focus(anchor)
fireEvent.blur(anchor)
- expect(onMouseEnter).toHaveBeenCalledOnce()
- expect(onMouseLeave).toHaveBeenCalledOnce()
+ expect(onPointerEnter).toHaveBeenCalledOnce()
+ expect(onPointerLeave).toHaveBeenCalledOnce()
+ expect(onPointerDown).toHaveBeenCalledOnce()
expect(onClick).toHaveBeenCalledOnce()
expect(onFocus).toHaveBeenCalledOnce()
expect(onBlur).toHaveBeenCalledOnce()
@@ -650,7 +752,7 @@ describe('Tooltip', () => {