@ornery/ui-grid-react 0.1.4 → 0.1.5
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/index.d.mts +24 -2
- package/dist/index.d.ts +24 -2
- package/dist/index.js +1166 -607
- package/dist/index.mjs +1101 -549
- package/package.json +1 -1
- package/src/UiGrid.tsx +174 -47
- package/src/gridStateMath.test.ts +49 -0
- package/src/gridStateMath.ts +32 -0
- package/src/index.ts +2 -0
- package/src/rustWasmGridEngine.test.ts +56 -0
- package/src/rustWasmGridEngine.ts +21 -0
- package/src/useGridState.ts +637 -328
- package/src/useVirtualScroll.ts +11 -10
- package/src/virtualScrollMath.test.ts +44 -0
- package/src/virtualScrollMath.ts +36 -0
package/package.json
CHANGED
package/src/UiGrid.tsx
CHANGED
|
@@ -19,7 +19,13 @@ export interface UiGridProps {
|
|
|
19
19
|
className?: string;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
export function UiGrid({
|
|
22
|
+
export function UiGrid({
|
|
23
|
+
options,
|
|
24
|
+
onRegisterApi,
|
|
25
|
+
cellRenderer,
|
|
26
|
+
expandableRenderer,
|
|
27
|
+
className,
|
|
28
|
+
}: UiGridProps) {
|
|
23
29
|
const state = useGridState(options, onRegisterApi);
|
|
24
30
|
|
|
25
31
|
const {
|
|
@@ -80,9 +86,18 @@ export function UiGrid({ options, onRegisterApi, cellRenderer, expandableRendere
|
|
|
80
86
|
style={{ gridColumn: '1 / -1', paddingInlineStart: `${item.depth * 1.25 + 1}rem` }}
|
|
81
87
|
onClick={() => state.toggleGroup(item)}
|
|
82
88
|
>
|
|
83
|
-
<strong>
|
|
84
|
-
|
|
85
|
-
|
|
89
|
+
<strong>
|
|
90
|
+
{item.field}: {item.label}
|
|
91
|
+
</strong>
|
|
92
|
+
<span>
|
|
93
|
+
{item.count} {labels.groupRowsSuffix}
|
|
94
|
+
</span>
|
|
95
|
+
<svg
|
|
96
|
+
className="toggle-icon group-disclosure-icon"
|
|
97
|
+
viewBox="0 0 24 24"
|
|
98
|
+
aria-hidden="true"
|
|
99
|
+
focusable={false}
|
|
100
|
+
>
|
|
86
101
|
<path d={item.collapsed ? 'M10 7l5 5-5 5z' : 'M7 10l5 5 5-5z'} />
|
|
87
102
|
</svg>
|
|
88
103
|
<span className="sr-only ui-grid-sr-only">{state.groupDisclosureLabel(item)}</span>
|
|
@@ -107,10 +122,13 @@ export function UiGrid({ options, onRegisterApi, cellRenderer, expandableRendere
|
|
|
107
122
|
if (item.kind !== 'row') return null;
|
|
108
123
|
const rowItem = item as RowItem;
|
|
109
124
|
|
|
110
|
-
return visibleColumns.map((column) =>
|
|
125
|
+
return visibleColumns.map((column) => {
|
|
126
|
+
const pinned = state.isPinned(column);
|
|
127
|
+
const pinOffset = pinned ? state.pinnedOffset(column) : null;
|
|
128
|
+
return (
|
|
111
129
|
<div
|
|
112
130
|
key={`${rowItem.row.id}-${column.name}`}
|
|
113
|
-
className={cellClassName(rowItem, column)}
|
|
131
|
+
className={`${cellClassName(rowItem, column)}${pinned ? ' is-pinned' : ''}`}
|
|
114
132
|
data-part="body-cell"
|
|
115
133
|
role="gridcell"
|
|
116
134
|
tabIndex={0}
|
|
@@ -120,8 +138,17 @@ export function UiGrid({ options, onRegisterApi, cellRenderer, expandableRendere
|
|
|
120
138
|
onClick={() => state.focusCell(rowItem.row, column)}
|
|
121
139
|
onDoubleClick={(e) => state.handleCellDoubleClick(rowItem.row, column, e)}
|
|
122
140
|
onKeyDown={(e) => state.handleCellKeyDown(rowItem.row, column, e)}
|
|
141
|
+
style={{
|
|
142
|
+
position: pinned ? 'sticky' : undefined,
|
|
143
|
+
left: pinOffset?.side === 'left' ? pinOffset.offset : undefined,
|
|
144
|
+
right: pinOffset?.side === 'right' ? pinOffset.offset : undefined,
|
|
145
|
+
zIndex: pinned ? 2 : undefined,
|
|
146
|
+
}}
|
|
123
147
|
>
|
|
124
|
-
<div
|
|
148
|
+
<div
|
|
149
|
+
className="cell-shell"
|
|
150
|
+
style={{ paddingInlineStart: state.cellIndent(rowItem.row, column) }}
|
|
151
|
+
>
|
|
125
152
|
{treeViewFeature && state.showTreeToggle(rowItem.row, column) && (
|
|
126
153
|
<button
|
|
127
154
|
type="button"
|
|
@@ -132,7 +159,9 @@ export function UiGrid({ options, onRegisterApi, cellRenderer, expandableRendere
|
|
|
132
159
|
onClick={(e) => state.toggleTreeRow(rowItem.row, e)}
|
|
133
160
|
>
|
|
134
161
|
<svg className="toggle-icon" viewBox="0 0 24 24" aria-hidden="true" focusable={false}>
|
|
135
|
-
<path
|
|
162
|
+
<path
|
|
163
|
+
d={state.isTreeRowExpanded(rowItem.row) ? 'M7 10l5 5 5-5z' : 'M10 7l5 5-5 5z'}
|
|
164
|
+
/>
|
|
136
165
|
</svg>
|
|
137
166
|
</button>
|
|
138
167
|
)}
|
|
@@ -164,14 +193,16 @@ export function UiGrid({ options, onRegisterApi, cellRenderer, expandableRendere
|
|
|
164
193
|
onBlur={(e) => state.handleEditorBlur(e)}
|
|
165
194
|
/>
|
|
166
195
|
) : cellRenderer ? (
|
|
167
|
-
cellRenderer(state.cellContext(rowItem.row, column)) ??
|
|
196
|
+
(cellRenderer(state.cellContext(rowItem.row, column)) ??
|
|
197
|
+
state.displayValue(rowItem.row, column))
|
|
168
198
|
) : (
|
|
169
199
|
state.displayValue(rowItem.row, column)
|
|
170
200
|
)}
|
|
171
201
|
</span>
|
|
172
202
|
</div>
|
|
173
203
|
</div>
|
|
174
|
-
|
|
204
|
+
);
|
|
205
|
+
});
|
|
175
206
|
}
|
|
176
207
|
|
|
177
208
|
function cellClassName(item: RowItem, column: GridColumnDef): string {
|
|
@@ -222,11 +253,21 @@ export function UiGrid({ options, onRegisterApi, cellRenderer, expandableRendere
|
|
|
222
253
|
</div>
|
|
223
254
|
|
|
224
255
|
<div className="hero-actions">
|
|
225
|
-
<button
|
|
256
|
+
<button
|
|
257
|
+
type="button"
|
|
258
|
+
className="action action-secondary"
|
|
259
|
+
data-part="action benchmark-action"
|
|
260
|
+
onClick={() => state.runBenchmark()}
|
|
261
|
+
>
|
|
226
262
|
Benchmark
|
|
227
263
|
</button>
|
|
228
264
|
{csvExportFeature && (
|
|
229
|
-
<button
|
|
265
|
+
<button
|
|
266
|
+
type="button"
|
|
267
|
+
className="action action-secondary"
|
|
268
|
+
data-part="action export-action"
|
|
269
|
+
onClick={() => state.exportCsv()}
|
|
270
|
+
>
|
|
230
271
|
Export CSV
|
|
231
272
|
</button>
|
|
232
273
|
)}
|
|
@@ -237,7 +278,11 @@ export function UiGrid({ options, onRegisterApi, cellRenderer, expandableRendere
|
|
|
237
278
|
</div>
|
|
238
279
|
</header>
|
|
239
280
|
|
|
240
|
-
<section
|
|
281
|
+
<section
|
|
282
|
+
className="metrics-strip"
|
|
283
|
+
data-part="metrics"
|
|
284
|
+
aria-label="Grid performance metrics"
|
|
285
|
+
>
|
|
241
286
|
<article data-part="metric-card">
|
|
242
287
|
<strong>{pipelineMs.toFixed(2)} ms</strong>
|
|
243
288
|
<span>pipeline</span>
|
|
@@ -256,15 +301,22 @@ export function UiGrid({ options, onRegisterApi, cellRenderer, expandableRendere
|
|
|
256
301
|
</article>
|
|
257
302
|
</section>
|
|
258
303
|
|
|
259
|
-
<section
|
|
304
|
+
<section
|
|
305
|
+
className="grid-frame ui-grid"
|
|
306
|
+
data-part="grid-frame"
|
|
307
|
+
role="grid"
|
|
308
|
+
aria-label={options.title ?? 'Data grid'}
|
|
309
|
+
>
|
|
260
310
|
<div className="grid-toolbar" data-part="grid-toolbar">
|
|
261
311
|
<div>
|
|
262
312
|
<strong>{visibleRowCount}</strong>
|
|
263
|
-
<span>
|
|
313
|
+
<span>
|
|
314
|
+
{labels.toolbarOf} {totalRows} {labels.toolbarRows}
|
|
315
|
+
</span>
|
|
264
316
|
</div>
|
|
265
317
|
<p>
|
|
266
|
-
`gridOptions` compatibility layer: sorting, filtering, grouping, column moving,
|
|
267
|
-
and virtualized rendering.
|
|
318
|
+
`gridOptions` compatibility layer: sorting, filtering, grouping, column moving,
|
|
319
|
+
templating, and virtualized rendering.
|
|
268
320
|
</p>
|
|
269
321
|
</div>
|
|
270
322
|
|
|
@@ -276,13 +328,22 @@ export function UiGrid({ options, onRegisterApi, cellRenderer, expandableRendere
|
|
|
276
328
|
role="row"
|
|
277
329
|
style={{ gridTemplateColumns }}
|
|
278
330
|
>
|
|
279
|
-
{visibleColumns.map((column) =>
|
|
331
|
+
{visibleColumns.map((column) => {
|
|
332
|
+
const pinned = state.isPinned(column);
|
|
333
|
+
const pinOffset = pinned ? state.pinnedOffset(column) : null;
|
|
334
|
+
return (
|
|
280
335
|
<div
|
|
281
336
|
key={column.name}
|
|
282
|
-
className={`header-cell ui-grid-header-cell${sortingFeature && state.sortDirection(column) !== 'none' ? ' is-active' : ''}`}
|
|
337
|
+
className={`header-cell ui-grid-header-cell${sortingFeature && state.sortDirection(column) !== 'none' ? ' is-active' : ''}${pinned ? ' is-pinned' : ''}`}
|
|
283
338
|
data-part="header-cell"
|
|
284
339
|
role="columnheader"
|
|
285
|
-
aria-sort={sortingFeature ? state.sortAriaSort(column) as any : undefined}
|
|
340
|
+
aria-sort={sortingFeature ? (state.sortAriaSort(column) as any) : undefined}
|
|
341
|
+
style={{
|
|
342
|
+
position: pinned ? 'sticky' : undefined,
|
|
343
|
+
left: pinOffset?.side === 'left' ? pinOffset.offset : undefined,
|
|
344
|
+
right: pinOffset?.side === 'right' ? pinOffset.offset : undefined,
|
|
345
|
+
zIndex: pinned ? 2 : undefined,
|
|
346
|
+
}}
|
|
286
347
|
>
|
|
287
348
|
<span className="header-label">{state.headerLabel(column)}</span>
|
|
288
349
|
|
|
@@ -297,36 +358,81 @@ export function UiGrid({ options, onRegisterApi, cellRenderer, expandableRendere
|
|
|
297
358
|
onClick={() => state.toggleSort(column)}
|
|
298
359
|
>
|
|
299
360
|
{renderSortIcon(column)}
|
|
300
|
-
<span className="sr-only ui-grid-sr-only">
|
|
361
|
+
<span className="sr-only ui-grid-sr-only">
|
|
362
|
+
{state.sortButtonLabel(column)}
|
|
363
|
+
</span>
|
|
301
364
|
</button>
|
|
302
365
|
)}
|
|
303
366
|
|
|
304
|
-
{groupingFeature &&
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
367
|
+
{groupingFeature &&
|
|
368
|
+
state.isGroupingEnabled() &&
|
|
369
|
+
column.enableGrouping !== false && (
|
|
370
|
+
<button
|
|
371
|
+
type="button"
|
|
372
|
+
className={`chip-action${state.isGrouped(column) ? ' chip-action-active' : ''}`}
|
|
373
|
+
data-part="group-toggle"
|
|
374
|
+
aria-label={state.groupingButtonLabel(column)}
|
|
375
|
+
title={state.groupingButtonLabel(column)}
|
|
376
|
+
onClick={(e) => state.toggleGrouping(column, e)}
|
|
377
|
+
>
|
|
378
|
+
<svg viewBox="0 0 24 24" aria-hidden="true" focusable={false}>
|
|
379
|
+
<path d="M4 6h8v4H4V6Zm0 8h8v4H4v-4Zm10-8h6v4h-6V6Zm0 8h6v4h-6v-4Z" />
|
|
380
|
+
</svg>
|
|
381
|
+
<span className="sr-only ui-grid-sr-only">
|
|
382
|
+
{state.groupingButtonLabel(column)}
|
|
383
|
+
</span>
|
|
384
|
+
</button>
|
|
385
|
+
)}
|
|
386
|
+
{state.pinningFeature &&
|
|
387
|
+
state.isPinningEnabled() &&
|
|
388
|
+
state.isColumnPinnable(column) && (
|
|
389
|
+
<button
|
|
390
|
+
type="button"
|
|
391
|
+
className={`chip-action${pinned ? ' chip-action-active' : ''}`}
|
|
392
|
+
data-part="pin-toggle"
|
|
393
|
+
aria-label={pinned ? labels.unpin : labels.pinLeft}
|
|
394
|
+
title={pinned ? labels.unpin : labels.pinLeft}
|
|
395
|
+
onClick={() => state.togglePin(column)}
|
|
396
|
+
>
|
|
397
|
+
<svg viewBox="0 0 24 24" aria-hidden="true" focusable={false}>
|
|
398
|
+
<path d="M16 12V4h1V2H7v2h1v8l-2 2v2h5v6l1 1 1-1v-6h5v-2l-2-2z" />
|
|
399
|
+
</svg>
|
|
400
|
+
<span className="sr-only ui-grid-sr-only">
|
|
401
|
+
{pinned ? labels.unpin : labels.pinLeft}
|
|
402
|
+
</span>
|
|
403
|
+
</button>
|
|
404
|
+
)}
|
|
319
405
|
</div>
|
|
320
406
|
</div>
|
|
321
|
-
|
|
407
|
+
);
|
|
408
|
+
})}
|
|
322
409
|
</div>
|
|
323
410
|
|
|
324
411
|
{/* Filter row */}
|
|
325
412
|
{filteringFeature && state.isFilteringEnabled() && (
|
|
326
|
-
<div
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
413
|
+
<div
|
|
414
|
+
className="filter-grid ui-grid-header"
|
|
415
|
+
data-part="filters"
|
|
416
|
+
style={{ gridTemplateColumns }}
|
|
417
|
+
>
|
|
418
|
+
{visibleColumns.map((column) => {
|
|
419
|
+
const pinned = state.isPinned(column);
|
|
420
|
+
const pinOffset = pinned ? state.pinnedOffset(column) : null;
|
|
421
|
+
return (
|
|
422
|
+
<label
|
|
423
|
+
key={column.name}
|
|
424
|
+
className={`filter-cell ui-grid-filter-container${pinned ? ' is-pinned' : ''}`}
|
|
425
|
+
data-part="filter-cell"
|
|
426
|
+
style={{
|
|
427
|
+
position: pinned ? 'sticky' : undefined,
|
|
428
|
+
left: pinOffset?.side === 'left' ? pinOffset.offset : undefined,
|
|
429
|
+
right: pinOffset?.side === 'right' ? pinOffset.offset : undefined,
|
|
430
|
+
zIndex: pinned ? 2 : undefined,
|
|
431
|
+
}}
|
|
432
|
+
>
|
|
433
|
+
<span className="sr-only ui-grid-sr-only">
|
|
434
|
+
{labels.filterColumn} {state.headerLabel(column)}
|
|
435
|
+
</span>
|
|
330
436
|
<input
|
|
331
437
|
className="ui-grid-filter-input"
|
|
332
438
|
type="text"
|
|
@@ -336,7 +442,8 @@ export function UiGrid({ options, onRegisterApi, cellRenderer, expandableRendere
|
|
|
336
442
|
onChange={(e) => state.updateFilter(column.name, e.target.value)}
|
|
337
443
|
/>
|
|
338
444
|
</label>
|
|
339
|
-
|
|
445
|
+
);
|
|
446
|
+
})}
|
|
340
447
|
</div>
|
|
341
448
|
)}
|
|
342
449
|
|
|
@@ -387,7 +494,12 @@ export function UiGrid({ options, onRegisterApi, cellRenderer, expandableRendere
|
|
|
387
494
|
|
|
388
495
|
{/* Pagination footer */}
|
|
389
496
|
{paginationFeature && state.showPaginationControls() && (
|
|
390
|
-
<footer
|
|
497
|
+
<footer
|
|
498
|
+
className="pagination-bar ui-grid-pagination"
|
|
499
|
+
data-part="pagination"
|
|
500
|
+
role="navigation"
|
|
501
|
+
aria-label={labels.paginationPage}
|
|
502
|
+
>
|
|
391
503
|
<p>{state.paginationSummary()}</p>
|
|
392
504
|
<div className="pagination-controls">
|
|
393
505
|
<button
|
|
@@ -397,12 +509,20 @@ export function UiGrid({ options, onRegisterApi, cellRenderer, expandableRendere
|
|
|
397
509
|
disabled={paginationCurrentPage <= 1}
|
|
398
510
|
onClick={() => state.previousPage()}
|
|
399
511
|
>
|
|
400
|
-
<svg
|
|
512
|
+
<svg
|
|
513
|
+
className="pagination-icon"
|
|
514
|
+
viewBox="0 0 24 24"
|
|
515
|
+
aria-hidden="true"
|
|
516
|
+
focusable={false}
|
|
517
|
+
>
|
|
401
518
|
<path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z" />
|
|
402
519
|
</svg>
|
|
403
520
|
<span className="sr-only">{labels.paginationPrevious}</span>
|
|
404
521
|
</button>
|
|
405
|
-
<span>
|
|
522
|
+
<span>
|
|
523
|
+
{labels.paginationPage} {paginationCurrentPage} {labels.paginationOf}{' '}
|
|
524
|
+
{paginationTotalPages}
|
|
525
|
+
</span>
|
|
406
526
|
<button
|
|
407
527
|
type="button"
|
|
408
528
|
className="action action-secondary pagination-button"
|
|
@@ -410,7 +530,12 @@ export function UiGrid({ options, onRegisterApi, cellRenderer, expandableRendere
|
|
|
410
530
|
disabled={paginationCurrentPage >= paginationTotalPages}
|
|
411
531
|
onClick={() => state.nextPage()}
|
|
412
532
|
>
|
|
413
|
-
<svg
|
|
533
|
+
<svg
|
|
534
|
+
className="pagination-icon"
|
|
535
|
+
viewBox="0 0 24 24"
|
|
536
|
+
aria-hidden="true"
|
|
537
|
+
focusable={false}
|
|
538
|
+
>
|
|
414
539
|
<path d="M8.59 16.59L10 18l6-6-6-6-1.41 1.41L13.17 12z" />
|
|
415
540
|
</svg>
|
|
416
541
|
<span className="sr-only">{labels.paginationNext}</span>
|
|
@@ -424,7 +549,9 @@ export function UiGrid({ options, onRegisterApi, cellRenderer, expandableRendere
|
|
|
424
549
|
onChange={(e) => state.onPageSizeChange(e.target.value)}
|
|
425
550
|
>
|
|
426
551
|
{state.pageSizeOptions().map((size) => (
|
|
427
|
-
<option key={size} value={size}>
|
|
552
|
+
<option key={size} value={size}>
|
|
553
|
+
{size}
|
|
554
|
+
</option>
|
|
428
555
|
))}
|
|
429
556
|
</select>
|
|
430
557
|
</label>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import type { GridColumnDef } from '@ornery/ui-grid';
|
|
3
|
+
import {
|
|
4
|
+
buildGridTemplateColumns,
|
|
5
|
+
computeViewportHeightPx,
|
|
6
|
+
computeViewportRows,
|
|
7
|
+
formatPaginationSummary,
|
|
8
|
+
orderVisibleColumns,
|
|
9
|
+
resolveBenchmarkIterations,
|
|
10
|
+
} from './gridStateMath';
|
|
11
|
+
|
|
12
|
+
describe('gridStateMath', () => {
|
|
13
|
+
const columns: GridColumnDef[] = [
|
|
14
|
+
{ name: 'status', width: '2fr' },
|
|
15
|
+
{ name: 'owner', visible: false },
|
|
16
|
+
{ name: 'revenue', width: '120px' },
|
|
17
|
+
{ name: 'customer' },
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
it('orders only visible columns by the provided column order', () => {
|
|
21
|
+
expect(orderVisibleColumns(columns, ['customer', 'revenue', 'status', 'owner']).map((column) => column.name)).toEqual([
|
|
22
|
+
'customer',
|
|
23
|
+
'revenue',
|
|
24
|
+
'status',
|
|
25
|
+
]);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('builds grid template columns deterministically', () => {
|
|
29
|
+
expect(buildGridTemplateColumns(orderVisibleColumns(columns, ['status', 'revenue', 'customer']))).toBe('2fr 120px minmax(11rem, max-content)');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('resolves benchmark iterations with a minimum of one', () => {
|
|
33
|
+
expect(resolveBenchmarkIterations(undefined, undefined)).toBe(25);
|
|
34
|
+
expect(resolveBenchmarkIterations(undefined, 7)).toBe(7);
|
|
35
|
+
expect(resolveBenchmarkIterations(0, 7)).toBe(1);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('formats pagination summaries', () => {
|
|
39
|
+
expect(formatPaginationSummary(0, 0, 0)).toBe('0-0 of 0');
|
|
40
|
+
expect(formatPaginationSummary(42, 10, 19)).toBe('11-20 of 42');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('computes viewport height and viewport rows', () => {
|
|
44
|
+
expect(computeViewportHeightPx(undefined, undefined)).toBe('560px');
|
|
45
|
+
expect(computeViewportHeightPx(620, 480)).toBe('620px');
|
|
46
|
+
expect(computeViewportRows(undefined, undefined)).toBe(13);
|
|
47
|
+
expect(computeViewportRows(220, 44)).toBe(5);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { GridColumnDef } from '@ornery/ui-grid';
|
|
2
|
+
import { gridColumnWidth } from '@ornery/ui-grid';
|
|
3
|
+
|
|
4
|
+
export function orderVisibleColumns(columns: readonly GridColumnDef[], order: readonly string[]): GridColumnDef[] {
|
|
5
|
+
return [...columns]
|
|
6
|
+
.filter((column) => column.visible !== false)
|
|
7
|
+
.sort((left, right) => order.indexOf(left.name) - order.indexOf(right.name));
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function buildGridTemplateColumns(columns: readonly GridColumnDef[]): string {
|
|
11
|
+
return columns.map((column) => gridColumnWidth(column)).join(' ');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function resolveBenchmarkIterations(iterations?: number, configuredIterations?: number): number {
|
|
15
|
+
return Math.max(1, iterations ?? configuredIterations ?? 25);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function formatPaginationSummary(totalItems: number, firstRowIndex: number, lastRowIndex: number): string {
|
|
19
|
+
if (totalItems === 0) {
|
|
20
|
+
return '0-0 of 0';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return `${firstRowIndex + 1}-${lastRowIndex + 1} of ${totalItems}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function computeViewportHeightPx(viewportHeight?: number, autoViewportHeight?: number | null): string {
|
|
27
|
+
return `${viewportHeight ?? autoViewportHeight ?? 560}px`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function computeViewportRows(viewportHeight?: number, rowHeight?: number): number {
|
|
31
|
+
return Math.max(1, Math.ceil((viewportHeight ?? 560) / (rowHeight ?? 44)));
|
|
32
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -4,6 +4,8 @@ export { useGridState } from './useGridState';
|
|
|
4
4
|
export type { UseGridStateResult } from './useGridState';
|
|
5
5
|
export { useVirtualScroll } from './useVirtualScroll';
|
|
6
6
|
export type { UseVirtualScrollOptions, UseVirtualScrollResult } from './useVirtualScroll';
|
|
7
|
+
export { orderVisibleColumns, buildGridTemplateColumns, resolveBenchmarkIterations, formatPaginationSummary, computeViewportHeightPx, computeViewportRows } from './gridStateMath';
|
|
8
|
+
export { enableReactUiGridWasmEngine, registerReactUiGridWasmEngineFromModule } from './rustWasmGridEngine';
|
|
7
9
|
|
|
8
10
|
export type {
|
|
9
11
|
GridOptions,
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
activeGridEngineBackend,
|
|
4
|
+
clearRustWasmGridEngine,
|
|
5
|
+
defaultGridEngine,
|
|
6
|
+
} from '@ornery/ui-grid';
|
|
7
|
+
import { registerReactUiGridWasmEngineFromModule } from './rustWasmGridEngine';
|
|
8
|
+
import { SORT_DIRECTIONS } from '@ornery/ui-grid';
|
|
9
|
+
import type { BuildGridPipelineContext, PipelineResult } from '@ornery/ui-grid';
|
|
10
|
+
|
|
11
|
+
function createContext(): BuildGridPipelineContext {
|
|
12
|
+
return {
|
|
13
|
+
options: {
|
|
14
|
+
id: 'react-engine-spec',
|
|
15
|
+
data: [
|
|
16
|
+
{ id: 'row-1', owner: 'Alice' },
|
|
17
|
+
{ id: 'row-2', owner: 'Bob' },
|
|
18
|
+
],
|
|
19
|
+
columnDefs: [{ name: 'owner' }],
|
|
20
|
+
},
|
|
21
|
+
columns: [{ name: 'owner' }],
|
|
22
|
+
activeFilters: {},
|
|
23
|
+
sortState: { columnName: null, direction: SORT_DIRECTIONS.none },
|
|
24
|
+
groupByColumns: [],
|
|
25
|
+
collapsedGroups: {},
|
|
26
|
+
hiddenRowReasons: {},
|
|
27
|
+
expandedRows: {},
|
|
28
|
+
expandedTreeRows: {},
|
|
29
|
+
currentPage: 1,
|
|
30
|
+
pageSize: 0,
|
|
31
|
+
rowSize: 44,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe('rustWasmGridEngine', () => {
|
|
36
|
+
beforeEach(() => {
|
|
37
|
+
clearRustWasmGridEngine();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('registers the real module shape into the shared engine seam', () => {
|
|
41
|
+
const sentinel: PipelineResult = {
|
|
42
|
+
visibleRows: [],
|
|
43
|
+
displayItems: [],
|
|
44
|
+
virtualizationEnabled: true,
|
|
45
|
+
pipelineMs: 0,
|
|
46
|
+
totalItems: 77,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
registerReactUiGridWasmEngineFromModule({
|
|
50
|
+
build_pipeline_js: () => sentinel,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
expect(defaultGridEngine.buildPipeline(createContext())).toBe(sentinel);
|
|
54
|
+
expect(activeGridEngineBackend()).toBe('rust-wasm');
|
|
55
|
+
});
|
|
56
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { BuildGridPipelineContext, PipelineResult } from '@ornery/ui-grid';
|
|
2
|
+
import { registerRustWasmGridEngine } from '@ornery/ui-grid';
|
|
3
|
+
|
|
4
|
+
const uiGridWasmModulePath = '../../../dist/ui-grid-wasm/ui_grid_wasm.js';
|
|
5
|
+
|
|
6
|
+
type UiGridWasmModule = {
|
|
7
|
+
build_pipeline_js(context: unknown): PipelineResult;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export function registerReactUiGridWasmEngineFromModule(module: UiGridWasmModule): void {
|
|
11
|
+
registerRustWasmGridEngine({
|
|
12
|
+
buildPipeline(context: BuildGridPipelineContext): PipelineResult {
|
|
13
|
+
return module.build_pipeline_js(context);
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function enableReactUiGridWasmEngine(): Promise<void> {
|
|
19
|
+
const module = await import(/* @vite-ignore */ uiGridWasmModulePath);
|
|
20
|
+
registerReactUiGridWasmEngineFromModule(module);
|
|
21
|
+
}
|