@fragments-sdk/ui 0.9.4 → 0.9.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/ui.css +443 -247
- package/dist/blocks/components/index.d.ts +0 -2
- package/dist/blocks/components/index.d.ts.map +1 -1
- package/dist/codeblock.cjs +187 -184
- package/dist/codeblock.cjs.map +1 -1
- package/dist/codeblock.js +183 -180
- package/dist/codeblock.js.map +1 -1
- package/dist/components/Box/Box.module.scss.cjs +73 -0
- package/dist/components/Box/Box.module.scss.cjs.map +1 -1
- package/dist/components/Box/Box.module.scss.js +73 -0
- package/dist/components/Box/Box.module.scss.js.map +1 -1
- package/dist/components/ButtonGroup/ButtonGroup.module.scss.cjs +6 -0
- package/dist/components/ButtonGroup/ButtonGroup.module.scss.cjs.map +1 -1
- package/dist/components/ButtonGroup/ButtonGroup.module.scss.js +6 -0
- package/dist/components/ButtonGroup/ButtonGroup.module.scss.js.map +1 -1
- package/dist/components/CodeBlock/CodeBlock.module.scss.cjs +20 -23
- package/dist/components/CodeBlock/CodeBlock.module.scss.cjs.map +1 -1
- package/dist/components/CodeBlock/CodeBlock.module.scss.js +20 -23
- package/dist/components/CodeBlock/CodeBlock.module.scss.js.map +1 -1
- package/dist/components/CodeBlock/index.d.ts +11 -7
- package/dist/components/CodeBlock/index.d.ts.map +1 -1
- package/dist/components/Combobox/Combobox.module.scss.cjs +15 -15
- package/dist/components/Combobox/Combobox.module.scss.js +15 -15
- package/dist/components/DataTable/DataTable.module.scss.cjs +84 -0
- package/dist/components/DataTable/DataTable.module.scss.cjs.map +1 -0
- package/dist/components/DataTable/DataTable.module.scss.js +84 -0
- package/dist/components/DataTable/DataTable.module.scss.js.map +1 -0
- package/dist/components/DataTable/index.cjs +383 -0
- package/dist/components/DataTable/index.cjs.map +1 -0
- package/dist/components/DataTable/index.d.ts +78 -0
- package/dist/components/DataTable/index.d.ts.map +1 -0
- package/dist/components/DataTable/index.js +366 -0
- package/dist/components/DataTable/index.js.map +1 -0
- package/dist/components/Drawer/Drawer.module.scss.cjs +9 -0
- package/dist/components/Drawer/Drawer.module.scss.cjs.map +1 -1
- package/dist/components/Drawer/Drawer.module.scss.js +9 -0
- package/dist/components/Drawer/Drawer.module.scss.js.map +1 -1
- package/dist/components/Image/Image.module.scss.cjs +12 -0
- package/dist/components/Image/Image.module.scss.cjs.map +1 -1
- package/dist/components/Image/Image.module.scss.js +12 -0
- package/dist/components/Image/Image.module.scss.js.map +1 -1
- package/dist/components/Link/Link.module.scss.cjs +3 -0
- package/dist/components/Link/Link.module.scss.cjs.map +1 -1
- package/dist/components/Link/Link.module.scss.js +3 -0
- package/dist/components/Link/Link.module.scss.js.map +1 -1
- package/dist/components/List/List.module.scss.cjs +5 -0
- package/dist/components/List/List.module.scss.cjs.map +1 -1
- package/dist/components/List/List.module.scss.js +5 -0
- package/dist/components/List/List.module.scss.js.map +1 -1
- package/dist/components/Loading/Loading.module.scss.cjs +5 -0
- package/dist/components/Loading/Loading.module.scss.cjs.map +1 -1
- package/dist/components/Loading/Loading.module.scss.js +5 -0
- package/dist/components/Loading/Loading.module.scss.js.map +1 -1
- package/dist/components/Markdown/Markdown.module.scss.cjs +1 -1
- package/dist/components/Markdown/Markdown.module.scss.js +1 -1
- package/dist/components/Message/Message.module.scss.cjs +22 -16
- package/dist/components/Message/Message.module.scss.cjs.map +1 -1
- package/dist/components/Message/Message.module.scss.js +22 -16
- package/dist/components/Message/Message.module.scss.js.map +1 -1
- package/dist/components/Message/index.cjs +5 -3
- package/dist/components/Message/index.cjs.map +1 -1
- package/dist/components/Message/index.d.ts +5 -1
- package/dist/components/Message/index.d.ts.map +1 -1
- package/dist/components/Message/index.js +5 -3
- package/dist/components/Message/index.js.map +1 -1
- package/dist/components/Skeleton/Skeleton.module.scss.cjs +14 -0
- package/dist/components/Skeleton/Skeleton.module.scss.cjs.map +1 -1
- package/dist/components/Skeleton/Skeleton.module.scss.js +14 -0
- package/dist/components/Skeleton/Skeleton.module.scss.js.map +1 -1
- package/dist/components/Stack/Stack.module.scss.cjs +14 -0
- package/dist/components/Stack/Stack.module.scss.cjs.map +1 -1
- package/dist/components/Stack/Stack.module.scss.js +14 -0
- package/dist/components/Stack/Stack.module.scss.js.map +1 -1
- package/dist/components/Table/Table.module.scss.cjs +21 -36
- package/dist/components/Table/Table.module.scss.cjs.map +1 -1
- package/dist/components/Table/Table.module.scss.js +21 -36
- package/dist/components/Table/Table.module.scss.js.map +1 -1
- package/dist/components/Table/index.d.ts +35 -55
- package/dist/components/Table/index.d.ts.map +1 -1
- package/dist/components/Text/Text.module.scss.cjs +14 -0
- package/dist/components/Text/Text.module.scss.cjs.map +1 -1
- package/dist/components/Text/Text.module.scss.js +14 -0
- package/dist/components/Text/Text.module.scss.js.map +1 -1
- package/dist/components/Textarea/Textarea.module.scss.cjs +4 -0
- package/dist/components/Textarea/Textarea.module.scss.cjs.map +1 -1
- package/dist/components/Textarea/Textarea.module.scss.js +4 -0
- package/dist/components/Textarea/Textarea.module.scss.js.map +1 -1
- package/dist/components/ToggleGroup/ToggleGroup.module.scss.cjs +5 -0
- package/dist/components/ToggleGroup/ToggleGroup.module.scss.cjs.map +1 -1
- package/dist/components/ToggleGroup/ToggleGroup.module.scss.js +5 -0
- package/dist/components/ToggleGroup/ToggleGroup.module.scss.js.map +1 -1
- package/dist/index.cjs +119 -117
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/table.cjs +44 -262
- package/dist/table.cjs.map +1 -1
- package/dist/table.js +47 -248
- package/dist/table.js.map +1 -1
- package/fragments.json +1 -1
- package/package.json +110 -118
- package/src/blocks/components/index.ts +0 -3
- package/src/components/CodeBlock/CodeBlock.module.scss +16 -34
- package/src/components/CodeBlock/index.tsx +351 -345
- package/src/components/Combobox/Combobox.module.scss +13 -9
- package/src/components/ConversationList/ConversationList.fragment.tsx +96 -129
- package/src/components/DataTable/DataTable.fragment.tsx +754 -0
- package/src/components/DataTable/DataTable.module.scss +300 -0
- package/src/components/DataTable/DataTable.test.tsx +224 -0
- package/src/components/DataTable/index.tsx +533 -0
- package/src/components/Message/Message.fragment.tsx +34 -0
- package/src/components/Message/Message.module.scss +11 -0
- package/src/components/Message/index.tsx +12 -3
- package/src/components/Table/Table.fragment.tsx +190 -175
- package/src/components/Table/Table.module.scss +15 -88
- package/src/components/Table/Table.test.tsx +184 -94
- package/src/components/Table/index.tsx +105 -374
- package/src/index.ts +15 -4
- package/src/tokens/_computed.scss +7 -6
- package/src/tokens/_density.scss +87 -47
- package/src/tokens/_variables.scss +46 -31
- package/dist/blocks/components/DataTable.d.ts +0 -19
- package/dist/blocks/components/DataTable.d.ts.map +0 -1
- package/src/blocks/components/DataTable.tsx +0 -124
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
@use '../../tokens/variables' as *;
|
|
2
|
+
@use '../../tokens/mixins' as *;
|
|
3
|
+
|
|
4
|
+
.wrapper {
|
|
5
|
+
overflow-x: auto;
|
|
6
|
+
-webkit-overflow-scrolling: touch;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.table {
|
|
10
|
+
width: 100%;
|
|
11
|
+
border-collapse: collapse;
|
|
12
|
+
font-family: var(--fui-font-sans, $fui-font-sans);
|
|
13
|
+
-webkit-font-smoothing: antialiased;
|
|
14
|
+
-moz-osx-font-smoothing: grayscale;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Caption for accessibility
|
|
18
|
+
.caption {
|
|
19
|
+
padding: var(--fui-space-3, $fui-space-3) 0;
|
|
20
|
+
font-size: var(--fui-font-size-sm, $fui-font-size-sm);
|
|
21
|
+
font-weight: var(--fui-font-weight-medium, $fui-font-weight-medium);
|
|
22
|
+
color: var(--fui-text-secondary, $fui-text-secondary);
|
|
23
|
+
text-align: left;
|
|
24
|
+
caption-side: top;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Visually hidden caption (screen readers only)
|
|
28
|
+
.captionHidden {
|
|
29
|
+
@include visually-hidden;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Size variants
|
|
33
|
+
.sm {
|
|
34
|
+
.th,
|
|
35
|
+
.td {
|
|
36
|
+
padding: var(--fui-padding-item-xs, $fui-padding-item-xs) var(--fui-padding-item-md, $fui-padding-item-md);
|
|
37
|
+
font-size: var(--fui-font-size-xs, $fui-font-size-xs);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.md {
|
|
42
|
+
.th,
|
|
43
|
+
.td {
|
|
44
|
+
padding: var(--fui-padding-item-xs, $fui-padding-item-xs) var(--fui-padding-item-md, $fui-padding-item-md);
|
|
45
|
+
font-size: var(--fui-font-size-sm, $fui-font-size-sm);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Header
|
|
50
|
+
.thead {
|
|
51
|
+
position: sticky;
|
|
52
|
+
top: 0;
|
|
53
|
+
z-index: 1;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.headerRow {
|
|
57
|
+
border-bottom: 1px solid var(--fui-border, $fui-border);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.th {
|
|
61
|
+
text-align: left;
|
|
62
|
+
font-weight: var(--fui-font-weight-medium, $fui-font-weight-medium);
|
|
63
|
+
color: var(--fui-text-secondary, $fui-text-secondary);
|
|
64
|
+
white-space: nowrap;
|
|
65
|
+
vertical-align: middle;
|
|
66
|
+
user-select: none;
|
|
67
|
+
|
|
68
|
+
&:first-child {
|
|
69
|
+
border-top-left-radius: var(--fui-radius-md, $fui-radius-md);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
&:last-child {
|
|
73
|
+
border-top-right-radius: var(--fui-radius-md, $fui-radius-md);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.headerContent {
|
|
78
|
+
display: flex;
|
|
79
|
+
align-items: center;
|
|
80
|
+
gap: var(--fui-space-1, $fui-space-1);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Sortable header cell (for focus styles)
|
|
84
|
+
.thSortable {
|
|
85
|
+
padding: 0;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.sortButton {
|
|
89
|
+
@include button-reset;
|
|
90
|
+
@include interactive-base;
|
|
91
|
+
|
|
92
|
+
display: flex;
|
|
93
|
+
align-items: center;
|
|
94
|
+
justify-content: space-between;
|
|
95
|
+
gap: var(--fui-space-1, $fui-space-1);
|
|
96
|
+
width: 100%;
|
|
97
|
+
padding: var(--fui-padding-item-xs, $fui-padding-item-xs) var(--fui-padding-item-md, $fui-padding-item-md);
|
|
98
|
+
color: inherit;
|
|
99
|
+
text-align: left;
|
|
100
|
+
transition: color var(--fui-transition-fast, $fui-transition-fast);
|
|
101
|
+
|
|
102
|
+
&:hover {
|
|
103
|
+
color: var(--fui-text-primary, $fui-text-primary);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.sortIndicator {
|
|
108
|
+
display: inline-flex;
|
|
109
|
+
align-items: center;
|
|
110
|
+
color: var(--fui-text-tertiary, $fui-text-tertiary);
|
|
111
|
+
flex-shrink: 0;
|
|
112
|
+
|
|
113
|
+
.sortButton:hover & {
|
|
114
|
+
color: var(--fui-text-secondary, $fui-text-secondary);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Body
|
|
119
|
+
.tbody {}
|
|
120
|
+
|
|
121
|
+
.row {
|
|
122
|
+
border-bottom: 1px solid var(--fui-border, $fui-border);
|
|
123
|
+
transition: background-color var(--fui-transition-fast, $fui-transition-fast);
|
|
124
|
+
|
|
125
|
+
&:last-child {
|
|
126
|
+
border-bottom: none;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
&:hover {
|
|
130
|
+
background-color: var(--fui-bg-hover, $fui-bg-hover);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.clickable {
|
|
135
|
+
cursor: pointer;
|
|
136
|
+
|
|
137
|
+
&:hover {
|
|
138
|
+
background-color: var(--fui-bg-hover, $fui-bg-hover);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
&:focus-visible {
|
|
142
|
+
@include focus-ring;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
&:active {
|
|
146
|
+
background-color: var(--fui-bg-active, $fui-bg-active);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.selected {
|
|
151
|
+
background-color: rgba($fui-color-accent, 0.08);
|
|
152
|
+
|
|
153
|
+
&:hover {
|
|
154
|
+
background-color: rgba($fui-color-accent, 0.12);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.td {
|
|
159
|
+
color: var(--fui-text-primary, $fui-text-primary);
|
|
160
|
+
vertical-align: middle;
|
|
161
|
+
line-height: var(--fui-line-height-normal, $fui-line-height-normal);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Striped rows
|
|
165
|
+
.striped {
|
|
166
|
+
.row:nth-child(even) {
|
|
167
|
+
background-color: var(--fui-bg-subtle, $fui-bg-subtle);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Hover and selected override stripe
|
|
171
|
+
.clickable:hover {
|
|
172
|
+
background-color: var(--fui-bg-hover, $fui-bg-hover);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
.clickable:active {
|
|
176
|
+
background-color: var(--fui-bg-active, $fui-bg-active);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.selected {
|
|
180
|
+
background-color: rgba($fui-color-accent, 0.08);
|
|
181
|
+
|
|
182
|
+
&:hover {
|
|
183
|
+
background-color: rgba($fui-color-accent, 0.12);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Bordered
|
|
189
|
+
.bordered {
|
|
190
|
+
border: 1px solid var(--fui-border, $fui-border);
|
|
191
|
+
border-radius: var(--fui-radius-md, $fui-radius-md);
|
|
192
|
+
overflow: hidden;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Fixed layout for explicit column sizes
|
|
196
|
+
.fixedLayout {
|
|
197
|
+
table-layout: fixed;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Checkbox column
|
|
201
|
+
.checkboxCell {
|
|
202
|
+
display: flex;
|
|
203
|
+
align-items: center;
|
|
204
|
+
justify-content: center;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Expandable rows
|
|
208
|
+
.expandCell {
|
|
209
|
+
display: inline-flex;
|
|
210
|
+
align-items: center;
|
|
211
|
+
gap: var(--fui-space-1, $fui-space-1);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.expandButton {
|
|
215
|
+
@include button-reset;
|
|
216
|
+
|
|
217
|
+
display: inline-flex;
|
|
218
|
+
align-items: center;
|
|
219
|
+
justify-content: center;
|
|
220
|
+
width: 20px;
|
|
221
|
+
height: 20px;
|
|
222
|
+
flex-shrink: 0;
|
|
223
|
+
border-radius: var(--fui-radius-sm, $fui-radius-sm);
|
|
224
|
+
color: var(--fui-text-tertiary, $fui-text-tertiary);
|
|
225
|
+
transition: color var(--fui-transition-fast, $fui-transition-fast),
|
|
226
|
+
background-color var(--fui-transition-fast, $fui-transition-fast);
|
|
227
|
+
|
|
228
|
+
&:hover {
|
|
229
|
+
color: var(--fui-text-primary, $fui-text-primary);
|
|
230
|
+
background-color: var(--fui-bg-hover, $fui-bg-hover);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
&:focus-visible {
|
|
234
|
+
@include focus-ring;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
.subRow {
|
|
239
|
+
background-color: var(--fui-bg-subtle, $fui-bg-subtle);
|
|
240
|
+
|
|
241
|
+
&:hover {
|
|
242
|
+
background-color: var(--fui-bg-hover, $fui-bg-hover);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Empty state
|
|
247
|
+
.emptyState {
|
|
248
|
+
display: flex;
|
|
249
|
+
align-items: center;
|
|
250
|
+
justify-content: center;
|
|
251
|
+
padding: var(--fui-space-12, $fui-space-12) var(--fui-space-6, $fui-space-6);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
.emptyMessage {
|
|
255
|
+
font-family: var(--fui-font-sans, $fui-font-sans);
|
|
256
|
+
font-size: var(--fui-font-size-sm, $fui-font-size-sm);
|
|
257
|
+
color: var(--fui-text-tertiary, $fui-text-tertiary);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Responsive: allow horizontal scroll on small screens
|
|
261
|
+
@include below-sm {
|
|
262
|
+
.wrapper {
|
|
263
|
+
margin-left: calc(-1 * var(--fui-space-4, $fui-space-4));
|
|
264
|
+
margin-right: calc(-1 * var(--fui-space-4, $fui-space-4));
|
|
265
|
+
padding-left: var(--fui-space-4, $fui-space-4);
|
|
266
|
+
padding-right: var(--fui-space-4, $fui-space-4);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// ============================================
|
|
271
|
+
// Accessibility: High Contrast Mode
|
|
272
|
+
// ============================================
|
|
273
|
+
|
|
274
|
+
@media (prefers-contrast: more) {
|
|
275
|
+
.headerRow {
|
|
276
|
+
border-bottom-width: 2px;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
.row {
|
|
280
|
+
border-bottom-width: 2px;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
.sortButton:focus-visible {
|
|
284
|
+
outline-width: 3px;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// ============================================
|
|
289
|
+
// Accessibility: Reduced Motion
|
|
290
|
+
// ============================================
|
|
291
|
+
|
|
292
|
+
@media (prefers-reduced-motion: reduce) {
|
|
293
|
+
.row {
|
|
294
|
+
transition: none;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
.sortButton {
|
|
298
|
+
transition: none;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { render, screen, userEvent, expectNoA11yViolations } from '../../test/utils';
|
|
3
|
+
import { DataTable, createColumns } from './index';
|
|
4
|
+
|
|
5
|
+
type Person = { id: string; name: string; age: number };
|
|
6
|
+
|
|
7
|
+
const columns = createColumns<Person>([
|
|
8
|
+
{ key: 'name', header: 'Name' },
|
|
9
|
+
{ key: 'age', header: 'Age' },
|
|
10
|
+
]);
|
|
11
|
+
|
|
12
|
+
const data: Person[] = [
|
|
13
|
+
{ id: '1', name: 'Alice', age: 30 },
|
|
14
|
+
{ id: '2', name: 'Bob', age: 25 },
|
|
15
|
+
{ id: '3', name: 'Carol', age: 35 },
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
describe('DataTable', () => {
|
|
19
|
+
it('renders a table element with column headers', () => {
|
|
20
|
+
render(<DataTable columns={columns} data={data} aria-label="People" />);
|
|
21
|
+
expect(screen.getByRole('table')).toBeInTheDocument();
|
|
22
|
+
const headers = screen.getAllByRole('columnheader');
|
|
23
|
+
expect(headers).toHaveLength(2);
|
|
24
|
+
expect(headers[0]).toHaveAttribute('scope', 'col');
|
|
25
|
+
expect(headers[0]).toHaveTextContent('Name');
|
|
26
|
+
expect(headers[1]).toHaveTextContent('Age');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('renders data rows', () => {
|
|
30
|
+
render(<DataTable columns={columns} data={data} aria-label="People" />);
|
|
31
|
+
const rows = screen.getAllByRole('row');
|
|
32
|
+
// 1 header row + 3 data rows
|
|
33
|
+
expect(rows).toHaveLength(4);
|
|
34
|
+
expect(screen.getByText('Alice')).toBeInTheDocument();
|
|
35
|
+
expect(screen.getByText('25')).toBeInTheDocument();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('renders caption when provided', () => {
|
|
39
|
+
render(<DataTable columns={columns} data={data} caption="People Table" aria-label="People" />);
|
|
40
|
+
expect(screen.getByText('People Table')).toBeInTheDocument();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('shows empty state message when data is empty', () => {
|
|
44
|
+
render(<DataTable columns={columns} data={[]} emptyMessage="Nothing here" aria-label="People" />);
|
|
45
|
+
expect(screen.getByText('Nothing here')).toBeInTheDocument();
|
|
46
|
+
expect(screen.queryByRole('table')).not.toBeInTheDocument();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('defaults to "No data available" empty message', () => {
|
|
50
|
+
render(<DataTable columns={columns} data={[]} aria-label="People" />);
|
|
51
|
+
expect(screen.getByText('No data available')).toBeInTheDocument();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('supports sortable columns with aria-sort', async () => {
|
|
55
|
+
const user = userEvent.setup();
|
|
56
|
+
render(<DataTable columns={columns} data={data} sortable aria-label="People" />);
|
|
57
|
+
const headers = screen.getAllByRole('columnheader');
|
|
58
|
+
// Initially aria-sort="none" for sortable columns
|
|
59
|
+
expect(headers[0]).toHaveAttribute('aria-sort', 'none');
|
|
60
|
+
|
|
61
|
+
// Click the sort button inside the first header
|
|
62
|
+
const sortButton = headers[0].querySelector('button')!;
|
|
63
|
+
await user.click(sortButton);
|
|
64
|
+
expect(headers[0]).toHaveAttribute('aria-sort', 'ascending');
|
|
65
|
+
|
|
66
|
+
await user.click(sortButton);
|
|
67
|
+
expect(headers[0]).toHaveAttribute('aria-sort', 'descending');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('calls onRowClick when a row is clicked', async () => {
|
|
71
|
+
const user = userEvent.setup();
|
|
72
|
+
const handleClick = vi.fn();
|
|
73
|
+
render(<DataTable columns={columns} data={data} onRowClick={handleClick} aria-label="People" />);
|
|
74
|
+
const rows = screen.getAllByRole('row');
|
|
75
|
+
// rows[0] is header, rows[1] is first data row
|
|
76
|
+
await user.click(rows[1]);
|
|
77
|
+
expect(handleClick).toHaveBeenCalledWith(data[0]);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('applies striped class when striped prop is true', () => {
|
|
81
|
+
const { container } = render(<DataTable columns={columns} data={data} striped aria-label="People" />);
|
|
82
|
+
expect(container.querySelector('.striped')).toBeInTheDocument();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('createColumns helper generates proper column defs', () => {
|
|
86
|
+
const cols = createColumns<Person>([
|
|
87
|
+
{ key: 'name', header: 'Full Name', width: 200 },
|
|
88
|
+
{ key: 'age', header: 'Years', cell: (row) => `${row.age} years` },
|
|
89
|
+
]);
|
|
90
|
+
expect(cols).toHaveLength(2);
|
|
91
|
+
expect(cols[0].id).toBe('name');
|
|
92
|
+
expect(cols[0].header).toBe('Full Name');
|
|
93
|
+
expect(cols[0].size).toBe(200);
|
|
94
|
+
expect(cols[1].id).toBe('age');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('supports row selection', () => {
|
|
98
|
+
render(
|
|
99
|
+
<DataTable
|
|
100
|
+
columns={columns}
|
|
101
|
+
data={data}
|
|
102
|
+
selectable
|
|
103
|
+
rowSelection={{ '1': true }}
|
|
104
|
+
getRowId={(row) => row.id}
|
|
105
|
+
aria-label="People"
|
|
106
|
+
/>
|
|
107
|
+
);
|
|
108
|
+
const rows = screen.getAllByRole('row');
|
|
109
|
+
// First data row (id='1') should have data-selected
|
|
110
|
+
expect(rows[1]).toHaveAttribute('data-selected');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('supports keyboard activation of clickable rows', async () => {
|
|
114
|
+
const user = userEvent.setup();
|
|
115
|
+
const handleClick = vi.fn();
|
|
116
|
+
render(<DataTable columns={columns} data={data} onRowClick={handleClick} aria-label="People" />);
|
|
117
|
+
const rows = screen.getAllByRole('row');
|
|
118
|
+
rows[1].focus();
|
|
119
|
+
await user.keyboard('{Enter}');
|
|
120
|
+
expect(handleClick).toHaveBeenCalledWith(data[0]);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('renders checkbox column when showCheckbox and selectable', async () => {
|
|
124
|
+
const user = userEvent.setup();
|
|
125
|
+
render(
|
|
126
|
+
<DataTable
|
|
127
|
+
columns={columns}
|
|
128
|
+
data={data}
|
|
129
|
+
selectable
|
|
130
|
+
showCheckbox
|
|
131
|
+
getRowId={(row) => row.id}
|
|
132
|
+
aria-label="People"
|
|
133
|
+
/>
|
|
134
|
+
);
|
|
135
|
+
// 2 data columns + 1 checkbox column = 3 headers
|
|
136
|
+
const headers = screen.getAllByRole('columnheader');
|
|
137
|
+
expect(headers).toHaveLength(3);
|
|
138
|
+
|
|
139
|
+
// "Select all" checkbox in header
|
|
140
|
+
const selectAll = screen.getByRole('checkbox', { name: 'Select all rows' });
|
|
141
|
+
expect(selectAll).toBeInTheDocument();
|
|
142
|
+
|
|
143
|
+
// Individual row checkboxes
|
|
144
|
+
const rowCheckboxes = screen.getAllByRole('checkbox', { name: /Select row/ });
|
|
145
|
+
expect(rowCheckboxes).toHaveLength(3);
|
|
146
|
+
|
|
147
|
+
// Click a row checkbox toggles selection
|
|
148
|
+
await user.click(rowCheckboxes[0]);
|
|
149
|
+
expect(rowCheckboxes[0]).toHaveAttribute('aria-checked', 'true');
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('does not render checkbox column when only showCheckbox without selectable', () => {
|
|
153
|
+
render(
|
|
154
|
+
<DataTable
|
|
155
|
+
columns={columns}
|
|
156
|
+
data={data}
|
|
157
|
+
showCheckbox
|
|
158
|
+
aria-label="People"
|
|
159
|
+
/>
|
|
160
|
+
);
|
|
161
|
+
// Should only have the 2 data columns
|
|
162
|
+
expect(screen.getAllByRole('columnheader')).toHaveLength(2);
|
|
163
|
+
expect(screen.queryByLabelText('Select all rows')).not.toBeInTheDocument();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('renders expandable sub-rows with expand/collapse buttons', async () => {
|
|
167
|
+
type Node = { id: string; name: string; children?: Node[] };
|
|
168
|
+
const treeData: Node[] = [
|
|
169
|
+
{
|
|
170
|
+
id: '1', name: 'Parent',
|
|
171
|
+
children: [
|
|
172
|
+
{ id: '1.1', name: 'Child A' },
|
|
173
|
+
{ id: '1.2', name: 'Child B' },
|
|
174
|
+
],
|
|
175
|
+
},
|
|
176
|
+
{ id: '2', name: 'Standalone' },
|
|
177
|
+
];
|
|
178
|
+
const treeCols = createColumns<Node>([
|
|
179
|
+
{ key: 'name', header: 'Name' },
|
|
180
|
+
]);
|
|
181
|
+
|
|
182
|
+
const user = userEvent.setup();
|
|
183
|
+
render(
|
|
184
|
+
<DataTable
|
|
185
|
+
columns={treeCols}
|
|
186
|
+
data={treeData}
|
|
187
|
+
getSubRows={(row) => row.children}
|
|
188
|
+
getRowId={(row) => row.id}
|
|
189
|
+
aria-label="Tree"
|
|
190
|
+
/>
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
// Initially only top-level rows visible (1 header + 2 data)
|
|
194
|
+
expect(screen.getAllByRole('row')).toHaveLength(3);
|
|
195
|
+
expect(screen.getByText('Parent')).toBeInTheDocument();
|
|
196
|
+
expect(screen.getByText('Standalone')).toBeInTheDocument();
|
|
197
|
+
|
|
198
|
+
// Expand button present for parent row
|
|
199
|
+
const expandBtn = screen.getByLabelText('Expand row');
|
|
200
|
+
expect(expandBtn).toHaveAttribute('aria-expanded', 'false');
|
|
201
|
+
|
|
202
|
+
// Click expand to show children
|
|
203
|
+
await user.click(expandBtn);
|
|
204
|
+
expect(screen.getAllByRole('row')).toHaveLength(5); // 1 header + 2 top + 2 children
|
|
205
|
+
expect(screen.getByText('Child A')).toBeInTheDocument();
|
|
206
|
+
expect(screen.getByText('Child B')).toBeInTheDocument();
|
|
207
|
+
|
|
208
|
+
// Button now shows "Collapse row"
|
|
209
|
+
const collapseBtn = screen.getByLabelText('Collapse row');
|
|
210
|
+
expect(collapseBtn).toHaveAttribute('aria-expanded', 'true');
|
|
211
|
+
|
|
212
|
+
// Child rows have data-depth attribute
|
|
213
|
+
const childRows = screen.getAllByRole('row').filter(r => r.getAttribute('data-depth'));
|
|
214
|
+
expect(childRows).toHaveLength(2);
|
|
215
|
+
expect(childRows[0]).toHaveAttribute('data-depth', '1');
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('has no accessibility violations', async () => {
|
|
219
|
+
const { container } = render(
|
|
220
|
+
<DataTable columns={columns} data={data} caption="People Table" aria-label="People" />
|
|
221
|
+
);
|
|
222
|
+
await expectNoA11yViolations(container);
|
|
223
|
+
});
|
|
224
|
+
});
|