@happyvertical/smrt-ui 0.42.4 → 0.42.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/README.md +157 -3
- package/dist/components/data/DataTable.svelte +1097 -111
- package/dist/components/data/DataTable.svelte.d.ts +1 -1
- package/dist/components/data/DataTable.svelte.d.ts.map +1 -1
- package/dist/components/data/DataTableController.d.ts +45 -5
- package/dist/components/data/DataTableController.d.ts.map +1 -1
- package/dist/components/data/DataTableController.js +153 -11
- package/dist/components/data/DataTableLayout.d.ts +37 -0
- package/dist/components/data/DataTableLayout.d.ts.map +1 -0
- package/dist/components/data/DataTableLayout.js +135 -0
- package/dist/components/data/DataTablePerformance.d.ts +31 -0
- package/dist/components/data/DataTablePerformance.d.ts.map +1 -0
- package/dist/components/data/DataTablePerformance.js +46 -0
- package/dist/components/data/DataTableVirtualization.d.ts +71 -0
- package/dist/components/data/DataTableVirtualization.d.ts.map +1 -0
- package/dist/components/data/DataTableVirtualization.js +100 -0
- package/dist/components/data/__benchmarks__/DataTable.bench.d.ts +2 -0
- package/dist/components/data/__benchmarks__/DataTable.bench.d.ts.map +1 -0
- package/dist/components/data/__benchmarks__/DataTable.bench.js +53 -0
- package/dist/components/data/__fixtures__/DataTableConformanceFixture.d.ts +27 -0
- package/dist/components/data/__fixtures__/DataTableConformanceFixture.d.ts.map +1 -0
- package/dist/components/data/__fixtures__/DataTableConformanceFixture.js +141 -0
- package/dist/components/data/__fixtures__/DataTablePerformanceFixture.d.ts +10 -0
- package/dist/components/data/__fixtures__/DataTablePerformanceFixture.d.ts.map +1 -0
- package/dist/components/data/__fixtures__/DataTablePerformanceFixture.js +23 -0
- package/dist/components/data/__tests__/DataTable.test.js +393 -8
- package/dist/components/data/__tests__/DataTableConformance.test.js +212 -0
- package/dist/components/data/__tests__/DataTableController.test.js +124 -2
- package/dist/components/data/__tests__/DataTableLayout.test.js +195 -0
- package/dist/components/data/__tests__/DataTablePerformance.test.js +21 -0
- package/dist/components/data/__tests__/DataTableVirtualization.test.js +80 -0
- package/dist/components/data/__tests__/DataTableVirtualizationComponent.test.js +255 -0
- package/dist/components/data/__tests__/data-surface.test.js +675 -0
- package/dist/components/data/data-surface.d.ts +246 -0
- package/dist/components/data/data-surface.d.ts.map +1 -0
- package/dist/components/data/data-surface.js +1102 -0
- package/dist/components/data/index.d.ts +3 -0
- package/dist/components/data/index.d.ts.map +1 -1
- package/dist/components/data/index.js +3 -0
- package/dist/components/data/types.d.ts +83 -1
- package/dist/components/data/types.d.ts.map +1 -1
- package/dist/i18n/strings.d.ts +25 -0
- package/dist/i18n/strings.d.ts.map +1 -1
- package/dist/i18n/strings.js +27 -2
- package/dist/svelte/playground/DataTablePreview.svelte +374 -5
- package/dist/svelte/playground/DataTablePreview.svelte.d.ts.map +1 -1
- package/dist/svelte/playground.js +2 -2
- package/package.json +3 -2
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { fireEvent, render, screen } from '@testing-library/svelte';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
3
|
+
import { createRawSnippet } from 'svelte';
|
|
4
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
5
|
+
import { expectNoA11yViolations } from '../../../test-support/a11y';
|
|
6
|
+
import DataTable from '../DataTable.svelte';
|
|
7
|
+
import { createDataTableController } from '../DataTableController.js';
|
|
8
|
+
const rows = Array.from({ length: 100 }, (_, index) => ({
|
|
9
|
+
id: `row-${index}`,
|
|
10
|
+
name: `Record ${index.toString().padStart(5, '0')}`,
|
|
11
|
+
}));
|
|
12
|
+
const columns = [{ id: 'name', label: 'Name', accessor: 'name' }];
|
|
13
|
+
const virtualization = { rowHeight: 20, viewportHeight: 100, overscan: 1 };
|
|
14
|
+
function getScrollContainer(container) {
|
|
15
|
+
const node = container.querySelector('.data-table-container');
|
|
16
|
+
if (!node)
|
|
17
|
+
throw new Error('Expected DataTable scroll container');
|
|
18
|
+
return node;
|
|
19
|
+
}
|
|
20
|
+
describe('DataTable virtualization seam', () => {
|
|
21
|
+
it('renders only a fixed-height body window with accessible row context', async () => {
|
|
22
|
+
const { container } = render(DataTable, {
|
|
23
|
+
props: { data: rows, columns, rowKey: 'id', virtualization },
|
|
24
|
+
});
|
|
25
|
+
const scrollContainer = getScrollContainer(container);
|
|
26
|
+
expect(screen.getByRole('columnheader', { name: 'Name' })).toBeInTheDocument();
|
|
27
|
+
expect(screen.getByRole('table')).toHaveAttribute('aria-rowcount', '101');
|
|
28
|
+
expect(getScrollContainer(container)).toHaveAttribute('role', 'region');
|
|
29
|
+
expect(getScrollContainer(container)).toHaveAttribute('tabindex', '0');
|
|
30
|
+
expect(screen.getByText('Record 00000')).toBeInTheDocument();
|
|
31
|
+
expect(screen.getByText('Record 00000').closest('tr')).toHaveAttribute('aria-rowindex', '2');
|
|
32
|
+
expect(screen.queryByText('Record 00020')).not.toBeInTheDocument();
|
|
33
|
+
expect(container.querySelectorAll('.data-table__virtual-spacer')).toHaveLength(1);
|
|
34
|
+
scrollContainer.scrollTop = 200;
|
|
35
|
+
await fireEvent.scroll(scrollContainer);
|
|
36
|
+
await vi.waitFor(() => {
|
|
37
|
+
expect(screen.queryByText('Record 00000')).not.toBeInTheDocument();
|
|
38
|
+
expect(screen.getByText('Record 00010')).toBeInTheDocument();
|
|
39
|
+
});
|
|
40
|
+
expect(screen.getByText('Record 00010').closest('tr')).toHaveAttribute('aria-rowindex', '12');
|
|
41
|
+
expect(container.querySelectorAll('.data-table__virtual-spacer')).toHaveLength(2);
|
|
42
|
+
});
|
|
43
|
+
it('keeps manual pages and selected stable IDs independent of the rendered window', async () => {
|
|
44
|
+
const footer = createRawSnippet(() => ({
|
|
45
|
+
render: () => '<strong>Remote summary</strong>',
|
|
46
|
+
}));
|
|
47
|
+
const controller = createDataTableController({
|
|
48
|
+
columnIds: ['name'],
|
|
49
|
+
modes: { filtering: 'manual', sorting: 'manual', pagination: 'manual' },
|
|
50
|
+
initialState: { page: 12, pageSize: 100 },
|
|
51
|
+
});
|
|
52
|
+
const { container } = render(DataTable, {
|
|
53
|
+
props: {
|
|
54
|
+
data: rows,
|
|
55
|
+
columns,
|
|
56
|
+
rowKey: 'id',
|
|
57
|
+
selectable: true,
|
|
58
|
+
controller,
|
|
59
|
+
totalRows: 10_000,
|
|
60
|
+
footer,
|
|
61
|
+
virtualization,
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
const scrollContainer = getScrollContainer(container);
|
|
65
|
+
expect(screen.getByRole('table')).toHaveAttribute('aria-rowcount', '10002');
|
|
66
|
+
expect(screen.getByText('Remote summary').closest('tr')).toHaveAttribute('aria-rowindex', '10002');
|
|
67
|
+
await userEvent.click(screen.getAllByRole('checkbox', { name: 'Select row 1101' })[0]);
|
|
68
|
+
expect(controller.getState().selectedRowIds).toEqual(['row-0']);
|
|
69
|
+
scrollContainer.scrollTop = 400;
|
|
70
|
+
await fireEvent.scroll(scrollContainer);
|
|
71
|
+
await vi.waitFor(() => expect(screen.getByText('Record 00020')).toBeInTheDocument());
|
|
72
|
+
expect(screen.getByText('Record 00020').closest('tr')).toHaveAttribute('aria-rowindex', '1122');
|
|
73
|
+
expect(controller.getState().selectedRowIds).toEqual(['row-0']);
|
|
74
|
+
expect(screen.queryByText('Record 00099')).not.toBeInTheDocument();
|
|
75
|
+
});
|
|
76
|
+
it('restores a controlled focused row by stable identity and reports row focus', async () => {
|
|
77
|
+
const onFocusedRowIdChange = vi.fn();
|
|
78
|
+
const { container } = render(DataTable, {
|
|
79
|
+
props: {
|
|
80
|
+
data: rows,
|
|
81
|
+
columns,
|
|
82
|
+
rowKey: 'id',
|
|
83
|
+
onRowClick: () => undefined,
|
|
84
|
+
virtualization: {
|
|
85
|
+
...virtualization,
|
|
86
|
+
focusedRowId: 'row-40',
|
|
87
|
+
onFocusedRowIdChange,
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
const scrollContainer = getScrollContainer(container);
|
|
92
|
+
const focusedRow = await screen.findByText('Record 00040');
|
|
93
|
+
expect(scrollContainer.scrollTop).toBeGreaterThan(0);
|
|
94
|
+
expect(document.activeElement).toBe(focusedRow.closest('tr'));
|
|
95
|
+
expect(onFocusedRowIdChange).toHaveBeenCalledWith('row-40');
|
|
96
|
+
});
|
|
97
|
+
it('restores focus without aliasing numeric and string row identities', async () => {
|
|
98
|
+
const identityRows = [
|
|
99
|
+
{ id: 1, name: 'Numeric row' },
|
|
100
|
+
{ id: '1', name: 'String row' },
|
|
101
|
+
];
|
|
102
|
+
const { container } = render(DataTable, {
|
|
103
|
+
props: {
|
|
104
|
+
data: identityRows,
|
|
105
|
+
columns,
|
|
106
|
+
rowKey: 'id',
|
|
107
|
+
virtualization: { ...virtualization, focusedRowId: '1' },
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
const stringRow = await screen.findByText('String row');
|
|
111
|
+
expect(stringRow.closest('tr')).toHaveAttribute('data-row-id', 'string:1');
|
|
112
|
+
expect(document.activeElement).toBe(stringRow.closest('tr'));
|
|
113
|
+
expect(container.querySelector('tr[data-row-id="number:1"]')).toBeTruthy();
|
|
114
|
+
});
|
|
115
|
+
it('scrolls the virtual body from the keyboard-accessible region', async () => {
|
|
116
|
+
const onScrollTopChange = vi.fn();
|
|
117
|
+
const { container } = render(DataTable, {
|
|
118
|
+
props: {
|
|
119
|
+
data: rows,
|
|
120
|
+
columns,
|
|
121
|
+
rowKey: 'id',
|
|
122
|
+
virtualization: { ...virtualization, onScrollTopChange },
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
const scrollContainer = getScrollContainer(container);
|
|
126
|
+
scrollContainer.focus();
|
|
127
|
+
await fireEvent.keyDown(scrollContainer, { key: 'PageDown' });
|
|
128
|
+
expect(scrollContainer.scrollTop).toBe(80);
|
|
129
|
+
expect(onScrollTopChange).toHaveBeenCalledWith(80);
|
|
130
|
+
await vi.waitFor(() => expect(screen.getByText('Record 00004')).toBeInTheDocument());
|
|
131
|
+
});
|
|
132
|
+
it('stripes virtual rows by their logical display index', async () => {
|
|
133
|
+
const { container } = render(DataTable, {
|
|
134
|
+
props: {
|
|
135
|
+
data: rows,
|
|
136
|
+
columns,
|
|
137
|
+
rowKey: 'id',
|
|
138
|
+
striped: true,
|
|
139
|
+
virtualization,
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
const scrollContainer = getScrollContainer(container);
|
|
143
|
+
scrollContainer.scrollTop = 40;
|
|
144
|
+
await fireEvent.scroll(scrollContainer);
|
|
145
|
+
await vi.waitFor(() => expect(screen.getByText('Record 00001')).toBeInTheDocument());
|
|
146
|
+
expect(screen.getByText('Record 00001').closest('tr')).toHaveClass('data-table__row--striped');
|
|
147
|
+
expect(screen.getByText('Record 00002').closest('tr')).not.toHaveClass('data-table__row--striped');
|
|
148
|
+
});
|
|
149
|
+
it('keeps the summary footer reachable at the end of the virtual scroll range', async () => {
|
|
150
|
+
const footer = createRawSnippet(() => ({
|
|
151
|
+
render: () => '<strong>Summary total</strong>',
|
|
152
|
+
}));
|
|
153
|
+
const offsetHeight = vi
|
|
154
|
+
.spyOn(HTMLElement.prototype, 'offsetHeight', 'get')
|
|
155
|
+
.mockImplementation(function () {
|
|
156
|
+
return this.tagName === 'TFOOT' ? 24 : 0;
|
|
157
|
+
});
|
|
158
|
+
const onScrollTopChange = vi.fn();
|
|
159
|
+
try {
|
|
160
|
+
const { container } = render(DataTable, {
|
|
161
|
+
props: {
|
|
162
|
+
data: rows,
|
|
163
|
+
columns,
|
|
164
|
+
rowKey: 'id',
|
|
165
|
+
footer,
|
|
166
|
+
virtualization: { ...virtualization, onScrollTopChange },
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
const scrollContainer = getScrollContainer(container);
|
|
170
|
+
await fireEvent.keyDown(scrollContainer, { key: 'End' });
|
|
171
|
+
expect(scrollContainer.scrollTop).toBe(1_924);
|
|
172
|
+
expect(onScrollTopChange).toHaveBeenCalledWith(1_924);
|
|
173
|
+
expect(screen.getByText('Summary total')).toBeInTheDocument();
|
|
174
|
+
}
|
|
175
|
+
finally {
|
|
176
|
+
offsetHeight.mockRestore();
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
it('accounts for grouped headers and structural summary rows in virtual context', async () => {
|
|
180
|
+
const offsetHeight = vi
|
|
181
|
+
.spyOn(HTMLElement.prototype, 'offsetHeight', 'get')
|
|
182
|
+
.mockImplementation(function () {
|
|
183
|
+
if (this.tagName === 'TFOOT')
|
|
184
|
+
return 24;
|
|
185
|
+
if (this.tagName === 'TBODY' &&
|
|
186
|
+
this.classList.contains('data-table__body--structural')) {
|
|
187
|
+
return 20;
|
|
188
|
+
}
|
|
189
|
+
return 0;
|
|
190
|
+
});
|
|
191
|
+
const groupedColumns = [
|
|
192
|
+
{
|
|
193
|
+
...columns[0],
|
|
194
|
+
headerPath: [{ id: 'identity', label: 'Identity' }],
|
|
195
|
+
},
|
|
196
|
+
];
|
|
197
|
+
try {
|
|
198
|
+
const { container } = render(DataTable, {
|
|
199
|
+
props: {
|
|
200
|
+
data: rows,
|
|
201
|
+
columns: groupedColumns,
|
|
202
|
+
rowKey: 'id',
|
|
203
|
+
structuralRows: [
|
|
204
|
+
{ id: 'subtotal', kind: 'subtotal', label: 'Current page' },
|
|
205
|
+
{ id: 'footer', kind: 'footer', label: 'All rows' },
|
|
206
|
+
],
|
|
207
|
+
virtualization,
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
const scrollContainer = getScrollContainer(container);
|
|
211
|
+
expect(screen.getByRole('table')).toHaveAttribute('aria-rowcount', '104');
|
|
212
|
+
expect(screen.getByText('Record 00000').closest('tr')).toHaveAttribute('aria-rowindex', '3');
|
|
213
|
+
expect(screen.getByText('Current page').closest('tr')).toHaveAttribute('aria-rowindex', '103');
|
|
214
|
+
expect(screen.getByText('All rows').closest('tr')).toHaveAttribute('aria-rowindex', '104');
|
|
215
|
+
await fireEvent.keyDown(scrollContainer, { key: 'End' });
|
|
216
|
+
expect(scrollContainer.scrollTop).toBe(1_944);
|
|
217
|
+
}
|
|
218
|
+
finally {
|
|
219
|
+
offsetHeight.mockRestore();
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
it('falls back for expansion without virtual scroll behavior', async () => {
|
|
223
|
+
const expandedContent = createRawSnippet(() => ({
|
|
224
|
+
render: () => '<p>Variable detail</p>',
|
|
225
|
+
}));
|
|
226
|
+
const footer = createRawSnippet(() => ({
|
|
227
|
+
render: () => '<strong>Summary total</strong>',
|
|
228
|
+
}));
|
|
229
|
+
const onScrollTopChange = vi.fn();
|
|
230
|
+
const { container } = render(DataTable, {
|
|
231
|
+
props: {
|
|
232
|
+
data: rows,
|
|
233
|
+
columns,
|
|
234
|
+
rowKey: 'id',
|
|
235
|
+
expandedContent,
|
|
236
|
+
footer,
|
|
237
|
+
virtualization: { ...virtualization, onScrollTopChange },
|
|
238
|
+
},
|
|
239
|
+
});
|
|
240
|
+
expect(screen.getByText('Record 00099')).toBeInTheDocument();
|
|
241
|
+
expect(screen.getByText('Summary total')).toBeInTheDocument();
|
|
242
|
+
expect(container.querySelector('.data-table__virtual-spacer')).toBeNull();
|
|
243
|
+
const scrollContainer = getScrollContainer(container);
|
|
244
|
+
expect(scrollContainer).not.toHaveAttribute('tabindex');
|
|
245
|
+
scrollContainer.scrollTop = 200;
|
|
246
|
+
await fireEvent.scroll(scrollContainer);
|
|
247
|
+
expect(onScrollTopChange).not.toHaveBeenCalled();
|
|
248
|
+
});
|
|
249
|
+
it('is axe-clean with virtual spacer rows', async () => {
|
|
250
|
+
const { container } = render(DataTable, {
|
|
251
|
+
props: { data: rows, columns, rowKey: 'id', virtualization },
|
|
252
|
+
});
|
|
253
|
+
await expectNoA11yViolations(container);
|
|
254
|
+
});
|
|
255
|
+
});
|