@ornery/ui-grid-react 0.1.4 → 0.1.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/UiGrid.d.ts +11 -0
- package/dist/UiGrid.d.ts.map +1 -0
- package/dist/gridStateMath.d.ts +8 -0
- package/dist/gridStateMath.d.ts.map +1 -0
- package/dist/index.d.ts +13 -133
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1248 -1202
- package/dist/index.mjs +1198 -1131
- package/dist/mountUiGrid.d.ts +4 -0
- package/dist/mountUiGrid.d.ts.map +1 -0
- package/dist/rustWasmGridEngine.d.ts +8 -0
- package/dist/rustWasmGridEngine.d.ts.map +1 -0
- package/dist/{index.d.mts → useGridState.d.ts} +14 -37
- package/dist/useGridState.d.ts.map +1 -0
- package/dist/useVirtualScroll.d.ts +20 -0
- package/dist/useVirtualScroll.d.ts.map +1 -0
- package/dist/virtualScrollMath.d.ts +17 -0
- package/dist/virtualScrollMath.d.ts.map +1 -0
- package/package.json +3 -3
- package/src/UiGrid.test.tsx +2 -1
- package/src/UiGrid.tsx +330 -74
- package/src/gridStateMath.test.ts +49 -0
- package/src/gridStateMath.ts +32 -0
- package/src/index.ts +3 -0
- package/src/mountUiGrid.tsx +10 -0
- package/src/rustWasmGridEngine.test.ts +56 -0
- package/src/rustWasmGridEngine.ts +23 -0
- package/src/ui-grid.css +161 -1
- package/src/useGridState.ts +664 -343
- package/src/useVirtualScroll.ts +13 -10
- package/src/virtualScrollMath.test.ts +44 -0
- package/src/virtualScrollMath.ts +36 -0
- package/tsconfig.build.json +6 -0
- package/tsconfig.dts.json +15 -0
- package/CLAUDE.md +0 -283
package/src/useVirtualScroll.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useCallback, useRef, useState } from 'react';
|
|
2
|
+
import { calculateVirtualWindow } from './virtualScrollMath';
|
|
2
3
|
|
|
3
4
|
export interface UseVirtualScrollOptions {
|
|
4
5
|
itemCount: number;
|
|
@@ -12,6 +13,7 @@ export interface UseVirtualScrollResult {
|
|
|
12
13
|
totalHeight: number;
|
|
13
14
|
offsetY: number;
|
|
14
15
|
onScroll: (event: React.UIEvent<HTMLDivElement>) => void;
|
|
16
|
+
setScrollTop: (scrollTop: number) => void;
|
|
15
17
|
viewportRef: React.RefObject<HTMLDivElement | null>;
|
|
16
18
|
scrollTop: number;
|
|
17
19
|
}
|
|
@@ -21,23 +23,24 @@ export function useVirtualScroll(options: UseVirtualScrollOptions): UseVirtualSc
|
|
|
21
23
|
const [scrollTop, setScrollTop] = useState(0);
|
|
22
24
|
const viewportRef = useRef<HTMLDivElement | null>(null);
|
|
23
25
|
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
26
|
+
const virtualWindow = calculateVirtualWindow({
|
|
27
|
+
itemCount,
|
|
28
|
+
itemSize,
|
|
29
|
+
viewportHeight,
|
|
30
|
+
overscan,
|
|
31
|
+
scrollTop,
|
|
32
|
+
});
|
|
31
33
|
|
|
32
34
|
const onScroll = useCallback((event: React.UIEvent<HTMLDivElement>) => {
|
|
33
35
|
setScrollTop(event.currentTarget.scrollTop);
|
|
34
36
|
}, []);
|
|
35
37
|
|
|
36
38
|
return {
|
|
37
|
-
visibleRange:
|
|
38
|
-
totalHeight,
|
|
39
|
-
offsetY,
|
|
39
|
+
visibleRange: virtualWindow.visibleRange,
|
|
40
|
+
totalHeight: virtualWindow.totalHeight,
|
|
41
|
+
offsetY: virtualWindow.offsetY,
|
|
40
42
|
onScroll,
|
|
43
|
+
setScrollTop,
|
|
41
44
|
viewportRef,
|
|
42
45
|
scrollTop,
|
|
43
46
|
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { calculateVirtualWindow } from './virtualScrollMath';
|
|
3
|
+
|
|
4
|
+
describe('virtualScrollMath', () => {
|
|
5
|
+
it('calculates the default overscanned window deterministically', () => {
|
|
6
|
+
expect(calculateVirtualWindow({
|
|
7
|
+
itemCount: 100,
|
|
8
|
+
itemSize: 44,
|
|
9
|
+
viewportHeight: 220,
|
|
10
|
+
scrollTop: 0,
|
|
11
|
+
})).toEqual({
|
|
12
|
+
visibleRange: { start: 0, end: 8 },
|
|
13
|
+
totalHeight: 4400,
|
|
14
|
+
offsetY: 0,
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('calculates a scrolled window deterministically', () => {
|
|
19
|
+
expect(calculateVirtualWindow({
|
|
20
|
+
itemCount: 100,
|
|
21
|
+
itemSize: 44,
|
|
22
|
+
viewportHeight: 220,
|
|
23
|
+
overscan: 3,
|
|
24
|
+
scrollTop: 440,
|
|
25
|
+
})).toEqual({
|
|
26
|
+
visibleRange: { start: 7, end: 18 },
|
|
27
|
+
totalHeight: 4400,
|
|
28
|
+
offsetY: 308,
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('handles zero item size safely', () => {
|
|
33
|
+
expect(calculateVirtualWindow({
|
|
34
|
+
itemCount: 10,
|
|
35
|
+
itemSize: 0,
|
|
36
|
+
viewportHeight: 220,
|
|
37
|
+
scrollTop: 88,
|
|
38
|
+
})).toEqual({
|
|
39
|
+
visibleRange: { start: 0, end: 0 },
|
|
40
|
+
totalHeight: 0,
|
|
41
|
+
offsetY: 0,
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export interface VirtualWindowRequest {
|
|
2
|
+
itemCount: number;
|
|
3
|
+
itemSize: number;
|
|
4
|
+
viewportHeight: number;
|
|
5
|
+
overscan?: number;
|
|
6
|
+
scrollTop: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface VirtualWindowResult {
|
|
10
|
+
visibleRange: { start: number; end: number };
|
|
11
|
+
totalHeight: number;
|
|
12
|
+
offsetY: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function calculateVirtualWindow(request: VirtualWindowRequest): VirtualWindowResult {
|
|
16
|
+
const overscan = request.overscan ?? 3;
|
|
17
|
+
|
|
18
|
+
if (request.itemCount <= 0 || request.itemSize <= 0) {
|
|
19
|
+
return {
|
|
20
|
+
visibleRange: { start: 0, end: 0 },
|
|
21
|
+
totalHeight: Math.max(0, request.itemCount) * Math.max(0, request.itemSize),
|
|
22
|
+
offsetY: 0,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const rawStart = Math.floor(request.scrollTop / request.itemSize) - overscan;
|
|
27
|
+
const start = Math.max(0, rawStart);
|
|
28
|
+
const rawEnd = rawStart + Math.ceil(request.viewportHeight / request.itemSize) + 2 * overscan;
|
|
29
|
+
const end = Math.min(request.itemCount, rawEnd);
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
visibleRange: { start, end },
|
|
33
|
+
totalHeight: request.itemCount * request.itemSize,
|
|
34
|
+
offsetY: start * request.itemSize,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "./tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"declaration": true,
|
|
5
|
+
"declarationMap": true,
|
|
6
|
+
"emitDeclarationOnly": true,
|
|
7
|
+
"outDir": "./dist",
|
|
8
|
+
"rootDir": "./src",
|
|
9
|
+
"paths": {
|
|
10
|
+
"@ornery/ui-grid": ["../../dist/ui-grid"]
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"include": ["src/**/*.ts", "src/**/*.tsx"],
|
|
14
|
+
"exclude": ["src/**/*.test.ts", "src/**/*.test.tsx"]
|
|
15
|
+
}
|
package/CLAUDE.md
DELETED
|
@@ -1,283 +0,0 @@
|
|
|
1
|
-
# @ornery/ui-grid-react — React Wrapper
|
|
2
|
-
|
|
3
|
-
## What to build
|
|
4
|
-
|
|
5
|
-
A React wrapper for the `@ornery/ui-grid` Angular library's pure-TypeScript core. This package publishes as `@ornery/ui-grid-react` and reuses 100% of the core logic — no code duplication. The React wrapper is a thin rendering layer.
|
|
6
|
-
|
|
7
|
-
## Critical: Core reuse
|
|
8
|
-
|
|
9
|
-
The entire core is pure TypeScript with ZERO Angular dependencies. Import everything from the sibling library path. In this monorepo, the path alias `@ornery/ui-grid` maps to `projects/ui-grid/src/public-api.ts`.
|
|
10
|
-
|
|
11
|
-
**Core files to reuse (all pure TS, no Angular):**
|
|
12
|
-
|
|
13
|
-
- `grid.core.pipeline.ts` → `buildGridPipeline()` — the entire data pipeline
|
|
14
|
-
- `grid.api.ts` → `createGridApi()`, `UiGridApi` — the API object
|
|
15
|
-
- `grid.models.ts` → `GridOptions`, `GridColumnDef`, `GridRow`, `GridLabels`, `DEFAULT_GRID_LABELS`, etc.
|
|
16
|
-
- `grid.features.ts` → `FEATURE_SORTING`, `FEATURE_FILTERING`, etc. — build-time feature flags
|
|
17
|
-
- `grid.core.viewmodel.ts` → `resolveGridLabels()`, `gridSortButtonLabel()`, `gridSortAriaSort()`, all label functions
|
|
18
|
-
- `grid.core.display.ts` → `buildGridCellContext()`, `formatGridCellDisplayValue()`
|
|
19
|
-
- `grid.core.edit.ts` → `findNextGridCell()`, `isPrintableGridKey()`, `buildGridFocusCellResult()`, etc.
|
|
20
|
-
- `grid.core.export.ts` → `exportCsvRows()`, `headerLabel()`
|
|
21
|
-
- `grid.core.types.ts` → `DisplayItem`, `GroupItem`, `RowItem`, `ExpandableItem`, `PipelineResult`, `BuildGridPipelineContext`
|
|
22
|
-
- `grid.constants.ts` → `SORT_DIRECTIONS`, `FILTER_CONDITIONS`
|
|
23
|
-
- `grid.utils.ts` → `getCellValue`, `getPathValue`, `setPathValue`, `titleize`, etc.
|
|
24
|
-
- `row-searcher.ts` → `runColumnFilter`, `setupFilters`
|
|
25
|
-
- `row-sorter.ts` → `getSortFn`
|
|
26
|
-
- `ui-grid.commands.ts` → All command functions (pure TS, depends only on grid.api + grid.core)
|
|
27
|
-
- `ui-grid.events.ts` → All event raisers (pure TS, depends only on grid.api)
|
|
28
|
-
- `ui-grid.host.ts` → `downloadGridCsvFile()` (pure TS), `observeGridHostSize()` (pure TS)
|
|
29
|
-
- `i18n/en-US.json` → Default labels
|
|
30
|
-
|
|
31
|
-
**Do NOT copy any core files. Import them.**
|
|
32
|
-
|
|
33
|
-
## File structure to create
|
|
34
|
-
|
|
35
|
-
```
|
|
36
|
-
projects/ui-grid-react/
|
|
37
|
-
package.json
|
|
38
|
-
tsconfig.json
|
|
39
|
-
CLAUDE.md ← this file
|
|
40
|
-
src/
|
|
41
|
-
index.ts ← public API exports
|
|
42
|
-
UiGrid.tsx ← main React component
|
|
43
|
-
useGridState.ts ← state management hook (replaces Angular signals)
|
|
44
|
-
useVirtualScroll.ts ← fixed-size row virtualization hook
|
|
45
|
-
ui-grid.css ← styles adapted from grid.core.styles.scss
|
|
46
|
-
UiGrid.test.tsx ← tests with vitest + @testing-library/react
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
## package.json
|
|
50
|
-
|
|
51
|
-
```json
|
|
52
|
-
{
|
|
53
|
-
"name": "@ornery/ui-grid-react",
|
|
54
|
-
"version": "0.1.0",
|
|
55
|
-
"description": "React wrapper for @ornery/ui-grid",
|
|
56
|
-
"main": "dist/index.js",
|
|
57
|
-
"module": "dist/index.mjs",
|
|
58
|
-
"types": "dist/index.d.ts",
|
|
59
|
-
"exports": {
|
|
60
|
-
".": {
|
|
61
|
-
"import": "./dist/index.mjs",
|
|
62
|
-
"require": "./dist/index.js",
|
|
63
|
-
"types": "./dist/index.d.ts"
|
|
64
|
-
},
|
|
65
|
-
"./styles": "./dist/ui-grid.css"
|
|
66
|
-
},
|
|
67
|
-
"peerDependencies": {
|
|
68
|
-
"react": "^18.0.0 || ^19.0.0",
|
|
69
|
-
"react-dom": "^18.0.0 || ^19.0.0",
|
|
70
|
-
"@ornery/ui-grid": "^0.1.0"
|
|
71
|
-
},
|
|
72
|
-
"devDependencies": {
|
|
73
|
-
"react": "^19.1.0",
|
|
74
|
-
"react-dom": "^19.1.0",
|
|
75
|
-
"@testing-library/react": "^16.0.0",
|
|
76
|
-
"@types/react": "^19.0.0",
|
|
77
|
-
"@types/react-dom": "^19.0.0",
|
|
78
|
-
"typescript": "~5.8.0",
|
|
79
|
-
"vitest": "^4.1.0",
|
|
80
|
-
"jsdom": "^26.0.0",
|
|
81
|
-
"tsup": "^8.0.0"
|
|
82
|
-
},
|
|
83
|
-
"scripts": {
|
|
84
|
-
"build": "tsup src/index.ts --format esm,cjs --dts --external react --external react-dom --external @ornery/ui-grid",
|
|
85
|
-
"test": "vitest run",
|
|
86
|
-
"test:watch": "vitest"
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
## tsconfig.json
|
|
92
|
-
|
|
93
|
-
Extend the root tsconfig. Add path alias for `@ornery/ui-grid`:
|
|
94
|
-
|
|
95
|
-
```json
|
|
96
|
-
{
|
|
97
|
-
"extends": "../../tsconfig.json",
|
|
98
|
-
"compilerOptions": {
|
|
99
|
-
"jsx": "react-jsx",
|
|
100
|
-
"outDir": "./dist",
|
|
101
|
-
"declaration": true,
|
|
102
|
-
"declarationMap": true,
|
|
103
|
-
"paths": {
|
|
104
|
-
"@ornery/ui-grid": ["../ui-grid/src/public-api.ts"]
|
|
105
|
-
}
|
|
106
|
-
},
|
|
107
|
-
"include": ["src"]
|
|
108
|
-
}
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
## useVirtualScroll hook
|
|
112
|
-
|
|
113
|
-
A lightweight fixed-size virtualizer (~50 lines). No external dependency.
|
|
114
|
-
|
|
115
|
-
**Interface:**
|
|
116
|
-
```ts
|
|
117
|
-
interface UseVirtualScrollOptions {
|
|
118
|
-
itemCount: number;
|
|
119
|
-
itemSize: number; // row height in px
|
|
120
|
-
viewportHeight: number; // container height in px
|
|
121
|
-
overscan?: number; // extra items above/below (default: 3)
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
interface UseVirtualScrollResult {
|
|
125
|
-
visibleRange: { start: number; end: number };
|
|
126
|
-
totalHeight: number;
|
|
127
|
-
offsetY: number;
|
|
128
|
-
onScroll: (event: React.UIEvent<HTMLDivElement>) => void;
|
|
129
|
-
viewportRef: React.RefObject<HTMLDivElement>;
|
|
130
|
-
scrollTop: number;
|
|
131
|
-
}
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
**Implementation:**
|
|
135
|
-
- Track `scrollTop` via `useState`
|
|
136
|
-
- Calculate `start = Math.floor(scrollTop / itemSize) - overscan`
|
|
137
|
-
- Calculate `end = start + Math.ceil(viewportHeight / itemSize) + 2 * overscan`
|
|
138
|
-
- Clamp to `[0, itemCount)`
|
|
139
|
-
- `totalHeight = itemCount * itemSize`
|
|
140
|
-
- `offsetY = start * itemSize`
|
|
141
|
-
- Return a div ref and onScroll handler
|
|
142
|
-
|
|
143
|
-
## useGridState hook
|
|
144
|
-
|
|
145
|
-
Replaces Angular signals with React state. This is the core bridge.
|
|
146
|
-
|
|
147
|
-
**Maps the Angular component's state 1:1:**
|
|
148
|
-
|
|
149
|
-
| Angular signal | React state |
|
|
150
|
-
|----------------|-------------|
|
|
151
|
-
| `activeFilters` | `useState<Record<string, string>>({})` |
|
|
152
|
-
| `groupByColumns` | `useState<string[]>([])` |
|
|
153
|
-
| `collapsedGroups` | `useState<Record<string, boolean>>({})` |
|
|
154
|
-
| `columnOrder` | `useState<string[]>([])` |
|
|
155
|
-
| `hiddenRowReasons` | `useState<Record<string, string[]>>({})` |
|
|
156
|
-
| `sortState` | `useState<SortState>(...)` |
|
|
157
|
-
| `focusedCell` | `useState<GridCellPosition \| null>(null)` |
|
|
158
|
-
| `editingCell` | `useState<GridCellPosition \| null>(null)` |
|
|
159
|
-
| `editingValue` | `useState('')` |
|
|
160
|
-
| `expandedRows` | `useState<Record<string, boolean>>({})` |
|
|
161
|
-
| `expandedTreeRows` | `useState<Record<string, boolean>>({})` |
|
|
162
|
-
| `currentPage` | `useState(1)` |
|
|
163
|
-
| `pageSize` | `useState(0)` |
|
|
164
|
-
| `benchmarkResult` | `useState<GridBenchmarkResult \| null>(null)` |
|
|
165
|
-
| `infiniteScrollState` | `useState<GridInfiniteScrollState>(...)` |
|
|
166
|
-
|
|
167
|
-
**Key behaviors:**
|
|
168
|
-
- Call `createGridApi()` once in a `useRef` with bindings that dispatch state updates
|
|
169
|
-
- Memoize `buildGridPipeline()` via `useMemo` dependent on all state inputs
|
|
170
|
-
- Memoize `visibleColumns` via `useMemo`
|
|
171
|
-
- Resolve `labels` via `useMemo(() => resolveGridLabels(options.labels), [options.labels])`
|
|
172
|
-
- Reset state when `options.id` changes (same as the Angular effect)
|
|
173
|
-
- Call `options.onRegisterApi?.(gridApi)` once
|
|
174
|
-
|
|
175
|
-
**Returns:**
|
|
176
|
-
- All computed values: `pipeline`, `visibleColumns`, `labels`, `gridTemplateColumns`, etc.
|
|
177
|
-
- All action dispatchers: `toggleSort()`, `updateFilter()`, `toggleGrouping()`, etc.
|
|
178
|
-
- `gridApi` ref
|
|
179
|
-
|
|
180
|
-
## UiGrid.tsx component
|
|
181
|
-
|
|
182
|
-
**Props:**
|
|
183
|
-
```tsx
|
|
184
|
-
interface UiGridProps {
|
|
185
|
-
options: GridOptions;
|
|
186
|
-
onRegisterApi?: (api: UiGridApi) => void;
|
|
187
|
-
/** Render prop for custom cell content. Return null to use default text. */
|
|
188
|
-
cellRenderer?: (context: GridCellTemplateContext) => React.ReactNode;
|
|
189
|
-
/** Render prop for expandable row content. */
|
|
190
|
-
expandableRenderer?: (context: GridExpandableTemplateContext) => React.ReactNode;
|
|
191
|
-
className?: string;
|
|
192
|
-
}
|
|
193
|
-
```
|
|
194
|
-
|
|
195
|
-
**Rendering structure:** Match the Angular template exactly — same CSS classes, same `data-*` attributes, same ARIA roles/attributes, same part attributes. Reference the Angular template at `projects/ui-grid/src/lib/grid/ui-grid.component.html`.
|
|
196
|
-
|
|
197
|
-
**Key sections:**
|
|
198
|
-
1. Hero header with title, benchmark button, export button, stats
|
|
199
|
-
2. Metrics strip
|
|
200
|
-
3. Grid frame (`role="grid"`) with:
|
|
201
|
-
- Header row (`role="row"`) with column headers (`role="columnheader"`, `aria-sort`)
|
|
202
|
-
- Sort buttons with SVG icons
|
|
203
|
-
- Group toggle buttons with SVG icons
|
|
204
|
-
- Filter row
|
|
205
|
-
- Display items via `ng-template` equivalent (just inline JSX):
|
|
206
|
-
- Group rows with disclosure chevron SVGs
|
|
207
|
-
- Expandable rows
|
|
208
|
-
- Data rows with cells (`role="gridcell"`, `tabindex="0"`)
|
|
209
|
-
- Tree toggle buttons with chevron SVGs
|
|
210
|
-
- Expand toggle buttons with chevron SVGs
|
|
211
|
-
- Cell editor input (when editing)
|
|
212
|
-
- Cell template / display value
|
|
213
|
-
- Virtual scroll viewport OR plain list
|
|
214
|
-
- Empty state
|
|
215
|
-
- Pagination footer with arrow SVG icons
|
|
216
|
-
|
|
217
|
-
**Feature flag guards:** Same pattern as Angular template — wrap sections in `{FEATURE_SORTING && ...}` etc.
|
|
218
|
-
|
|
219
|
-
**Focus management:** For cell focus/editor focus, use `useRef` + `useEffect` instead of the Angular `focusGridRenderedCell`/`focusGridEditor` (those use shadowRoot which React won't have). Query the grid container ref directly.
|
|
220
|
-
|
|
221
|
-
**Keyboard handling:** Port `handleCellKeyDown` and `handleEditorKeyDown` logic directly — it's all pure key checking + command dispatch.
|
|
222
|
-
|
|
223
|
-
## Styles (ui-grid.css)
|
|
224
|
-
|
|
225
|
-
Adapt from `projects/ui-grid/src/lib/grid/grid.core.styles.scss`:
|
|
226
|
-
- Replace `:host` selectors with `.ui-grid-host`
|
|
227
|
-
- Replace `:host *` with `.ui-grid-host *`
|
|
228
|
-
- Keep ALL CSS custom properties identical (same `--ui-grid-*` variables)
|
|
229
|
-
- Keep all class names identical
|
|
230
|
-
- Add `.ui-grid-host { display: block; color: var(--ui-grid-cell-color); }`
|
|
231
|
-
- Ship as plain CSS (no modules needed — class names are scoped by convention)
|
|
232
|
-
|
|
233
|
-
## Tests
|
|
234
|
-
|
|
235
|
-
Use vitest + @testing-library/react + jsdom.
|
|
236
|
-
|
|
237
|
-
**Port these key scenarios from the Angular spec:**
|
|
238
|
-
1. Registers the API and renders headers and rows
|
|
239
|
-
2. Filters rows and renders empty state
|
|
240
|
-
3. Sorts rows and cycles sort state from header button
|
|
241
|
-
4. Groups rows and collapses groups
|
|
242
|
-
5. Exports visible rows as CSV
|
|
243
|
-
6. Virtualizes rows when count crosses threshold
|
|
244
|
-
7. Paginates rows
|
|
245
|
-
8. Keyboard cell editing (commit, navigate, cancel)
|
|
246
|
-
9. Resolves custom i18n label overrides
|
|
247
|
-
10. Feature flags disable unused template sections
|
|
248
|
-
|
|
249
|
-
## Public API (index.ts)
|
|
250
|
-
|
|
251
|
-
```ts
|
|
252
|
-
export { UiGrid } from './UiGrid';
|
|
253
|
-
export type { UiGridProps } from './UiGrid';
|
|
254
|
-
export { useGridState } from './useGridState';
|
|
255
|
-
export { useVirtualScroll } from './useVirtualScroll';
|
|
256
|
-
|
|
257
|
-
// Re-export core types consumers need
|
|
258
|
-
export type {
|
|
259
|
-
GridOptions,
|
|
260
|
-
GridColumnDef,
|
|
261
|
-
GridRow,
|
|
262
|
-
GridRecord,
|
|
263
|
-
GridLabels,
|
|
264
|
-
GridCellTemplateContext,
|
|
265
|
-
GridExpandableTemplateContext,
|
|
266
|
-
GridCellEditableContext,
|
|
267
|
-
GridBenchmarkResult,
|
|
268
|
-
GridSavedState,
|
|
269
|
-
SortState,
|
|
270
|
-
} from '@ornery/ui-grid';
|
|
271
|
-
|
|
272
|
-
export type { UiGridApi } from '@ornery/ui-grid';
|
|
273
|
-
export { DEFAULT_GRID_LABELS } from '@ornery/ui-grid';
|
|
274
|
-
```
|
|
275
|
-
|
|
276
|
-
## Important notes
|
|
277
|
-
|
|
278
|
-
- Do NOT use Shadow DOM — React doesn't support it well. Use a regular div with `className="ui-grid-host"`.
|
|
279
|
-
- Do NOT add any Angular dependencies.
|
|
280
|
-
- The `GridOptions.cellTemplate` field is `TemplateRef` (Angular-specific). The React wrapper ignores it and uses the `cellRenderer` prop instead.
|
|
281
|
-
- Same for `expandableRowTemplate` → use `expandableRenderer` prop.
|
|
282
|
-
- The `GridOptions.onRegisterApi` still works — it's called with the same `UiGridApi` object.
|
|
283
|
-
- Keep the `GridColumnDef.cellRenderer` function (returns string) — it's already framework-agnostic.
|