@opencorestack/opengridx 3.5.0 → 3.6.0
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/CHANGELOG.md +48 -0
- package/README.md +6 -3
- package/dist/ai.cjs +4 -2
- package/dist/ai.cjs.map +1 -1
- package/dist/ai.d.ts +2 -2
- package/dist/ai.es.js +4 -2
- package/dist/ai.es.js.map +1 -1
- package/dist/index.d.ts +32 -0
- package/dist/opengridx.css +1 -1
- package/dist/opengridx.es.js +6924 -6691
- package/dist/opengridx.es.js.map +1 -1
- package/dist/opengridx.umd.js +17 -17
- package/dist/opengridx.umd.js.map +1 -1
- package/docs/API_REFERENCE.md +14 -1
- package/docs/README.md +2 -0
- package/docs/architecture/datagrid-orchestration.md +2 -1
- package/docs/features/ai-toolkit.md +2 -2
- package/docs/features/export-guide.md +2 -0
- package/docs/features/header-filters.md +34 -0
- package/docs/features/keyboard-navigation.md +2 -0
- package/docs/features/nextjs.md +241 -0
- package/docs/features/pdf-export.md +2 -0
- package/docs/features/pinning.md +2 -0
- package/docs/features/rtl.md +58 -0
- package/docs/features/virtualization.md +5 -5
- package/docs/performance.md +180 -1
- package/docs/roadmap.md +5 -3
- package/lib/ai/promptHandler.ts +12 -8
- package/lib/ai/validate.ts +5 -2
- package/lib/components/AiAssistant/GridAiAssistantPanel.css +1 -1
- package/lib/components/Cell/Cell.css +29 -9
- package/lib/components/Cell/Cell.tsx +2 -2
- package/lib/components/ColumnGroupHeader/ColumnGroupHeader.css +2 -2
- package/lib/components/ColumnResizeHandle/ColumnResizeHandle.css +3 -3
- package/lib/components/ColumnResizeHandle/ColumnResizeHandle.tsx +18 -2
- package/lib/components/DataGrid/DataGrid.css +12 -6
- package/lib/components/DataGrid/DataGrid.tsx +9 -1
- package/lib/components/DataGrid/GridAggregationFooter.tsx +3 -3
- package/lib/components/DataGrid/GridListView.tsx +3 -1
- package/lib/components/Header/ColumnMenu.css +1 -1
- package/lib/components/Header/ColumnMenu.tsx +14 -6
- package/lib/components/Header/Header.css +18 -10
- package/lib/components/Header/Header.tsx +16 -9
- package/lib/components/Header/headerColumnStyle.ts +3 -2
- package/lib/components/HeaderFilter/HeaderFilter.css +8 -2
- package/lib/components/HeaderFilter/HeaderFilterCell.tsx +21 -18
- package/lib/components/HeaderFilter/HeaderFilterOperatorMenu.tsx +5 -4
- package/lib/components/HeaderFilter/HeaderFilterRow.tsx +9 -3
- package/lib/components/ListView/ListViewRow.tsx +1 -1
- package/lib/components/Pagination/Pagination.css +4 -0
- package/lib/components/Pagination/Pagination.tsx +4 -4
- package/lib/components/Row/Row.tsx +3 -3
- package/lib/components/SkeletonRow/SkeletonRow.tsx +2 -2
- package/lib/components/Toolbar/GridToolbar.tsx +8 -28
- package/lib/components/Toolbar/PivotPanel.tsx +4 -7
- package/lib/components/ui/ExpandIcon.css +13 -0
- package/lib/components/ui/ExpandIcon.tsx +2 -4
- package/lib/hooks/core/useGridCellSelection.ts +20 -5
- package/lib/hooks/core/useGridColumns.tsx +2 -2
- package/lib/hooks/core/useGridDevWarnings.ts +21 -6
- package/lib/hooks/core/useGridKeyboardNavigation.ts +7 -4
- package/lib/hooks/core/useGridMaxContentHeight.ts +16 -0
- package/lib/hooks/core/useGridScrollSync.ts +28 -5
- package/lib/hooks/core/useGridVirtualization.ts +19 -0
- package/lib/hooks/core/useLayout.ts +13 -6
- package/lib/hooks/useRowGrouping.ts +9 -9
- package/lib/types/index.ts +30 -0
- package/lib/utils/aggregation/index.ts +56 -1
- package/lib/utils/direction.ts +52 -0
- package/lib/utils/export/exportToExcelAdvanced.ts +9 -2
- package/lib/utils/export/exportToPdf.ts +2 -1
- package/lib/utils/filtering/index.ts +58 -6
- package/lib/utils/headerFilterLocale.ts +37 -0
- package/lib/utils/maxElementHeight.ts +53 -0
- package/lib/utils/scroll/index.ts +5 -3
- package/lib/utils/sorting/index.ts +141 -26
- package/lib/utils/values.ts +10 -6
- package/llms.txt +4 -2
- package/package.json +3 -2
package/docs/performance.md
CHANGED
|
@@ -87,7 +87,12 @@ import '@opencorestack/opengridx/styles';
|
|
|
87
87
|
|
|
88
88
|
## Know the browser's height limit
|
|
89
89
|
|
|
90
|
-
The grid sizes its scroll content to `rowCount × rowHeight` pixels and does not scale scroll positions. Browsers cap an element's height
|
|
90
|
+
The grid sizes its scroll content to `rowCount × rowHeight` pixels and does not scale scroll positions. Browsers cap an element's height, and the [benchmark](#reach-the-browser-height-ceiling) measures what happens past the cap:
|
|
91
|
+
|
|
92
|
+
- **Chromium and WebKit** clamp the content at 33,554,428 px. Rows below that cannot be scrolled to: at the default 52 px row height that is every row past 645,277.
|
|
93
|
+
- **Firefox** has a lower limit, about 17.9M px, and collapses a taller element instead of clamping it. Up to 3.5.0 that made a grid with more content unscrollable in Firefox (only the first screen of rows was reachable). **Since 3.5.1 the grid measures the browser's limit once and caps its scroll content to it**, so Firefox scrolls normally and reaches the rows above the cap: about 344,000 rows at 52 px, 559,000 at 32 px.
|
|
94
|
+
|
|
95
|
+
In development the grid warns when rows are cut off, naming how many can be scrolled to in that browser. For more rows, use pagination, `paginationMode="server"` or infinite scroll with a `dataSource`, or a smaller `rowHeight` (`density="compact"`, 32 px, keeps 1M rows under the Chromium and WebKit cap).
|
|
91
96
|
|
|
92
97
|
## Use server-side operations for very large datasets
|
|
93
98
|
|
|
@@ -116,9 +121,183 @@ Without a `dataSource`, fetch the rows yourself from the change callbacks and co
|
|
|
116
121
|
In server modes the grid shows `rows` as given: it does not re-filter, re-sort or slice them (v3.0+; earlier versions did unless a `dataSource` was set).
|
|
117
122
|
|
|
118
123
|
|
|
124
|
+
## Benchmarks
|
|
125
|
+
|
|
126
|
+
Measured with `npm run bench` (`bench/`), which installs the packed npm package into a production Vite + React 19 app and drives it with Playwright in Chromium, Firefox and WebKit. The numbers below are from one machine (Apple M1, 8 GB RAM): compare them with each other and with your own runs, not with other machines.
|
|
127
|
+
|
|
128
|
+
**In short** (Chromium, median; 100k and 1M rows from the run below, 10k and 500k from the 3.5.0 run of 2026-10-09, before the faster sort, quick filter and grouping):
|
|
129
|
+
|
|
130
|
+
| Rows | Mount | Sort (number / string) | Filter | Quick filter | Group + sum | Scroll frames over 16.7 ms | JS heap |
|
|
131
|
+
| ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
|
|
132
|
+
| 10,000 (3.5.0) | 79 ms | 34 / 42 ms | 19 ms | 52 ms | 30 ms | 0 % | 2.9 MB |
|
|
133
|
+
| 100,000 | 75 ms | 49 / 66 ms | 20 ms | 165 ms | 74 ms | 0 % | 10.4 MB |
|
|
134
|
+
| 500,000 (3.5.0) | 295 ms | 493 / 641 ms | 59 ms | 958 ms | 331 ms | 0 % | 39.4 MB |
|
|
135
|
+
| 1,000,000 | 621 ms | 467 / 467 ms | 109 ms | 1,813 ms | 693 ms | 0 % | 76.9 MB |
|
|
136
|
+
|
|
137
|
+
### Before and after the faster sort, quick filter and grouping (unreleased)
|
|
138
|
+
|
|
139
|
+
Medians of 5 runs, the same machine, the 3.5.1 code (`122d431`) and the changed code (`e1df2f1`) measured back to back on 2026-10-12 (`bench/results/2026-10-12-apple-m1-darwin-before.json` and `bench/results/2026-10-12-apple-m1-darwin.json`). Load average at the start of each run: 2.9 (before) and 4.6 (after); an IDE, a browser and other tools were open. Row counts after every filter and grouping were identical in both runs.
|
|
140
|
+
|
|
141
|
+
| 1M rows, 52 px | Chromium before → after | Firefox before → after | WebKit before → after |
|
|
142
|
+
| :--- | ---: | ---: | ---: |
|
|
143
|
+
| Sort, number column | 1,716 → 467 ms | 1,935 → 468 ms | 1,126 → 340 ms |
|
|
144
|
+
| Sort, string column | 1,510 → 467 ms | 2,338 → 560 ms | 2,939 → 562 ms |
|
|
145
|
+
| Quick filter, one term (first keystroke) | 2,127 → 1,813 ms | 3,283 → 2,789 ms | 3,646 → 3,166 ms |
|
|
146
|
+
| Group by region + `sum` | 924 → 693 ms | 1,879 → 1,776 ms | 818 → 675 ms |
|
|
147
|
+
| Filter, number `>` | 144 → 97 ms | 203 → 125 ms | 142 → 89 ms |
|
|
148
|
+
| Mount | 804 → 621 ms | 1,216 → 925 ms | 620 → 547 ms |
|
|
149
|
+
|
|
150
|
+
| 100k rows, 52 px | Chromium before → after | Firefox before → after | WebKit before → after |
|
|
151
|
+
| :--- | ---: | ---: | ---: |
|
|
152
|
+
| Sort, number column | 99 → 49 ms | 153 → 74 ms | 120 → 87 ms |
|
|
153
|
+
| Sort, string column | 154 → 66 ms | 193 → 75 ms | 202 → 91 ms |
|
|
154
|
+
| Quick filter, one term | 207 → 165 ms | 372 → 287 ms | 294 → 282 ms |
|
|
155
|
+
| Group by region + `sum` | 79 → 74 ms | 108 → 78 ms | 103 → 107 ms |
|
|
156
|
+
|
|
157
|
+
What changed: sort keys are read once per row into typed arrays and strings are ranked by sorting only their distinct values with the collator (the collator no longer runs in every comparison, which was most of WebKit's string sort); the row heights after a sort or filter are computed in one pass without id lookups when no detail panel is open; cells of plain columns are read without allocating; the quick filter keeps its texts by row position for the rows array it searched and matches with plain loops; aggregation reads cells row by row. The quick filter's first keystroke is still dominated by reading, formatting (`valueFormatter`) and lower-casing every cell, and Firefox's grouping by allocation of per-row tree nodes and metadata; the Firefox runs at 1M rows varied widely (up to several seconds) in both runs.
|
|
158
|
+
|
|
159
|
+
- **Scrolling keeps up with the display at every size.** Chromium missed no frames; Firefox missed 0–2.4 %. The DOM holds about 19 row elements at rest (28 at 32 px) and at most 86 while scrolling fast, whatever the row count. WebKit scrolls steadily without missed frames too, but misses one or two frames each time the scroll position jumps far (the *during the jumps* rows below). Firefox's scroll numbers at 500k and 1M rows are not comparable: the grid cannot scroll there (see Reach).
|
|
160
|
+
- **Sorting, the quick filter and grouping run on the main thread in one piece.** Up to 100k rows they finish in a few frames; at 1M a sort blocks the page for about 0.35–0.6 s in every engine, grouping for 0.7 s (1.8 s in Firefox), and the first quick-filter keystroke for about 1.8–3 s. For sorting or searching a million rows interactively, use server-side sorting and filtering.
|
|
161
|
+
- **Reach:** past the browser's height cap rows cannot be scrolled to, and in Firefox the grid stops scrolling altogether. See [Reach](#reach-the-browser-height-ceiling).
|
|
162
|
+
|
|
163
|
+
### Method
|
|
164
|
+
|
|
165
|
+
- **The app is the built package.** `bench/app` installs the tarball from `npm pack`, like a consumer, and is a production build (no React development checks).
|
|
166
|
+
- **Data:** 12 columns (`id`, two strings, two `singleSelect`, four numbers of which two have a `valueFormatter`, a date, a boolean, a short code), `id` and `name` pinned left. Rows come from a seeded random generator in the page and are generated before any timer starts.
|
|
167
|
+
- **Page:** 1280×800, the grid fills it, headless browsers launched by Playwright, one new browser context per run.
|
|
168
|
+
- **Timing:** each operation is timed from the call (`root.render`, `apiRef.setSortModel`, `setFilterModel`, `setRowGroupingModel` + `setAggregationModel`) to the first painted frame that shows its result: the page waits on `requestAnimationFrame` until the grid's DOM shows the result, then one more frame so that frame has been painted. The time therefore includes React's render and commit, style, layout and paint, and up to one frame of waiting for the next refresh. The method is described in detail at the top of `bench/app/src/harness.tsx`.
|
|
169
|
+
- **Operations:** sort ascending on `amount` (number) and on `name` (string); filter `name contains "nova"` (6 % of rows) and `amount > 5000` (50 %); one quick-filter term over all columns (the first search reads and formats every cell, so this is the first keystroke's cost); group by `region` (8 groups) with `sum` of `amount`. Each is undone before the next.
|
|
170
|
+
- **Scroll:** 5 s driven from `requestAnimationFrame`: 3 s at a constant 2,400 px/s, then a jump to a random position anywhere in the scrollable range every 100 ms. Frame times are the gaps between consecutive animation frames. Headless browsers do not pace frames by a display, so idle frames already vary between about 16.6 and 18 ms (17–21 ms in WebKit); a frame therefore counts as *over 16.7 ms* when it took more than 1.5 refresh intervals (25 ms at 60 Hz, a missed refresh) and as *over 33 ms* above 2.5 intervals (41.7 ms, two or more missed).
|
|
171
|
+
- **Chromium only:** long tasks (`PerformanceObserver('longtask')`; Firefox and WebKit have no such entries) and the JS heap (Chrome DevTools Protocol `Runtime.getHeapUsage` after a forced garbage collection, after mount minus the same page with the rows generated and no grid, so it is what the grid adds on top of your data).
|
|
172
|
+
- **DOM size:** row elements and cells at the end of the scroll test, and row elements after scrolling back to the top and letting the grid settle.
|
|
173
|
+
- **Repeats:** 6 runs per row count and engine; the first is discarded as warm-up, the tables show the median of the other 5 and the min–max range.
|
|
174
|
+
- **Machine load:** the runner records the load average before and after. This machine had a steady load of about 3.5–4 from desktop apps (an IDE, a browser) during the run; nothing else heavy ran.
|
|
175
|
+
|
|
176
|
+
### Results
|
|
177
|
+
|
|
178
|
+
<!-- bench:start -->
|
|
179
|
+
<!-- Generated by `node bench/report.mjs --write docs/performance.md` from bench/results/2026-10-12-apple-m1-darwin.json. Do not edit by hand. -->
|
|
180
|
+
|
|
181
|
+
- **Machine:** Apple M1, 8 cores, 8 GB RAM, macOS 26.5.1 (arm64)
|
|
182
|
+
- **Browsers:** Chromium 145.0.7632.6, Firefox 146.0.1, WebKit 26.0 (Playwright, headless, 1280×800 page)
|
|
183
|
+
- **Package:** @opencorestack/opengridx 3.5.1 (commit `e1df2f1`), production build
|
|
184
|
+
- **Date:** 2026-10-12; load average at start 4.61 / 5.66 / 5.46, at end 2.31 / 6.16 / 7.41
|
|
185
|
+
- **Runs:** median of 5 runs after one discarded warm-up run, with the min–max range in brackets
|
|
186
|
+
|
|
187
|
+
¹ Chromium only: Firefox and WebKit have no long-task entries, and the heap is read through the Chrome DevTools Protocol.
|
|
188
|
+
|
|
189
|
+
#### 100,000 rows, 52 px rows
|
|
190
|
+
|
|
191
|
+
| Metric | Chromium | Firefox | WebKit |
|
|
192
|
+
| :--- | ---: | ---: | ---: |
|
|
193
|
+
| Mount | 75 ms (68–131) | 142 ms (113–144) | 138 ms (126–194) |
|
|
194
|
+
| Sort, number column | 49 ms (42–64) | 74 ms (64–85) | 87 ms (70–92) |
|
|
195
|
+
| Sort, string column | 66 ms (61–87) | 75 ms (59–89) | 91 ms (81–126) |
|
|
196
|
+
| Filter, string `contains` | 20 ms (18–37) | 34 ms (21–56) | 47 ms (43–47) |
|
|
197
|
+
| Filter, number `>` | 16 ms (14–28) | 32 ms (24–37) | 30 ms (28–37) |
|
|
198
|
+
| Quick filter, one term | 165 ms (156–190) | 287 ms (266–346) | 282 ms (257–339) |
|
|
199
|
+
| Group by region + `sum` | 74 ms (70–77) | 78 ms (71–97) | 107 ms (94–148) |
|
|
200
|
+
| Scroll frame time p50 | 16.7 ms | 16.7 ms | 17.0 ms |
|
|
201
|
+
| Scroll frame time p95 | 18.4 ms (18.2–18.6) | 17.6 ms (17.3–33.3) | 49.0 ms (44.0–62.0) |
|
|
202
|
+
| Scroll frame time p99 | 18.6 ms (18.6–18.7) | 33.3 ms (17.7–50.0) | 60.0 ms (53.0–92.0) |
|
|
203
|
+
| Scroll frames over 16.7 ms | 0.0 % (0.0–0.3) | 2.7 % (0.7–7.6) | 11.8 % (8.1–20.8) |
|
|
204
|
+
| Scroll frames over 33 ms | 0.0 % | 0.0 % (0.0–1.5) | 7.4 % (6.6–10.1) |
|
|
205
|
+
| … over 16.7 ms while scrolling steadily | 0.0 % | 1.1 % (0.0–5.4) | 0.6 % (0.0–8.5) |
|
|
206
|
+
| … over 16.7 ms during the jumps | 0.0 % (0.0–0.8) | 5.3 % (1.7–15.1) | 38.3 % (24.4–65.3) |
|
|
207
|
+
| Long tasks during scroll ¹ | 0 | – | – |
|
|
208
|
+
| Longest task during a sort ¹ | 59 ms (55–81) | – | – |
|
|
209
|
+
| JS heap for the grid ¹ | 10.4 MB | – | – |
|
|
210
|
+
| Row elements in the DOM, end of scroll | 76 | 76 | 76 |
|
|
211
|
+
| Cells in the DOM, end of scroll | 912 | 912 | 912 |
|
|
212
|
+
| Row elements in the DOM, at rest | 19 | 19 | 19 |
|
|
213
|
+
| Reach: last row on screen at the bottom | all rows | all rows | all rows |
|
|
214
|
+
|
|
215
|
+
#### 1,000,000 rows, 52 px rows
|
|
216
|
+
|
|
217
|
+
| Metric | Chromium | Firefox | WebKit |
|
|
218
|
+
| :--- | ---: | ---: | ---: |
|
|
219
|
+
| Mount | 621 ms (522–1,024) | 925 ms (861–2,775) | 547 ms (490–616) |
|
|
220
|
+
| Sort, number column | 467 ms (376–546) | 468 ms (430–1,287) | 340 ms (304–690) |
|
|
221
|
+
| Sort, string column | 467 ms (415–530) | 560 ms (433–1,257) | 562 ms (461–639) |
|
|
222
|
+
| Filter, string `contains` | 109 ms (92–113) | 89 ms (80–185) | 95 ms (89–194) |
|
|
223
|
+
| Filter, number `>` | 97 ms (90–123) | 125 ms (102–434) | 89 ms (81–121) |
|
|
224
|
+
| Quick filter, one term | 1,813 ms (1,430–2,183) | 2,789 ms (2,644–7,252) | 3,166 ms (2,877–3,463) |
|
|
225
|
+
| Group by region + `sum` | 693 ms (599–818) | 1,776 ms (1,176–3,715) | 675 ms (614–854) |
|
|
226
|
+
| Scroll frame time p50 | 16.7 ms | 16.7 ms (16.7–50.0) | 17.0 ms |
|
|
227
|
+
| Scroll frame time p95 | 18.4 ms (18.4–18.5) | 33.3 ms (17.4–166.6) | 44.0 ms (44.0–46.0) |
|
|
228
|
+
| Scroll frame time p99 | 18.6 ms (18.6–33.3) | 34.1 ms (33.3–283.4) | 47.0 ms (46.0–52.0) |
|
|
229
|
+
| Scroll frames over 16.7 ms | 0.3 % (0.0–1.0) | 9.5 % (1.7–79.5) | 7.7 % (7.2–9.0) |
|
|
230
|
+
| Scroll frames over 33 ms | 0.0 % (0.0–0.3) | 0.4 % (0.0–54.2) | 7.2 % (6.8–7.9) |
|
|
231
|
+
| … over 16.7 ms while scrolling steadily | 0.0 % | 6.5 % (0.0–75.0) | 0.0 % (0.0–0.6) |
|
|
232
|
+
| … over 16.7 ms during the jumps | 0.8 % (0.0–2.8) | 14.3 % (4.3–94.7) | 22.6 % (20.4–26.4) |
|
|
233
|
+
| Long tasks during scroll ¹ | 0 | – | – |
|
|
234
|
+
| Longest task during a sort ¹ | 462 ms (410–523) | – | – |
|
|
235
|
+
| JS heap for the grid ¹ | 76.9 MB | – | – |
|
|
236
|
+
| Row elements in the DOM, end of scroll | 77 | 76 | 77 |
|
|
237
|
+
| Cells in the DOM, end of scroll | 924 | 912 | 924 |
|
|
238
|
+
| Row elements in the DOM, at rest | 19 | 19 | 19 |
|
|
239
|
+
| Reach: last row on screen at the bottom | 645,277 (64.5 %) | 344,139 (34.4 %) | 645,277 (64.5 %) |
|
|
240
|
+
|
|
241
|
+
#### 1,000,000 rows, 32 px rows
|
|
242
|
+
|
|
243
|
+
| Metric | Chromium | Firefox | WebKit |
|
|
244
|
+
| :--- | ---: | ---: | ---: |
|
|
245
|
+
| Mount | 741 ms (581–1,283) | 1,356 ms (968–5,966) | 475 ms (472–553) |
|
|
246
|
+
| Sort, number column | 453 ms (403–523) | 627 ms (441–917) | 327 ms (321–338) |
|
|
247
|
+
| Sort, string column | 497 ms (437–673) | 693 ms (489–6,628) | 504 ms (379–566) |
|
|
248
|
+
| Filter, string `contains` | 106 ms (96–162) | 192 ms (147–268) | 100 ms (97–108) |
|
|
249
|
+
| Filter, number `>` | 103 ms (86–136) | 179 ms (154–635) | 91 ms (87–190) |
|
|
250
|
+
| Quick filter, one term | 1,621 ms (1,617–2,569) | 4,265 ms (3,490–10,444) | 3,082 ms (2,684–3,608) |
|
|
251
|
+
| Group by region + `sum` | 880 ms (641–1,137) | 2,096 ms (957–3,309) | 640 ms (579–723) |
|
|
252
|
+
| Scroll frame time p50 | 16.7 ms | 16.7 ms (16.7–49.4) | 17.0 ms |
|
|
253
|
+
| Scroll frame time p95 | 17.6 ms (17.4–17.6) | 66.7 ms (33.2–199.3) | 49.0 ms (48.0–51.0) |
|
|
254
|
+
| Scroll frame time p99 | 17.7 ms (17.6–33.3) | 100.0 ms (33.7–333.3) | 52.0 ms (50.0–55.0) |
|
|
255
|
+
| Scroll frames over 16.7 ms | 0.7 % (0.0–1.0) | 38.4 % (6.7–82.7) | 15.5 % (15.4–15.8) |
|
|
256
|
+
| Scroll frames over 33 ms | 0.0 % (0.0–0.7) | 18.6 % (0.0–50.6) | 7.8 % (7.7–8.8) |
|
|
257
|
+
| … over 16.7 ms while scrolling steadily | 0.0 % (0.0–0.6) | 28.0 % (1.1–80.3) | 0.0 % (0.0–0.6) |
|
|
258
|
+
| … over 16.7 ms during the jumps | 0.8 % (0.0–2.6) | 63.5 % (16.5–96.4) | 52.0 % (50.6–57.4) |
|
|
259
|
+
| Long tasks during scroll ¹ | 0 (0–1) | – | – |
|
|
260
|
+
| Longest task during a sort ¹ | 490 ms (429–655) | – | – |
|
|
261
|
+
| JS heap for the grid ¹ | 77.3 MB | – | – |
|
|
262
|
+
| Row elements in the DOM, end of scroll | 86 | 86 | 86 |
|
|
263
|
+
| Cells in the DOM, end of scroll | 1,032 | 1,032 | 1,032 |
|
|
264
|
+
| Row elements in the DOM, at rest | 28 | 28 | 28 |
|
|
265
|
+
| Reach: last row on screen at the bottom | all rows | 559,224 (55.9 %) | all rows |
|
|
266
|
+
|
|
267
|
+
#### Reach: rows that can be scrolled into view
|
|
268
|
+
|
|
269
|
+
| Rows × row height | Content height | Chromium | Firefox | WebKit |
|
|
270
|
+
| :--- | ---: | ---: | ---: | ---: |
|
|
271
|
+
| 100,000 × 52 px | 5,200,000 px | all | all | all |
|
|
272
|
+
| 1,000,000 × 52 px | 52,000,000 px | 645,277 (64.5 %) | 344,139 (34.4 %) | 645,277 (64.5 %) |
|
|
273
|
+
| 1,000,000 × 32 px | 32,000,000 px | all | 559,224 (55.9 %) | all |
|
|
274
|
+
<!-- bench:end -->
|
|
275
|
+
|
|
276
|
+
### Reach: the browser height ceiling
|
|
277
|
+
|
|
278
|
+
The grid makes its scroll area as tall as all the rows: `rowCount × rowHeight` pixels. It does not compress scroll positions, so every row keeps its real offset. Browsers limit how tall an element can be, and the *Reach* rows above report the last row that actually appears on screen after scrolling to the very bottom:
|
|
279
|
+
|
|
280
|
+
- **Chromium and WebKit** stop the element at 33,554,428 px. Everything above that is reachable, everything below is not. At 52 px that is 645,277 rows, so 1M rows at 52 px reach 64.5 %. At 32 px (`density="compact"`), 1M rows need 32,000,000 px and are all reachable.
|
|
281
|
+
- **Firefox** has a lower limit, about 17.9M px, and handles it differently: an element taller than that collapses instead of stopping at the limit. On 3.5.0, which these tables were measured on, the grid then had nothing to scroll: 500k rows at 52 px (26M px) and 1M rows at either height showed only their first screen of rows (24 or 33 rows). **3.5.1 fixes this**: the grid caps its scroll content to the limit it measures, so Firefox reaches about 344,000 rows at 52 px and 559,000 at 32 px, like Chromium and WebKit reach theirs.
|
|
282
|
+
|
|
283
|
+
If you need more rows than this in one scrolling grid, page them (`pagination`), load them from the server (`paginationMode="server"`, or infinite scroll with a `dataSource`), or reduce the row height.
|
|
284
|
+
|
|
285
|
+
### Rerun
|
|
286
|
+
|
|
287
|
+
```bash
|
|
288
|
+
npx playwright install chromium firefox webkit # once
|
|
289
|
+
npm run bench # all engines, 10k/100k/500k/1M, about 30 min
|
|
290
|
+
npm run bench -- --quick # 1 run per size, no 500k, a few minutes
|
|
291
|
+
npm run bench -- --rows 100000 --engine firefox # one size, one engine
|
|
292
|
+
node bench/report.mjs # Markdown tables of the newest results file
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
Results are written to `bench/results/<date>-<machine>.json` with the machine, browser versions and package version. Close other heavy programs first and keep a laptop on power. More options: [Testing → The benchmark](contributing/testing.md#the-benchmark). A weekly CI job runs a quick Chromium pass and keeps the JSON as a build artifact; CI machines are shared and noisy, so it is for spotting trends, not for comparing with these numbers.
|
|
296
|
+
|
|
119
297
|
## Related
|
|
120
298
|
|
|
121
299
|
- [Virtualization](./features/virtualization.md)
|
|
122
300
|
- [Server-Side Data](./features/data-source.md)
|
|
123
301
|
- [Sorting & Pagination](./features/sorting-pagination.md)
|
|
124
302
|
- [Infinite Scroll](./features/infinite-scroll.md)
|
|
303
|
+
- [Testing → The benchmark](./contributing/testing.md#the-benchmark)
|
package/docs/roadmap.md
CHANGED
|
@@ -89,9 +89,9 @@ This document tracks the current status of features in `OpenGridX` and outlines
|
|
|
89
89
|
|
|
90
90
|
### Across the quarter
|
|
91
91
|
|
|
92
|
-
* **Published benchmark
|
|
93
|
-
* **Next.js App Router guide
|
|
94
|
-
* **RTL check
|
|
92
|
+
* ✅ **Published benchmark** (done 9 Oct): reproducible 10k / 100k / 500k / 1M-row numbers (scroll frame times, sort, filter and grouping time, memory, the reachable row) in [`docs/performance.md`](performance.md#benchmarks); rerun with `npm run bench`.
|
|
93
|
+
* ✅ **Next.js App Router guide** (done 12 Oct): [`docs/features/nextjs.md`](features/nextjs.md), verified with `next build` / `next start` on Next.js 15.5 and 16.4. The ES bundle now starts with `'use client'`.
|
|
94
|
+
* ✅ **RTL check** (done 12 Oct): [`docs/features/rtl.md`](features/rtl.md). Fixed: text alignment, horizontal scrolling and column virtualization, arrow keys, the resize and fill handles, range outlines and auto-scroll, toolbar panel placement. Pinned columns remain a limitation (Q1 2027).
|
|
95
95
|
* **Demo site**: source viewer on all examples (`DocsLayout` with Vite `?raw`) and syntax highlighting, done alongside each release's demo page.
|
|
96
96
|
|
|
97
97
|
### Definition of done for each feature
|
|
@@ -117,6 +117,7 @@ This document tracks the current status of features in `OpenGridX` and outlines
|
|
|
117
117
|
|
|
118
118
|
## 🔭 Next (Q1 2027)
|
|
119
119
|
|
|
120
|
+
|
|
120
121
|
* **Row reordering inside tree data and row groups**, touch-friendly (MUI #4821, 405 👍, not in MUI X).
|
|
121
122
|
* **Find & highlight** (Ctrl+F through cells), **right-click context menu**, **Excel-style value (set) filter**.
|
|
122
123
|
* **Calculated columns** (`[price] * [qty]` defined by users).
|
|
@@ -124,6 +125,7 @@ This document tracks the current status of features in `OpenGridX` and outlines
|
|
|
124
125
|
* **Sparklines** and a charts adapter that passes grid state to any chart library.
|
|
125
126
|
* **AI toolkit, part 3**: smart paste (unstructured text → pending rows) and `apiRef.getAiContext()` for "summarise selection"; an `opengridx-mcp` docs server for coding assistants.
|
|
126
127
|
* **Tailwind / shadcn theme preset**, sticky header against page scroll.
|
|
128
|
+
* ✅ **RTL, part 2** (done 12 Oct, [`rtl.md`](features/rtl.md)): pinned columns and pinned system columns with logical sticky offsets and mirrored shadows, column menu pinning by screen side, mirrored pagination arrows and tree chevrons, a direction change on a mounted grid, column reorder by drag checked. Remaining: hard-coded English menu labels (no `localeText` yet), and the bidi-mirrored header filter operator symbols.
|
|
127
129
|
|
|
128
130
|
### Not planned
|
|
129
131
|
|
package/lib/ai/promptHandler.ts
CHANGED
|
@@ -50,18 +50,22 @@ function emptyResult(errors: GridAiValidationError[], message?: string): GridAiP
|
|
|
50
50
|
/** Validates a model reply into a branded result. Never throws. */
|
|
51
51
|
export function readModelReply(reply: unknown, columns: readonly GridAiColumn[], parts: GridAiPartOptions): GridAiPromptResult {
|
|
52
52
|
let json: unknown = reply;
|
|
53
|
-
let
|
|
53
|
+
let around = '';
|
|
54
54
|
if (typeof reply === 'string') {
|
|
55
55
|
const text = readModelText(reply);
|
|
56
56
|
if (text.json === null) return emptyResult([], text.message || undefined);
|
|
57
|
-
|
|
58
|
-
message
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
57
|
+
around = text.message;
|
|
58
|
+
// Parsed here so a `message` key inside the JSON is read as for an object reply. Bad JSON is
|
|
59
|
+
// passed on as text: validateGridAiState reports it.
|
|
60
|
+
try {
|
|
61
|
+
json = JSON.parse(text.json) as unknown;
|
|
62
|
+
} catch {
|
|
63
|
+
json = text.json;
|
|
64
|
+
}
|
|
64
65
|
}
|
|
66
|
+
// A reply such as { filterModel, message: 'Showing paid orders' }: the validator skips the message.
|
|
67
|
+
const inner = isPlainObject(json) && hasOwn(json, 'message') && typeof json.message === 'string' ? json.message.trim() : '';
|
|
68
|
+
const message = [around, inner].filter(Boolean).join('\n\n');
|
|
65
69
|
const result: GridAiPromptResult = validateGridAiState(json, columns, parts);
|
|
66
70
|
if (message) result.message = message;
|
|
67
71
|
return result;
|
package/lib/ai/validate.ts
CHANGED
|
@@ -241,8 +241,8 @@ function readPart(ctx: Ctx, part: GridAiPart, raw: unknown, path: string, state:
|
|
|
241
241
|
|
|
242
242
|
/**
|
|
243
243
|
* Checks a model's reply against the columns and returns the state that is safe to apply.
|
|
244
|
-
* `json` is an object or a JSON string (bad JSON gives one error and an empty state).
|
|
245
|
-
* parts, fields, operators and enum values are dropped, each with an error; values are coerced to
|
|
244
|
+
* `json` is an object or a JSON string (bad JSON gives one error and an empty state). A top-level
|
|
245
|
+
* string `message` (the model's reply text) is skipped, not reported. Unknown parts, fields, operators and enum values are dropped, each with an error; values are coerced to
|
|
246
246
|
* the column type (`"1,200"` → 1200, a date → `"YYYY-MM-DD"`); filter groups left empty are
|
|
247
247
|
* removed. Never throws, and never returns a field the columns do not allow.
|
|
248
248
|
*
|
|
@@ -283,6 +283,9 @@ function validate(json: unknown, columns: readonly GridAiColumn[], options: Grid
|
|
|
283
283
|
const part = ALL_PARTS.find((p) => PART_KEYS[p] === key);
|
|
284
284
|
if (key === 'schemaVersion') {
|
|
285
285
|
if (input[key] !== 1) fail(ctx, key, `Unsupported schemaVersion ${show(input[key])}`);
|
|
286
|
+
} else if (key === 'message') {
|
|
287
|
+
// The model's reply text next to the state (createGridAiPromptHandler shows it): not state.
|
|
288
|
+
if (typeof input[key] !== 'string') fail(ctx, key, `Expected text, got ${show(input[key])}`);
|
|
286
289
|
} else if (!part) fail(ctx, key, 'Unknown part');
|
|
287
290
|
else if (!allowed.includes(part)) fail(ctx, key, 'Part not allowed');
|
|
288
291
|
else readPart(ctx, part, input[key], key, state);
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
display: flex;
|
|
4
4
|
align-items: center;
|
|
5
5
|
padding: var(--ogx-grid-cell-padding-y) var(--ogx-grid-cell-padding-x);
|
|
6
|
-
border-
|
|
6
|
+
border-inline-end: 1px solid var(--ogx-grid-border-color);
|
|
7
7
|
font-size: var(--ogx-grid-cell-font-size);
|
|
8
8
|
color: var(--ogx-grid-row-text);
|
|
9
9
|
line-height: var(--ogx-grid-line-height);
|
|
@@ -52,6 +52,19 @@
|
|
|
52
52
|
.ogx__cell--range-right {
|
|
53
53
|
--ogx-range-edge-right: inset -2px 0 0 0 var(--ogx-range-border);
|
|
54
54
|
}
|
|
55
|
+
/* Right to left: `--range-left` marks the range's first column, which is on the right. */
|
|
56
|
+
.ogx__cell--range-left:dir(rtl) {
|
|
57
|
+
--ogx-range-edge-left: 0 0 0 0 transparent;
|
|
58
|
+
--ogx-range-edge-right: inset -2px 0 0 0 var(--ogx-range-border);
|
|
59
|
+
}
|
|
60
|
+
.ogx__cell--range-right:dir(rtl) {
|
|
61
|
+
--ogx-range-edge-right: 0 0 0 0 transparent;
|
|
62
|
+
--ogx-range-edge-left: inset 2px 0 0 0 var(--ogx-range-border);
|
|
63
|
+
}
|
|
64
|
+
.ogx__cell--range-left.ogx__cell--range-right:dir(rtl) {
|
|
65
|
+
--ogx-range-edge-left: inset 2px 0 0 0 var(--ogx-range-border);
|
|
66
|
+
--ogx-range-edge-right: inset -2px 0 0 0 var(--ogx-range-border);
|
|
67
|
+
}
|
|
55
68
|
/*
|
|
56
69
|
* Fill handle (v3.5): the square on the bottom-right cell of the range. The cell clips its overflow,
|
|
57
70
|
* so the 24px hit area lies inside the corner and the visible square sits in its corner.
|
|
@@ -59,7 +72,7 @@
|
|
|
59
72
|
*/
|
|
60
73
|
.ogx__cell-fill-handle {
|
|
61
74
|
position: absolute;
|
|
62
|
-
|
|
75
|
+
inset-inline-end: 0;
|
|
63
76
|
bottom: 0;
|
|
64
77
|
width: 24px;
|
|
65
78
|
height: 24px;
|
|
@@ -72,7 +85,7 @@
|
|
|
72
85
|
.ogx__cell-fill-handle::after {
|
|
73
86
|
content: '';
|
|
74
87
|
position: absolute;
|
|
75
|
-
|
|
88
|
+
inset-inline-end: 1px;
|
|
76
89
|
bottom: 1px;
|
|
77
90
|
width: 7px;
|
|
78
91
|
height: 7px;
|
|
@@ -101,7 +114,7 @@
|
|
|
101
114
|
}
|
|
102
115
|
.ogx__cell--align-left {
|
|
103
116
|
justify-content: flex-start;
|
|
104
|
-
text-align:
|
|
117
|
+
text-align: start;
|
|
105
118
|
}
|
|
106
119
|
.ogx__cell--align-center {
|
|
107
120
|
justify-content: center;
|
|
@@ -109,7 +122,7 @@
|
|
|
109
122
|
}
|
|
110
123
|
.ogx__cell--align-right {
|
|
111
124
|
justify-content: flex-end;
|
|
112
|
-
text-align:
|
|
125
|
+
text-align: end;
|
|
113
126
|
}
|
|
114
127
|
.ogx__cell--pinned-left,
|
|
115
128
|
.ogx__cell--pinned-right {
|
|
@@ -119,11 +132,11 @@
|
|
|
119
132
|
background-color: var(--ogx-grid-background);
|
|
120
133
|
}
|
|
121
134
|
.ogx__cell--pinned-right {
|
|
122
|
-
border-
|
|
135
|
+
border-inline-start: 1px solid var(--ogx-grid-border-color);
|
|
123
136
|
}
|
|
124
137
|
/* When the columns are narrower than the grid, the free space goes before the right-pinned section. */
|
|
125
138
|
.ogx__cell--pinned-right-first {
|
|
126
|
-
margin-
|
|
139
|
+
margin-inline-start: auto;
|
|
127
140
|
}
|
|
128
141
|
.ogx:not(.ogx--row-spanning) .ogx__row:hover .ogx__cell--pinned-left,
|
|
129
142
|
.ogx:not(.ogx--row-spanning) .ogx__row:hover .ogx__cell--pinned-right {
|
|
@@ -146,7 +159,7 @@
|
|
|
146
159
|
content: '';
|
|
147
160
|
position: absolute;
|
|
148
161
|
top: 0;
|
|
149
|
-
|
|
162
|
+
inset-inline-end: -10px;
|
|
150
163
|
bottom: 0;
|
|
151
164
|
width: 10px;
|
|
152
165
|
pointer-events: none;
|
|
@@ -156,12 +169,19 @@
|
|
|
156
169
|
content: '';
|
|
157
170
|
position: absolute;
|
|
158
171
|
top: 0;
|
|
159
|
-
|
|
172
|
+
inset-inline-start: -10px;
|
|
160
173
|
bottom: 0;
|
|
161
174
|
width: 10px;
|
|
162
175
|
pointer-events: none;
|
|
163
176
|
box-shadow: inset -10px 0 8px -8px rgba(0, 0, 0, 0.1);
|
|
164
177
|
}
|
|
178
|
+
/* Right to left: the start-pinned section is on the right, so its shadow falls on its left side. */
|
|
179
|
+
.ogx__cell--pinned-left-last:dir(rtl)::after {
|
|
180
|
+
box-shadow: inset -10px 0 8px -8px rgba(0, 0, 0, 0.1);
|
|
181
|
+
}
|
|
182
|
+
.ogx__cell--pinned-right-first:dir(rtl)::before {
|
|
183
|
+
box-shadow: inset 10px 0 8px -8px rgba(0, 0, 0, 0.1);
|
|
184
|
+
}
|
|
165
185
|
.ogx__cell--hidden {
|
|
166
186
|
visibility: hidden;
|
|
167
187
|
border: none;
|
|
@@ -258,9 +258,9 @@ function CellImpl<R extends GridRowModel = GridRowModel>(props: CellProps<R>) {
|
|
|
258
258
|
|
|
259
259
|
if (isPinned && pinnedOffset !== undefined) {
|
|
260
260
|
if (pinnedPosition === 'left') {
|
|
261
|
-
style.
|
|
261
|
+
style.insetInlineStart = pinnedOffset;
|
|
262
262
|
} else {
|
|
263
|
-
style.
|
|
263
|
+
style.insetInlineEnd = pinnedOffset;
|
|
264
264
|
}
|
|
265
265
|
}
|
|
266
266
|
|
|
@@ -23,12 +23,12 @@
|
|
|
23
23
|
box-sizing: border-box;
|
|
24
24
|
position: relative;
|
|
25
25
|
overflow: hidden;
|
|
26
|
-
/* Same border UI as primary header cells: half-height
|
|
26
|
+
/* Same border UI as primary header cells: half-height divider on the end edge */
|
|
27
27
|
}
|
|
28
28
|
.ogx-col-group-cell:not(:last-child)::after {
|
|
29
29
|
content: '';
|
|
30
30
|
position: absolute;
|
|
31
|
-
|
|
31
|
+
inset-inline-end: 0;
|
|
32
32
|
top: 25%;
|
|
33
33
|
height: 50%;
|
|
34
34
|
width: 1px;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
position: absolute;
|
|
3
3
|
top: 0;
|
|
4
4
|
/* Inside the header cell: the cell clips overflow, so a handle straddling the border lost its outer half. */
|
|
5
|
-
|
|
5
|
+
inset-inline-end: 0;
|
|
6
6
|
width: 8px;
|
|
7
7
|
height: 100%;
|
|
8
8
|
cursor: col-resize;
|
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
}
|
|
17
17
|
/* Right-pinned columns grow leftwards: their handle sits on the left edge. */
|
|
18
18
|
.ogx-column-resize-handle--start {
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
inset-inline-end: auto;
|
|
20
|
+
inset-inline-start: 0;
|
|
21
21
|
justify-content: flex-start;
|
|
22
22
|
}
|
|
23
23
|
.ogx-column-resize-handle:focus-visible .ogx-column-resize-handle__line {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
2
|
import { clampResizeWidth } from './clampResizeWidth';
|
|
3
|
+
import { isRtl, logicalArrowKey } from '../../utils/direction';
|
|
3
4
|
|
|
4
5
|
|
|
5
6
|
export interface ColumnResizeHandleProps {
|
|
@@ -23,6 +24,8 @@ interface ActiveResize {
|
|
|
23
24
|
pointerId: number;
|
|
24
25
|
startX: number;
|
|
25
26
|
startWidth: number;
|
|
27
|
+
/** The grid lays out right to left: the handle's edge grows the column towards the left. */
|
|
28
|
+
rtl: boolean;
|
|
26
29
|
lastUpdate: number;
|
|
27
30
|
moved: boolean;
|
|
28
31
|
}
|
|
@@ -53,7 +56,8 @@ export function ColumnResizeHandle(props: ColumnResizeHandleProps) {
|
|
|
53
56
|
useEffect(() => () => { activeRef.current = null; }, []);
|
|
54
57
|
|
|
55
58
|
const widthAt = useCallback((active: ActiveResize, clientX: number) => {
|
|
56
|
-
|
|
59
|
+
// Pointer movement towards the column's end edge, in screen pixels.
|
|
60
|
+
const deltaX = (clientX - active.startX) * (active.rtl ? -1 : 1);
|
|
57
61
|
const raw = active.startWidth + (edge === 'start' ? -deltaX : deltaX);
|
|
58
62
|
return clampResizeWidth(raw, active.startWidth, minWidth, maxWidth);
|
|
59
63
|
}, [edge, minWidth, maxWidth]);
|
|
@@ -67,6 +71,7 @@ export function ColumnResizeHandle(props: ColumnResizeHandleProps) {
|
|
|
67
71
|
startX: event.clientX,
|
|
68
72
|
// The logical stored width: reliable for pinned, flex, and normal columns.
|
|
69
73
|
startWidth: currentWidth,
|
|
74
|
+
rtl: isRtl(event.currentTarget),
|
|
70
75
|
lastUpdate: 0,
|
|
71
76
|
moved: false,
|
|
72
77
|
};
|
|
@@ -97,6 +102,16 @@ export function ColumnResizeHandle(props: ColumnResizeHandleProps) {
|
|
|
97
102
|
if (!active || event.pointerId !== active.pointerId) return;
|
|
98
103
|
activeRef.current = null;
|
|
99
104
|
setIsDragging(false);
|
|
105
|
+
// Release explicitly: the implicit release only follows a real pointerup, and a capture left on
|
|
106
|
+
// this handle (a synthetic press on the live mouse pointer, or an interrupted drag) would keep
|
|
107
|
+
// the pointer's later events from reaching the rest of the page.
|
|
108
|
+
if (event.currentTarget.hasPointerCapture?.(event.pointerId)) {
|
|
109
|
+
try {
|
|
110
|
+
event.currentTarget.releasePointerCapture(event.pointerId);
|
|
111
|
+
} catch {
|
|
112
|
+
// Already released.
|
|
113
|
+
}
|
|
114
|
+
}
|
|
100
115
|
if (active.moved || event.clientX !== active.startX) {
|
|
101
116
|
// The drag may end over the header cell, which would take the synthesized click as a sort.
|
|
102
117
|
suppressNextClick();
|
|
@@ -114,7 +129,8 @@ export function ColumnResizeHandle(props: ColumnResizeHandleProps) {
|
|
|
114
129
|
if (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight') return;
|
|
115
130
|
event.preventDefault();
|
|
116
131
|
event.stopPropagation();
|
|
117
|
-
const
|
|
132
|
+
const key = logicalArrowKey(event.key, isRtl(event.currentTarget));
|
|
133
|
+
const step = (event.shiftKey ? 50 : 10) * (key === 'ArrowRight' ? 1 : -1);
|
|
118
134
|
onResize(field, clampResizeWidth(currentWidth + step, currentWidth, minWidth, maxWidth));
|
|
119
135
|
}, [field, currentWidth, onResize, onAutosize, minWidth, maxWidth]);
|
|
120
136
|
|
|
@@ -243,7 +243,7 @@
|
|
|
243
243
|
justify-content: center;
|
|
244
244
|
overflow: hidden;
|
|
245
245
|
box-sizing: border-box;
|
|
246
|
-
border-
|
|
246
|
+
border-inline-end: 1px solid var(--ogx-grid-border-color);
|
|
247
247
|
position: relative;
|
|
248
248
|
}
|
|
249
249
|
.ogx__aggregation-cell--pinned-left,
|
|
@@ -265,7 +265,7 @@
|
|
|
265
265
|
background: var(--ogx-grid-header-background);
|
|
266
266
|
}
|
|
267
267
|
.ogx__aggregation-cell--pinned-right {
|
|
268
|
-
border-
|
|
268
|
+
border-inline-start: 1px solid var(--ogx-grid-border-color);
|
|
269
269
|
}
|
|
270
270
|
.ogx__aggregation-cell--pinned-left-last {
|
|
271
271
|
overflow: visible;
|
|
@@ -274,26 +274,32 @@
|
|
|
274
274
|
content: '';
|
|
275
275
|
position: absolute;
|
|
276
276
|
top: 0;
|
|
277
|
-
|
|
277
|
+
inset-inline-end: -10px;
|
|
278
278
|
bottom: 0;
|
|
279
279
|
width: 10px;
|
|
280
280
|
pointer-events: none;
|
|
281
281
|
box-shadow: inset 10px 0 8px -8px rgba(0, 0, 0, 0.1);
|
|
282
282
|
}
|
|
283
283
|
.ogx__aggregation-cell--pinned-right-first {
|
|
284
|
-
margin-
|
|
284
|
+
margin-inline-start: auto;
|
|
285
285
|
overflow: visible;
|
|
286
286
|
}
|
|
287
287
|
.ogx__aggregation-cell--pinned-right-first::before {
|
|
288
288
|
content: '';
|
|
289
289
|
position: absolute;
|
|
290
290
|
top: 0;
|
|
291
|
-
|
|
291
|
+
inset-inline-start: -10px;
|
|
292
292
|
bottom: 0;
|
|
293
293
|
width: 10px;
|
|
294
294
|
pointer-events: none;
|
|
295
295
|
box-shadow: inset -10px 0 8px -8px rgba(0, 0, 0, 0.1);
|
|
296
296
|
}
|
|
297
|
+
.ogx__aggregation-cell--pinned-left-last:dir(rtl)::after {
|
|
298
|
+
box-shadow: inset -10px 0 8px -8px rgba(0, 0, 0, 0.1);
|
|
299
|
+
}
|
|
300
|
+
.ogx__aggregation-cell--pinned-right-first:dir(rtl)::before {
|
|
301
|
+
box-shadow: inset 10px 0 8px -8px rgba(0, 0, 0, 0.1);
|
|
302
|
+
}
|
|
297
303
|
.ogx__aggregation-label {
|
|
298
304
|
font-size: 0.7em;
|
|
299
305
|
text-transform: uppercase;
|
|
@@ -321,7 +327,7 @@
|
|
|
321
327
|
}
|
|
322
328
|
.ogx__group-cell-content {
|
|
323
329
|
font-weight: 500;
|
|
324
|
-
text-align:
|
|
330
|
+
text-align: start;
|
|
325
331
|
display: flex;
|
|
326
332
|
align-items: center;
|
|
327
333
|
gap: 4px;
|
|
@@ -4,6 +4,8 @@ import { useGridKeyboardNavigation } from '../../hooks/core/useGridKeyboardNavig
|
|
|
4
4
|
import { useGridControlledState } from '../../hooks/core/useGridControlledState';
|
|
5
5
|
import { useGridThemeDimensions } from '../../hooks/core/useGridThemeDimensions';
|
|
6
6
|
import { useGridRowPipeline, type GridRowPipelineResult } from '../../hooks/core/useGridRowPipeline';
|
|
7
|
+
import { useGridMaxContentHeight } from '../../hooks/core/useGridMaxContentHeight';
|
|
8
|
+
import { countReachableRows } from '../../utils/maxElementHeight';
|
|
7
9
|
import { useGridVirtualization } from '../../hooks/core/useGridVirtualization';
|
|
8
10
|
import { useGridColumns } from '../../hooks/core/useGridColumns';
|
|
9
11
|
import { useGridVisibleRows } from '../../hooks/core/useGridVisibleRows';
|
|
@@ -585,12 +587,14 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
585
587
|
onRowsScrollEnd, overscanRowCount, rowCount: layout.unpinnedRowsLength, autoHeight, setDimensions, viewportRef,
|
|
586
588
|
});
|
|
587
589
|
|
|
590
|
+
const maxContentHeight = useGridMaxContentHeight(containerRef);
|
|
588
591
|
const virtualization = useGridVirtualization({
|
|
589
592
|
layout, scrollTop, scrollLeft,
|
|
590
593
|
viewportWidth: state.dimensions.viewportWidth,
|
|
591
594
|
viewportHeight: state.dimensions.viewportHeight,
|
|
592
595
|
autoHeight, rowReordering, hasDetailPanel, checkboxSelection, pinCheckboxColumn, pinExpandColumn,
|
|
593
596
|
overscanRows,
|
|
597
|
+
maxContentHeight: maxContentHeight ?? Infinity,
|
|
594
598
|
});
|
|
595
599
|
|
|
596
600
|
|
|
@@ -743,6 +747,9 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
743
747
|
totalRowCount: pinnedTopRows.length + centerRows.length + pinnedBottomRows.length,
|
|
744
748
|
pinnedRowsIgnored: isHierarchyEnabled && Boolean(pinnedRows?.top?.length || pinnedRows?.bottom?.length),
|
|
745
749
|
scrollHeight: virtualization.totalHeight,
|
|
750
|
+
maxContentHeight,
|
|
751
|
+
reachableRowCount: countReachableRows(virtualization.cumulativeHeights, virtualization.virtualContainerHeight),
|
|
752
|
+
unpinnedRowCount: virtualization.cumulativeHeights.length,
|
|
746
753
|
listViewWithoutColumn: listViewRequested && !listViewColumn,
|
|
747
754
|
listViewRowCount: listView && listViewColumn ? allRenderableRows.length : 0,
|
|
748
755
|
});
|
|
@@ -984,6 +991,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
984
991
|
filterModel={filterModel}
|
|
985
992
|
onFilterModelChange={handleFilterModelChange}
|
|
986
993
|
onOpenFilterPanel={columnsPanel.openFiltersPanel}
|
|
994
|
+
localeText={localeText}
|
|
987
995
|
/>
|
|
988
996
|
|
|
989
997
|
<GridPinnedRows<R>
|
|
@@ -1008,7 +1016,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
1008
1016
|
|
|
1009
1017
|
<GridVirtualRows<R>
|
|
1010
1018
|
{...rowRenderProps}
|
|
1011
|
-
virtualContainerHeight={virtualization.
|
|
1019
|
+
virtualContainerHeight={virtualization.virtualContainerHeight}
|
|
1012
1020
|
offsetTop={spanRowWindow.offsetTop}
|
|
1013
1021
|
effectiveLoading={effectiveLoading}
|
|
1014
1022
|
visibleRows={visibleRows}
|
|
@@ -68,7 +68,7 @@ export function GridAggregationFooter({
|
|
|
68
68
|
<div
|
|
69
69
|
key={key}
|
|
70
70
|
className={`ogx__aggregation-spacer${pinned ? ' ogx__aggregation-spacer--pinned' : ''}`}
|
|
71
|
-
style={pinned ? { left } : undefined}
|
|
71
|
+
style={pinned ? { insetInlineStart: left } : undefined}
|
|
72
72
|
aria-hidden="true"
|
|
73
73
|
/>
|
|
74
74
|
);
|
|
@@ -116,8 +116,8 @@ export function GridAggregationFooter({
|
|
|
116
116
|
maxWidth: colWidth,
|
|
117
117
|
textAlign: (col.align as React.CSSProperties['textAlign']) || 'left',
|
|
118
118
|
};
|
|
119
|
-
if (pinnedPosition === 'left' && pinnedOffset !== undefined) style.
|
|
120
|
-
if (pinnedPosition === 'right' && pinnedOffset !== undefined) style.
|
|
119
|
+
if (pinnedPosition === 'left' && pinnedOffset !== undefined) style.insetInlineStart = pinnedOffset;
|
|
120
|
+
if (pinnedPosition === 'right' && pinnedOffset !== undefined) style.insetInlineEnd = pinnedOffset;
|
|
121
121
|
|
|
122
122
|
return (
|
|
123
123
|
<div
|