@ornery/ui-grid-react 0.1.4
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/CLAUDE.md +283 -0
- package/demo/index.html +16 -0
- package/demo/main.tsx +95 -0
- package/demo/vite.config.ts +13 -0
- package/dist/index.d.mts +133 -0
- package/dist/index.d.ts +133 -0
- package/dist/index.js +2020 -0
- package/dist/index.mjs +2050 -0
- package/package.json +41 -0
- package/src/UiGrid.test.tsx +370 -0
- package/src/UiGrid.tsx +440 -0
- package/src/index.ts +23 -0
- package/src/ui-grid.css +521 -0
- package/src/useGridState.ts +1414 -0
- package/src/useVirtualScroll.ts +44 -0
- package/tsconfig.json +14 -0
- package/vitest.config.ts +14 -0
package/src/UiGrid.tsx
ADDED
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type {
|
|
3
|
+
GridOptions,
|
|
4
|
+
GridCellTemplateContext,
|
|
5
|
+
GridExpandableTemplateContext,
|
|
6
|
+
UiGridApi,
|
|
7
|
+
GridColumnDef,
|
|
8
|
+
GridRow,
|
|
9
|
+
} from '@ornery/ui-grid';
|
|
10
|
+
import type { DisplayItem, RowItem } from '@ornery/ui-grid';
|
|
11
|
+
import { useGridState } from './useGridState';
|
|
12
|
+
import { useVirtualScroll } from './useVirtualScroll';
|
|
13
|
+
|
|
14
|
+
export interface UiGridProps {
|
|
15
|
+
options: GridOptions;
|
|
16
|
+
onRegisterApi?: (api: UiGridApi) => void;
|
|
17
|
+
cellRenderer?: (context: GridCellTemplateContext) => React.ReactNode;
|
|
18
|
+
expandableRenderer?: (context: GridExpandableTemplateContext) => React.ReactNode;
|
|
19
|
+
className?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function UiGrid({ options, onRegisterApi, cellRenderer, expandableRenderer, className }: UiGridProps) {
|
|
23
|
+
const state = useGridState(options, onRegisterApi);
|
|
24
|
+
|
|
25
|
+
const {
|
|
26
|
+
pipeline,
|
|
27
|
+
visibleColumns,
|
|
28
|
+
labels,
|
|
29
|
+
gridTemplateColumns,
|
|
30
|
+
gridContainerRef,
|
|
31
|
+
displayItems,
|
|
32
|
+
virtualizationEnabled,
|
|
33
|
+
pipelineMs,
|
|
34
|
+
visibleRowCount,
|
|
35
|
+
totalRows,
|
|
36
|
+
benchmarkResult,
|
|
37
|
+
rowSize,
|
|
38
|
+
viewportHeightPx,
|
|
39
|
+
editingValue,
|
|
40
|
+
sortingFeature,
|
|
41
|
+
filteringFeature,
|
|
42
|
+
groupingFeature,
|
|
43
|
+
paginationFeature,
|
|
44
|
+
cellEditFeature,
|
|
45
|
+
expandableFeature,
|
|
46
|
+
treeViewFeature,
|
|
47
|
+
csvExportFeature,
|
|
48
|
+
paginationCurrentPage,
|
|
49
|
+
paginationTotalPages,
|
|
50
|
+
paginationSelectedPageSize,
|
|
51
|
+
} = state;
|
|
52
|
+
|
|
53
|
+
const virtualScroll = useVirtualScroll({
|
|
54
|
+
itemCount: displayItems.length,
|
|
55
|
+
itemSize: rowSize,
|
|
56
|
+
viewportHeight: options.viewportHeight ?? 560,
|
|
57
|
+
overscan: 3,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const itemsToRender = virtualizationEnabled
|
|
61
|
+
? displayItems.slice(virtualScroll.visibleRange.start, virtualScroll.visibleRange.end)
|
|
62
|
+
: displayItems;
|
|
63
|
+
|
|
64
|
+
const onViewportScroll = (event: React.UIEvent<HTMLDivElement>) => {
|
|
65
|
+
virtualScroll.onScroll(event);
|
|
66
|
+
const startIndex = Math.floor(event.currentTarget.scrollTop / rowSize);
|
|
67
|
+
state.onViewportScroll(startIndex);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
function renderDisplayItem(item: DisplayItem) {
|
|
71
|
+
if (groupingFeature && state.isGroupItem(item)) {
|
|
72
|
+
return (
|
|
73
|
+
<button
|
|
74
|
+
key={item.id}
|
|
75
|
+
type="button"
|
|
76
|
+
className="group-row ui-grid-row ui-grid-group-row"
|
|
77
|
+
data-part="group-row"
|
|
78
|
+
role="row"
|
|
79
|
+
aria-expanded={!item.collapsed}
|
|
80
|
+
style={{ gridColumn: '1 / -1', paddingInlineStart: `${item.depth * 1.25 + 1}rem` }}
|
|
81
|
+
onClick={() => state.toggleGroup(item)}
|
|
82
|
+
>
|
|
83
|
+
<strong>{item.field}: {item.label}</strong>
|
|
84
|
+
<span>{item.count} {labels.groupRowsSuffix}</span>
|
|
85
|
+
<svg className="toggle-icon group-disclosure-icon" viewBox="0 0 24 24" aria-hidden="true" focusable={false}>
|
|
86
|
+
<path d={item.collapsed ? 'M10 7l5 5-5 5z' : 'M7 10l5 5 5-5z'} />
|
|
87
|
+
</svg>
|
|
88
|
+
<span className="sr-only ui-grid-sr-only">{state.groupDisclosureLabel(item)}</span>
|
|
89
|
+
</button>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (expandableFeature && state.isExpandableItem(item)) {
|
|
94
|
+
const ctx = state.expandedContext(item.row);
|
|
95
|
+
return (
|
|
96
|
+
<div
|
|
97
|
+
key={item.id}
|
|
98
|
+
className="expandable-row ui-grid-row ui-grid-expandable-row"
|
|
99
|
+
data-part="expandable-row"
|
|
100
|
+
style={{ gridColumn: '1 / -1', minHeight: `${item.row.expandedRowHeight}px` }}
|
|
101
|
+
>
|
|
102
|
+
{expandableRenderer?.(ctx)}
|
|
103
|
+
</div>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (item.kind !== 'row') return null;
|
|
108
|
+
const rowItem = item as RowItem;
|
|
109
|
+
|
|
110
|
+
return visibleColumns.map((column) => (
|
|
111
|
+
<div
|
|
112
|
+
key={`${rowItem.row.id}-${column.name}`}
|
|
113
|
+
className={cellClassName(rowItem, column)}
|
|
114
|
+
data-part="body-cell"
|
|
115
|
+
role="gridcell"
|
|
116
|
+
tabIndex={0}
|
|
117
|
+
data-row-id={rowItem.row.id}
|
|
118
|
+
data-col-name={column.name}
|
|
119
|
+
onFocus={() => state.focusCell(rowItem.row, column)}
|
|
120
|
+
onClick={() => state.focusCell(rowItem.row, column)}
|
|
121
|
+
onDoubleClick={(e) => state.handleCellDoubleClick(rowItem.row, column, e)}
|
|
122
|
+
onKeyDown={(e) => state.handleCellKeyDown(rowItem.row, column, e)}
|
|
123
|
+
>
|
|
124
|
+
<div className="cell-shell" style={{ paddingInlineStart: state.cellIndent(rowItem.row, column) }}>
|
|
125
|
+
{treeViewFeature && state.showTreeToggle(rowItem.row, column) && (
|
|
126
|
+
<button
|
|
127
|
+
type="button"
|
|
128
|
+
className="row-toggle row-toggle-tree"
|
|
129
|
+
data-part="tree-toggle"
|
|
130
|
+
aria-label={state.treeToggleLabel(rowItem.row)}
|
|
131
|
+
aria-expanded={state.isTreeRowExpanded(rowItem.row)}
|
|
132
|
+
onClick={(e) => state.toggleTreeRow(rowItem.row, e)}
|
|
133
|
+
>
|
|
134
|
+
<svg className="toggle-icon" viewBox="0 0 24 24" aria-hidden="true" focusable={false}>
|
|
135
|
+
<path d={state.isTreeRowExpanded(rowItem.row) ? 'M7 10l5 5 5-5z' : 'M10 7l5 5-5 5z'} />
|
|
136
|
+
</svg>
|
|
137
|
+
</button>
|
|
138
|
+
)}
|
|
139
|
+
{expandableFeature && state.showExpandToggle(rowItem.row, column) && (
|
|
140
|
+
<button
|
|
141
|
+
type="button"
|
|
142
|
+
className="row-toggle row-toggle-expand"
|
|
143
|
+
data-part="expand-toggle"
|
|
144
|
+
aria-label={state.expandToggleLabel(rowItem.row)}
|
|
145
|
+
aria-expanded={rowItem.row.expanded}
|
|
146
|
+
onClick={(e) => state.toggleRowExpansion(rowItem.row, e)}
|
|
147
|
+
>
|
|
148
|
+
<svg className="toggle-icon" viewBox="0 0 24 24" aria-hidden="true" focusable={false}>
|
|
149
|
+
<path d={rowItem.row.expanded ? 'M7 10l5 5 5-5z' : 'M10 7l5 5-5 5z'} />
|
|
150
|
+
</svg>
|
|
151
|
+
</button>
|
|
152
|
+
)}
|
|
153
|
+
<span className="cell-value">
|
|
154
|
+
{cellEditFeature && state.isEditingCell(rowItem.row, column) ? (
|
|
155
|
+
<input
|
|
156
|
+
className="cell-editor"
|
|
157
|
+
data-row-id={rowItem.row.id}
|
|
158
|
+
data-col-name={column.name}
|
|
159
|
+
aria-label={state.headerLabel(column)}
|
|
160
|
+
type={state.editorInputType(column)}
|
|
161
|
+
defaultValue={editingValue}
|
|
162
|
+
onChange={(e) => state.updateEditingValue(e.target.value)}
|
|
163
|
+
onKeyDown={(e) => state.handleEditorKeyDown(e)}
|
|
164
|
+
onBlur={(e) => state.handleEditorBlur(e)}
|
|
165
|
+
/>
|
|
166
|
+
) : cellRenderer ? (
|
|
167
|
+
cellRenderer(state.cellContext(rowItem.row, column)) ?? state.displayValue(rowItem.row, column)
|
|
168
|
+
) : (
|
|
169
|
+
state.displayValue(rowItem.row, column)
|
|
170
|
+
)}
|
|
171
|
+
</span>
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
174
|
+
));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function cellClassName(item: RowItem, column: GridColumnDef): string {
|
|
178
|
+
const classes = ['body-cell', 'ui-grid-cell'];
|
|
179
|
+
if (state.isOddStripedRow(item)) classes.push('body-cell-odd');
|
|
180
|
+
if (column.align === 'center') classes.push('align-center');
|
|
181
|
+
if (column.align === 'end') classes.push('align-end');
|
|
182
|
+
if (state.isFocusedCell(item.row, column)) classes.push('cell-focused');
|
|
183
|
+
if (cellEditFeature && state.isEditingCell(item.row, column)) classes.push('cell-editing');
|
|
184
|
+
return classes.join(' ');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function renderSortIcon(column: GridColumnDef) {
|
|
188
|
+
const direction = state.sortDirection(column);
|
|
189
|
+
switch (direction) {
|
|
190
|
+
case 'asc':
|
|
191
|
+
return (
|
|
192
|
+
<svg viewBox="0 0 24 24" aria-hidden="true" focusable={false}>
|
|
193
|
+
<path d="M12 5l-6 6h4v8h4v-8h4z" />
|
|
194
|
+
</svg>
|
|
195
|
+
);
|
|
196
|
+
case 'desc':
|
|
197
|
+
return (
|
|
198
|
+
<svg viewBox="0 0 24 24" aria-hidden="true" focusable={false}>
|
|
199
|
+
<path d="M12 19l6-6h-4V5h-4v8H6z" />
|
|
200
|
+
</svg>
|
|
201
|
+
);
|
|
202
|
+
default:
|
|
203
|
+
return (
|
|
204
|
+
<svg viewBox="0 0 24 24" aria-hidden="true" focusable={false}>
|
|
205
|
+
<path d="M7 6h10v2H7V6Zm0 5h7v2H7v-2Zm0 5h4v2H7v-2Z" />
|
|
206
|
+
</svg>
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return (
|
|
212
|
+
<div className={`ui-grid-host ${className ?? ''}`} ref={gridContainerRef}>
|
|
213
|
+
<section className="grid-shell ui-grid-shell" data-part="shell">
|
|
214
|
+
<header className="grid-hero ui-grid-toolbar-shell" data-part="hero">
|
|
215
|
+
<div>
|
|
216
|
+
<p className="eyebrow">React wrapper for @ornery/ui-grid</p>
|
|
217
|
+
<h1>{options.title ?? 'UI Grid'}</h1>
|
|
218
|
+
<p className="deck">
|
|
219
|
+
Familiar `gridOptions` and `onRegisterApi`, built with React hooks, virtualization,
|
|
220
|
+
grouping, sorting, filtering, and column ordering.
|
|
221
|
+
</p>
|
|
222
|
+
</div>
|
|
223
|
+
|
|
224
|
+
<div className="hero-actions">
|
|
225
|
+
<button type="button" className="action action-secondary" data-part="action benchmark-action" onClick={() => state.runBenchmark()}>
|
|
226
|
+
Benchmark
|
|
227
|
+
</button>
|
|
228
|
+
{csvExportFeature && (
|
|
229
|
+
<button type="button" className="action action-secondary" data-part="action export-action" onClick={() => state.exportCsv()}>
|
|
230
|
+
Export CSV
|
|
231
|
+
</button>
|
|
232
|
+
)}
|
|
233
|
+
<div className="stats-card" data-part="stats-card">
|
|
234
|
+
<span>{visibleRowCount}</span>
|
|
235
|
+
<small>{labels.statsVisibleRows}</small>
|
|
236
|
+
</div>
|
|
237
|
+
</div>
|
|
238
|
+
</header>
|
|
239
|
+
|
|
240
|
+
<section className="metrics-strip" data-part="metrics" aria-label="Grid performance metrics">
|
|
241
|
+
<article data-part="metric-card">
|
|
242
|
+
<strong>{pipelineMs.toFixed(2)} ms</strong>
|
|
243
|
+
<span>pipeline</span>
|
|
244
|
+
</article>
|
|
245
|
+
<article data-part="metric-card">
|
|
246
|
+
<strong>{virtualizationEnabled ? 'On' : 'Off'}</strong>
|
|
247
|
+
<span>virtualization</span>
|
|
248
|
+
</article>
|
|
249
|
+
<article data-part="metric-card">
|
|
250
|
+
<strong>{state.groupByColumns.length}</strong>
|
|
251
|
+
<span>group columns</span>
|
|
252
|
+
</article>
|
|
253
|
+
<article data-part="metric-card">
|
|
254
|
+
<strong>{benchmarkResult?.averageMs?.toFixed(2) || '—'}</strong>
|
|
255
|
+
<span>benchmark avg</span>
|
|
256
|
+
</article>
|
|
257
|
+
</section>
|
|
258
|
+
|
|
259
|
+
<section className="grid-frame ui-grid" data-part="grid-frame" role="grid" aria-label={options.title ?? 'Data grid'}>
|
|
260
|
+
<div className="grid-toolbar" data-part="grid-toolbar">
|
|
261
|
+
<div>
|
|
262
|
+
<strong>{visibleRowCount}</strong>
|
|
263
|
+
<span>{labels.toolbarOf} {totalRows} {labels.toolbarRows}</span>
|
|
264
|
+
</div>
|
|
265
|
+
<p>
|
|
266
|
+
`gridOptions` compatibility layer: sorting, filtering, grouping, column moving, templating,
|
|
267
|
+
and virtualized rendering.
|
|
268
|
+
</p>
|
|
269
|
+
</div>
|
|
270
|
+
|
|
271
|
+
<div className="grid-table ui-grid-contents-wrapper" data-part="grid-table">
|
|
272
|
+
{/* Header row */}
|
|
273
|
+
<div
|
|
274
|
+
className="header-grid ui-grid-header ui-grid-header-canvas"
|
|
275
|
+
data-part="header"
|
|
276
|
+
role="row"
|
|
277
|
+
style={{ gridTemplateColumns }}
|
|
278
|
+
>
|
|
279
|
+
{visibleColumns.map((column) => (
|
|
280
|
+
<div
|
|
281
|
+
key={column.name}
|
|
282
|
+
className={`header-cell ui-grid-header-cell${sortingFeature && state.sortDirection(column) !== 'none' ? ' is-active' : ''}`}
|
|
283
|
+
data-part="header-cell"
|
|
284
|
+
role="columnheader"
|
|
285
|
+
aria-sort={sortingFeature ? state.sortAriaSort(column) as any : undefined}
|
|
286
|
+
>
|
|
287
|
+
<span className="header-label">{state.headerLabel(column)}</span>
|
|
288
|
+
|
|
289
|
+
<div className="header-actions">
|
|
290
|
+
{sortingFeature && (
|
|
291
|
+
<button
|
|
292
|
+
type="button"
|
|
293
|
+
className={`header-action${!state.isColumnSortable(column) ? ' header-action-disabled' : ''}`}
|
|
294
|
+
disabled={!state.isColumnSortable(column)}
|
|
295
|
+
aria-label={state.sortButtonLabel(column)}
|
|
296
|
+
title={state.sortButtonLabel(column)}
|
|
297
|
+
onClick={() => state.toggleSort(column)}
|
|
298
|
+
>
|
|
299
|
+
{renderSortIcon(column)}
|
|
300
|
+
<span className="sr-only ui-grid-sr-only">{state.sortButtonLabel(column)}</span>
|
|
301
|
+
</button>
|
|
302
|
+
)}
|
|
303
|
+
|
|
304
|
+
{groupingFeature && state.isGroupingEnabled() && column.enableGrouping !== false && (
|
|
305
|
+
<button
|
|
306
|
+
type="button"
|
|
307
|
+
className={`chip-action${state.isGrouped(column) ? ' chip-action-active' : ''}`}
|
|
308
|
+
data-part="group-toggle"
|
|
309
|
+
aria-label={state.groupingButtonLabel(column)}
|
|
310
|
+
title={state.groupingButtonLabel(column)}
|
|
311
|
+
onClick={(e) => state.toggleGrouping(column, e)}
|
|
312
|
+
>
|
|
313
|
+
<svg viewBox="0 0 24 24" aria-hidden="true" focusable={false}>
|
|
314
|
+
<path d="M4 6h8v4H4V6Zm0 8h8v4H4v-4Zm10-8h6v4h-6V6Zm0 8h6v4h-6v-4Z" />
|
|
315
|
+
</svg>
|
|
316
|
+
<span className="sr-only ui-grid-sr-only">{state.groupingButtonLabel(column)}</span>
|
|
317
|
+
</button>
|
|
318
|
+
)}
|
|
319
|
+
</div>
|
|
320
|
+
</div>
|
|
321
|
+
))}
|
|
322
|
+
</div>
|
|
323
|
+
|
|
324
|
+
{/* Filter row */}
|
|
325
|
+
{filteringFeature && state.isFilteringEnabled() && (
|
|
326
|
+
<div className="filter-grid ui-grid-header" data-part="filters" style={{ gridTemplateColumns }}>
|
|
327
|
+
{visibleColumns.map((column) => (
|
|
328
|
+
<label key={column.name} className="filter-cell ui-grid-filter-container" data-part="filter-cell">
|
|
329
|
+
<span className="sr-only ui-grid-sr-only">{labels.filterColumn} {state.headerLabel(column)}</span>
|
|
330
|
+
<input
|
|
331
|
+
className="ui-grid-filter-input"
|
|
332
|
+
type="text"
|
|
333
|
+
defaultValue={state.filterValue(column.name)}
|
|
334
|
+
placeholder={state.filterPlaceholder(column)}
|
|
335
|
+
disabled={state.isFilterInputDisabled(column)}
|
|
336
|
+
onChange={(e) => state.updateFilter(column.name, e.target.value)}
|
|
337
|
+
/>
|
|
338
|
+
</label>
|
|
339
|
+
))}
|
|
340
|
+
</div>
|
|
341
|
+
)}
|
|
342
|
+
|
|
343
|
+
{/* Body */}
|
|
344
|
+
{displayItems.length > 0 ? (
|
|
345
|
+
virtualizationEnabled ? (
|
|
346
|
+
<div
|
|
347
|
+
className="grid-viewport ui-grid-viewport"
|
|
348
|
+
data-part="viewport"
|
|
349
|
+
ref={virtualScroll.viewportRef}
|
|
350
|
+
style={{ height: viewportHeightPx, overflow: 'auto', position: 'relative' }}
|
|
351
|
+
onScroll={onViewportScroll}
|
|
352
|
+
>
|
|
353
|
+
<div style={{ height: `${virtualScroll.totalHeight}px`, position: 'relative' }}>
|
|
354
|
+
<div
|
|
355
|
+
className="body-grid ui-grid-canvas"
|
|
356
|
+
data-part="body"
|
|
357
|
+
role="rowgroup"
|
|
358
|
+
style={{
|
|
359
|
+
gridTemplateColumns,
|
|
360
|
+
position: 'absolute',
|
|
361
|
+
top: 0,
|
|
362
|
+
left: 0,
|
|
363
|
+
right: 0,
|
|
364
|
+
transform: `translateY(${virtualScroll.offsetY}px)`,
|
|
365
|
+
}}
|
|
366
|
+
>
|
|
367
|
+
{itemsToRender.map(renderDisplayItem)}
|
|
368
|
+
</div>
|
|
369
|
+
</div>
|
|
370
|
+
</div>
|
|
371
|
+
) : (
|
|
372
|
+
<div
|
|
373
|
+
className="body-grid ui-grid-canvas"
|
|
374
|
+
data-part="body"
|
|
375
|
+
role="rowgroup"
|
|
376
|
+
style={{ gridTemplateColumns }}
|
|
377
|
+
>
|
|
378
|
+
{displayItems.map(renderDisplayItem)}
|
|
379
|
+
</div>
|
|
380
|
+
)
|
|
381
|
+
) : (
|
|
382
|
+
<div className="empty-state ui-grid-no-row-overlay" data-part="empty-state">
|
|
383
|
+
<strong>{options.emptyMessage ?? labels.emptyHeading}</strong>
|
|
384
|
+
<p>{labels.emptyDescription}</p>
|
|
385
|
+
</div>
|
|
386
|
+
)}
|
|
387
|
+
|
|
388
|
+
{/* Pagination footer */}
|
|
389
|
+
{paginationFeature && state.showPaginationControls() && (
|
|
390
|
+
<footer className="pagination-bar ui-grid-pagination" data-part="pagination" role="navigation" aria-label={labels.paginationPage}>
|
|
391
|
+
<p>{state.paginationSummary()}</p>
|
|
392
|
+
<div className="pagination-controls">
|
|
393
|
+
<button
|
|
394
|
+
type="button"
|
|
395
|
+
className="action action-secondary pagination-button"
|
|
396
|
+
aria-label={labels.paginationPrevious}
|
|
397
|
+
disabled={paginationCurrentPage <= 1}
|
|
398
|
+
onClick={() => state.previousPage()}
|
|
399
|
+
>
|
|
400
|
+
<svg className="pagination-icon" viewBox="0 0 24 24" aria-hidden="true" focusable={false}>
|
|
401
|
+
<path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z" />
|
|
402
|
+
</svg>
|
|
403
|
+
<span className="sr-only">{labels.paginationPrevious}</span>
|
|
404
|
+
</button>
|
|
405
|
+
<span>{labels.paginationPage} {paginationCurrentPage} {labels.paginationOf} {paginationTotalPages}</span>
|
|
406
|
+
<button
|
|
407
|
+
type="button"
|
|
408
|
+
className="action action-secondary pagination-button"
|
|
409
|
+
aria-label={labels.paginationNext}
|
|
410
|
+
disabled={paginationCurrentPage >= paginationTotalPages}
|
|
411
|
+
onClick={() => state.nextPage()}
|
|
412
|
+
>
|
|
413
|
+
<svg className="pagination-icon" viewBox="0 0 24 24" aria-hidden="true" focusable={false}>
|
|
414
|
+
<path d="M8.59 16.59L10 18l6-6-6-6-1.41 1.41L13.17 12z" />
|
|
415
|
+
</svg>
|
|
416
|
+
<span className="sr-only">{labels.paginationNext}</span>
|
|
417
|
+
</button>
|
|
418
|
+
{state.pageSizeOptions().length > 0 && (
|
|
419
|
+
<label className="pagination-size">
|
|
420
|
+
<span className="sr-only">{labels.paginationRows}</span>
|
|
421
|
+
<select
|
|
422
|
+
aria-label={labels.paginationRows}
|
|
423
|
+
value={paginationSelectedPageSize}
|
|
424
|
+
onChange={(e) => state.onPageSizeChange(e.target.value)}
|
|
425
|
+
>
|
|
426
|
+
{state.pageSizeOptions().map((size) => (
|
|
427
|
+
<option key={size} value={size}>{size}</option>
|
|
428
|
+
))}
|
|
429
|
+
</select>
|
|
430
|
+
</label>
|
|
431
|
+
)}
|
|
432
|
+
</div>
|
|
433
|
+
</footer>
|
|
434
|
+
)}
|
|
435
|
+
</div>
|
|
436
|
+
</section>
|
|
437
|
+
</section>
|
|
438
|
+
</div>
|
|
439
|
+
);
|
|
440
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export { UiGrid } from './UiGrid';
|
|
2
|
+
export type { UiGridProps } from './UiGrid';
|
|
3
|
+
export { useGridState } from './useGridState';
|
|
4
|
+
export type { UseGridStateResult } from './useGridState';
|
|
5
|
+
export { useVirtualScroll } from './useVirtualScroll';
|
|
6
|
+
export type { UseVirtualScrollOptions, UseVirtualScrollResult } from './useVirtualScroll';
|
|
7
|
+
|
|
8
|
+
export type {
|
|
9
|
+
GridOptions,
|
|
10
|
+
GridColumnDef,
|
|
11
|
+
GridRow,
|
|
12
|
+
GridRecord,
|
|
13
|
+
GridLabels,
|
|
14
|
+
GridCellTemplateContext,
|
|
15
|
+
GridExpandableTemplateContext,
|
|
16
|
+
GridCellEditableContext,
|
|
17
|
+
GridBenchmarkResult,
|
|
18
|
+
GridSavedState,
|
|
19
|
+
SortState,
|
|
20
|
+
} from '@ornery/ui-grid';
|
|
21
|
+
|
|
22
|
+
export type { UiGridApi } from '@ornery/ui-grid';
|
|
23
|
+
export { DEFAULT_GRID_LABELS } from '@ornery/ui-grid';
|