@lovett/ui 0.0.10 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-RBYWGBQ2.js +2752 -0
- package/dist/chunk-RBYWGBQ2.js.map +1 -0
- package/dist/index.d.ts +6064 -80
- package/dist/index.js +21561 -9696
- package/dist/index.js.map +1 -1
- package/dist/rich-composer-impl-5NO443A6.js +1859 -0
- package/dist/rich-composer-impl-5NO443A6.js.map +1 -0
- package/dist/styles.css +1586 -2
- package/dist/theme-v2.css +228 -0
- package/dist/tokens.css +121 -0
- package/package.json +8 -1
- package/src/__tests__/anchor.test.tsx +422 -0
- package/src/__tests__/avatar.test.tsx +272 -0
- package/src/__tests__/bar-chart.test.tsx +809 -0
- package/src/__tests__/board.test.tsx +420 -0
- package/src/__tests__/chart-math.test.ts +922 -0
- package/src/__tests__/chart-series.test.ts +339 -0
- package/src/__tests__/code-block.test.tsx +134 -0
- package/src/__tests__/combobox.test.tsx +677 -0
- package/src/__tests__/display-popover.test.tsx +195 -0
- package/src/__tests__/display-store.test.tsx +208 -0
- package/src/__tests__/donut-chart.test.tsx +397 -0
- package/src/__tests__/dropdown-menu.test.tsx +572 -0
- package/src/__tests__/filter-menu.test.tsx +175 -0
- package/src/__tests__/gauge-ring.test.tsx +233 -0
- package/src/__tests__/helpers/geometry.ts +58 -0
- package/src/__tests__/layer-stack.test.tsx +228 -0
- package/src/__tests__/line-chart.test.tsx +612 -0
- package/src/__tests__/popover.test.tsx +460 -0
- package/src/__tests__/ranked-bars.test.tsx +343 -0
- package/src/__tests__/remark-underline.test.ts +194 -0
- package/src/__tests__/select.test.tsx +543 -0
- package/src/__tests__/sparkline.test.tsx +368 -0
- package/src/__tests__/stat-layer.test.tsx +271 -0
- package/src/__tests__/stat-strip.test.tsx +175 -0
- package/src/__tests__/status.test.tsx +293 -0
- package/src/__tests__/tabs.test.tsx +303 -0
- package/src/__tests__/token-shape.test.ts +132 -2
- package/src/__tests__/tooltip.test.tsx +355 -0
- package/src/avatar.tsx +352 -0
- package/src/bar-chart.tsx +1214 -0
- package/src/board.tsx +658 -0
- package/src/calculator-shell-v2.tsx +19 -39
- package/src/chart-frame.tsx +960 -0
- package/src/chart-legend.tsx +304 -0
- package/src/chart-tooltip.tsx +267 -0
- package/src/code-block.tsx +62 -8
- package/src/combobox.tsx +796 -0
- package/src/delta-chip.tsx +263 -0
- package/src/detail/__tests__/activity-pane.test.tsx +186 -0
- package/src/detail/__tests__/detail-chrome.test.tsx +134 -0
- package/src/detail/__tests__/detail-surface.test.tsx +529 -0
- package/src/detail/__tests__/field-row.test.tsx +357 -0
- package/src/detail/activity-pane.tsx +507 -0
- package/src/detail/calendar.tsx +355 -0
- package/src/detail/detail-divider.tsx +261 -0
- package/src/detail/detail-header.tsx +287 -0
- package/src/detail/detail-menu.tsx +254 -0
- package/src/detail/detail-surface.tsx +1110 -0
- package/src/detail/field-list.tsx +196 -0
- package/src/detail/field-row.tsx +1131 -0
- package/src/detail/index.ts +58 -0
- package/src/detail/segmented-choice.tsx +94 -0
- package/src/detail/types.ts +129 -0
- package/src/display-popover.tsx +487 -0
- package/src/display-store.tsx +242 -0
- package/src/donut-chart.tsx +988 -0
- package/src/dropdown-menu.tsx +412 -151
- package/src/filter-core/EXPORTS.md +85 -0
- package/src/filter-core/__tests__/columns.test.ts +159 -0
- package/src/filter-core/__tests__/faceting.test.ts +193 -0
- package/src/filter-core/__tests__/filter-fns.test.ts +519 -0
- package/src/filter-core/__tests__/operators.test.ts +235 -0
- package/src/filter-core/__tests__/state.test.ts +268 -0
- package/src/filter-core/__tests__/url.test.ts +350 -0
- package/src/filter-core/columns.ts +134 -0
- package/src/filter-core/date-utils.ts +38 -0
- package/src/filter-core/examples/task-filter-columns.ts +121 -0
- package/src/filter-core/faceting.ts +120 -0
- package/src/filter-core/filter-fns.ts +335 -0
- package/src/filter-core/index.ts +105 -0
- package/src/filter-core/operators.ts +433 -0
- package/src/filter-core/state.ts +280 -0
- package/src/filter-core/types.ts +247 -0
- package/src/filter-core/url.ts +261 -0
- package/src/filter-dropdown.tsx +12 -0
- package/src/filter-menu.tsx +649 -0
- package/src/floating-drawer.tsx +19 -1
- package/src/gauge-ring.tsx +435 -0
- package/src/hue.ts +52 -0
- package/src/index.ts +402 -0
- package/src/kbd.tsx +27 -4
- package/src/lib/anchor.ts +427 -0
- package/src/lib/chart.ts +866 -0
- package/src/lib/focus.ts +74 -0
- package/src/lib/layer-stack.ts +215 -0
- package/src/lib/refs.ts +31 -0
- package/src/lib/remark-underline.ts +443 -0
- package/src/lib/series.ts +169 -0
- package/src/line-chart.tsx +1176 -0
- package/src/markdown.tsx +26 -7
- package/src/metric-card.tsx +57 -22
- package/src/modal.tsx +73 -66
- package/src/popover.tsx +407 -0
- package/src/progress-ledger.tsx +304 -0
- package/src/ranked-bars.tsx +386 -0
- package/src/segmented-pill.tsx +32 -9
- package/src/select.tsx +646 -0
- package/src/sortable.tsx +315 -1
- package/src/sparkline.tsx +416 -0
- package/src/stat-card.tsx +376 -0
- package/src/stat-row.tsx +108 -70
- package/src/stat-strip.tsx +327 -0
- package/src/status.tsx +215 -0
- package/src/styles.css +1586 -2
- package/src/tabs.tsx +206 -25
- package/src/task-card.tsx +610 -0
- package/src/thread/__tests__/comment-body-hostile.test.tsx +331 -0
- package/src/thread/__tests__/comment-tree.test.ts +151 -0
- package/src/thread/__tests__/emoji.test.ts +187 -0
- package/src/thread/__tests__/fixtures/thread-fixture.ts +235 -0
- package/src/thread/__tests__/link-preview-source.test.ts +120 -0
- package/src/thread/__tests__/link-preview.test.tsx +600 -0
- package/src/thread/__tests__/markdown-format.test.ts +82 -0
- package/src/thread/__tests__/markdown-spec.test.ts +469 -0
- package/src/thread/__tests__/relative-time.test.ts +71 -0
- package/src/thread/__tests__/rich-composer.test.tsx +799 -0
- package/src/thread/__tests__/scroll-caret.test.ts +58 -0
- package/src/thread/__tests__/suggestion-list.test.tsx +648 -0
- package/src/thread/__tests__/thread-scroll-ownership.test.tsx +88 -0
- package/src/thread/__tests__/thread.test.tsx +662 -0
- package/src/thread/__tests__/use-attachments.test.tsx +679 -0
- package/src/thread/actions.tsx +196 -0
- package/src/thread/attachments.tsx +1071 -0
- package/src/thread/comment-body.tsx +148 -0
- package/src/thread/comment-tree.ts +182 -0
- package/src/thread/comment.tsx +915 -0
- package/src/thread/composer-footer.tsx +125 -0
- package/src/thread/composer.tsx +319 -0
- package/src/thread/emoji.ts +283 -0
- package/src/thread/index.ts +153 -0
- package/src/thread/link-preview.tsx +341 -0
- package/src/thread/markdown-format.ts +155 -0
- package/src/thread/markdown-spec.ts +754 -0
- package/src/thread/rail.tsx +372 -0
- package/src/thread/reactions.tsx +415 -0
- package/src/thread/relative-time.tsx +94 -0
- package/src/thread/rich-composer-impl.tsx +1601 -0
- package/src/thread/rich-composer.tsx +195 -0
- package/src/thread/scroll-caret.ts +37 -0
- package/src/thread/suggestion-list.tsx +182 -0
- package/src/thread/thread.tsx +718 -0
- package/src/thread/types.ts +218 -0
- package/src/thread/use-attachments.ts +598 -0
- package/src/thread/use-now.ts +73 -0
- package/src/thread/use-thread.ts +316 -0
- package/src/tokens.css +121 -0
- package/src/tooltip.tsx +297 -0
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tooltip — hover / focus label on lib/anchor + lib/layer-stack.
|
|
3
|
+
*
|
|
4
|
+
* Contract under test: the child is the trigger (no wrapper), role=tooltip
|
|
5
|
+
* + aria-describedby wiring while open, the 300 ms hover delay and 0 ms
|
|
6
|
+
* close, immediate open on keyboard focus but NOT on pointer focus,
|
|
7
|
+
* press-to-dismiss, Escape through the layer stack (a tooltip in a Modal
|
|
8
|
+
* closes alone), never focusable / pointer-events none, controlled state,
|
|
9
|
+
* child handler + ref composition, and positioning with mocked geometry.
|
|
10
|
+
*/
|
|
11
|
+
import { createRef } from 'react'
|
|
12
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
13
|
+
import { act, render, screen } from '@testing-library/react'
|
|
14
|
+
import userEvent from '@testing-library/user-event'
|
|
15
|
+
import Modal from '../modal'
|
|
16
|
+
import { Tooltip } from '../tooltip'
|
|
17
|
+
import { mockGeometry, setViewport } from './helpers/geometry'
|
|
18
|
+
|
|
19
|
+
const trigger = () => screen.getByRole('button', { name: 'Archive' })
|
|
20
|
+
const tip = () => screen.getByRole('tooltip')
|
|
21
|
+
const advance = (ms: number) =>
|
|
22
|
+
act(() => {
|
|
23
|
+
vi.advanceTimersByTime(ms)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
describe('Tooltip', () => {
|
|
27
|
+
let user: ReturnType<typeof userEvent.setup>
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
vi.useFakeTimers()
|
|
30
|
+
// RTL's asyncWrapper drains with a real setTimeout(0) unless it finds a
|
|
31
|
+
// jest-style advanceTimersByTime; without this shim every user-event
|
|
32
|
+
// call hangs under vitest fake timers.
|
|
33
|
+
vi.stubGlobal('jest', {
|
|
34
|
+
advanceTimersByTime: (ms: number) => vi.advanceTimersByTime(ms),
|
|
35
|
+
})
|
|
36
|
+
user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime })
|
|
37
|
+
})
|
|
38
|
+
afterEach(() => {
|
|
39
|
+
vi.useRealTimers()
|
|
40
|
+
vi.unstubAllGlobals()
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
function Basic(props: Partial<React.ComponentProps<typeof Tooltip>> = {}) {
|
|
44
|
+
return (
|
|
45
|
+
<Tooltip content="Archive this task" {...props}>
|
|
46
|
+
<button type="button" aria-label="Archive">
|
|
47
|
+
A
|
|
48
|
+
</button>
|
|
49
|
+
</Tooltip>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
describe('rendering', () => {
|
|
54
|
+
it('renders the child as-is: no wrapper, no tooltip, no aria-describedby while closed', () => {
|
|
55
|
+
const { container } = render(<Basic />)
|
|
56
|
+
expect(container.firstElementChild).toBe(trigger())
|
|
57
|
+
expect(screen.queryByRole('tooltip')).toBeNull()
|
|
58
|
+
expect(trigger()).not.toHaveAttribute('aria-describedby')
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('forwards the child ref', () => {
|
|
62
|
+
const ref = createRef<HTMLButtonElement>()
|
|
63
|
+
render(
|
|
64
|
+
<Tooltip content="x">
|
|
65
|
+
<button ref={ref} type="button" aria-label="Archive" />
|
|
66
|
+
</Tooltip>,
|
|
67
|
+
)
|
|
68
|
+
expect(ref.current).toBe(trigger())
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('throws for a non-element child', () => {
|
|
72
|
+
const spy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
73
|
+
expect(() =>
|
|
74
|
+
render(
|
|
75
|
+
// @ts-expect-error — the contract is one element child; this pins the runtime guard.
|
|
76
|
+
<Tooltip content="x">plain text</Tooltip>,
|
|
77
|
+
),
|
|
78
|
+
).toThrow(/exactly one element child/)
|
|
79
|
+
spy.mockRestore()
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('with null content, the child passes through and nothing ever opens', async () => {
|
|
83
|
+
render(<Basic content={null} />)
|
|
84
|
+
await user.hover(trigger())
|
|
85
|
+
advance(1000)
|
|
86
|
+
expect(screen.queryByRole('tooltip')).toBeNull()
|
|
87
|
+
})
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
describe('hover', () => {
|
|
91
|
+
it('opens after the 300 ms delay, portaled to body, and wires aria-describedby', async () => {
|
|
92
|
+
const { container } = render(<Basic />)
|
|
93
|
+
await user.hover(trigger())
|
|
94
|
+
expect(screen.queryByRole('tooltip')).toBeNull()
|
|
95
|
+
advance(299)
|
|
96
|
+
expect(screen.queryByRole('tooltip')).toBeNull()
|
|
97
|
+
advance(1)
|
|
98
|
+
const t = tip()
|
|
99
|
+
expect(t).toHaveTextContent('Archive this task')
|
|
100
|
+
expect(t.parentElement).toBe(document.body)
|
|
101
|
+
expect(container.contains(t)).toBe(false)
|
|
102
|
+
expect(trigger()).toHaveAttribute('aria-describedby', t.id)
|
|
103
|
+
expect(t).toHaveAttribute('data-slot', 'tooltip')
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('closes immediately on unhover and drops aria-describedby', async () => {
|
|
107
|
+
render(<Basic />)
|
|
108
|
+
await user.hover(trigger())
|
|
109
|
+
advance(300)
|
|
110
|
+
expect(tip()).toBeInTheDocument()
|
|
111
|
+
await user.unhover(trigger())
|
|
112
|
+
expect(screen.queryByRole('tooltip')).toBeNull()
|
|
113
|
+
expect(trigger()).not.toHaveAttribute('aria-describedby')
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('leaving before the delay cancels the open', async () => {
|
|
117
|
+
render(<Basic />)
|
|
118
|
+
await user.hover(trigger())
|
|
119
|
+
advance(150)
|
|
120
|
+
await user.unhover(trigger())
|
|
121
|
+
advance(1000)
|
|
122
|
+
expect(screen.queryByRole('tooltip')).toBeNull()
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('honours custom openDelay / closeDelay', async () => {
|
|
126
|
+
render(<Basic openDelay={50} closeDelay={100} />)
|
|
127
|
+
await user.hover(trigger())
|
|
128
|
+
advance(50)
|
|
129
|
+
expect(tip()).toBeInTheDocument()
|
|
130
|
+
await user.unhover(trigger())
|
|
131
|
+
expect(tip()).toBeInTheDocument()
|
|
132
|
+
advance(100)
|
|
133
|
+
expect(screen.queryByRole('tooltip')).toBeNull()
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it('merges an existing aria-describedby rather than replacing it', async () => {
|
|
137
|
+
render(
|
|
138
|
+
<>
|
|
139
|
+
<p id="hint">hint</p>
|
|
140
|
+
<Tooltip content="x">
|
|
141
|
+
<button type="button" aria-label="Archive" aria-describedby="hint" />
|
|
142
|
+
</Tooltip>
|
|
143
|
+
</>,
|
|
144
|
+
)
|
|
145
|
+
await user.hover(trigger())
|
|
146
|
+
advance(300)
|
|
147
|
+
expect(trigger().getAttribute('aria-describedby')).toBe(`hint ${tip().id}`)
|
|
148
|
+
await user.unhover(trigger())
|
|
149
|
+
expect(trigger()).toHaveAttribute('aria-describedby', 'hint')
|
|
150
|
+
})
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
describe('focus', () => {
|
|
154
|
+
it('opens immediately on keyboard focus and closes on blur', async () => {
|
|
155
|
+
render(
|
|
156
|
+
<>
|
|
157
|
+
<Basic />
|
|
158
|
+
<button type="button">Next</button>
|
|
159
|
+
</>,
|
|
160
|
+
)
|
|
161
|
+
await user.tab()
|
|
162
|
+
expect(document.activeElement).toBe(trigger())
|
|
163
|
+
expect(tip()).toBeInTheDocument()
|
|
164
|
+
expect(trigger()).toHaveAttribute('aria-describedby', tip().id)
|
|
165
|
+
await user.tab()
|
|
166
|
+
expect(screen.queryByRole('tooltip')).toBeNull()
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
it('a focus that arrives by pointer (a click) does not open it', async () => {
|
|
170
|
+
render(<Basic />)
|
|
171
|
+
await user.click(trigger())
|
|
172
|
+
expect(document.activeElement).toBe(trigger())
|
|
173
|
+
advance(1000)
|
|
174
|
+
expect(screen.queryByRole('tooltip')).toBeNull()
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
it('pressing the trigger dismisses an open tooltip', async () => {
|
|
178
|
+
render(<Basic />)
|
|
179
|
+
await user.hover(trigger())
|
|
180
|
+
advance(300)
|
|
181
|
+
expect(tip()).toBeInTheDocument()
|
|
182
|
+
await user.pointer({ keys: '[MouseLeft>]', target: trigger() })
|
|
183
|
+
expect(screen.queryByRole('tooltip')).toBeNull()
|
|
184
|
+
await user.pointer({ keys: '[/MouseLeft]', target: trigger() })
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
it('removes its document press listeners on release and on unmount (no stale closure per interrupted press)', async () => {
|
|
188
|
+
const added = vi.spyOn(document, 'addEventListener')
|
|
189
|
+
const removed = vi.spyOn(document, 'removeEventListener')
|
|
190
|
+
try {
|
|
191
|
+
const { unmount } = render(<Basic />)
|
|
192
|
+
const pressListeners = () =>
|
|
193
|
+
added.mock.calls.filter(([type]) => type === 'pointerup' || type === 'pointercancel')
|
|
194
|
+
const removedPressListeners = () =>
|
|
195
|
+
removed.mock.calls.filter(([type]) => type === 'pointerup' || type === 'pointercancel')
|
|
196
|
+
|
|
197
|
+
// A full press: the listeners go on at pointerdown and come off at pointerup.
|
|
198
|
+
await user.pointer({ keys: '[MouseLeft>]', target: trigger() })
|
|
199
|
+
expect(pressListeners()).toHaveLength(2)
|
|
200
|
+
await user.pointer({ keys: '[/MouseLeft]', target: trigger() })
|
|
201
|
+
expect(removedPressListeners().map(([, fn]) => fn)).toEqual(
|
|
202
|
+
pressListeners().map(([, fn]) => fn),
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
// An interrupted press: unmount mid-press must remove them too.
|
|
206
|
+
added.mockClear()
|
|
207
|
+
removed.mockClear()
|
|
208
|
+
await user.pointer({ keys: '[MouseLeft>]', target: trigger() })
|
|
209
|
+
expect(pressListeners()).toHaveLength(2)
|
|
210
|
+
unmount()
|
|
211
|
+
expect(removedPressListeners().map(([, fn]) => fn)).toEqual(
|
|
212
|
+
pressListeners().map(([, fn]) => fn),
|
|
213
|
+
)
|
|
214
|
+
} finally {
|
|
215
|
+
added.mockRestore()
|
|
216
|
+
removed.mockRestore()
|
|
217
|
+
}
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
it('never traps or moves focus: the tip is not focusable and does not take the pointer', async () => {
|
|
221
|
+
render(<Basic />)
|
|
222
|
+
await user.tab()
|
|
223
|
+
const t = tip()
|
|
224
|
+
expect(t).not.toHaveAttribute('tabindex')
|
|
225
|
+
expect(t).toHaveClass('pointer-events-none')
|
|
226
|
+
expect(document.activeElement).toBe(trigger())
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
it('composes the child handlers', async () => {
|
|
230
|
+
const onFocus = vi.fn()
|
|
231
|
+
const onPointerEnter = vi.fn()
|
|
232
|
+
render(
|
|
233
|
+
<Tooltip content="x">
|
|
234
|
+
<button type="button" aria-label="Archive" onFocus={onFocus} onPointerEnter={onPointerEnter} />
|
|
235
|
+
</Tooltip>,
|
|
236
|
+
)
|
|
237
|
+
await user.hover(trigger())
|
|
238
|
+
await user.tab()
|
|
239
|
+
expect(onPointerEnter).toHaveBeenCalledTimes(1)
|
|
240
|
+
expect(onFocus).toHaveBeenCalledTimes(1)
|
|
241
|
+
})
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
describe('Escape + layers', () => {
|
|
245
|
+
it('Escape dismisses and leaves focus where it was', async () => {
|
|
246
|
+
render(<Basic />)
|
|
247
|
+
await user.tab()
|
|
248
|
+
expect(tip()).toBeInTheDocument()
|
|
249
|
+
await user.keyboard('{Escape}')
|
|
250
|
+
expect(screen.queryByRole('tooltip')).toBeNull()
|
|
251
|
+
expect(document.activeElement).toBe(trigger())
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
it('inside a Modal, Escape closes only the tooltip; the next Escape closes the modal', async () => {
|
|
255
|
+
const onClose = vi.fn()
|
|
256
|
+
render(
|
|
257
|
+
<Modal isOpen onClose={onClose} title="Settings">
|
|
258
|
+
<Basic />
|
|
259
|
+
</Modal>,
|
|
260
|
+
)
|
|
261
|
+
await user.hover(trigger())
|
|
262
|
+
advance(300)
|
|
263
|
+
expect(tip()).toBeInTheDocument()
|
|
264
|
+
await user.keyboard('{Escape}')
|
|
265
|
+
expect(screen.queryByRole('tooltip')).toBeNull()
|
|
266
|
+
expect(onClose).not.toHaveBeenCalled()
|
|
267
|
+
await user.keyboard('{Escape}')
|
|
268
|
+
expect(onClose).toHaveBeenCalledTimes(1)
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
it('does not listen for Escape while closed', async () => {
|
|
272
|
+
const onOpenChange = vi.fn()
|
|
273
|
+
render(<Basic onOpenChange={onOpenChange} />)
|
|
274
|
+
await user.keyboard('{Escape}')
|
|
275
|
+
expect(onOpenChange).not.toHaveBeenCalled()
|
|
276
|
+
})
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
describe('controlled / uncontrolled', () => {
|
|
280
|
+
it('defaultOpen renders it open', () => {
|
|
281
|
+
render(<Basic defaultOpen />)
|
|
282
|
+
expect(tip()).toBeInTheDocument()
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
it('controlled `open` renders regardless of hover; onOpenChange reports intent', async () => {
|
|
286
|
+
const onOpenChange = vi.fn()
|
|
287
|
+
render(<Basic open={false} onOpenChange={onOpenChange} />)
|
|
288
|
+
await user.hover(trigger())
|
|
289
|
+
advance(300)
|
|
290
|
+
expect(onOpenChange).toHaveBeenCalledWith(true)
|
|
291
|
+
expect(screen.queryByRole('tooltip')).toBeNull()
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
it('controlled open=true shows it and unhover reports false', async () => {
|
|
295
|
+
const onOpenChange = vi.fn()
|
|
296
|
+
render(<Basic open onOpenChange={onOpenChange} />)
|
|
297
|
+
expect(tip()).toBeInTheDocument()
|
|
298
|
+
await user.hover(trigger())
|
|
299
|
+
await user.unhover(trigger())
|
|
300
|
+
expect(onOpenChange).toHaveBeenCalledWith(false)
|
|
301
|
+
expect(tip()).toBeInTheDocument()
|
|
302
|
+
})
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
describe('surface + positioning (mocked geometry)', () => {
|
|
306
|
+
let restore: () => void
|
|
307
|
+
beforeEach(() => {
|
|
308
|
+
restore = mockGeometry()
|
|
309
|
+
setViewport(1000, 800)
|
|
310
|
+
})
|
|
311
|
+
afterEach(() => restore())
|
|
312
|
+
|
|
313
|
+
it('sits above the trigger, centred, with the popover-family chrome and the entry motion', async () => {
|
|
314
|
+
render(
|
|
315
|
+
<Tooltip content="x" className="mine">
|
|
316
|
+
<button type="button" aria-label="Archive" data-rect="400,300,80,32" />
|
|
317
|
+
</Tooltip>,
|
|
318
|
+
)
|
|
319
|
+
await user.tab()
|
|
320
|
+
const t = tip()
|
|
321
|
+
expect(t.style.position).toBe('fixed')
|
|
322
|
+
expect(t).toHaveAttribute('data-side', 'top')
|
|
323
|
+
expect(t).toHaveAttribute('data-align', 'center')
|
|
324
|
+
expect(t.style.left).toBe('440px')
|
|
325
|
+
expect(t.style.top).toBe(`${300 - 6}px`)
|
|
326
|
+
expect(t).toHaveAttribute('data-positioned', 'true')
|
|
327
|
+
expect(t).toHaveClass('ds-enter-pop', 'mine')
|
|
328
|
+
expect(t.style.background).toContain('--popover')
|
|
329
|
+
expect(t.style.padding).toContain('--space-1')
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
it('flips below the trigger at the top of the viewport', async () => {
|
|
333
|
+
render(
|
|
334
|
+
<Tooltip content="x">
|
|
335
|
+
<button type="button" aria-label="Archive" data-rect="400,4,80,32" />
|
|
336
|
+
</Tooltip>,
|
|
337
|
+
)
|
|
338
|
+
await user.tab()
|
|
339
|
+
expect(tip()).toHaveAttribute('data-side', 'bottom')
|
|
340
|
+
expect(tip().style.top).toBe(`${4 + 32 + 6}px`)
|
|
341
|
+
})
|
|
342
|
+
|
|
343
|
+
it('honours side / align / offset', async () => {
|
|
344
|
+
render(
|
|
345
|
+
<Tooltip content="x" side="right" align="start" offset={10}>
|
|
346
|
+
<button type="button" aria-label="Archive" data-rect="400,300,80,32" />
|
|
347
|
+
</Tooltip>,
|
|
348
|
+
)
|
|
349
|
+
await user.tab()
|
|
350
|
+
expect(tip()).toHaveAttribute('data-side', 'right')
|
|
351
|
+
expect(tip().style.left).toBe(`${480 + 10}px`)
|
|
352
|
+
expect(tip().style.top).toBe('300px')
|
|
353
|
+
})
|
|
354
|
+
})
|
|
355
|
+
})
|