@happyvertical/smrt-ui 0.53.1 → 0.53.3

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 (205) hide show
  1. package/package.json +3 -3
  2. package/dist/actions/__tests__/ripple.test.js +0 -82
  3. package/dist/actions/__tests__/swipe-dismiss.test.js +0 -69
  4. package/dist/components/calendar/__tests__/CalendarView.test.js +0 -226
  5. package/dist/components/calendar/__tests__/calendar-model.test.js +0 -138
  6. package/dist/components/chat/__tests__/chat-primitives.test.js +0 -118
  7. package/dist/components/data/__tests__/CollectionList.test.js +0 -37
  8. package/dist/components/data/__tests__/DataTable.test.js +0 -807
  9. package/dist/components/data/__tests__/DataTableConformance.test.js +0 -212
  10. package/dist/components/data/__tests__/DataTableController.test.js +0 -371
  11. package/dist/components/data/__tests__/DataTableIdentity.test.js +0 -29
  12. package/dist/components/data/__tests__/DataTableLayout.test.js +0 -195
  13. package/dist/components/data/__tests__/DataTablePerformance.test.js +0 -21
  14. package/dist/components/data/__tests__/DataTableResponsive.test.js +0 -80
  15. package/dist/components/data/__tests__/DataTableVirtualization.test.js +0 -80
  16. package/dist/components/data/__tests__/DataTableVirtualizationComponent.test.js +0 -255
  17. package/dist/components/data/__tests__/SortableHeader.test.js +0 -101
  18. package/dist/components/data/__tests__/data-surface.test.js +0 -746
  19. package/dist/components/data/__tests__/data-table-helpers.test.js +0 -36
  20. package/dist/components/data/__tests__/data-table-surface.test.js +0 -49
  21. package/dist/components/data/__tests__/link-step-surface.test.js +0 -172
  22. package/dist/components/data/__tests__/list-sort-state.svelte.test.js +0 -71
  23. package/dist/components/data/__tests__/list-sort.test.js +0 -91
  24. package/dist/components/data/__tests__/mounted-data-surface.test.js +0 -794
  25. package/dist/components/data/__tests__/responsive-table.fixture.svelte +0 -29
  26. package/dist/components/data/__tests__/responsive-table.fixture.svelte.d.ts +0 -7
  27. package/dist/components/data/__tests__/responsive-table.fixture.svelte.d.ts.map +0 -1
  28. package/dist/components/data/__tests__/surface-navigation.test.js +0 -121
  29. package/dist/components/display/__tests__/ConfidenceBadge.test.js +0 -109
  30. package/dist/components/display/__tests__/CurrencyDisplay.test.js +0 -304
  31. package/dist/components/display/__tests__/CurrencyDisplayI18nHarness.svelte +0 -20
  32. package/dist/components/display/__tests__/CurrencyDisplayI18nHarness.svelte.d.ts +0 -19
  33. package/dist/components/display/__tests__/CurrencyDisplayI18nHarness.svelte.d.ts.map +0 -1
  34. package/dist/components/display/__tests__/CurrencyDisplaySsrHarness.svelte +0 -7
  35. package/dist/components/display/__tests__/CurrencyDisplaySsrHarness.svelte.d.ts +0 -19
  36. package/dist/components/display/__tests__/CurrencyDisplaySsrHarness.svelte.d.ts.map +0 -1
  37. package/dist/components/display/__tests__/DateDisplay.test.js +0 -195
  38. package/dist/components/display/__tests__/Icon.test.js +0 -223
  39. package/dist/components/display/__tests__/StatusBadge.test.js +0 -191
  40. package/dist/components/feedback/__tests__/ConfirmDialog.test.js +0 -181
  41. package/dist/components/feedback/__tests__/LoadingOverlay.test.js +0 -113
  42. package/dist/components/feedback/__tests__/Modal.test.js +0 -72
  43. package/dist/components/feedback/__tests__/PhoneSheet.test.js +0 -101
  44. package/dist/components/feedback/__tests__/ProgressBar.test.js +0 -89
  45. package/dist/components/feedback/__tests__/WorkingStrip.test.js +0 -172
  46. package/dist/components/feedback/__tests__/confirm-message.fixture.svelte +0 -9
  47. package/dist/components/feedback/__tests__/confirm-message.fixture.svelte.d.ts +0 -19
  48. package/dist/components/feedback/__tests__/confirm-message.fixture.svelte.d.ts.map +0 -1
  49. package/dist/components/feedback/__tests__/toast.test.js +0 -190
  50. package/dist/components/forms/__tests__/CameraCapture.test.js +0 -504
  51. package/dist/components/forms/__tests__/Combobox.test.js +0 -354
  52. package/dist/components/forms/__tests__/ControlHitArea.test.js +0 -73
  53. package/dist/components/forms/__tests__/DictationButton.test.js +0 -176
  54. package/dist/components/forms/__tests__/Fieldset.test.js +0 -37
  55. package/dist/components/forms/__tests__/Form.test.js +0 -201
  56. package/dist/components/forms/__tests__/FormActionBar.test.js +0 -22
  57. package/dist/components/forms/__tests__/FormGroup.test.js +0 -48
  58. package/dist/components/forms/__tests__/Input.test.js +0 -49
  59. package/dist/components/forms/__tests__/Listbox.test.js +0 -303
  60. package/dist/components/forms/__tests__/MultiSelect.test.js +0 -281
  61. package/dist/components/forms/__tests__/SearchInput.test.js +0 -92
  62. package/dist/components/forms/__tests__/Select.test.js +0 -37
  63. package/dist/components/forms/__tests__/SignaturePad.test.js +0 -338
  64. package/dist/components/forms/__tests__/StagedControlReview.test.js +0 -702
  65. package/dist/components/forms/__tests__/Textarea.test.js +0 -39
  66. package/dist/components/forms/__tests__/Toggle.test.js +0 -87
  67. package/dist/components/forms/__tests__/camera-capture-session.test.js +0 -207
  68. package/dist/components/forms/__tests__/capture-form-retry.test.js +0 -166
  69. package/dist/components/forms/__tests__/capture-form.fixture.svelte +0 -38
  70. package/dist/components/forms/__tests__/capture-form.fixture.svelte.d.ts +0 -10
  71. package/dist/components/forms/__tests__/capture-form.fixture.svelte.d.ts.map +0 -1
  72. package/dist/components/forms/__tests__/capture-test-env.d.ts +0 -70
  73. package/dist/components/forms/__tests__/capture-test-env.d.ts.map +0 -1
  74. package/dist/components/forms/__tests__/capture-test-env.js +0 -122
  75. package/dist/components/forms/__tests__/capture-touch.fixture.svelte +0 -15
  76. package/dist/components/forms/__tests__/capture-touch.fixture.svelte.d.ts +0 -8
  77. package/dist/components/forms/__tests__/capture-touch.fixture.svelte.d.ts.map +0 -1
  78. package/dist/components/forms/__tests__/combobox-form.fixture.svelte +0 -29
  79. package/dist/components/forms/__tests__/combobox-form.fixture.svelte.d.ts +0 -14
  80. package/dist/components/forms/__tests__/combobox-form.fixture.svelte.d.ts.map +0 -1
  81. package/dist/components/forms/__tests__/combobox-labels.fixture.svelte +0 -19
  82. package/dist/components/forms/__tests__/combobox-labels.fixture.svelte.d.ts +0 -11
  83. package/dist/components/forms/__tests__/combobox-labels.fixture.svelte.d.ts.map +0 -1
  84. package/dist/components/forms/__tests__/composite-controls.fixture.svelte +0 -47
  85. package/dist/components/forms/__tests__/composite-controls.fixture.svelte.d.ts +0 -12
  86. package/dist/components/forms/__tests__/composite-controls.fixture.svelte.d.ts.map +0 -1
  87. package/dist/components/forms/__tests__/composite-controls.test.js +0 -128
  88. package/dist/components/forms/__tests__/composite-registration.fixture.svelte +0 -29
  89. package/dist/components/forms/__tests__/composite-registration.fixture.svelte.d.ts +0 -8
  90. package/dist/components/forms/__tests__/composite-registration.fixture.svelte.d.ts.map +0 -1
  91. package/dist/components/forms/__tests__/composite-user-edit.fixture.svelte +0 -41
  92. package/dist/components/forms/__tests__/composite-user-edit.fixture.svelte.d.ts +0 -13
  93. package/dist/components/forms/__tests__/composite-user-edit.fixture.svelte.d.ts.map +0 -1
  94. package/dist/components/forms/__tests__/control-interaction.test.js +0 -3421
  95. package/dist/components/forms/__tests__/control-proposals.test.js +0 -63
  96. package/dist/components/forms/__tests__/core-controls.fixture.svelte +0 -50
  97. package/dist/components/forms/__tests__/core-controls.fixture.svelte.d.ts +0 -9
  98. package/dist/components/forms/__tests__/core-controls.fixture.svelte.d.ts.map +0 -1
  99. package/dist/components/forms/__tests__/core-controls.test.js +0 -311
  100. package/dist/components/forms/__tests__/decimal-sliders.fixture.svelte +0 -25
  101. package/dist/components/forms/__tests__/decimal-sliders.fixture.svelte.d.ts +0 -8
  102. package/dist/components/forms/__tests__/decimal-sliders.fixture.svelte.d.ts.map +0 -1
  103. package/dist/components/forms/__tests__/dictation-recording.test.js +0 -582
  104. package/dist/components/forms/__tests__/dictation.test.js +0 -486
  105. package/dist/components/forms/__tests__/form-attachment.fixture.svelte +0 -28
  106. package/dist/components/forms/__tests__/form-attachment.fixture.svelte.d.ts +0 -11
  107. package/dist/components/forms/__tests__/form-attachment.fixture.svelte.d.ts.map +0 -1
  108. package/dist/components/forms/__tests__/form-group-input.fixture.svelte +0 -16
  109. package/dist/components/forms/__tests__/form-group-input.fixture.svelte.d.ts +0 -9
  110. package/dist/components/forms/__tests__/form-group-input.fixture.svelte.d.ts.map +0 -1
  111. package/dist/components/forms/__tests__/form-retry-form.fixture.svelte +0 -31
  112. package/dist/components/forms/__tests__/form-retry-form.fixture.svelte.d.ts +0 -13
  113. package/dist/components/forms/__tests__/form-retry-form.fixture.svelte.d.ts.map +0 -1
  114. package/dist/components/forms/__tests__/form-retry-form.test.js +0 -107
  115. package/dist/components/forms/__tests__/hidden-controls-form-retry.test.js +0 -69
  116. package/dist/components/forms/__tests__/hidden-controls-form.fixture.svelte +0 -29
  117. package/dist/components/forms/__tests__/hidden-controls-form.fixture.svelte.d.ts +0 -9
  118. package/dist/components/forms/__tests__/hidden-controls-form.fixture.svelte.d.ts.map +0 -1
  119. package/dist/components/forms/__tests__/listbox-form.fixture.svelte +0 -27
  120. package/dist/components/forms/__tests__/listbox-form.fixture.svelte.d.ts +0 -13
  121. package/dist/components/forms/__tests__/listbox-form.fixture.svelte.d.ts.map +0 -1
  122. package/dist/components/forms/__tests__/long-press.test.js +0 -175
  123. package/dist/components/forms/__tests__/multiselect-form.fixture.svelte +0 -27
  124. package/dist/components/forms/__tests__/multiselect-form.fixture.svelte.d.ts +0 -13
  125. package/dist/components/forms/__tests__/multiselect-form.fixture.svelte.d.ts.map +0 -1
  126. package/dist/components/forms/__tests__/native-file-field.test.js +0 -177
  127. package/dist/components/forms/__tests__/place-picker.fixture.svelte +0 -33
  128. package/dist/components/forms/__tests__/place-picker.fixture.svelte.d.ts +0 -12
  129. package/dist/components/forms/__tests__/place-picker.fixture.svelte.d.ts.map +0 -1
  130. package/dist/components/forms/__tests__/segmented-posting.fixture.svelte +0 -28
  131. package/dist/components/forms/__tests__/segmented-posting.fixture.svelte.d.ts +0 -12
  132. package/dist/components/forms/__tests__/segmented-posting.fixture.svelte.d.ts.map +0 -1
  133. package/dist/components/forms/__tests__/segmented-posting.test.js +0 -92
  134. package/dist/components/forms/__tests__/select-interaction.fixture.svelte +0 -25
  135. package/dist/components/forms/__tests__/select-interaction.fixture.svelte.d.ts +0 -9
  136. package/dist/components/forms/__tests__/select-interaction.fixture.svelte.d.ts.map +0 -1
  137. package/dist/components/forms/__tests__/signature-pad-logic.test.js +0 -54
  138. package/dist/components/forms/__tests__/staged-review-fieldset.fixture.svelte +0 -21
  139. package/dist/components/forms/__tests__/staged-review-fieldset.fixture.svelte.d.ts +0 -8
  140. package/dist/components/forms/__tests__/staged-review-fieldset.fixture.svelte.d.ts.map +0 -1
  141. package/dist/components/forms/__tests__/staged-review-outer-fieldset.fixture.svelte +0 -23
  142. package/dist/components/forms/__tests__/staged-review-outer-fieldset.fixture.svelte.d.ts +0 -8
  143. package/dist/components/forms/__tests__/staged-review-outer-fieldset.fixture.svelte.d.ts.map +0 -1
  144. package/dist/components/forms/__tests__/staged-review.fixture.svelte +0 -56
  145. package/dist/components/forms/__tests__/staged-review.fixture.svelte.d.ts +0 -12
  146. package/dist/components/forms/__tests__/staged-review.fixture.svelte.d.ts.map +0 -1
  147. package/dist/components/forms/__tests__/touch-density.fixture.svelte +0 -34
  148. package/dist/components/forms/__tests__/touch-density.fixture.svelte.d.ts +0 -8
  149. package/dist/components/forms/__tests__/touch-density.fixture.svelte.d.ts.map +0 -1
  150. package/dist/components/forms/__tests__/touch-density.test.js +0 -66
  151. package/dist/components/forms/form-retry/__tests__/boundaries.test.js +0 -23
  152. package/dist/components/forms/form-retry/__tests__/content-digest.test.js +0 -70
  153. package/dist/components/forms/form-retry/__tests__/controller.test.js +0 -802
  154. package/dist/components/forms/form-retry/__tests__/fake-kit.d.ts +0 -82
  155. package/dist/components/forms/form-retry/__tests__/fake-kit.d.ts.map +0 -1
  156. package/dist/components/forms/form-retry/__tests__/fake-kit.js +0 -180
  157. package/dist/components/forms/form-retry/__tests__/form-draft.test.js +0 -169
  158. package/dist/components/forms/form-retry/__tests__/ssr.test.js +0 -37
  159. package/dist/components/forms/form-retry/__tests__/storage.d.ts +0 -26
  160. package/dist/components/forms/form-retry/__tests__/storage.d.ts.map +0 -1
  161. package/dist/components/forms/form-retry/__tests__/storage.js +0 -63
  162. package/dist/components/forms/form-retry/__tests__/submission-key.test.js +0 -236
  163. package/dist/components/forms/form-retry/__tests__/submitted-draft.test.js +0 -334
  164. package/dist/components/layout/__tests__/Container.test.js +0 -62
  165. package/dist/components/layout/__tests__/EmptyState.test.js +0 -83
  166. package/dist/components/layout/__tests__/Footer.test.js +0 -50
  167. package/dist/components/layout/__tests__/Grid.test.js +0 -121
  168. package/dist/components/layout/__tests__/Header.test.js +0 -48
  169. package/dist/components/layout/__tests__/Masthead.test.js +0 -93
  170. package/dist/components/layout/__tests__/PageHeader.test.js +0 -166
  171. package/dist/components/layout/__tests__/SummaryCard.test.js +0 -104
  172. package/dist/components/nav/__tests__/FilterChips.test.js +0 -94
  173. package/dist/components/nav/__tests__/LinkTabs.test.js +0 -117
  174. package/dist/components/nav/__tests__/Tabs.test.js +0 -128
  175. package/dist/components/roles/__tests__/RoleSelector.test.js +0 -134
  176. package/dist/components/ui/__tests__/Badge.test.js +0 -76
  177. package/dist/components/ui/__tests__/Button.test.js +0 -166
  178. package/dist/components/ui/__tests__/Card.test.js +0 -103
  179. package/dist/components/ui/__tests__/IconToggle.test.js +0 -59
  180. package/dist/components/ui/__tests__/IconToggleGroup.test.js +0 -30
  181. package/dist/components/ui/__tests__/Pagination.test.js +0 -113
  182. package/dist/components/ui/__tests__/Popover.test.js +0 -44
  183. package/dist/components/ui/__tests__/dropdown-popover-icon.test.js +0 -125
  184. package/dist/components/ui/__tests__/gap-primitives-interactive.test.js +0 -112
  185. package/dist/components/ui/__tests__/gap-primitives.test.js +0 -84
  186. package/dist/components/ui/__tests__/interaction-primitives.fixture.svelte +0 -13
  187. package/dist/components/ui/__tests__/interaction-primitives.fixture.svelte.d.ts +0 -19
  188. package/dist/components/ui/__tests__/interaction-primitives.fixture.svelte.d.ts.map +0 -1
  189. package/dist/components/ui/__tests__/interaction-primitives.test.js +0 -31
  190. package/dist/components/ui/__tests__/phone-touch-targets.test.js +0 -39
  191. package/dist/i18n/__tests__/i18n.test.js +0 -74
  192. package/dist/i18n/__tests__/render-parity.spec.js +0 -37
  193. package/dist/svelte/__tests__/playground.test.js +0 -20
  194. package/dist/themes/__tests__/control-boundary-contrast.test.js +0 -112
  195. package/dist/themes/__tests__/create-theme.test.js +0 -79
  196. package/dist/themes/__tests__/css-generator.test.js +0 -129
  197. package/dist/themes/__tests__/happyvertical-theme.test.js +0 -231
  198. package/dist/themes/__tests__/registry.test.js +0 -43
  199. package/dist/themes/__tests__/styles-lockstep.test.js +0 -32
  200. package/dist/themes/__tests__/theme-provider.test.js +0 -147
  201. package/dist/themes/__tests__/theme-script.test.js +0 -130
  202. package/dist/themes/__tests__/token-aliases.test.js +0 -178
  203. package/dist/utils/forms/__tests__/formatters.test.js +0 -27
  204. package/dist/utils/theme/__tests__/color.test.js +0 -89
  205. package/dist/utils/theme/__tests__/typography.test.js +0 -11
@@ -1,807 +0,0 @@
1
- /**
2
- * Golden test for DataTable (Sweep L4, #1423).
3
- *
4
- * Covers the semantic table structure (caption → accessible name, column
5
- * headers, cells, row count), the empty state, sortable-header interaction
6
- * (aria-sort transition), and axe-cleanliness.
7
- */
8
- import { render, screen, within } from '@testing-library/svelte';
9
- import userEvent from '@testing-library/user-event';
10
- import { createRawSnippet } from 'svelte';
11
- import { describe, expect, it, vi } from 'vitest';
12
- import { expectNoA11yViolations } from '../../../test-support/a11y';
13
- import DataTable from '../DataTable.svelte';
14
- import { createDataTableController, transitionDataTableState, } from '../DataTableController.js';
15
- const columns = [
16
- { id: 'name', label: 'Name', accessor: 'name', sortable: true },
17
- { id: 'age', label: 'Age', accessor: 'age' },
18
- ];
19
- const data = [
20
- { id: 'ada', name: 'Ada', age: 36 },
21
- { id: 'linus', name: 'Linus', age: 54 },
22
- ];
23
- function setHorizontalOverflow(container, clientWidth, scrollWidth) {
24
- Object.defineProperties(container, {
25
- clientWidth: { configurable: true, value: clientWidth },
26
- scrollWidth: { configurable: true, value: scrollWidth },
27
- scrollLeft: { configurable: true, value: 0, writable: true },
28
- });
29
- window.dispatchEvent(new Event('resize'));
30
- }
31
- describe('DataTable', () => {
32
- it('renders caption, headers, cells, and a row per datum', () => {
33
- render(DataTable, { props: { data, columns, caption: 'People' } });
34
- expect(screen.getByRole('table', { name: 'People' })).toBeInTheDocument();
35
- expect(screen.getByRole('columnheader', { name: 'Name' })).toBeInTheDocument();
36
- expect(screen.getByRole('cell', { name: 'Ada' })).toBeInTheDocument();
37
- // header row + one row per datum
38
- expect(screen.getAllByRole('row')).toHaveLength(data.length + 1);
39
- });
40
- it('renders the empty state when there is no data', () => {
41
- render(DataTable, { props: { data: [], columns } });
42
- expect(screen.getByText('No data available')).toBeInTheDocument();
43
- });
44
- it('uses action-oriented labels and complete aria-sort transitions for sortable headers', async () => {
45
- render(DataTable, { props: { data, columns, sortable: true } });
46
- const nameHeader = screen.getByRole('columnheader', { name: 'Name' });
47
- // aria-sort is only present on the actively-sorted column.
48
- expect(nameHeader).not.toHaveAttribute('aria-sort');
49
- await userEvent.click(screen.getByRole('button', { name: 'Sort Name ascending' }));
50
- expect(nameHeader).toHaveAttribute('aria-sort', 'ascending');
51
- await userEvent.click(screen.getByRole('button', { name: 'Sort Name descending' }));
52
- expect(nameHeader).toHaveAttribute('aria-sort', 'descending');
53
- await userEvent.click(screen.getByRole('button', { name: 'Clear sorting for Name' }));
54
- expect(nameHeader).not.toHaveAttribute('aria-sort');
55
- });
56
- it('starts a date/count column descending and toggles without clearing when not clearable', async () => {
57
- render(DataTable, {
58
- props: {
59
- data,
60
- columns: [
61
- columns[0],
62
- {
63
- id: 'age',
64
- label: 'Age',
65
- accessor: 'age',
66
- sortable: true,
67
- sortFirstDirection: 'desc',
68
- },
69
- ],
70
- sortable: true,
71
- sortClearable: false,
72
- },
73
- });
74
- const ageHeader = screen.getByRole('columnheader', { name: 'Age' });
75
- await userEvent.click(screen.getByRole('button', { name: 'Sort Age descending' }));
76
- expect(ageHeader).toHaveAttribute('aria-sort', 'descending');
77
- const firstCell = () => screen.getAllByRole('row')[1]?.textContent ?? '';
78
- expect(firstCell()).toContain('Linus');
79
- await userEvent.click(screen.getByRole('button', { name: 'Sort Age ascending' }));
80
- expect(ageHeader).toHaveAttribute('aria-sort', 'ascending');
81
- expect(firstCell()).toContain('Ada');
82
- await userEvent.click(screen.getByRole('button', { name: 'Sort Age descending' }));
83
- expect(ageHeader).toHaveAttribute('aria-sort', 'descending');
84
- });
85
- it('announces the next action for each rule in a multi-column sort', async () => {
86
- const multiSortColumns = [columns[0], { ...columns[1], sortable: true }];
87
- render(DataTable, {
88
- props: { data, columns: multiSortColumns, sortable: true },
89
- });
90
- await userEvent.click(screen.getByRole('button', { name: 'Sort Name ascending' }));
91
- await userEvent.click(screen.getByRole('button', { name: 'Sort Age ascending' }), { shiftKey: true });
92
- expect(screen.getByRole('button', { name: 'Sort Age descending' })).toBeInTheDocument();
93
- });
94
- it('keeps custom sortable headers interactive unless they explicitly opt out', async () => {
95
- const customHeader = createRawSnippet(() => ({
96
- render: () => '<span>Custom name</span>',
97
- }));
98
- const customColumns = [{ ...columns[0], header: customHeader }, columns[1]];
99
- render(DataTable, {
100
- props: { data, columns: customColumns, sortable: true },
101
- });
102
- await userEvent.click(screen.getByRole('button', { name: 'Sort Name ascending' }));
103
- expect(screen.getByText('Custom name')).toBeInTheDocument();
104
- expect(screen.getByRole('columnheader', { name: 'Custom name' })).toHaveAttribute('aria-sort', 'ascending');
105
- const { container } = render(DataTable, {
106
- props: {
107
- data,
108
- columns: [
109
- { ...columns[0], header: customHeader, headerSortMode: 'manual' },
110
- columns[1],
111
- ],
112
- sortable: true,
113
- },
114
- });
115
- expect(within(container).queryByRole('button', { name: 'Sort Name ascending' })).not.toBeInTheDocument();
116
- });
117
- it('keeps automatic sort controls separate from interactive custom headers', async () => {
118
- const customHeader = createRawSnippet(() => ({
119
- render: () => '<button type="button">Column actions</button>',
120
- }));
121
- const customColumns = [{ ...columns[0], header: customHeader }, columns[1]];
122
- render(DataTable, {
123
- props: { data, columns: customColumns, sortable: true },
124
- });
125
- const customAction = screen.getByRole('button', { name: 'Column actions' });
126
- const sortAction = screen.getByRole('button', {
127
- name: 'Sort Name ascending',
128
- });
129
- expect(sortAction).not.toContainElement(customAction);
130
- await userEvent.click(customAction);
131
- expect(screen.getAllByRole('columnheader')[0]).not.toHaveAttribute('aria-sort');
132
- await userEvent.click(sortAction);
133
- expect(screen.getAllByRole('columnheader')[0]).toHaveAttribute('aria-sort', 'ascending');
134
- });
135
- it('routes a human sort click through the same controller transition as a command', async () => {
136
- const controller = createDataTableController({
137
- columnIds: columns.map((column) => column.id),
138
- });
139
- const before = controller.getState();
140
- render(DataTable, { props: { data, columns, sortable: true, controller } });
141
- await userEvent.click(screen.getByRole('button', { name: 'Sort Name ascending' }));
142
- expect(controller.getState()).toEqual(transitionDataTableState(before, {
143
- type: 'toggleSorting',
144
- columnId: 'name',
145
- multi: false,
146
- }));
147
- });
148
- it('waits for a controlled controller host before rendering a proposed interaction', async () => {
149
- const initialState = createDataTableController({
150
- columnIds: columns.map((column) => column.id),
151
- }).getState();
152
- const onStateChange = vi.fn();
153
- const controller = createDataTableController({
154
- state: initialState,
155
- columnIds: columns.map((column) => column.id),
156
- onStateChange,
157
- });
158
- render(DataTable, { props: { data, columns, sortable: true, controller } });
159
- const nameHeader = screen.getByRole('columnheader', { name: 'Name' });
160
- await userEvent.click(screen.getByRole('button', { name: 'Sort Name ascending' }));
161
- expect(nameHeader).not.toHaveAttribute('aria-sort');
162
- controller.replaceState(onStateChange.mock.calls[0][0]);
163
- await vi.waitFor(() => expect(nameHeader).toHaveAttribute('aria-sort', 'ascending'));
164
- });
165
- it('filters and paginates client-side rows', async () => {
166
- render(DataTable, {
167
- props: {
168
- data: [...data, { id: 'grace', name: 'Grace', age: 85 }],
169
- columns,
170
- filterFn: (person) => person.age > 40,
171
- pageSize: 1,
172
- },
173
- });
174
- expect(screen.getByRole('cell', { name: 'Linus' })).toBeInTheDocument();
175
- expect(screen.queryByRole('cell', { name: 'Grace' })).not.toBeInTheDocument();
176
- await userEvent.click(screen.getByRole('button', { name: 'Next page' }));
177
- expect(screen.getByRole('cell', { name: 'Grace' })).toBeInTheDocument();
178
- });
179
- it('ignores local filters targeting a non-filterable column', () => {
180
- const controller = createDataTableController({
181
- columnIds: columns.map((column) => column.id),
182
- initialState: {
183
- filters: [{ columnId: 'age', operator: 'gte', value: 40 }],
184
- },
185
- });
186
- render(DataTable, {
187
- props: {
188
- data,
189
- columns: [columns[0], { ...columns[1], filterable: false }],
190
- controller,
191
- },
192
- });
193
- expect(screen.getByRole('cell', { name: 'Ada' })).toBeInTheDocument();
194
- expect(screen.getByRole('cell', { name: 'Linus' })).toBeInTheDocument();
195
- });
196
- it('clamps a controller page changed after mount against the current total', async () => {
197
- const controller = createDataTableController({
198
- columnIds: columns.map((column) => column.id),
199
- initialState: { page: 1, pageSize: 1 },
200
- });
201
- render(DataTable, { props: { data, columns, controller } });
202
- controller.replaceState({ ...controller.getState(), page: 4 });
203
- await vi.waitFor(() => expect(controller.getState().page).toBe(2));
204
- expect(screen.getByRole('cell', { name: 'Linus' })).toBeInTheDocument();
205
- });
206
- it.each([
207
- [
208
- 'local/local/local',
209
- { filtering: 'local', sorting: 'local', pagination: 'local' },
210
- ['Grace'],
211
- ],
212
- [
213
- 'local/local/manual',
214
- { filtering: 'local', sorting: 'local', pagination: 'manual' },
215
- ['Grace', 'Linus'],
216
- ],
217
- [
218
- 'local/manual/local',
219
- { filtering: 'local', sorting: 'manual', pagination: 'local' },
220
- ['Linus'],
221
- ],
222
- [
223
- 'local/manual/manual',
224
- { filtering: 'local', sorting: 'manual', pagination: 'manual' },
225
- ['Linus', 'Grace'],
226
- ],
227
- [
228
- 'manual/local/local',
229
- { filtering: 'manual', sorting: 'local', pagination: 'local' },
230
- ['Grace'],
231
- ],
232
- [
233
- 'manual/local/manual',
234
- { filtering: 'manual', sorting: 'local', pagination: 'manual' },
235
- ['Grace', 'Linus', 'Ada'],
236
- ],
237
- [
238
- 'manual/manual/local',
239
- { filtering: 'manual', sorting: 'manual', pagination: 'local' },
240
- ['Ada'],
241
- ],
242
- [
243
- 'manual/manual/manual',
244
- { filtering: 'manual', sorting: 'manual', pagination: 'manual' },
245
- ['Ada', 'Linus', 'Grace'],
246
- ],
247
- ])('applies each local/manual mode combination exactly once (%s)', (_name, modes, expected) => {
248
- const controller = createDataTableController({
249
- modes,
250
- initialState: {
251
- filters: [{ columnId: 'age', operator: 'gte', value: 40 }],
252
- sorting: [{ columnId: 'age', direction: 'desc' }],
253
- page: 1,
254
- pageSize: 1,
255
- },
256
- columnIds: columns.map((column) => column.id),
257
- });
258
- render(DataTable, {
259
- props: {
260
- data: [...data, { id: 'grace', name: 'Grace', age: 85 }],
261
- columns,
262
- controller,
263
- rowKey: 'id',
264
- },
265
- });
266
- const names = screen
267
- .getAllByRole('row')
268
- .slice(1)
269
- .map((row) => within(row).getAllByRole('cell')[0].textContent);
270
- expect(names).toEqual(expected);
271
- });
272
- it('keeps stable selection IDs across pages and labels the page scope', async () => {
273
- const onSelectionChange = vi.fn();
274
- render(DataTable, {
275
- props: {
276
- data,
277
- columns,
278
- pageSize: 1,
279
- rowKey: 'id',
280
- selectable: true,
281
- onSelectionChange,
282
- },
283
- });
284
- await userEvent.click(screen.getByRole('checkbox', { name: 'Select all rows on this page' }));
285
- await userEvent.click(screen.getByRole('button', { name: 'Next page' }));
286
- await userEvent.click(screen.getByRole('checkbox', { name: 'Select all rows on this page' }));
287
- expect(onSelectionChange).toHaveBeenLastCalledWith(new Set(['ada', 'linus']));
288
- });
289
- it('honors legacy selected bindings during controller reconciliation', async () => {
290
- const props = {
291
- data,
292
- columns,
293
- rowKey: 'id',
294
- selectable: true,
295
- selected: new Set(['ada']),
296
- };
297
- const { rerender } = render(DataTable, { props });
298
- const expectSelected = (name) => {
299
- expect(screen.getByRole('cell', { name }).closest('tr')).toHaveClass('data-table__row--selected');
300
- };
301
- await vi.waitFor(() => expectSelected('Ada'));
302
- await rerender({ ...props, selected: new Set(['linus']) });
303
- await vi.waitFor(() => {
304
- expectSelected('Linus');
305
- expect(screen.getByRole('cell', { name: 'Ada' }).closest('tr')).not.toHaveClass('data-table__row--selected');
306
- });
307
- });
308
- it('keeps stable selection and expansion through sort, filter, and data refresh', async () => {
309
- const controller = createDataTableController({
310
- columnIds: columns.map((column) => column.id),
311
- });
312
- const expandedContent = createRawSnippet(() => ({
313
- render: () => '<p>Row detail</p>',
314
- }));
315
- const props = {
316
- data,
317
- columns,
318
- rowKey: 'id',
319
- selectable: true,
320
- expandedContent,
321
- controller,
322
- };
323
- const { rerender } = render(DataTable, { props });
324
- const expectAdaSelectionAndExpansion = () => {
325
- const adaRow = screen.getByRole('cell', { name: 'Ada' }).closest('tr');
326
- expect(adaRow).toHaveClass('data-table__row--selected');
327
- expect(adaRow?.nextElementSibling).toHaveTextContent('Row detail');
328
- };
329
- const visibleNames = () => screen
330
- .getAllByRole('row')
331
- .slice(1)
332
- .filter((row) => !row.classList.contains('data-table__row--expanded'))
333
- .map((row) => within(row)
334
- .getAllByRole('cell')
335
- .find((cell) => ['Ada', 'Linus'].includes(cell.textContent ?? ''))
336
- ?.textContent)
337
- .filter((name) => name !== undefined);
338
- await userEvent.click(screen.getByRole('checkbox', { name: 'Select row 1' }));
339
- await userEvent.click(screen.getByRole('button', { name: 'Expand row 1' }));
340
- expectAdaSelectionAndExpansion();
341
- controller.dispatch({
342
- type: 'setSorting',
343
- sorting: [{ columnId: 'age', direction: 'desc' }],
344
- });
345
- await vi.waitFor(() => {
346
- expect(visibleNames()).toEqual(['Linus', 'Ada']);
347
- expectAdaSelectionAndExpansion();
348
- });
349
- controller.dispatch({ type: 'setSearch', search: 'Ada' });
350
- await vi.waitFor(() => {
351
- expect(screen.queryByRole('cell', { name: 'Linus' })).not.toBeInTheDocument();
352
- expectAdaSelectionAndExpansion();
353
- });
354
- controller.dispatch({ type: 'setSearch', search: '' });
355
- await rerender({
356
- ...props,
357
- data: [
358
- { id: 'linus', name: 'Linus', age: 10 },
359
- { id: 'ada', name: 'Ada', age: 90 },
360
- ],
361
- });
362
- await vi.waitFor(() => {
363
- expect(visibleNames()).toEqual(['Ada', 'Linus']);
364
- expectAdaSelectionAndExpansion();
365
- });
366
- });
367
- it('fails closed when durable selection, expansion, manual, or agent modes lack a rowKey', () => {
368
- expect(() => render(DataTable, { props: { data, columns, selectable: true } })).toThrow(/rowKey is required/);
369
- expect(() => render(DataTable, {
370
- props: {
371
- data,
372
- columns,
373
- expandedContent: createRawSnippet(() => ({ render: () => '' })),
374
- },
375
- })).toThrow(/rowKey is required/);
376
- expect(() => render(DataTable, {
377
- props: { data, columns, agentAddressable: true },
378
- })).toThrow(/rowKey is required/);
379
- expect(() => render(DataTable, {
380
- props: { data, columns, modes: { pagination: 'manual' } },
381
- })).toThrow(/rowKey is required/);
382
- });
383
- it('links localized expansion controls to their row detail', async () => {
384
- render(DataTable, {
385
- props: {
386
- data,
387
- columns,
388
- rowKey: 'id',
389
- expandedContent: createRawSnippet(() => ({
390
- render: () => '<p>Row detail</p>',
391
- })),
392
- },
393
- });
394
- const expandButton = screen.getByRole('button', { name: 'Expand row 1' });
395
- await userEvent.click(expandButton);
396
- expect(screen.getByText('Row detail')).toBeInTheDocument();
397
- expect(expandButton).toHaveAttribute('aria-expanded', 'true');
398
- const expansionId = expandButton.getAttribute('aria-controls');
399
- expect(expansionId).toBeTruthy();
400
- expect(document.getElementById(expansionId ?? '')).toHaveTextContent('Row detail');
401
- expect(screen.getByRole('button', { name: 'Collapse row 1' })).toBe(expandButton);
402
- });
403
- it('keeps clickable rows semantic and ignores nested row actions', async () => {
404
- const onRowClick = vi.fn();
405
- const actionCell = createRawSnippet(() => ({
406
- render: () => '<button type="button">Archive row</button>',
407
- }));
408
- const expandedContent = createRawSnippet(() => ({
409
- render: () => '<p>Row detail</p>',
410
- }));
411
- render(DataTable, {
412
- props: {
413
- data,
414
- columns: [
415
- ...columns,
416
- { id: 'actions', label: 'Actions', cell: actionCell },
417
- ],
418
- rowKey: 'id',
419
- selectable: true,
420
- expandedContent,
421
- onRowClick,
422
- rowLabel: (person) => person.name,
423
- },
424
- });
425
- const adaRow = screen.getByRole('cell', { name: 'Ada' }).closest('tr');
426
- expect(adaRow).not.toHaveAttribute('role');
427
- expect(adaRow).toHaveAttribute('tabindex', '0');
428
- await userEvent.click(screen.getByRole('checkbox', { name: 'Select Ada' }));
429
- await userEvent.click(screen.getByRole('button', { name: 'Expand Ada' }));
430
- await userEvent.click(screen.getAllByRole('button', { name: 'Archive row' })[0]);
431
- expect(onRowClick).not.toHaveBeenCalled();
432
- await userEvent.click(screen.getByRole('cell', { name: 'Ada' }));
433
- expect(onRowClick).toHaveBeenLastCalledWith(data[0], 0);
434
- adaRow?.focus();
435
- await userEvent.keyboard('{Enter}');
436
- expect(onRowClick).toHaveBeenLastCalledWith(data[0], 0);
437
- });
438
- it('announces async states without replacing usable stale rows', async () => {
439
- const onRetry = vi.fn();
440
- const props = {
441
- data,
442
- columns,
443
- rowKey: 'id',
444
- selectable: true,
445
- loading: true,
446
- stale: true,
447
- partialResults: true,
448
- onRetry,
449
- };
450
- const { rerender } = render(DataTable, { props });
451
- expect(screen.getByRole('table')).toHaveAttribute('aria-busy', 'true');
452
- expect(screen.getByRole('cell', { name: 'Ada' })).toBeInTheDocument();
453
- expect(screen.getByRole('status')).toHaveTextContent('Refreshing table data');
454
- expect(screen.getByRole('status')).toHaveTextContent('Showing stale results');
455
- expect(screen.getByRole('status')).toHaveTextContent('Showing partial results');
456
- await userEvent.click(screen.getByRole('checkbox', { name: 'Select row 1' }));
457
- await rerender({ ...props, error: 'Request failed' });
458
- expect(screen.getByRole('cell', { name: 'Ada' })).toBeInTheDocument();
459
- expect(screen.getByRole('table')).toHaveAttribute('aria-busy', 'false');
460
- expect(screen.getByRole('alert')).toHaveTextContent('Request failed');
461
- await userEvent.click(screen.getByRole('button', { name: 'Retry' }));
462
- expect(onRetry).toHaveBeenCalledTimes(1);
463
- await rerender({
464
- ...props,
465
- data: [],
466
- stale: false,
467
- partialResults: false,
468
- error: null,
469
- });
470
- expect(screen.getByRole('status')).toHaveTextContent('Loading table data');
471
- expect(screen.queryByText('No data available')).not.toBeInTheDocument();
472
- await rerender({
473
- ...props,
474
- data: [],
475
- loading: false,
476
- error: 'Request failed',
477
- });
478
- expect(screen.getByRole('alert')).toHaveTextContent('Request failed');
479
- await rerender({ ...props, data: [], loading: false, error: '' });
480
- expect(screen.getByRole('alert')).toHaveTextContent('Unable to load table data');
481
- });
482
- it('rejects duplicate stable row keys and local totals that imply server paging', () => {
483
- expect(() => render(DataTable, {
484
- props: {
485
- data: [data[0], { ...data[1], id: 'ada' }],
486
- columns,
487
- rowKey: 'id',
488
- },
489
- })).toThrow(/unique row ids/);
490
- expect(() => render(DataTable, {
491
- props: { data, columns, rowKey: 'id', totalRows: 10 },
492
- })).toThrow(/only valid when pagination mode is manual/);
493
- });
494
- it('uses stable row IDs as the final local sort tie-breaker', () => {
495
- const controller = createDataTableController({
496
- columnIds: columns.map((column) => column.id),
497
- initialState: { sorting: [{ columnId: 'age', direction: 'asc' }] },
498
- });
499
- render(DataTable, {
500
- props: {
501
- data: [
502
- { id: 'z', name: 'Zed', age: 36 },
503
- { id: 'a', name: 'Ada', age: 36 },
504
- ],
505
- columns,
506
- rowKey: 'id',
507
- controller,
508
- },
509
- });
510
- const names = screen
511
- .getAllByRole('row')
512
- .slice(1)
513
- .map((row) => within(row).getAllByRole('cell')[0].textContent);
514
- expect(names).toEqual(['Ada', 'Zed']);
515
- });
516
- it('makes only an overflowing narrow table a named, focusable scroll region', async () => {
517
- const { container } = render(DataTable, {
518
- props: { data, columns, caption: 'People' },
519
- });
520
- const tableContainer = container.querySelector('.data-table-container');
521
- expect(tableContainer).not.toHaveAttribute('role');
522
- expect(tableContainer).not.toHaveAttribute('tabindex');
523
- setHorizontalOverflow(tableContainer, 320, 960);
524
- await vi.waitFor(() => {
525
- expect(screen.getByRole('region', {
526
- name: 'People table, scroll horizontally to view more columns',
527
- })).toBe(tableContainer);
528
- expect(tableContainer).toHaveAttribute('tabindex', '0');
529
- expect(screen.getByText('More columns →')).toBeVisible();
530
- expect(tableContainer).not.toContainElement(screen.getByText('More columns →'));
531
- });
532
- await expectNoA11yViolations(container);
533
- setHorizontalOverflow(tableContainer, 320, 320);
534
- await vi.waitFor(() => {
535
- expect(tableContainer).not.toHaveAttribute('role');
536
- expect(tableContainer).not.toHaveAttribute('tabindex');
537
- expect(screen.queryByText('More columns →')).not.toBeInTheDocument();
538
- });
539
- });
540
- it('scrolls an overflowing narrow table with the keyboard', async () => {
541
- const user = userEvent.setup();
542
- const { container } = render(DataTable, {
543
- props: { data, columns, caption: 'People' },
544
- });
545
- const tableContainer = container.querySelector('.data-table-container');
546
- setHorizontalOverflow(tableContainer, 320, 960);
547
- await vi.waitFor(() => expect(tableContainer).toHaveAttribute('role', 'region'));
548
- tableContainer.focus();
549
- await user.keyboard('{ArrowRight}');
550
- expect(tableContainer.scrollLeft).toBe(256);
551
- expect(screen.getByText('← More columns')).toBeVisible();
552
- await user.keyboard('{End}');
553
- expect(tableContainer.scrollLeft).toBe(640);
554
- await user.keyboard('{Home}');
555
- expect(tableContainer.scrollLeft).toBe(0);
556
- });
557
- it('renders grouped headers from final visible leaf columns without losing leaf sort controls', async () => {
558
- const groupedColumns = [
559
- {
560
- ...columns[0],
561
- headerPath: [{ id: 'identity', label: 'Identity' }],
562
- },
563
- {
564
- ...columns[1],
565
- sortable: true,
566
- headerPath: [{ id: 'measures', label: 'Measures' }],
567
- },
568
- ];
569
- render(DataTable, {
570
- props: { data, columns: groupedColumns, sortable: true },
571
- });
572
- const identity = screen.getByRole('columnheader', { name: 'Identity' });
573
- expect(identity).toHaveAttribute('scope', 'colgroup');
574
- expect(identity).toHaveAttribute('colspan', '1');
575
- expect(screen.getAllByRole('row')).toHaveLength(data.length + 2);
576
- await userEvent.click(screen.getByRole('button', { name: 'Sort Name ascending' }));
577
- expect(screen.getByRole('columnheader', { name: 'Name' })).toHaveAttribute('aria-sort', 'ascending');
578
- });
579
- it('keeps a group header aligned when all of its leaves are pinned', () => {
580
- const controller = createDataTableController({
581
- columnIds: columns.map((column) => column.id),
582
- initialState: {
583
- columnPinning: [
584
- { columnId: 'name', position: 'start' },
585
- { columnId: 'age', position: 'start' },
586
- ],
587
- },
588
- });
589
- render(DataTable, {
590
- props: {
591
- data,
592
- controller,
593
- columns: columns.map((column) => ({
594
- ...column,
595
- headerPath: [{ id: 'detail', label: 'Detail' }],
596
- })),
597
- },
598
- });
599
- const group = screen.getByRole('columnheader', { name: 'Detail' });
600
- expect(group).toHaveClass('data-table__cell--pinned-start');
601
- expect(group).toHaveStyle({ left: '0px' });
602
- });
603
- it('keeps report structural rows semantic and outside data-row selection', () => {
604
- const { container } = render(DataTable, {
605
- props: {
606
- data,
607
- columns,
608
- rowKey: 'id',
609
- selectable: true,
610
- structuralRows: [
611
- {
612
- id: 'subtotal',
613
- kind: 'subtotal',
614
- label: 'Current page total',
615
- values: { age: 90 },
616
- },
617
- {
618
- id: 'footer',
619
- kind: 'footer',
620
- label: 'All people',
621
- values: { age: 90 },
622
- },
623
- ],
624
- },
625
- });
626
- expect(screen.getByRole('rowheader', { name: /Subtotal: Current page total/ })).toBeInTheDocument();
627
- expect(screen.getByRole('rowheader', { name: /Footer: All people/ })).toBeInTheDocument();
628
- expect(screen.getAllByRole('checkbox')).toHaveLength(data.length + 1);
629
- expect(container.querySelectorAll('tbody.data-table__body--structural input')).toHaveLength(0);
630
- expect(container.querySelector('tfoot')).toContainElement(screen.getByRole('rowheader', { name: /Footer: All people/ }));
631
- });
632
- it('uses controlled width and pin state for the accessible header resize control', async () => {
633
- const controller = createDataTableController({
634
- columnIds: columns.map((column) => column.id),
635
- initialState: {
636
- columnWidths: [{ columnId: 'name', width: 120 }],
637
- columnPinning: [{ columnId: 'name', position: 'start' }],
638
- },
639
- });
640
- const { container } = render(DataTable, {
641
- props: {
642
- data,
643
- columns: [{ ...columns[0], resizable: true }, columns[1]],
644
- controller,
645
- },
646
- });
647
- const nameHeader = screen.getByRole('columnheader', { name: 'Name' });
648
- expect(nameHeader).toHaveClass('data-table__cell--pinned-start');
649
- expect(nameHeader).toHaveStyle({ width: '120px', left: '0px' });
650
- expect(nameHeader).toHaveAttribute('data-column-id', 'name');
651
- const separator = screen.getByRole('separator', { name: 'Resize Name' });
652
- separator.focus();
653
- await userEvent.keyboard('{ArrowRight}');
654
- expect(controller.getState().columnWidths).toContainEqual({
655
- columnId: 'name',
656
- width: 128,
657
- });
658
- expect(container.querySelector('[data-column-id="name"]')).toHaveAttribute('data-column-id', 'name');
659
- });
660
- it('uses rem constraints and rendered header widths for resize announcements', async () => {
661
- const getBoundingClientRect = vi
662
- .spyOn(HTMLElement.prototype, 'getBoundingClientRect')
663
- .mockReturnValue({ width: 208 });
664
- const originalRootFontSize = document.documentElement.style.fontSize;
665
- try {
666
- document.documentElement.style.fontSize = '16px';
667
- const controller = createDataTableController({
668
- columnIds: columns.map((column) => column.id),
669
- });
670
- render(DataTable, {
671
- props: {
672
- data,
673
- controller,
674
- columns: [
675
- { ...columns[0], minWidth: '13rem', resizable: true },
676
- columns[1],
677
- ],
678
- },
679
- });
680
- const separator = screen.getByRole('separator', { name: 'Resize Name' });
681
- await vi.waitFor(() => expect(separator).toHaveAttribute('aria-valuenow', '208'));
682
- separator.focus();
683
- await userEvent.keyboard('{Home}');
684
- expect(controller.getState().columnWidths).toContainEqual({
685
- columnId: 'name',
686
- width: 208,
687
- });
688
- }
689
- finally {
690
- getBoundingClientRect.mockRestore();
691
- document.documentElement.style.fontSize = originalRootFontSize;
692
- }
693
- });
694
- it('clamps restored widths to the declared header constraints', () => {
695
- const controller = createDataTableController({
696
- columnIds: columns.map((column) => column.id),
697
- initialState: {
698
- columnWidths: [{ columnId: 'name', width: 200 }],
699
- },
700
- });
701
- render(DataTable, {
702
- props: {
703
- data,
704
- controller,
705
- columns: [
706
- { ...columns[0], maxWidth: '100px', resizable: true },
707
- columns[1],
708
- ],
709
- },
710
- });
711
- expect(screen.getByRole('columnheader', { name: 'Name' })).toHaveStyle({
712
- width: '100px',
713
- });
714
- expect(screen.getByRole('separator', { name: 'Resize Name' })).toHaveAttribute('aria-valuenow', '100');
715
- });
716
- it('keeps custom resizable header content as the header name', () => {
717
- const customHeader = createRawSnippet(() => ({
718
- render: () => '<span>Account total in USD</span>',
719
- }));
720
- render(DataTable, {
721
- props: {
722
- data,
723
- columns: [
724
- { ...columns[0], header: customHeader, resizable: true },
725
- columns[1],
726
- ],
727
- },
728
- });
729
- expect(screen.getByRole('columnheader', { name: /Account total in USD/ })).toBeInTheDocument();
730
- });
731
- it('keeps statically hidden columns hidden when a controller restores them visible', async () => {
732
- const controller = createDataTableController({
733
- columnIds: columns.map((column) => column.id),
734
- initialState: {
735
- columnVisibility: [
736
- { columnId: 'name', visible: true },
737
- { columnId: 'age', visible: true },
738
- ],
739
- },
740
- });
741
- render(DataTable, {
742
- props: {
743
- data,
744
- columns: [columns[0], { ...columns[1], hidden: true }],
745
- controller,
746
- },
747
- });
748
- await vi.waitFor(() => expect(controller.getState().columnVisibility).toContainEqual({
749
- columnId: 'age',
750
- visible: false,
751
- }));
752
- expect(screen.queryByRole('columnheader', { name: 'Age' })).not.toBeInTheDocument();
753
- });
754
- it('is axe-clean', async () => {
755
- const { container } = render(DataTable, {
756
- props: { data, columns, caption: 'People' },
757
- });
758
- await expectNoA11yViolations(container);
759
- });
760
- });
761
- describe('DataTable phoneLayout', () => {
762
- it('keeps the scrolling table unless asked for cards', () => {
763
- const { container } = render(DataTable, {
764
- props: { data, columns, caption: 'People' },
765
- });
766
- expect(container.querySelector('.data-table-container--phone-cards')).toBeNull();
767
- });
768
- it('marks the container for the phone card layout and keeps the table semantics', () => {
769
- const { container } = render(DataTable, {
770
- props: { data, columns, caption: 'People', phoneLayout: 'cards' },
771
- });
772
- expect(container.querySelector('.data-table-container.data-table-container--phone-cards')).not.toBeNull();
773
- expect(screen.getByRole('table', { name: 'People' })).toBeInTheDocument();
774
- expect(screen.getAllByRole('row')).toHaveLength(3);
775
- });
776
- it('gives cards explicit table roles and a visible column label per cell', async () => {
777
- const { container } = render(DataTable, {
778
- props: { data, columns, caption: 'People', phoneLayout: 'cards' },
779
- });
780
- // Explicit roles survive the block/grid restyle (WebKit drops implicit
781
- // table roles once display changes).
782
- expect(container.querySelector('table')).toHaveAttribute('role', 'table');
783
- for (const row of container.querySelectorAll('tr')) {
784
- expect(row).toHaveAttribute('role', 'row');
785
- }
786
- for (const header of container.querySelectorAll('thead th')) {
787
- expect(header).toHaveAttribute('role', 'columnheader');
788
- }
789
- const cells = container.querySelectorAll('tbody td[data-column-id]');
790
- expect(cells.length).toBeGreaterThan(0);
791
- for (const cell of cells) {
792
- expect(cell).toHaveAttribute('role', 'cell');
793
- const column = columns.find((candidate) => candidate.id === cell.getAttribute('data-column-id'));
794
- expect(cell).toHaveAttribute('data-label', column?.label);
795
- }
796
- // The header stays in the accessibility tree (visually hidden, not removed).
797
- expect(screen.getAllByRole('columnheader').length).toBe(columns.length);
798
- await expectNoA11yViolations(container);
799
- });
800
- it('adds no explicit roles or labels outside cards mode', () => {
801
- const { container } = render(DataTable, {
802
- props: { data, columns, caption: 'People' },
803
- });
804
- expect(container.querySelector('table')).not.toHaveAttribute('role');
805
- expect(container.querySelector('[data-label]')).toBeNull();
806
- });
807
- });