@titan-design/react-ui 0.0.2 → 0.1.1

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 (206) hide show
  1. package/dist/{chunk-NA3EES23.mjs → chunk-MGW37MPO.mjs} +26 -4
  2. package/dist/chunk-MGW37MPO.mjs.map +1 -0
  3. package/dist/index.d.mts +115 -2
  4. package/dist/index.d.ts +115 -2
  5. package/dist/index.js +262 -1
  6. package/dist/index.js.map +1 -1
  7. package/dist/index.mjs +225 -3
  8. package/dist/index.mjs.map +1 -1
  9. package/dist/theme/index.d.mts +124 -109
  10. package/dist/theme/index.d.ts +124 -109
  11. package/dist/theme/index.js +24 -1
  12. package/dist/theme/index.js.map +1 -1
  13. package/dist/theme/index.mjs +1 -1
  14. package/package.json +12 -5
  15. package/src/components/custom/DateTime/DateTime.stories.tsx +180 -0
  16. package/src/components/custom/DateTime/DateTime.test.tsx +152 -0
  17. package/src/components/custom/DateTime/DateTime.tsx +235 -0
  18. package/src/components/custom/DateTime/index.ts +2 -0
  19. package/src/components/custom/EmptyState/EmptyState.stories.tsx +99 -0
  20. package/src/components/custom/EmptyState/EmptyState.test.tsx +106 -0
  21. package/src/components/custom/EmptyState/EmptyState.tsx +68 -0
  22. package/src/components/custom/EmptyState/index.ts +2 -0
  23. package/src/components/custom/Metric/Metric.stories.tsx +133 -0
  24. package/src/components/custom/Metric/Metric.test.tsx +120 -0
  25. package/src/components/custom/Metric/Metric.tsx +92 -0
  26. package/src/components/custom/Metric/index.ts +2 -0
  27. package/src/components/custom/Sidebar/Sidebar.stories.tsx +227 -0
  28. package/src/components/custom/Sidebar/Sidebar.test.tsx +185 -0
  29. package/src/components/custom/Sidebar/Sidebar.tsx +257 -0
  30. package/src/components/custom/Sidebar/index.ts +18 -0
  31. package/src/components/custom/Table/Table.stories.tsx +481 -0
  32. package/src/components/custom/Table/Table.test.tsx +531 -0
  33. package/src/components/custom/Table/Table.tsx +696 -0
  34. package/src/components/custom/Table/index.ts +22 -0
  35. package/src/components/custom/Typography/Typography.stories.tsx +153 -0
  36. package/src/components/custom/Typography/Typography.test.tsx +107 -0
  37. package/src/components/custom/Typography/Typography.tsx +218 -0
  38. package/src/components/custom/Typography/index.ts +19 -0
  39. package/src/components/custom/index.ts +8 -0
  40. package/src/components/custom/stepper/Stepper.stories.tsx +245 -0
  41. package/src/components/custom/stepper/Stepper.test.tsx +230 -0
  42. package/src/components/custom/stepper/Stepper.tsx +250 -0
  43. package/src/components/custom/stepper/index.ts +10 -0
  44. package/src/components/index.ts +5 -0
  45. package/src/components/ui/alert/Alert.stories.tsx +154 -0
  46. package/src/components/ui/alert/Alert.test.tsx +202 -0
  47. package/src/components/ui/alert/Alert.tsx +170 -0
  48. package/src/components/ui/alert/index.ts +8 -0
  49. package/src/components/ui/autocomplete/Autocomplete.stories.tsx +299 -0
  50. package/src/components/ui/autocomplete/Autocomplete.test.tsx +247 -0
  51. package/src/components/ui/autocomplete/Autocomplete.tsx +332 -0
  52. package/src/components/ui/autocomplete/index.ts +2 -0
  53. package/src/components/ui/avatar/Avatar.stories.tsx +105 -0
  54. package/src/components/ui/avatar/Avatar.test.tsx +135 -0
  55. package/src/components/ui/avatar/Avatar.tsx +168 -0
  56. package/src/components/ui/avatar/index.ts +2 -0
  57. package/src/components/ui/badge/Badge.stories.tsx +100 -0
  58. package/src/components/ui/badge/Badge.test.tsx +86 -0
  59. package/src/components/ui/badge/Badge.tsx +105 -0
  60. package/src/components/ui/badge/index.ts +2 -0
  61. package/src/components/ui/breadcrumbs/Breadcrumbs.stories.tsx +70 -0
  62. package/src/components/ui/breadcrumbs/Breadcrumbs.test.tsx +156 -0
  63. package/src/components/ui/breadcrumbs/Breadcrumbs.tsx +127 -0
  64. package/src/components/ui/breadcrumbs/index.ts +2 -0
  65. package/src/components/ui/button/Button.stories.tsx +216 -0
  66. package/src/components/ui/button/Button.test.tsx +132 -0
  67. package/src/components/ui/button/Button.tsx +242 -0
  68. package/src/components/ui/button/index.ts +10 -0
  69. package/src/components/ui/card/Card.stories.tsx +443 -0
  70. package/src/components/ui/card/Card.test.tsx +265 -0
  71. package/src/components/ui/card/Card.tsx +380 -0
  72. package/src/components/ui/card/index.ts +20 -0
  73. package/src/components/ui/checkbox/Checkbox.stories.tsx +126 -0
  74. package/src/components/ui/checkbox/Checkbox.test.tsx +152 -0
  75. package/src/components/ui/checkbox/Checkbox.tsx +163 -0
  76. package/src/components/ui/checkbox/index.ts +2 -0
  77. package/src/components/ui/chip/Chip.stories.tsx +112 -0
  78. package/src/components/ui/chip/Chip.test.tsx +119 -0
  79. package/src/components/ui/chip/Chip.tsx +151 -0
  80. package/src/components/ui/chip/index.ts +2 -0
  81. package/src/components/ui/collapse/Collapse.stories.tsx +313 -0
  82. package/src/components/ui/collapse/Collapse.test.tsx +298 -0
  83. package/src/components/ui/collapse/Collapse.tsx +275 -0
  84. package/src/components/ui/collapse/index.ts +18 -0
  85. package/src/components/ui/data-row/DataRow.stories.tsx +78 -0
  86. package/src/components/ui/data-row/DataRow.test.tsx +78 -0
  87. package/src/components/ui/data-row/DataRow.tsx +29 -0
  88. package/src/components/ui/data-row/index.ts +2 -0
  89. package/src/components/ui/divider/Divider.stories.tsx +71 -0
  90. package/src/components/ui/divider/Divider.test.tsx +55 -0
  91. package/src/components/ui/divider/Divider.tsx +37 -0
  92. package/src/components/ui/divider/index.ts +2 -0
  93. package/src/components/ui/drawer/Drawer.stories.tsx +228 -0
  94. package/src/components/ui/drawer/Drawer.test.tsx +142 -0
  95. package/src/components/ui/drawer/Drawer.tsx +228 -0
  96. package/src/components/ui/drawer/index.ts +9 -0
  97. package/src/components/ui/form-field/FormField.stories.tsx +260 -0
  98. package/src/components/ui/form-field/FormField.test.tsx +280 -0
  99. package/src/components/ui/form-field/FormField.tsx +308 -0
  100. package/src/components/ui/form-field/index.ts +2 -0
  101. package/src/components/ui/help-tip/HelpTip.stories.tsx +250 -0
  102. package/src/components/ui/help-tip/HelpTip.test.tsx +198 -0
  103. package/src/components/ui/help-tip/HelpTip.tsx +229 -0
  104. package/src/components/ui/help-tip/index.ts +2 -0
  105. package/src/components/ui/icon-box/IconBox.stories.tsx +139 -0
  106. package/src/components/ui/icon-box/IconBox.test.tsx +120 -0
  107. package/src/components/ui/icon-box/IconBox.tsx +57 -0
  108. package/src/components/ui/icon-box/index.ts +1 -0
  109. package/src/components/ui/index.ts +36 -0
  110. package/src/components/ui/input/Input.stories.tsx +192 -0
  111. package/src/components/ui/input/Input.test.tsx +86 -0
  112. package/src/components/ui/input/Input.tsx +273 -0
  113. package/src/components/ui/input/index.ts +2 -0
  114. package/src/components/ui/link/Link.stories.tsx +78 -0
  115. package/src/components/ui/link/Link.test.tsx +94 -0
  116. package/src/components/ui/link/Link.tsx +86 -0
  117. package/src/components/ui/link/index.ts +2 -0
  118. package/src/components/ui/list-item/ListItem.stories.tsx +146 -0
  119. package/src/components/ui/list-item/ListItem.test.tsx +128 -0
  120. package/src/components/ui/list-item/ListItem.tsx +89 -0
  121. package/src/components/ui/list-item/index.ts +14 -0
  122. package/src/components/ui/menu/Menu.stories.tsx +180 -0
  123. package/src/components/ui/menu/Menu.test.tsx +353 -0
  124. package/src/components/ui/menu/Menu.tsx +222 -0
  125. package/src/components/ui/menu/index.ts +2 -0
  126. package/src/components/ui/modal/Modal.stories.tsx +350 -0
  127. package/src/components/ui/modal/Modal.test.tsx +159 -0
  128. package/src/components/ui/modal/Modal.tsx +254 -0
  129. package/src/components/ui/modal/index.ts +19 -0
  130. package/src/components/ui/popover/Popover.stories.tsx +190 -0
  131. package/src/components/ui/popover/Popover.test.tsx +277 -0
  132. package/src/components/ui/popover/Popover.tsx +162 -0
  133. package/src/components/ui/popover/index.ts +2 -0
  134. package/src/components/ui/progress/Progress.stories.tsx +190 -0
  135. package/src/components/ui/progress/Progress.test.tsx +181 -0
  136. package/src/components/ui/progress/Progress.tsx +320 -0
  137. package/src/components/ui/progress/index.ts +9 -0
  138. package/src/components/ui/radio/Radio.stories.tsx +199 -0
  139. package/src/components/ui/radio/Radio.test.tsx +166 -0
  140. package/src/components/ui/radio/Radio.tsx +200 -0
  141. package/src/components/ui/radio/index.ts +2 -0
  142. package/src/components/ui/section/Section.stories.tsx +111 -0
  143. package/src/components/ui/section/Section.test.tsx +121 -0
  144. package/src/components/ui/section/Section.tsx +52 -0
  145. package/src/components/ui/section/index.ts +6 -0
  146. package/src/components/ui/select/Select.stories.tsx +286 -0
  147. package/src/components/ui/select/Select.test.tsx +180 -0
  148. package/src/components/ui/select/Select.tsx +236 -0
  149. package/src/components/ui/select/index.ts +2 -0
  150. package/src/components/ui/skeleton/Skeleton.stories.tsx +153 -0
  151. package/src/components/ui/skeleton/Skeleton.test.tsx +162 -0
  152. package/src/components/ui/skeleton/Skeleton.tsx +229 -0
  153. package/src/components/ui/skeleton/index.ts +16 -0
  154. package/src/components/ui/spinner/Spinner.stories.tsx +67 -0
  155. package/src/components/ui/spinner/Spinner.test.tsx +67 -0
  156. package/src/components/ui/spinner/Spinner.tsx +72 -0
  157. package/src/components/ui/spinner/index.ts +2 -0
  158. package/src/components/ui/stack/Stack.stories.tsx +127 -0
  159. package/src/components/ui/stack/Stack.test.tsx +184 -0
  160. package/src/components/ui/stack/Stack.tsx +76 -0
  161. package/src/components/ui/stack/index.ts +2 -0
  162. package/src/components/ui/surface/Surface.stories.tsx +91 -0
  163. package/src/components/ui/surface/Surface.test.tsx +63 -0
  164. package/src/components/ui/surface/Surface.tsx +51 -0
  165. package/src/components/ui/surface/index.ts +1 -0
  166. package/src/components/ui/switch/Switch.stories.tsx +111 -0
  167. package/src/components/ui/switch/Switch.test.tsx +112 -0
  168. package/src/components/ui/switch/Switch.tsx +103 -0
  169. package/src/components/ui/switch/index.ts +2 -0
  170. package/src/components/ui/tabs/Tabs.stories.tsx +213 -0
  171. package/src/components/ui/tabs/Tabs.test.tsx +192 -0
  172. package/src/components/ui/tabs/Tabs.tsx +253 -0
  173. package/src/components/ui/tabs/index.ts +16 -0
  174. package/src/components/ui/toast/Toast.stories.tsx +255 -0
  175. package/src/components/ui/toast/Toast.test.tsx +207 -0
  176. package/src/components/ui/toast/Toast.tsx +327 -0
  177. package/src/components/ui/toast/index.ts +8 -0
  178. package/src/components/ui/toolbar-button/ToolbarButton.stories.tsx +415 -0
  179. package/src/components/ui/toolbar-button/ToolbarButton.test.tsx +224 -0
  180. package/src/components/ui/toolbar-button/ToolbarButton.tsx +307 -0
  181. package/src/components/ui/toolbar-button/index.ts +7 -0
  182. package/src/components/ui/tooltip/Tooltip.stories.tsx +126 -0
  183. package/src/components/ui/tooltip/Tooltip.test.tsx +179 -0
  184. package/src/components/ui/tooltip/Tooltip.tsx +143 -0
  185. package/src/components/ui/tooltip/index.ts +2 -0
  186. package/src/index.ts +10 -0
  187. package/src/test/setup.ts +19 -0
  188. package/src/theme/Colors.stories.tsx +258 -0
  189. package/src/theme/config.ts +193 -0
  190. package/src/theme/elevation.stories.tsx +346 -0
  191. package/src/theme/elevation.test.ts +27 -0
  192. package/src/theme/elevation.ts +567 -0
  193. package/src/theme/index.ts +39 -0
  194. package/src/theme/shadows.stories.tsx +523 -0
  195. package/src/theme/shadows.ts +439 -0
  196. package/src/theme/tokens/index.ts +2 -0
  197. package/src/theme/tokens/primitives.ts +354 -0
  198. package/src/theme/tokens/semantic.ts +381 -0
  199. package/src/types/jest-axe.d.ts +16 -0
  200. package/src/types/nativewind.d.ts +54 -0
  201. package/src/utils/cn.ts +14 -0
  202. package/src/utils/colors.ts +140 -0
  203. package/src/utils/form.ts +188 -0
  204. package/src/utils/index.ts +29 -0
  205. package/src/utils/useTheme.ts +72 -0
  206. package/dist/chunk-NA3EES23.mjs.map +0 -1
@@ -0,0 +1,531 @@
1
+ import { describe, it, expect, vi } from 'vitest'
2
+ import { render, screen, fireEvent } from '@testing-library/react'
3
+ import { axe } from 'jest-axe'
4
+ import {
5
+ Table,
6
+ TableHeader,
7
+ TableBody,
8
+ TableRow,
9
+ TableHeaderCell,
10
+ TableCell,
11
+ TablePagination,
12
+ TableEmptyState,
13
+ TableSelectAllCell,
14
+ TableSelectCell,
15
+ } from './Table'
16
+
17
+ function renderBasicTable() {
18
+ return render(
19
+ <Table>
20
+ <TableHeader>
21
+ <TableRow>
22
+ <TableHeaderCell>Name</TableHeaderCell>
23
+ <TableHeaderCell>Email</TableHeaderCell>
24
+ <TableHeaderCell>Status</TableHeaderCell>
25
+ </TableRow>
26
+ </TableHeader>
27
+ <TableBody>
28
+ <TableRow>
29
+ <TableCell>John Doe</TableCell>
30
+ <TableCell>john@example.com</TableCell>
31
+ <TableCell>Active</TableCell>
32
+ </TableRow>
33
+ <TableRow>
34
+ <TableCell>Jane Smith</TableCell>
35
+ <TableCell>jane@example.com</TableCell>
36
+ <TableCell>Inactive</TableCell>
37
+ </TableRow>
38
+ </TableBody>
39
+ </Table>
40
+ )
41
+ }
42
+
43
+ describe('Table', () => {
44
+ it('renders header and body content', () => {
45
+ renderBasicTable()
46
+ expect(screen.getByText('Name')).toBeInTheDocument()
47
+ expect(screen.getByText('Email')).toBeInTheDocument()
48
+ expect(screen.getByText('John Doe')).toBeInTheDocument()
49
+ expect(screen.getByText('jane@example.com')).toBeInTheDocument()
50
+ })
51
+
52
+ it('renders string children in TableCell as text', () => {
53
+ renderBasicTable()
54
+ expect(screen.getByText('Active')).toBeInTheDocument()
55
+ })
56
+
57
+ it('renders custom children in TableCell', () => {
58
+ render(
59
+ <Table>
60
+ <TableBody>
61
+ <TableRow>
62
+ <TableCell>
63
+ <span data-testid="custom-cell">Custom Content</span>
64
+ </TableCell>
65
+ </TableRow>
66
+ </TableBody>
67
+ </Table>
68
+ )
69
+ expect(screen.getByTestId('custom-cell')).toBeInTheDocument()
70
+ })
71
+
72
+ describe('loading state', () => {
73
+ it('shows loading skeleton when isLoading', () => {
74
+ render(
75
+ <Table isLoading>
76
+ <TableBody>
77
+ <TableRow>
78
+ <TableCell>Hidden content</TableCell>
79
+ </TableRow>
80
+ </TableBody>
81
+ </Table>
82
+ )
83
+ // Loading skeleton replaces children
84
+ expect(screen.queryByText('Hidden content')).not.toBeInTheDocument()
85
+ })
86
+
87
+ it('respects loadingRowCount', () => {
88
+ render(<Table isLoading loadingRowCount={3} />)
89
+ // Should render without errors
90
+ })
91
+ })
92
+
93
+ describe('sorting', () => {
94
+ it('renders sortable header cells', () => {
95
+ const onSort = vi.fn()
96
+ render(
97
+ <Table sortColumn="name" sortDirection="asc" onSort={onSort}>
98
+ <TableHeader>
99
+ <TableRow>
100
+ <TableHeaderCell sortKey="name">Name</TableHeaderCell>
101
+ <TableHeaderCell sortKey="email">Email</TableHeaderCell>
102
+ </TableRow>
103
+ </TableHeader>
104
+ </Table>
105
+ )
106
+ expect(screen.getByText('Name')).toBeInTheDocument()
107
+ expect(screen.getByText('Email')).toBeInTheDocument()
108
+ })
109
+
110
+ it('calls onSort when sortable header cell is clicked', () => {
111
+ const onSort = vi.fn()
112
+ render(
113
+ <Table onSort={onSort}>
114
+ <TableHeader>
115
+ <TableRow>
116
+ <TableHeaderCell sortKey="name">Name</TableHeaderCell>
117
+ </TableRow>
118
+ </TableHeader>
119
+ </Table>
120
+ )
121
+
122
+ fireEvent.click(screen.getByText('Name'))
123
+ expect(onSort).toHaveBeenCalledWith('name')
124
+ })
125
+
126
+ it('shows sort direction indicator for sorted column', () => {
127
+ render(
128
+ <Table sortColumn="name" sortDirection="asc" onSort={vi.fn()}>
129
+ <TableHeader>
130
+ <TableRow>
131
+ <TableHeaderCell sortKey="name">Name</TableHeaderCell>
132
+ </TableRow>
133
+ </TableHeader>
134
+ </Table>
135
+ )
136
+ // Ascending arrow
137
+ expect(screen.getByText('\u2191')).toBeInTheDocument()
138
+ })
139
+
140
+ it('shows descending indicator', () => {
141
+ render(
142
+ <Table sortColumn="name" sortDirection="desc" onSort={vi.fn()}>
143
+ <TableHeader>
144
+ <TableRow>
145
+ <TableHeaderCell sortKey="name">Name</TableHeaderCell>
146
+ </TableRow>
147
+ </TableHeader>
148
+ </Table>
149
+ )
150
+ expect(screen.getByText('\u2193')).toBeInTheDocument()
151
+ })
152
+
153
+ it('shows neutral sort indicator for unsorted sortable column', () => {
154
+ render(
155
+ <Table sortColumn="email" sortDirection="asc" onSort={vi.fn()}>
156
+ <TableHeader>
157
+ <TableRow>
158
+ <TableHeaderCell sortKey="name">Name</TableHeaderCell>
159
+ </TableRow>
160
+ </TableHeader>
161
+ </Table>
162
+ )
163
+ expect(screen.getByText('\u2195')).toBeInTheDocument()
164
+ })
165
+
166
+ it('non-sortable header cell does not trigger onSort', () => {
167
+ const onSort = vi.fn()
168
+ render(
169
+ <Table onSort={onSort}>
170
+ <TableHeader>
171
+ <TableRow>
172
+ <TableHeaderCell>Actions</TableHeaderCell>
173
+ </TableRow>
174
+ </TableHeader>
175
+ </Table>
176
+ )
177
+ fireEvent.click(screen.getByText('Actions'))
178
+ expect(onSort).not.toHaveBeenCalled()
179
+ })
180
+ })
181
+
182
+ describe('row selection', () => {
183
+ it('renders select all checkbox when selectable', () => {
184
+ render(
185
+ <Table selectable selectedRows={new Set()} rowIds={['1', '2']}>
186
+ <TableHeader>
187
+ <TableRow>
188
+ <TableSelectAllCell />
189
+ <TableHeaderCell>Name</TableHeaderCell>
190
+ </TableRow>
191
+ </TableHeader>
192
+ </Table>
193
+ )
194
+ expect(screen.getByRole('checkbox')).toBeInTheDocument()
195
+ })
196
+
197
+ it('does not render select cells when not selectable', () => {
198
+ render(
199
+ <Table selectable={false}>
200
+ <TableHeader>
201
+ <TableRow>
202
+ <TableSelectAllCell />
203
+ <TableHeaderCell>Name</TableHeaderCell>
204
+ </TableRow>
205
+ </TableHeader>
206
+ </Table>
207
+ )
208
+ expect(screen.queryByRole('checkbox')).not.toBeInTheDocument()
209
+ })
210
+
211
+ it('calls onSelectAll when select all is clicked', () => {
212
+ const onSelectAll = vi.fn()
213
+ render(
214
+ <Table selectable selectedRows={new Set()} rowIds={['1', '2']} onSelectAll={onSelectAll}>
215
+ <TableHeader>
216
+ <TableRow>
217
+ <TableSelectAllCell />
218
+ </TableRow>
219
+ </TableHeader>
220
+ </Table>
221
+ )
222
+
223
+ fireEvent.click(screen.getByRole('checkbox'))
224
+ expect(onSelectAll).toHaveBeenCalledWith(true)
225
+ })
226
+
227
+ it('calls onSelectRow for individual row selection', () => {
228
+ const onSelectRow = vi.fn()
229
+ render(
230
+ <Table selectable selectedRows={new Set()} onSelectRow={onSelectRow}>
231
+ <TableBody>
232
+ <TableRow>
233
+ <TableSelectCell rowId="1" />
234
+ <TableCell>John</TableCell>
235
+ </TableRow>
236
+ </TableBody>
237
+ </Table>
238
+ )
239
+
240
+ fireEvent.click(screen.getByRole('checkbox'))
241
+ expect(onSelectRow).toHaveBeenCalledWith('1', true)
242
+ })
243
+
244
+ it('shows checked state for selected rows', () => {
245
+ render(
246
+ <Table selectable selectedRows={new Set(['1'])}>
247
+ <TableBody>
248
+ <TableRow>
249
+ <TableSelectCell rowId="1" />
250
+ </TableRow>
251
+ </TableBody>
252
+ </Table>
253
+ )
254
+
255
+ const checkbox = screen.getByRole('checkbox')
256
+ expect(checkbox).toBeInTheDocument()
257
+ })
258
+ })
259
+
260
+ describe('TableRow', () => {
261
+ it('renders with selected style', () => {
262
+ render(
263
+ <Table>
264
+ <TableBody>
265
+ <TableRow isSelected>
266
+ <TableCell>Selected row</TableCell>
267
+ </TableRow>
268
+ </TableBody>
269
+ </Table>
270
+ )
271
+ expect(screen.getByText('Selected row')).toBeInTheDocument()
272
+ })
273
+
274
+ it('supports non-hoverable rows', () => {
275
+ render(
276
+ <Table>
277
+ <TableBody>
278
+ <TableRow isHoverable={false}>
279
+ <TableCell>Static row</TableCell>
280
+ </TableRow>
281
+ </TableBody>
282
+ </Table>
283
+ )
284
+ expect(screen.getByText('Static row')).toBeInTheDocument()
285
+ })
286
+ })
287
+
288
+ describe('TableHeaderCell alignment', () => {
289
+ const alignments = ['left', 'center', 'right'] as const
290
+ alignments.forEach((align) => {
291
+ it(`renders with ${align} alignment`, () => {
292
+ render(
293
+ <Table>
294
+ <TableHeader>
295
+ <TableRow>
296
+ <TableHeaderCell align={align}>Header</TableHeaderCell>
297
+ </TableRow>
298
+ </TableHeader>
299
+ </Table>
300
+ )
301
+ expect(screen.getByText('Header')).toBeInTheDocument()
302
+ })
303
+ })
304
+ })
305
+
306
+ describe('TableCell alignment', () => {
307
+ const alignments = ['left', 'center', 'right'] as const
308
+ alignments.forEach((align) => {
309
+ it(`renders with ${align} alignment`, () => {
310
+ render(
311
+ <Table>
312
+ <TableBody>
313
+ <TableRow>
314
+ <TableCell align={align}>Content</TableCell>
315
+ </TableRow>
316
+ </TableBody>
317
+ </Table>
318
+ )
319
+ expect(screen.getByText('Content')).toBeInTheDocument()
320
+ })
321
+ })
322
+ })
323
+
324
+ describe('TablePagination', () => {
325
+ it('renders pagination info', () => {
326
+ render(
327
+ <TablePagination
328
+ page={0}
329
+ pageSize={10}
330
+ totalItems={50}
331
+ onPageChange={vi.fn()}
332
+ />
333
+ )
334
+ expect(screen.getByText('1-10 of 50')).toBeInTheDocument()
335
+ })
336
+
337
+ it('calls onPageChange when next is clicked', () => {
338
+ const onPageChange = vi.fn()
339
+ render(
340
+ <TablePagination
341
+ page={0}
342
+ pageSize={10}
343
+ totalItems={50}
344
+ onPageChange={onPageChange}
345
+ />
346
+ )
347
+
348
+ fireEvent.click(screen.getByLabelText('Next page'))
349
+ expect(onPageChange).toHaveBeenCalledWith(1)
350
+ })
351
+
352
+ it('calls onPageChange when previous is clicked', () => {
353
+ const onPageChange = vi.fn()
354
+ render(
355
+ <TablePagination
356
+ page={2}
357
+ pageSize={10}
358
+ totalItems={50}
359
+ onPageChange={onPageChange}
360
+ />
361
+ )
362
+
363
+ fireEvent.click(screen.getByLabelText('Previous page'))
364
+ expect(onPageChange).toHaveBeenCalledWith(1)
365
+ })
366
+
367
+ it('disables previous button on first page', () => {
368
+ render(
369
+ <TablePagination
370
+ page={0}
371
+ pageSize={10}
372
+ totalItems={50}
373
+ onPageChange={vi.fn()}
374
+ />
375
+ )
376
+ const prevButton = screen.getByLabelText('Previous page')
377
+ expect(prevButton).toHaveAttribute('aria-disabled', 'true')
378
+ })
379
+
380
+ it('disables next button on last page', () => {
381
+ render(
382
+ <TablePagination
383
+ page={4}
384
+ pageSize={10}
385
+ totalItems={50}
386
+ onPageChange={vi.fn()}
387
+ />
388
+ )
389
+ const nextButton = screen.getByLabelText('Next page')
390
+ expect(nextButton).toHaveAttribute('aria-disabled', 'true')
391
+ })
392
+
393
+ it('calls onPageSizeChange when page size option is clicked', () => {
394
+ const onPageSizeChange = vi.fn()
395
+ render(
396
+ <TablePagination
397
+ page={0}
398
+ pageSize={10}
399
+ totalItems={50}
400
+ onPageChange={vi.fn()}
401
+ onPageSizeChange={onPageSizeChange}
402
+ />
403
+ )
404
+
405
+ fireEvent.click(screen.getByText('25'))
406
+ expect(onPageSizeChange).toHaveBeenCalledWith(25)
407
+ })
408
+
409
+ it('renders custom pageSizeOptions', () => {
410
+ render(
411
+ <TablePagination
412
+ page={0}
413
+ pageSize={5}
414
+ totalItems={50}
415
+ pageSizeOptions={[5, 15, 30]}
416
+ onPageChange={vi.fn()}
417
+ />
418
+ )
419
+ expect(screen.getByText('5')).toBeInTheDocument()
420
+ expect(screen.getByText('15')).toBeInTheDocument()
421
+ expect(screen.getByText('30')).toBeInTheDocument()
422
+ })
423
+
424
+ it('shows correct range for middle pages', () => {
425
+ render(
426
+ <TablePagination
427
+ page={2}
428
+ pageSize={10}
429
+ totalItems={50}
430
+ onPageChange={vi.fn()}
431
+ />
432
+ )
433
+ expect(screen.getByText('21-30 of 50')).toBeInTheDocument()
434
+ })
435
+
436
+ it('shows correct range for last partial page', () => {
437
+ render(
438
+ <TablePagination
439
+ page={2}
440
+ pageSize={10}
441
+ totalItems={25}
442
+ onPageChange={vi.fn()}
443
+ />
444
+ )
445
+ expect(screen.getByText('21-25 of 25')).toBeInTheDocument()
446
+ })
447
+ })
448
+
449
+ describe('TableEmptyState', () => {
450
+ it('renders with default text', () => {
451
+ render(<TableEmptyState />)
452
+ expect(screen.getByText('No data')).toBeInTheDocument()
453
+ expect(screen.getByText('There are no items to display.')).toBeInTheDocument()
454
+ })
455
+
456
+ it('renders with custom title and description', () => {
457
+ render(
458
+ <TableEmptyState title="No users" description="Add users to see them here." />
459
+ )
460
+ expect(screen.getByText('No users')).toBeInTheDocument()
461
+ expect(screen.getByText('Add users to see them here.')).toBeInTheDocument()
462
+ })
463
+
464
+ it('renders with action element', () => {
465
+ render(
466
+ <TableEmptyState
467
+ title="No data"
468
+ action={<button>Add Item</button>}
469
+ />
470
+ )
471
+ expect(screen.getByText('Add Item')).toBeInTheDocument()
472
+ })
473
+
474
+ it('renders with custom icon', () => {
475
+ render(
476
+ <TableEmptyState
477
+ title="No data"
478
+ icon={<span data-testid="empty-icon">E</span>}
479
+ />
480
+ )
481
+ expect(screen.getByTestId('empty-icon')).toBeInTheDocument()
482
+ })
483
+ })
484
+
485
+ describe('accessibility', () => {
486
+ it('has no accessibility violations for basic table', async () => {
487
+ const { container } = renderBasicTable()
488
+ const results = await axe(container)
489
+ expect(results).toHaveNoViolations()
490
+ })
491
+
492
+ it('has no accessibility violations for pagination', async () => {
493
+ const { container } = render(
494
+ <TablePagination
495
+ page={0}
496
+ pageSize={10}
497
+ totalItems={50}
498
+ onPageChange={vi.fn()}
499
+ />
500
+ )
501
+ const results = await axe(container)
502
+ expect(results).toHaveNoViolations()
503
+ })
504
+
505
+ it('sortable header has button role and label', () => {
506
+ render(
507
+ <Table onSort={vi.fn()}>
508
+ <TableHeader>
509
+ <TableRow>
510
+ <TableHeaderCell sortKey="name">Name</TableHeaderCell>
511
+ </TableRow>
512
+ </TableHeader>
513
+ </Table>
514
+ )
515
+ expect(screen.getByRole('button', { name: /sort by name/i })).toBeInTheDocument()
516
+ })
517
+
518
+ it('pagination buttons have accessible labels', () => {
519
+ render(
520
+ <TablePagination
521
+ page={1}
522
+ pageSize={10}
523
+ totalItems={50}
524
+ onPageChange={vi.fn()}
525
+ />
526
+ )
527
+ expect(screen.getByLabelText('Previous page')).toBeInTheDocument()
528
+ expect(screen.getByLabelText('Next page')).toBeInTheDocument()
529
+ })
530
+ })
531
+ })