@opencorestack/opengridx 3.5.1 → 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.
Files changed (73) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +2 -0
  3. package/dist/ai.cjs +4 -2
  4. package/dist/ai.cjs.map +1 -1
  5. package/dist/ai.d.ts +2 -2
  6. package/dist/ai.es.js +4 -2
  7. package/dist/ai.es.js.map +1 -1
  8. package/dist/index.d.ts +32 -0
  9. package/dist/opengridx.css +1 -1
  10. package/dist/opengridx.es.js +6573 -6390
  11. package/dist/opengridx.es.js.map +1 -1
  12. package/dist/opengridx.umd.js +16 -16
  13. package/dist/opengridx.umd.js.map +1 -1
  14. package/docs/API_REFERENCE.md +14 -1
  15. package/docs/README.md +2 -0
  16. package/docs/features/ai-toolkit.md +2 -2
  17. package/docs/features/export-guide.md +2 -0
  18. package/docs/features/header-filters.md +34 -0
  19. package/docs/features/keyboard-navigation.md +2 -0
  20. package/docs/features/nextjs.md +241 -0
  21. package/docs/features/pdf-export.md +2 -0
  22. package/docs/features/pinning.md +2 -0
  23. package/docs/features/rtl.md +58 -0
  24. package/docs/performance.md +86 -118
  25. package/docs/roadmap.md +4 -2
  26. package/lib/ai/promptHandler.ts +12 -8
  27. package/lib/ai/validate.ts +5 -2
  28. package/lib/components/AiAssistant/GridAiAssistantPanel.css +1 -1
  29. package/lib/components/Cell/Cell.css +29 -9
  30. package/lib/components/Cell/Cell.tsx +2 -2
  31. package/lib/components/ColumnGroupHeader/ColumnGroupHeader.css +2 -2
  32. package/lib/components/ColumnResizeHandle/ColumnResizeHandle.css +3 -3
  33. package/lib/components/ColumnResizeHandle/ColumnResizeHandle.tsx +18 -2
  34. package/lib/components/DataGrid/DataGrid.css +12 -6
  35. package/lib/components/DataGrid/DataGrid.tsx +1 -0
  36. package/lib/components/DataGrid/GridAggregationFooter.tsx +3 -3
  37. package/lib/components/DataGrid/GridListView.tsx +3 -1
  38. package/lib/components/Header/ColumnMenu.css +1 -1
  39. package/lib/components/Header/ColumnMenu.tsx +14 -6
  40. package/lib/components/Header/Header.css +18 -10
  41. package/lib/components/Header/Header.tsx +16 -9
  42. package/lib/components/Header/headerColumnStyle.ts +3 -2
  43. package/lib/components/HeaderFilter/HeaderFilter.css +8 -2
  44. package/lib/components/HeaderFilter/HeaderFilterCell.tsx +21 -18
  45. package/lib/components/HeaderFilter/HeaderFilterOperatorMenu.tsx +5 -4
  46. package/lib/components/HeaderFilter/HeaderFilterRow.tsx +9 -3
  47. package/lib/components/ListView/ListViewRow.tsx +1 -1
  48. package/lib/components/Pagination/Pagination.css +4 -0
  49. package/lib/components/Pagination/Pagination.tsx +4 -4
  50. package/lib/components/Row/Row.tsx +3 -3
  51. package/lib/components/SkeletonRow/SkeletonRow.tsx +2 -2
  52. package/lib/components/Toolbar/GridToolbar.tsx +8 -28
  53. package/lib/components/Toolbar/PivotPanel.tsx +4 -7
  54. package/lib/components/ui/ExpandIcon.css +13 -0
  55. package/lib/components/ui/ExpandIcon.tsx +2 -4
  56. package/lib/hooks/core/useGridCellSelection.ts +20 -5
  57. package/lib/hooks/core/useGridColumns.tsx +2 -2
  58. package/lib/hooks/core/useGridKeyboardNavigation.ts +7 -4
  59. package/lib/hooks/core/useGridScrollSync.ts +28 -5
  60. package/lib/hooks/core/useLayout.ts +13 -6
  61. package/lib/hooks/useRowGrouping.ts +9 -9
  62. package/lib/types/index.ts +30 -0
  63. package/lib/utils/aggregation/index.ts +56 -1
  64. package/lib/utils/direction.ts +52 -0
  65. package/lib/utils/export/exportToExcelAdvanced.ts +9 -2
  66. package/lib/utils/export/exportToPdf.ts +2 -1
  67. package/lib/utils/filtering/index.ts +58 -6
  68. package/lib/utils/headerFilterLocale.ts +37 -0
  69. package/lib/utils/scroll/index.ts +5 -3
  70. package/lib/utils/sorting/index.ts +141 -26
  71. package/lib/utils/values.ts +10 -6
  72. package/llms.txt +4 -2
  73. package/package.json +1 -1
@@ -125,17 +125,39 @@ In server modes the grid shows `rows` as given: it does not re-filter, re-sort o
125
125
 
126
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
127
 
128
- **In short** (Chromium, median):
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
129
 
130
130
  | Rows | Mount | Sort (number / string) | Filter | Quick filter | Group + sum | Scroll frames over 16.7 ms | JS heap |
131
131
  | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
132
- | 10,000 | 79 ms | 34 / 42 ms | 19 ms | 52 ms | 30 ms | 0 % | 2.9 MB |
133
- | 100,000 | 89 ms | 102 / 160 ms | 28 ms | 205 ms | 87 ms | 0 % | 10.4 MB |
134
- | 500,000 | 295 ms | 493 / 641 ms | 59 ms | 958 ms | 331 ms | 0 % | 39.4 MB |
135
- | 1,000,000 | 688 ms | 1,105 / 1,316 ms | 95 ms | 1,845 ms | 681 ms | 0 % | 79.1 MB |
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.
136
158
 
137
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).
138
- - **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 1.1–1.3 s in Chromium (up to 2.9 s for a string sort in WebKit), and the first quick-filter keystroke for about 2–3 s. For sorting or searching a million rows interactively, use server-side sorting and filtering.
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.
139
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).
140
162
 
141
163
  ### Method
@@ -154,155 +176,101 @@ Measured with `npm run bench` (`bench/`), which installs the packed npm package
154
176
  ### Results
155
177
 
156
178
  <!-- bench:start -->
157
- <!-- Generated by `node bench/report.mjs --write docs/performance.md` from bench/results/2026-10-09-apple-m1-darwin.json. Do not edit by hand. -->
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. -->
158
180
 
159
181
  - **Machine:** Apple M1, 8 cores, 8 GB RAM, macOS 26.5.1 (arm64)
160
182
  - **Browsers:** Chromium 145.0.7632.6, Firefox 146.0.1, WebKit 26.0 (Playwright, headless, 1280×800 page)
161
- - **Package:** @opencorestack/opengridx 3.5.0 (commit `f2ad5cb`), production build
162
- - **Date:** 2026-10-09; load average at start 3.88 / 3.73 / 4.04, at end 3.46 / 3.71 / 3.77
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
163
185
  - **Runs:** median of 5 runs after one discarded warm-up run, with the min–max range in brackets
164
186
 
165
187
  ¹ Chromium only: Firefox and WebKit have no long-task entries, and the heap is read through the Chrome DevTools Protocol.
166
188
 
167
- #### 10,000 rows, 52 px rows
168
-
169
- | Metric | Chromium | Firefox | WebKit |
170
- | :--- | ---: | ---: | ---: |
171
- | Mount | 79 ms (59–88) | 84 ms (82–85) | 116 ms (110–128) |
172
- | Sort, number column | 34 ms (28–38) | 44 ms (43–46) | 81 ms (72–85) |
173
- | Sort, string column | 42 ms (22–45) | 56 ms (36–58) | 83 ms (76–108) |
174
- | Filter, string `contains` | 19 ms (14–31) | 31 ms (26–40) | 51 ms (47–57) |
175
- | Filter, number `>` | 18 ms (13–19) | 25 ms (24–28) | 37 ms (34–39) |
176
- | Quick filter, one term | 52 ms (33–60) | 76 ms (68–83) | 96 ms (84–110) |
177
- | Group by region + `sum` | 30 ms (22–37) | 41 ms (31–47) | 77 ms (72–126) |
178
- | Scroll frame time p50 | 16.7 ms | 16.7 ms | 17.0 ms |
179
- | Scroll frame time p95 | 17.6 ms (17.5–17.6) | 17.6 ms (17.6–18.6) | 45.0 ms (43.0–46.0) |
180
- | Scroll frame time p99 | 17.6 ms (17.6–17.7) | 33.3 ms (33.3–34.1) | 49.0 ms (47.0–58.0) |
181
- | Scroll frames over 16.7 ms | 0.0 % (0.0–0.3) | 1.7 % (1.4–4.2) | 8.6 % (7.6–9.7) |
182
- | Scroll frames over 33 ms | 0.0 % | 0.0 % (0.0–0.7) | 6.5 % (5.4–7.1) |
183
- | … over 16.7 ms while scrolling steadily | 0.0 % | 0.6 % (0.0–4.7) | 0.0 % |
184
- | … over 16.7 ms during the jumps | 0.0 % (0.0–0.8) | 3.5 % (2.6–4.3) | 26.1 % (21.9–29.9) |
185
- | Long tasks during scroll ¹ | 0 | – | – |
186
- | Longest task during a sort ¹ | none | – | – |
187
- | JS heap for the grid ¹ | 2.9 MB | – | – |
188
- | Row elements in the DOM, end of scroll | 76 | 76 | 76 |
189
- | Cells in the DOM, end of scroll | 912 | 912 | 912 |
190
- | Row elements in the DOM, at rest | 19 | 19 | 19 |
191
- | Reach: last row on screen at the bottom | all rows | all rows | all rows |
192
-
193
189
  #### 100,000 rows, 52 px rows
194
190
 
195
191
  | Metric | Chromium | Firefox | WebKit |
196
192
  | :--- | ---: | ---: | ---: |
197
- | Mount | 89 ms (86–100) | 138 ms (133–155) | 157 ms (138–204) |
198
- | Sort, number column | 102 ms (85–119) | 129 ms (120–136) | 159 ms (126–237) |
199
- | Sort, string column | 160 ms (152–165) | 213 ms (180–224) | 241 ms (211–259) |
200
- | Filter, string `contains` | 28 ms (20–34) | 42 ms (28–46) | 56 ms (47–109) |
201
- | Filter, number `>` | 33 ms (27–39) | 39 ms (24–57) | 48 ms (31–60) |
202
- | Quick filter, one term | 205 ms (193–232) | 348 ms (322–359) | 323 ms (269–340) |
203
- | Group by region + `sum` | 87 ms (81–91) | 107 ms (99–109) | 118 ms (105–143) |
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) |
204
200
  | Scroll frame time p50 | 16.7 ms | 16.7 ms | 17.0 ms |
205
- | Scroll frame time p95 | 17.4 ms (17.3–17.5) | 17.5 ms (17.4–17.6) | 44.0 ms (43.0–45.0) |
206
- | Scroll frame time p99 | 17.6 ms (17.6–17.7) | 33.3 ms | 48.0 ms (47.0–55.0) |
207
- | Scroll frames over 16.7 ms | 0.0 % (0.0–0.3) | 2.4 % (1.0–2.7) | 7.9 % (7.1–9.3) |
208
- | Scroll frames over 33 ms | 0.0 % | 0.0 % (0.0–0.3) | 6.4 % (5.4–7.0) |
209
- | … over 16.7 ms while scrolling steadily | 0.0 % | 0.6 % (0.0–1.7) | 0.0 % (0.0–0.6) |
210
- | … over 16.7 ms during the jumps | 0.0 % (0.0–0.8) | 4.3 % (1.7–6.2) | 22.7 % (20.2–26.1) |
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) |
211
207
  | Long tasks during scroll ¹ | 0 | – | – |
212
- | Longest task during a sort ¹ | 155 ms (144–160) | – | – |
208
+ | Longest task during a sort ¹ | 59 ms (55–81) | – | – |
213
209
  | JS heap for the grid ¹ | 10.4 MB | – | – |
214
210
  | Row elements in the DOM, end of scroll | 76 | 76 | 76 |
215
211
  | Cells in the DOM, end of scroll | 912 | 912 | 912 |
216
212
  | Row elements in the DOM, at rest | 19 | 19 | 19 |
217
213
  | Reach: last row on screen at the bottom | all rows | all rows | all rows |
218
214
 
219
- #### 500,000 rows, 52 px rows
220
-
221
- | Metric | Chromium | Firefox | WebKit |
222
- | :--- | ---: | ---: | ---: |
223
- | Mount | 295 ms (287–307) | 482 ms (473–505) | 402 ms (338–463) |
224
- | Sort, number column | 493 ms (485–567) | 660 ms (629–936) | 483 ms (413–564) |
225
- | Sort, string column | 641 ms (600–807) | 1,105 ms (1,045–1,188) | 1,407 ms (1,203–1,568) |
226
- | Filter, string `contains` | 59 ms (51–67) | 63 ms (56–68) | 75 ms (72–87) |
227
- | Filter, number `>` | 80 ms (70–111) | 100 ms (92–111) | 89 ms (77–101) |
228
- | Quick filter, one term | 958 ms (836–976) | 1,565 ms (1,503–1,828) | 1,442 ms (1,275–1,619) |
229
- | Group by region + `sum` | 331 ms (307–338) | 641 ms (419–675) | 386 ms (351–1,244) |
230
- | Scroll frame time p50 | 16.7 ms | 16.7 ms | 17.0 ms |
231
- | Scroll frame time p95 | 17.6 ms (17.5–17.6) | 17.5 ms (17.5–17.6) | 45.0 ms (44.0–46.0) |
232
- | Scroll frame time p99 | 17.7 ms (17.6–17.7) | 17.6 ms (17.6–17.7) | 49.0 ms (46.0–50.0) |
233
- | Scroll frames over 16.7 ms | 0.0 % (0.0–0.7) | 0.0 % | 7.7 % (7.6–10.1) |
234
- | Scroll frames over 33 ms | 0.0 % (0.0–0.3) | 0.0 % | 7.3 % (5.8–7.8) |
235
- | … over 16.7 ms while scrolling steadily | 0.0 % | 0.0 % | 0.0 % (0.0–1.1) |
236
- | … over 16.7 ms during the jumps | 0.0 % (0.0–1.8) | 0.0 % | 23.1 % (20.6–28.4) |
237
- | Long tasks during scroll ¹ | 0 | – | – |
238
- | Longest task during a sort ¹ | 635 ms (595–799) | – | – |
239
- | JS heap for the grid ¹ | 39.4 MB | – | – |
240
- | Row elements in the DOM, end of scroll | 76 | 22 | 76 |
241
- | Cells in the DOM, end of scroll | 912 | 264 | 912 |
242
- | Row elements in the DOM, at rest | 19 | 19 | 19 |
243
- | Reach: last row on screen at the bottom | all rows | 24 (<0.1 %) | all rows |
244
-
245
215
  #### 1,000,000 rows, 52 px rows
246
216
 
247
217
  | Metric | Chromium | Firefox | WebKit |
248
218
  | :--- | ---: | ---: | ---: |
249
- | Mount | 688 ms (673–715) | 1,097 ms (1,011–1,503) | 589 ms (576–735) |
250
- | Sort, number column | 1,105 ms (1,094–1,140) | 1,540 ms (1,477–1,795) | 1,023 ms (956–1,075) |
251
- | Sort, string column | 1,316 ms (1,227–1,463) | 2,434 ms (2,371–2,716) | 2,913 ms (2,715–3,277) |
252
- | Filter, string `contains` | 95 ms (87–152) | 100 ms (90–108) | 102 ms (102–112) |
253
- | Filter, number `>` | 144 ms (129–149) | 192 ms (175–237) | 136 ms (123–242) |
254
- | Quick filter, one term | 1,845 ms (1,761–1,977) | 3,161 ms (2,920–3,327) | 2,914 ms (2,762–3,272) |
255
- | Group by region + `sum` | 681 ms (664–703) | 1,754 ms (1,695–2,059) | 764 ms (720–901) |
256
- | Scroll frame time p50 | 16.7 ms | 16.7 ms | 17.0 ms |
257
- | Scroll frame time p95 | 17.4 ms (17.4–17.6) | 17.6 ms (17.4–17.6) | 44.0 ms (44.0–46.0) |
258
- | Scroll frame time p99 | 17.7 ms (17.6–17.7) | 17.6 ms (17.6–17.7) | 46.0 ms (45.0–48.0) |
259
- | Scroll frames over 16.7 ms | 0.0 % (0.0–0.7) | 0.0 % (0.0–0.3) | 7.2 % (7.1–9.3) |
260
- | Scroll frames over 33 ms | 0.0 % | 0.0 % | 7.0 % (6.5–7.1) |
261
- | … over 16.7 ms while scrolling steadily | 0.0 % | 0.0 % (0.0–0.6) | 0.0 % |
262
- | … over 16.7 ms during the jumps | 0.0 % (0.0–1.7) | 0.0 % | 20.8 % (20.0–28.1) |
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) |
263
233
  | Long tasks during scroll ¹ | 0 | – | – |
264
- | Longest task during a sort ¹ | 1,307 ms (1,220–1,455) | – | – |
265
- | JS heap for the grid ¹ | 79.1 MB | – | – |
266
- | Row elements in the DOM, end of scroll | 77 | 22 | 77 |
267
- | Cells in the DOM, end of scroll | 924 | 264 | 924 |
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 |
268
238
  | Row elements in the DOM, at rest | 19 | 19 | 19 |
269
- | Reach: last row on screen at the bottom | 645,277 (64.5 %) | 24 (<0.1 %) | 645,277 (64.5 %) |
239
+ | Reach: last row on screen at the bottom | 645,277 (64.5 %) | 344,139 (34.4 %) | 645,277 (64.5 %) |
270
240
 
271
241
  #### 1,000,000 rows, 32 px rows
272
242
 
273
243
  | Metric | Chromium | Firefox | WebKit |
274
244
  | :--- | ---: | ---: | ---: |
275
- | Mount | 674 ms (668–680) | 1,106 ms (1,032–1,329) | 604 ms (585–621) |
276
- | Sort, number column | 1,143 ms (1,090–1,254) | 1,475 ms (1,462–1,837) | 1,077 ms (986–1,137) |
277
- | Sort, string column | 1,295 ms (1,236–1,456) | 2,517 ms (2,268–2,675) | 2,758 ms (2,720–2,947) |
278
- | Filter, string `contains` | 92 ms (91–109) | 99 ms (99–109) | 107 ms (106–121) |
279
- | Filter, number `>` | 137 ms (133–140) | 206 ms (172–232) | 140 ms (126–161) |
280
- | Quick filter, one term | 1,818 ms (1,651–1,855) | 2,960 ms (2,910–3,200) | 2,750 ms (2,677–2,776) |
281
- | Group by region + `sum` | 694 ms (673–758) | 2,071 ms (1,224–2,745) | 749 ms (689–798) |
282
- | Scroll frame time p50 | 16.7 ms | 16.7 ms | 17.0 ms |
283
- | Scroll frame time p95 | 17.5 ms (17.5–18.5) | 17.5 ms (17.3–17.5) | 49.0 ms (48.0–49.0) |
284
- | Scroll frame time p99 | 17.6 ms (17.6–18.7) | 17.6 ms (17.6–17.7) | 52.0 ms (51.0–52.0) |
285
- | Scroll frames over 16.7 ms | 0.0 % (0.0–0.3) | 0.0 % (0.0–0.3) | 15.5 % (15.4–15.8) |
286
- | Scroll frames over 33 ms | 0.0 % | 0.0 % | 7.7 % (7.7–7.8) |
287
- | … over 16.7 ms while scrolling steadily | 0.0 % (0.0–0.6) | 0.0 % (0.0–0.6) | 0.0 % (0.0–0.6) |
288
- | … over 16.7 ms during the jumps | 0.0 % | 0.0 % | 51.3 % (50.6–53.3) |
289
- | Long tasks during scroll ¹ | 0 | – | – |
290
- | Longest task during a sort ¹ | 1,286 ms (1,229–1,436) | – | – |
291
- | JS heap for the grid ¹ | 79.5 MB | – | – |
292
- | Row elements in the DOM, end of scroll | 86 | 32 | 86 |
293
- | Cells in the DOM, end of scroll | 1,032 | 384 | 1,032 |
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 |
294
264
  | Row elements in the DOM, at rest | 28 | 28 | 28 |
295
- | Reach: last row on screen at the bottom | all rows | 33 (<0.1 %) | all rows |
265
+ | Reach: last row on screen at the bottom | all rows | 559,224 (55.9 %) | all rows |
296
266
 
297
267
  #### Reach: rows that can be scrolled into view
298
268
 
299
269
  | Rows × row height | Content height | Chromium | Firefox | WebKit |
300
270
  | :--- | ---: | ---: | ---: | ---: |
301
- | 10,000 × 52 px | 520,000 px | all | all | all |
302
271
  | 100,000 × 52 px | 5,200,000 px | all | all | all |
303
- | 500,000 × 52 px | 26,000,000 px | all | 24 (<0.1 %) | all |
304
- | 1,000,000 × 52 px | 52,000,000 px | 645,277 (64.5 %) | 24 (<0.1 %) | 645,277 (64.5 %) |
305
- | 1,000,000 × 32 px | 32,000,000 px | all | 33 (<0.1 %) | 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 |
306
274
  <!-- bench:end -->
307
275
 
308
276
  ### Reach: the browser height ceiling
package/docs/roadmap.md CHANGED
@@ -90,8 +90,8 @@ This document tracks the current status of features in `OpenGridX` and outlines
90
90
  ### Across the quarter
91
91
 
92
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**: using the grid from a `'use client'` component, with the stylesheet in `app/layout.tsx`.
94
- * **RTL check**: verify right-to-left layouts and document the result.
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
 
@@ -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 message = '';
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
- json = text.json;
58
- message = text.message;
59
- } else if (isPlainObject(reply) && hasOwn(reply, 'message') && typeof reply.message === 'string') {
60
- // A reply such as { filterModel, message: 'Showing paid orders' }: the message is not state.
61
- const { message: text, ...rest } = reply;
62
- json = rest;
63
- message = text as string;
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;
@@ -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). Unknown
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);
@@ -157,7 +157,7 @@
157
157
  background: transparent;
158
158
  color: var(--ogx-color-primary);
159
159
  font: inherit;
160
- text-align: left;
160
+ text-align: start;
161
161
  cursor: pointer;
162
162
  }
163
163
  .ogx-toolbar__ai-btn {
@@ -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-right: 1px solid var(--ogx-grid-border-color);
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
- right: 0;
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
- right: 1px;
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: left;
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: right;
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-left: 1px solid var(--ogx-grid-border-color);
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-left: auto;
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
- right: -10px;
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
- left: -10px;
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.left = pinnedOffset;
261
+ style.insetInlineStart = pinnedOffset;
262
262
  } else {
263
- style.right = pinnedOffset;
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 right divider */
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
- right: 0;
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
- right: 0;
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
- right: auto;
20
- left: 0;
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
- const deltaX = clientX - active.startX;
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 step = (event.shiftKey ? 50 : 10) * (event.key === 'ArrowRight' ? 1 : -1);
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