@tangle-network/ui 11.51.7 → 11.51.9

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.
Files changed (66) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +6 -0
  3. package/dist/chat/chat-message.js +2 -7
  4. package/dist/chat/user-message.js +3 -5
  5. package/dist/files/file-card.js +1 -1
  6. package/dist/files/file-tree.d.ts +0 -6
  7. package/dist/files/file-tree.js +2 -6
  8. package/dist/index.d.ts +2 -2
  9. package/dist/index.js +2 -2
  10. package/dist/primitives/hover-card.d.ts +22 -13
  11. package/dist/primitives/hover-card.js +149 -40
  12. package/dist/primitives/popover.d.ts +3 -1
  13. package/dist/primitives/popover.js +15 -12
  14. package/dist/primitives/upload-progress.d.ts +0 -6
  15. package/dist/primitives/upload-progress.js +2 -6
  16. package/dist/run/run-group.js +2 -2
  17. package/dist/run/run-item-primitives.js +2 -2
  18. package/dist/run/run-row-shell.js +2 -2
  19. package/dist/utils/format.d.ts +14 -12
  20. package/dist/utils/format.js +50 -30
  21. package/dist/utils.d.ts +2 -2
  22. package/dist/utils.js +2 -2
  23. package/package.json +11 -2
  24. package/src/chat/chat-message.tsx +2 -7
  25. package/src/chat/user-message.tsx +3 -5
  26. package/src/editor/source-markdown-editor.test.tsx +7 -7
  27. package/src/files/file-tree.tsx +2 -7
  28. package/src/primitives/hover-card.tsx +64 -74
  29. package/src/primitives/popover.tsx +3 -3
  30. package/src/primitives/upload-progress.tsx +2 -7
  31. package/src/utils/format.ts +50 -33
  32. package/src/utils/index.ts +1 -1
  33. package/src/editor/document-editor-pane.test.tsx +0 -60
  34. package/src/editor/editor-lazy.test.tsx +0 -102
  35. package/src/editor/editor-peers.test.ts +0 -250
  36. package/src/editor/editor-provider.test.tsx +0 -226
  37. package/src/editor/markdown-document-editor.test.tsx +0 -126
  38. package/src/editor/tiptap-editor.test.tsx +0 -228
  39. package/src/files/file-artifact-pane.test.tsx +0 -301
  40. package/src/files/file-card.test.tsx +0 -62
  41. package/src/files/file-list.test.tsx +0 -81
  42. package/src/files/pdf-loader.test.ts +0 -130
  43. package/src/files/pdf-viewer.test.tsx +0 -245
  44. package/src/files/rich-file-tree.test.tsx +0 -90
  45. package/src/markdown/api-presentation.test.tsx +0 -63
  46. package/src/markdown/code-block.test.tsx +0 -201
  47. package/src/markdown/markdown-table.test.tsx +0 -76
  48. package/src/markdown/markdown.test.tsx +0 -166
  49. package/src/openui/schema.test.tsx +0 -158
  50. package/src/primitives/button.test.tsx +0 -230
  51. package/src/primitives/card.test.tsx +0 -71
  52. package/src/primitives/chip.test.tsx +0 -200
  53. package/src/primitives/date-picker.test.tsx +0 -146
  54. package/src/primitives/dialog.test.tsx +0 -75
  55. package/src/primitives/input.test.tsx +0 -161
  56. package/src/primitives/interaction-states.test.tsx +0 -287
  57. package/src/primitives/pagination-alert.test.tsx +0 -180
  58. package/src/primitives/popover-collapsible-alert-dialog-context-menu-slider.test.tsx +0 -165
  59. package/src/primitives/radio-group.test.tsx +0 -26
  60. package/src/primitives/segmented-control.test.tsx +0 -451
  61. package/src/primitives/table.test.tsx +0 -72
  62. package/src/primitives/theme-toggle.test.tsx +0 -168
  63. package/src/primitives/toast.test.tsx +0 -130
  64. package/src/primitives/wizard.test.tsx +0 -106
  65. package/src/run/expandable-content.test.tsx +0 -52
  66. package/src/utils/format.test.ts +0 -58
@@ -1,451 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
2
- import { render, screen } from "@testing-library/react"
3
- import userEvent from "@testing-library/user-event"
4
- import { SegmentedControl, type SegmentedControlOption } from "./segmented-control"
5
-
6
- const options: SegmentedControlOption[] = [
7
- { value: "all", label: "All" },
8
- { value: "personal", label: "Personal" },
9
- { value: "team", label: "Team" },
10
- ]
11
-
12
- describe("SegmentedControl", () => {
13
- it("renders all options as radio buttons", () => {
14
- render(
15
- <SegmentedControl value="all" onValueChange={vi.fn()} options={options} />,
16
- )
17
- const radios = screen.getAllByRole("radio")
18
- expect(radios).toHaveLength(3)
19
- expect(screen.getByText("All")).toBeInTheDocument()
20
- expect(screen.getByText("Personal")).toBeInTheDocument()
21
- expect(screen.getByText("Team")).toBeInTheDocument()
22
- })
23
-
24
- it("marks the active option with aria-checked", () => {
25
- render(
26
- <SegmentedControl value="personal" onValueChange={vi.fn()} options={options} />,
27
- )
28
- const personalRadio = screen.getByRole("radio", { name: "Personal" })
29
- const allRadio = screen.getByRole("radio", { name: "All" })
30
-
31
- expect(personalRadio).toHaveAttribute("aria-checked", "true")
32
- expect(allRadio).toHaveAttribute("aria-checked", "false")
33
- })
34
-
35
- it("sets tabIndex 0 on active option and -1 on inactive options", () => {
36
- render(
37
- <SegmentedControl value="all" onValueChange={vi.fn()} options={options} />,
38
- )
39
- const radios = screen.getAllByRole("radio")
40
- expect(radios[0]).toHaveAttribute("tabindex", "0")
41
- expect(radios[1]).toHaveAttribute("tabindex", "-1")
42
- expect(radios[2]).toHaveAttribute("tabindex", "-1")
43
- })
44
-
45
- it("calls onValueChange when clicking an inactive option", async () => {
46
- const user = userEvent.setup()
47
- const onChange = vi.fn()
48
- render(
49
- <SegmentedControl value="all" onValueChange={onChange} options={options} />,
50
- )
51
-
52
- await user.click(screen.getByRole("radio", { name: "Team" }))
53
- expect(onChange).toHaveBeenCalledWith("team")
54
- })
55
-
56
- it("does not call onValueChange when clicking the already-active option", async () => {
57
- const user = userEvent.setup()
58
- const onChange = vi.fn()
59
- render(
60
- <SegmentedControl value="all" onValueChange={onChange} options={options} />,
61
- )
62
-
63
- await user.click(screen.getByRole("radio", { name: "All" }))
64
- expect(onChange).not.toHaveBeenCalled()
65
- })
66
-
67
- it("renders adornment when provided", () => {
68
- const withAdornment: SegmentedControlOption[] = [
69
- { value: "all", label: "All", adornment: <span data-testid="count">42</span> },
70
- { value: "personal", label: "Personal" },
71
- ]
72
- render(
73
- <SegmentedControl value="all" onValueChange={vi.fn()} options={withAdornment} />,
74
- )
75
- expect(screen.getByTestId("count")).toBeInTheDocument()
76
- expect(screen.getByText("42")).toBeInTheDocument()
77
- })
78
-
79
- // --- Keyboard navigation ---
80
-
81
- it("navigates to next option on ArrowRight", async () => {
82
- const user = userEvent.setup()
83
- const onChange = vi.fn()
84
- render(
85
- <SegmentedControl value="all" onValueChange={onChange} options={options} />,
86
- )
87
-
88
- screen.getByRole("radio", { name: "All" }).focus()
89
- await user.keyboard("{ArrowRight}")
90
-
91
- expect(onChange).toHaveBeenCalledWith("personal")
92
- })
93
-
94
- it("moves DOM focus to the target option on arrow key", async () => {
95
- const user = userEvent.setup()
96
- render(
97
- <SegmentedControl value="all" onValueChange={vi.fn()} options={options} />,
98
- )
99
-
100
- const personalRadio = screen.getByRole("radio", { name: "Personal" })
101
- screen.getByRole("radio", { name: "All" }).focus()
102
- await user.keyboard("{ArrowRight}")
103
-
104
- expect(document.activeElement).toBe(personalRadio)
105
- })
106
-
107
- it("navigates to previous option on ArrowLeft", async () => {
108
- const user = userEvent.setup()
109
- const onChange = vi.fn()
110
- render(
111
- <SegmentedControl value="personal" onValueChange={onChange} options={options} />,
112
- )
113
-
114
- screen.getByRole("radio", { name: "Personal" }).focus()
115
- await user.keyboard("{ArrowLeft}")
116
-
117
- expect(onChange).toHaveBeenCalledWith("all")
118
- })
119
-
120
- it("wraps around on ArrowRight from last option", async () => {
121
- const user = userEvent.setup()
122
- const onChange = vi.fn()
123
- render(
124
- <SegmentedControl value="team" onValueChange={onChange} options={options} />,
125
- )
126
-
127
- screen.getByRole("radio", { name: "Team" }).focus()
128
- await user.keyboard("{ArrowRight}")
129
-
130
- expect(onChange).toHaveBeenCalledWith("all")
131
- })
132
-
133
- it("wraps around on ArrowLeft from first option", async () => {
134
- const user = userEvent.setup()
135
- const onChange = vi.fn()
136
- render(
137
- <SegmentedControl value="all" onValueChange={onChange} options={options} />,
138
- )
139
-
140
- screen.getByRole("radio", { name: "All" }).focus()
141
- await user.keyboard("{ArrowLeft}")
142
-
143
- expect(onChange).toHaveBeenCalledWith("team")
144
- })
145
-
146
- it("navigates to first option on Home", async () => {
147
- const user = userEvent.setup()
148
- const onChange = vi.fn()
149
- render(
150
- <SegmentedControl value="team" onValueChange={onChange} options={options} />,
151
- )
152
-
153
- screen.getByRole("radio", { name: "Team" }).focus()
154
- await user.keyboard("{Home}")
155
-
156
- expect(onChange).toHaveBeenCalledWith("all")
157
- })
158
-
159
- it("navigates to last option on End", async () => {
160
- const user = userEvent.setup()
161
- const onChange = vi.fn()
162
- render(
163
- <SegmentedControl value="all" onValueChange={onChange} options={options} />,
164
- )
165
-
166
- screen.getByRole("radio", { name: "All" }).focus()
167
- await user.keyboard("{End}")
168
-
169
- expect(onChange).toHaveBeenCalledWith("team")
170
- })
171
-
172
- it("does not fire onValueChange when Home is pressed on first option", async () => {
173
- const user = userEvent.setup()
174
- const onChange = vi.fn()
175
- render(
176
- <SegmentedControl value="all" onValueChange={onChange} options={options} />,
177
- )
178
-
179
- screen.getByRole("radio", { name: "All" }).focus()
180
- await user.keyboard("{Home}")
181
-
182
- expect(onChange).not.toHaveBeenCalled()
183
- })
184
-
185
- it("does not fire onValueChange when End is pressed on last option", async () => {
186
- const user = userEvent.setup()
187
- const onChange = vi.fn()
188
- render(
189
- <SegmentedControl value="team" onValueChange={onChange} options={options} />,
190
- )
191
-
192
- screen.getByRole("radio", { name: "Team" }).focus()
193
- await user.keyboard("{End}")
194
-
195
- expect(onChange).not.toHaveBeenCalled()
196
- })
197
-
198
- // --- Edge cases ---
199
-
200
- it("does not crash on keyboard input when options array is empty", async () => {
201
- const user = userEvent.setup()
202
- const onChange = vi.fn()
203
- const { container } = render(
204
- <SegmentedControl value="all" onValueChange={onChange} options={[]} />,
205
- )
206
-
207
- const radiogroup = container.querySelector("[role='radiogroup']") as HTMLElement
208
- radiogroup.focus()
209
- await user.keyboard("{ArrowRight}")
210
- await user.keyboard("{Home}")
211
- await user.keyboard("{End}")
212
-
213
- expect(onChange).not.toHaveBeenCalled()
214
- })
215
-
216
- it("gives first option tabIndex 0 when value does not match any option", () => {
217
- render(
218
- <SegmentedControl value={"unknown" as string} onValueChange={vi.fn()} options={options} />,
219
- )
220
- const radios = screen.getAllByRole("radio")
221
- expect(radios[0]).toHaveAttribute("tabindex", "0")
222
- expect(radios[1]).toHaveAttribute("tabindex", "-1")
223
- expect(radios[2]).toHaveAttribute("tabindex", "-1")
224
- })
225
-
226
- it("navigates from first option when value does not match any option", async () => {
227
- const user = userEvent.setup()
228
- const onChange = vi.fn()
229
- render(
230
- <SegmentedControl value={"unknown" as string} onValueChange={onChange} options={options} />,
231
- )
232
-
233
- screen.getByRole("radio", { name: "All" }).focus()
234
- await user.keyboard("{ArrowRight}")
235
-
236
- expect(onChange).toHaveBeenCalledWith("personal")
237
- })
238
-
239
- it("supports ArrowDown as alias for ArrowRight", async () => {
240
- const user = userEvent.setup()
241
- const onChange = vi.fn()
242
- render(
243
- <SegmentedControl value="all" onValueChange={onChange} options={options} />,
244
- )
245
-
246
- screen.getByRole("radio", { name: "All" }).focus()
247
- await user.keyboard("{ArrowDown}")
248
-
249
- expect(onChange).toHaveBeenCalledWith("personal")
250
- })
251
-
252
- it("supports ArrowUp as alias for ArrowLeft", async () => {
253
- const user = userEvent.setup()
254
- const onChange = vi.fn()
255
- render(
256
- <SegmentedControl value="personal" onValueChange={onChange} options={options} />,
257
- )
258
-
259
- screen.getByRole("radio", { name: "Personal" }).focus()
260
- await user.keyboard("{ArrowUp}")
261
-
262
- expect(onChange).toHaveBeenCalledWith("all")
263
- })
264
-
265
- // --- Variants ---
266
-
267
- it("applies row variant classes by default", () => {
268
- render(
269
- <SegmentedControl value="all" onValueChange={vi.fn()} options={options} />,
270
- )
271
- const radiogroup = screen.getByRole("radiogroup")
272
- expect(radiogroup.className).toContain("flex-wrap")
273
- expect(radiogroup.className).toContain("rounded-lg")
274
- // The row track is one quiet well: the hairline border, not a field's strong border.
275
- expect(radiogroup.className).toContain("bg-[var(--well)]")
276
- expect(radiogroup.className).toContain("border-border")
277
- expect(radiogroup.className).not.toContain("border-[var(--field-border)]")
278
- })
279
-
280
- it("applies tabs variant classes with nowrap", () => {
281
- render(
282
- <SegmentedControl
283
- value="all"
284
- onValueChange={vi.fn()}
285
- options={options}
286
- variant="tabs"
287
- />,
288
- )
289
- const radiogroup = screen.getByRole("radiogroup")
290
- expect(radiogroup.className).toContain("flex-nowrap")
291
- expect(radiogroup.className).toContain("overflow-x-auto")
292
- expect(radiogroup.className).toContain("border-b")
293
- expect(radiogroup.className).not.toContain("bg-[var(--well)]")
294
- })
295
-
296
- it("passes aria-label to the radiogroup", () => {
297
- render(
298
- <SegmentedControl
299
- value="all"
300
- onValueChange={vi.fn()}
301
- options={options}
302
- aria-label="Scope filter"
303
- />,
304
- )
305
- expect(screen.getByRole("radiogroup")).toHaveAttribute("aria-label", "Scope filter")
306
- })
307
-
308
- it("passes aria-labelledby to the radiogroup", () => {
309
- render(
310
- <SegmentedControl
311
- value="all"
312
- onValueChange={vi.fn()}
313
- options={options}
314
- aria-labelledby="heading-id"
315
- />,
316
- )
317
- expect(screen.getByRole("radiogroup")).toHaveAttribute("aria-labelledby", "heading-id")
318
- })
319
-
320
- it("passes id to the radiogroup", () => {
321
- render(
322
- <SegmentedControl
323
- value="all"
324
- onValueChange={vi.fn()}
325
- options={options}
326
- id="scope-control"
327
- />,
328
- )
329
- expect(screen.getByRole("radiogroup")).toHaveAttribute("id", "scope-control")
330
- })
331
- })
332
-
333
- describe("SegmentedControl test ids", () => {
334
- it("renders option and group test ids for end-to-end selectors", () => {
335
- render(
336
- <SegmentedControl
337
- aria-label="Workbench view"
338
- data-testid="workbench-tabs"
339
- value="code"
340
- onValueChange={vi.fn()}
341
- options={[
342
- { value: "code", label: "Code", testId: "workbench-tab-code" },
343
- { value: "preview", label: "Preview", testId: "workbench-tab-preview" },
344
- ]}
345
- />,
346
- )
347
- expect(screen.getByTestId("workbench-tabs")).toHaveAttribute("role", "radiogroup")
348
- expect(screen.getByTestId("workbench-tab-preview")).toHaveAttribute("aria-checked", "false")
349
- expect(screen.getByTestId("workbench-tab-code")).toHaveAttribute("aria-checked", "true")
350
- })
351
- })
352
-
353
- describe("SegmentedControl tabs on a narrow strip", () => {
354
- // jsdom has no layout. Model a 250px strip holding six 100px tabs, so the
355
- // strip scrolls and some tabs sit past its edges.
356
- const STRIP = 250
357
- const TAB = 100
358
- const scroll = new WeakMap<Element, number>()
359
- const overrides: Array<[object, string, PropertyDescriptor | undefined]> = []
360
- const define = (proto: object, key: string, descriptor: PropertyDescriptor) => {
361
- overrides.push([proto, key, Object.getOwnPropertyDescriptor(proto, key)])
362
- Object.defineProperty(proto, key, { configurable: true, ...descriptor })
363
- }
364
- const isStrip = (el: Element) => el.getAttribute("role") === "radiogroup"
365
- // Options sit side by side; the edge chevrons take no room in the row.
366
- const index = (el: Element) => Array.from(el.parentElement?.querySelectorAll(':scope > [role="radio"]') ?? []).indexOf(el)
367
-
368
- beforeEach(() => {
369
- define(HTMLElement.prototype, "offsetLeft", { get(this: HTMLElement) { return isStrip(this) ? 0 : index(this) * TAB } })
370
- define(HTMLElement.prototype, "offsetWidth", { get(this: HTMLElement) { return isStrip(this) ? STRIP : TAB } })
371
- define(Element.prototype, "clientWidth", { get(this: Element) { return isStrip(this) ? STRIP : TAB } })
372
- define(Element.prototype, "scrollWidth", { get(this: Element) { return isStrip(this) ? this.querySelectorAll(':scope > [role="radio"]').length * TAB : TAB } })
373
- define(Element.prototype, "scrollLeft", {
374
- get(this: Element) { return scroll.get(this) ?? 0 },
375
- set(this: Element, v: number) { scroll.set(this, v) },
376
- })
377
- })
378
- afterEach(() => {
379
- for (const [proto, key, original] of overrides.reverse()) {
380
- if (original) Object.defineProperty(proto, key, original)
381
- else delete (proto as Record<string, unknown>)[key]
382
- }
383
- overrides.length = 0
384
- })
385
-
386
- const work: SegmentedControlOption[] = ["Today", "Messages", "Tasks", "Operations", "Payments", "People"].map(
387
- (label) => ({ value: label.toLowerCase(), label }),
388
- )
389
-
390
- it("scrolls the strip, not the page, so the selected tab is in view", () => {
391
- const { rerender } = render(
392
- <SegmentedControl aria-label="Work" variant="tabs" value="payments" onValueChange={vi.fn()} options={work} />,
393
- )
394
- const strip = screen.getByRole("radiogroup")
395
- // Payments spans 400-500px: its right edge clears the 32px chevron, plus a 4px gap, in the 250px view.
396
- expect(strip.scrollLeft).toBe(500 - STRIP + 32 + 4)
397
-
398
- rerender(<SegmentedControl aria-label="Work" variant="tabs" value="today" onValueChange={vi.fn()} options={work} />)
399
- expect(strip.scrollLeft).toBe(0)
400
- })
401
-
402
- it("shows a chevron at the edge that has more tabs past it", () => {
403
- const { rerender } = render(
404
- <SegmentedControl aria-label="Work" variant="tabs" value="today" onValueChange={vi.fn()} options={work} />,
405
- )
406
- const strip = screen.getByRole("radiogroup")
407
- expect(strip).toHaveAttribute("data-overflow-end", "true")
408
- expect(strip).not.toHaveAttribute("data-overflow-start")
409
- expect(strip.querySelector('[data-scroll-cue="end"]')).not.toBeNull()
410
- expect(strip.querySelector('[data-scroll-cue="start"]')).toBeNull()
411
-
412
- rerender(<SegmentedControl aria-label="Work" variant="tabs" value="people" onValueChange={vi.fn()} options={work} />)
413
- expect(strip).toHaveAttribute("data-overflow-start", "true")
414
- expect(strip).not.toHaveAttribute("data-overflow-end")
415
- expect(strip.querySelector('[data-scroll-cue="start"]')).not.toBeNull()
416
- expect(strip.querySelector('[data-scroll-cue="end"]')).toBeNull()
417
- })
418
-
419
- it("keeps the chevrons out of the radio group and the tab order", () => {
420
- render(<SegmentedControl aria-label="Work" variant="tabs" value="today" onValueChange={vi.fn()} options={work} />)
421
- expect(screen.getAllByRole("radio")).toHaveLength(work.length)
422
- const cue = screen.getByRole("radiogroup").querySelector('[data-scroll-cue="end"]')
423
- expect(cue).toHaveAttribute("aria-hidden", "true")
424
- expect(cue).toHaveAttribute("tabindex", "-1")
425
- })
426
-
427
- it("leaves a strip that fits without chevrons", () => {
428
- render(<SegmentedControl aria-label="Work" variant="tabs" value="all" onValueChange={vi.fn()} options={options.slice(0, 2)} />)
429
- const strip = screen.getByRole("radiogroup")
430
- expect(strip).not.toHaveAttribute("data-overflow-start")
431
- expect(strip).not.toHaveAttribute("data-overflow-end")
432
- expect(strip.querySelector("[data-scroll-cue]")).toBeNull()
433
- })
434
- })
435
-
436
- describe("SegmentedControl stable labels and touch targets", () => {
437
- it.each(["row", "tabs"] as const)("keeps one label weight whichever %s option is selected", (variant) => {
438
- render(<SegmentedControl aria-label="Scope" variant={variant} value="personal" onValueChange={vi.fn()} options={options} />)
439
- for (const radio of screen.getAllByRole("radio")) {
440
- expect(radio.className).toContain("font-medium")
441
- expect(radio.className).not.toMatch(/font-(semibold|bold)/)
442
- }
443
- })
444
-
445
- it("gives each tab a 44px hit height on a coarse pointer", () => {
446
- render(<SegmentedControl aria-label="Scope" variant="tabs" value="all" onValueChange={vi.fn()} options={options} />)
447
- for (const radio of screen.getAllByRole("radio")) {
448
- expect(radio.className).toContain("pointer-coarse:min-h-11")
449
- }
450
- })
451
- })
@@ -1,72 +0,0 @@
1
- import { createRef } from "react";
2
- import { render, screen, within } from "@testing-library/react";
3
- import userEvent from "@testing-library/user-event";
4
- import { describe, expect, it, vi } from "vitest";
5
- import { Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow, type TableProps } from "./table";
6
-
7
- function UsageTable(props: TableProps) {
8
- return <Table {...props}>
9
- <TableCaption>Usage</TableCaption>
10
- <TableHeader><TableRow><TableHead scope="col" id="resource">Resource</TableHead><TableHead scope="col" id="count">Count</TableHead></TableRow></TableHeader>
11
- <TableBody><TableRow><TableHead scope="row" id="builds">Builds</TableHead><TableCell headers="builds count">3</TableCell></TableRow></TableBody>
12
- <TableFooter><TableRow><TableCell colSpan={2}>One resource</TableCell></TableRow></TableFooter>
13
- </Table>;
14
- }
15
-
16
- describe("Table", () => {
17
- it("preserves the default wrapper without imposing an unnamed region or tab stop", () => {
18
- render(<UsageTable />);
19
- const table = screen.getByRole("table", { name: "Usage" });
20
- expect(table.parentElement?.tagName).toBe("DIV");
21
- expect(table.parentElement).toHaveClass("relative", "w-full", "overflow-auto");
22
- expect(table.parentElement).not.toHaveAttribute("role");
23
- expect(table.parentElement).not.toHaveAttribute("tabindex");
24
- });
25
-
26
- it("retains native caption, table sections, headers, scope and spans", () => {
27
- render(<UsageTable />);
28
- const table = screen.getByRole("table", { name: "Usage" });
29
- expect(Array.from(table.children).map((node) => node.tagName)).toEqual(["CAPTION", "THEAD", "TBODY", "TFOOT"]);
30
- expect(screen.getByRole("columnheader", { name: "Resource" })).toHaveAttribute("scope", "col");
31
- expect(screen.getByRole("rowheader", { name: "Builds" })).toHaveAttribute("scope", "row");
32
- expect(screen.getByRole("cell", { name: "3" })).toHaveAttribute("headers", "builds count");
33
- expect(screen.getByRole("cell", { name: "One resource" })).toHaveAttribute("colspan", "2");
34
- expect(screen.queryByRole("grid")).toBeNull();
35
- });
36
-
37
- it("allows a caller-named keyboard-focusable wrapper separately from table attributes", async () => {
38
- const user = userEvent.setup();
39
- render(<><h2 id="usage-title">Resource usage</h2><UsageTable id="usage-table" className="min-w-[40rem]" wrapperProps={{ role: "region", "aria-labelledby": "usage-title", tabIndex: 0, className: "max-h-80" }} /></>);
40
- const region = screen.getByRole("region", { name: "Resource usage" });
41
- const table = within(region).getByRole("table", { name: "Usage" });
42
- expect(region).toHaveClass("max-h-80", "overflow-auto");
43
- expect(table).toHaveAttribute("id", "usage-table");
44
- expect(table).not.toHaveAttribute("tabindex");
45
- expect(table).not.toHaveClass("max-h-80");
46
- await user.tab();
47
- expect(region).toHaveFocus();
48
- });
49
-
50
- it("returns a bare table for a caller-owned scroll region, without leaking wrapper props", () => {
51
- render(<div role="region" aria-label="Outer" className="overflow-auto">
52
- <UsageTable wrapper={false} wrapperProps={{ id: "unused-wrapper" }} />
53
- </div>);
54
- const outer = screen.getByRole("region", { name: "Outer" });
55
- const table = screen.getByRole("table", { name: "Usage" });
56
- expect(outer.firstElementChild).toBe(table);
57
- expect(outer.querySelectorAll(".overflow-auto")).toHaveLength(0);
58
- expect(table).not.toHaveAttribute("wrapper");
59
- expect(table).not.toHaveAttribute("wrapperProps");
60
- expect(table).not.toHaveAttribute("id", "unused-wrapper");
61
- });
62
-
63
- it("keeps the public ref and native attributes/handlers on the table", async () => {
64
- const ref = createRef<HTMLTableElement>();
65
- const onClick = vi.fn();
66
- render(<Table ref={ref} cellPadding={0} onClick={onClick} aria-label="Details"><TableBody><TableRow><TableCell>Open</TableCell></TableRow></TableBody></Table>);
67
- expect(ref.current).toBe(screen.getByRole("table", { name: "Details" }));
68
- expect(ref.current).toHaveAttribute("cellpadding", "0");
69
- await userEvent.setup().click(screen.getByRole("cell", { name: "Open" }));
70
- expect(onClick).toHaveBeenCalledTimes(1);
71
- });
72
- });