@opencorestack/opengridx 3.5.1 → 3.7.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 (114) hide show
  1. package/CHANGELOG.md +53 -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 +62 -0
  9. package/dist/opengridx.css +1 -1
  10. package/dist/opengridx.es.js +9643 -9024
  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 +17 -3
  15. package/docs/README.md +2 -0
  16. package/docs/architecture/datagrid-orchestration.md +46 -29
  17. package/docs/architecture/grid-row-meta.md +1 -1
  18. package/docs/customization/slots-api.md +1 -1
  19. package/docs/customization/theming.md +43 -0
  20. package/docs/features/ai-toolkit.md +2 -2
  21. package/docs/features/export-guide.md +2 -0
  22. package/docs/features/header-filters.md +34 -0
  23. package/docs/features/keyboard-navigation.md +2 -0
  24. package/docs/features/nextjs.md +241 -0
  25. package/docs/features/pdf-export.md +2 -0
  26. package/docs/features/pinning.md +2 -0
  27. package/docs/features/rtl.md +58 -0
  28. package/docs/features/virtualization.md +16 -2
  29. package/docs/performance.md +89 -119
  30. package/docs/roadmap.md +4 -2
  31. package/lib/ai/promptHandler.ts +12 -8
  32. package/lib/ai/validate.ts +5 -2
  33. package/lib/components/AiAssistant/GridAiAssistantPanel.css +4 -2
  34. package/lib/components/Cell/Cell.css +31 -9
  35. package/lib/components/Cell/Cell.tsx +2 -2
  36. package/lib/components/ColumnGroupHeader/ColumnGroupHeader.css +4 -4
  37. package/lib/components/ColumnResizeHandle/ColumnResizeHandle.css +3 -3
  38. package/lib/components/ColumnResizeHandle/ColumnResizeHandle.tsx +18 -2
  39. package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.css +3 -1
  40. package/lib/components/DataGrid/DataGrid.css +19 -13
  41. package/lib/components/DataGrid/DataGrid.tsx +163 -670
  42. package/lib/components/DataGrid/GridAggregationFooter.tsx +3 -3
  43. package/lib/components/DataGrid/GridListView.tsx +3 -1
  44. package/lib/components/DataGrid/GridRoot.tsx +239 -0
  45. package/lib/components/DataGrid/GridViewport.tsx +314 -0
  46. package/lib/components/FilterPanel/FilterPanel.css +9 -8
  47. package/lib/components/Header/ColumnMenu.css +5 -4
  48. package/lib/components/Header/ColumnMenu.tsx +14 -6
  49. package/lib/components/Header/Header.css +23 -14
  50. package/lib/components/Header/Header.tsx +16 -9
  51. package/lib/components/Header/headerColumnStyle.ts +3 -2
  52. package/lib/components/HeaderFilter/HeaderFilter.css +9 -3
  53. package/lib/components/HeaderFilter/HeaderFilterCell.tsx +21 -18
  54. package/lib/components/HeaderFilter/HeaderFilterOperatorMenu.tsx +5 -4
  55. package/lib/components/HeaderFilter/HeaderFilterRow.tsx +9 -3
  56. package/lib/components/ListView/ListViewRow.tsx +1 -1
  57. package/lib/components/Pagination/Pagination.css +5 -1
  58. package/lib/components/Pagination/Pagination.tsx +4 -4
  59. package/lib/components/Row/Row.css +2 -2
  60. package/lib/components/Row/Row.tsx +3 -3
  61. package/lib/components/SkeletonRow/SkeletonRow.tsx +2 -2
  62. package/lib/components/Toolbar/GridToolbar.css +8 -7
  63. package/lib/components/Toolbar/GridToolbar.tsx +8 -28
  64. package/lib/components/Toolbar/PivotPanel.css +3 -3
  65. package/lib/components/Toolbar/PivotPanel.tsx +4 -7
  66. package/lib/components/ui/ExpandIcon.css +13 -0
  67. package/lib/components/ui/ExpandIcon.tsx +2 -4
  68. package/lib/hooks/core/useGridCellSelection.ts +20 -5
  69. package/lib/hooks/core/useGridColumns.tsx +2 -2
  70. package/lib/hooks/core/useGridDevWarnings.ts +3 -1
  71. package/lib/hooks/core/useGridEditPipeline.ts +55 -0
  72. package/lib/hooks/core/useGridKeyboardNavigation.ts +7 -4
  73. package/lib/hooks/core/useGridRangeEditing.ts +129 -0
  74. package/lib/hooks/core/useGridRenderWindow.ts +66 -0
  75. package/lib/hooks/core/useGridScrollSync.ts +28 -5
  76. package/lib/hooks/core/useGridScrollViewport.ts +42 -0
  77. package/lib/hooks/core/useGridSelectionAndStateApi.ts +61 -0
  78. package/lib/hooks/core/useGridToolbarStack.ts +49 -0
  79. package/lib/hooks/core/useGridViewApis.ts +46 -0
  80. package/lib/hooks/core/useLayout.ts +13 -6
  81. package/lib/hooks/useRowGrouping.ts +9 -9
  82. package/lib/styles/base/variables.css +12 -0
  83. package/lib/theme/DataGridThemeProvider.tsx +15 -0
  84. package/lib/theme/__baseline__/default-theme.darwin.chromium.json +1503 -0
  85. package/lib/theme/__baseline__/default-theme.darwin.firefox.json +1503 -0
  86. package/lib/theme/__baseline__/default-theme.darwin.webkit.json +1503 -0
  87. package/lib/theme/types.ts +31 -0
  88. package/lib/types/ai.ts +109 -0
  89. package/lib/types/api.ts +169 -0
  90. package/lib/types/columns.ts +341 -0
  91. package/lib/types/dataSource.ts +40 -0
  92. package/lib/types/editing.ts +77 -0
  93. package/lib/types/export.ts +164 -0
  94. package/lib/types/filtering.ts +52 -0
  95. package/lib/types/grouping.ts +27 -0
  96. package/lib/types/index.ts +18 -1672
  97. package/lib/types/locale.ts +51 -0
  98. package/lib/types/props.ts +405 -0
  99. package/lib/types/rows.ts +132 -0
  100. package/lib/types/selection.ts +66 -0
  101. package/lib/types/slots.ts +56 -0
  102. package/lib/types/sorting.ts +25 -0
  103. package/lib/types/state.ts +35 -0
  104. package/lib/utils/aggregation/index.ts +56 -1
  105. package/lib/utils/direction.ts +52 -0
  106. package/lib/utils/export/exportToExcelAdvanced.ts +9 -2
  107. package/lib/utils/export/exportToPdf.ts +2 -1
  108. package/lib/utils/filtering/index.ts +58 -6
  109. package/lib/utils/headerFilterLocale.ts +37 -0
  110. package/lib/utils/scroll/index.ts +5 -3
  111. package/lib/utils/sorting/index.ts +141 -26
  112. package/lib/utils/values.ts +10 -6
  113. package/llms.txt +4 -2
  114. package/package.json +4 -2
@@ -72,8 +72,8 @@ export const SkeletonRow: React.FC<SkeletonRowProps> = ({
72
72
  width,
73
73
  minWidth: col.minWidth,
74
74
  maxWidth: col.maxWidth,
75
- left: col.left,
76
- right: col.right,
75
+ insetInlineStart: col.left,
76
+ insetInlineEnd: col.right,
77
77
  position: col.isSpacer ? 'relative' : (col.pinned ? 'sticky' : 'relative'),
78
78
  zIndex: col.zIndex
79
79
  }}
@@ -80,6 +80,7 @@
80
80
  z-index: 9999;
81
81
  overflow: hidden;
82
82
  animation: ogx-panel-in 0.15s ease-out;
83
+ font-family: var(--ogx-overlay-font-family, inherit);
83
84
  }
84
85
  @keyframes ogx-panel-in {
85
86
  from { opacity: 0; transform: translateY(-6px) scale(0.98); }
@@ -97,7 +98,7 @@
97
98
  display: flex;
98
99
  align-items: center;
99
100
  gap: var(--ogx-spacing-xs);
100
- font-family: var(--ogx-font-family);
101
+ font-family: var(--ogx-overlay-font-family, var(--ogx-font-family));
101
102
  font-size: var(--ogx-font-size-sm);
102
103
  font-weight: 600;
103
104
  color: var(--ogx-grid-header-text);
@@ -108,7 +109,7 @@
108
109
  border-radius: var(--ogx-border-radius-sm);
109
110
  background: transparent;
110
111
  color: var(--ogx-toolbar-btn-danger-text, var(--ogx-color-error, #ef4444));
111
- font-family: var(--ogx-font-family);
112
+ font-family: var(--ogx-overlay-font-family, var(--ogx-font-family));
112
113
  font-size: var(--ogx-font-size-xs);
113
114
  font-weight: 500;
114
115
  cursor: pointer;
@@ -124,7 +125,7 @@
124
125
  border-radius: var(--ogx-border-radius-sm);
125
126
  background: transparent;
126
127
  color: var(--ogx-grid-header-text);
127
- font-family: var(--ogx-font-family);
128
+ font-family: var(--ogx-overlay-font-family, var(--ogx-font-family));
128
129
  font-size: var(--ogx-font-size-xs);
129
130
  font-weight: 500;
130
131
  cursor: pointer;
@@ -142,7 +143,7 @@
142
143
  }
143
144
  .ogx-toolbar__empty {
144
145
  padding: var(--ogx-spacing-md) var(--ogx-spacing-md);
145
- font-family: var(--ogx-font-family);
146
+ font-family: var(--ogx-overlay-font-family, var(--ogx-font-family));
146
147
  font-size: var(--ogx-font-size-sm);
147
148
  color: var(--ogx-grid-header-text);
148
149
  line-height: 1.5;
@@ -171,9 +172,9 @@
171
172
  }
172
173
  .ogx-toolbar__agg-label {
173
174
  flex: 0 0 100px;
174
- font-family: var(--ogx-font-family);
175
- font-size: var(--ogx-font-size-xs);
176
- font-weight: 500;
175
+ font-family: var(--ogx-overlay-font-family, var(--ogx-font-family));
176
+ font-size: var(--ogx-overlay-font-size, var(--ogx-font-size-xs));
177
+ font-weight: var(--ogx-overlay-font-weight, 500);
177
178
  color: var(--ogx-grid-row-text);
178
179
  overflow: hidden;
179
180
  text-overflow: ellipsis;
@@ -4,6 +4,7 @@ import type { GridColDef, GridAggregationModel, GridPivotModel, GridFilterModel
4
4
  import { AGGREGATION_FUNCTIONS } from '../../utils/aggregation';
5
5
  import type { BuiltInAggFn } from '../../utils/aggregation';
6
6
  import { getViewportWidth } from '../../utils/viewport';
7
+ import { getPanelPosition, type GridPanelPosition } from '../../utils/direction';
7
8
  import { GridToolbarHostContext } from '../../hooks/core/gridToolbarHostContext';
8
9
  import { PivotPanel, PivotIcon } from './PivotPanel';
9
10
  import { GlobalSearch } from './GlobalSearch';
@@ -149,7 +150,7 @@ function FilterIcon() {
149
150
  );
150
151
  }
151
152
 
152
- interface PanelPosition { top: number; right: number; }
153
+ type PanelPosition = GridPanelPosition;
153
154
 
154
155
  function AggregationPanel({
155
156
  anchorRef,
@@ -176,14 +177,7 @@ function AggregationPanel({
176
177
  setPortalContainer(anchorRef.current?.closest('.ogx-theme-provider') ?? document.body);
177
178
  }, [anchorRef]);
178
179
 
179
- const computePos = useCallback((): PanelPosition => {
180
- if (!anchorRef.current) return { top: 0, right: 0 };
181
- const rect = anchorRef.current.getBoundingClientRect();
182
- return {
183
- top: rect.bottom + 6,
184
- right: getViewportWidth() - rect.right,
185
- };
186
- }, [anchorRef]);
180
+ const computePos = useCallback((): PanelPosition => getPanelPosition(anchorRef.current, getViewportWidth()), [anchorRef]);
187
181
 
188
182
  useEffect(() => {
189
183
  setPos(computePos());
@@ -221,7 +215,7 @@ function AggregationPanel({
221
215
  className="ogx-toolbar__panel"
222
216
  role="dialog"
223
217
  aria-label="Summaries configuration"
224
- style={{ top: pos.top, right: pos.right, width: 400, maxWidth: 'calc(100vw - 24px)' }}
218
+ style={{ top: pos.top, right: pos.right, left: pos.left, width: 400, maxWidth: 'calc(100vw - 24px)' }}
225
219
  >
226
220
  <div className="ogx-toolbar__panel-header">
227
221
  <span className="ogx-toolbar__panel-title">
@@ -309,14 +303,7 @@ function ColumnsPanelWrapper({
309
303
  setPortalContainer(anchorRef.current?.closest('.ogx-theme-provider') ?? document.body);
310
304
  }, [anchorRef]);
311
305
 
312
- const computePos = useCallback((): PanelPosition => {
313
- if (!anchorRef.current) return { top: 0, right: 0 };
314
- const rect = anchorRef.current.getBoundingClientRect();
315
- return {
316
- top: rect.bottom + 6,
317
- right: getViewportWidth() - rect.right,
318
- };
319
- }, [anchorRef]);
306
+ const computePos = useCallback((): PanelPosition => getPanelPosition(anchorRef.current, getViewportWidth()), [anchorRef]);
320
307
 
321
308
  useEffect(() => {
322
309
  setPos(computePos());
@@ -354,7 +341,7 @@ function ColumnsPanelWrapper({
354
341
  className="ogx-toolbar__panel"
355
342
  role="dialog"
356
343
  aria-label="Column visibility"
357
- style={{ top: pos.top, right: pos.right, width: 280, padding: 0 }}
344
+ style={{ top: pos.top, right: pos.right, left: pos.left, width: 280, padding: 0 }}
358
345
  >
359
346
  <ColumnVisibilityPanel
360
347
  columns={columns}
@@ -398,14 +385,7 @@ function FilterPanelWrapper({
398
385
  setPortalContainer(anchorRef.current?.closest('.ogx-theme-provider') ?? document.body);
399
386
  }, [anchorRef]);
400
387
 
401
- const computePos = useCallback((): PanelPosition => {
402
- if (!anchorRef.current) return { top: 0, right: 0 };
403
- const rect = anchorRef.current.getBoundingClientRect();
404
- return {
405
- top: rect.bottom + 6,
406
- right: getViewportWidth() - rect.right,
407
- };
408
- }, [anchorRef]);
388
+ const computePos = useCallback((): PanelPosition => getPanelPosition(anchorRef.current, getViewportWidth()), [anchorRef]);
409
389
 
410
390
  useEffect(() => {
411
391
  setPos(computePos());
@@ -442,7 +422,7 @@ function FilterPanelWrapper({
442
422
  className="ogx-toolbar__panel ogx-toolbar__panel--filter"
443
423
  role="dialog"
444
424
  aria-label="Advanced filters"
445
- style={{ top: pos.top, right: pos.right, width: 540, maxWidth: 'calc(100vw - 24px)' }}
425
+ style={{ top: pos.top, right: pos.right, left: pos.left, width: 540, maxWidth: 'calc(100vw - 24px)' }}
446
426
  >
447
427
  <div className="ogx-toolbar__panel-header">
448
428
  <span className="ogx-toolbar__panel-title">
@@ -9,7 +9,7 @@
9
9
  z-index: 9999;
10
10
  overflow: hidden;
11
11
  animation: ogx-panel-in 0.15s ease-out;
12
- font-family: var(--ogx-font-family);
12
+ font-family: var(--ogx-overlay-font-family, var(--ogx-font-family));
13
13
  }
14
14
  @keyframes ogx-panel-in {
15
15
  from { opacity: 0; transform: translateY(-6px) scale(0.98); }
@@ -37,7 +37,7 @@
37
37
  border-radius: var(--ogx-border-radius-sm);
38
38
  background: transparent;
39
39
  color: var(--ogx-color-error, #ef4444);
40
- font-family: var(--ogx-font-family);
40
+ font-family: var(--ogx-overlay-font-family, var(--ogx-font-family));
41
41
  font-size: var(--ogx-font-size-xs);
42
42
  font-weight: 500;
43
43
  cursor: pointer;
@@ -163,7 +163,7 @@
163
163
  border-radius: 999px;
164
164
  background: var(--ogx-grid-header-background);
165
165
  color: var(--ogx-grid-row-text);
166
- font-family: var(--ogx-font-family);
166
+ font-family: var(--ogx-overlay-font-family, var(--ogx-font-family));
167
167
  font-size: 12px;
168
168
  font-weight: 500;
169
169
  cursor: pointer;
@@ -3,6 +3,7 @@ import ReactDOM from 'react-dom';
3
3
  import type { GridColDef } from '../../types';
4
4
  import type { GridPivotModel, GridPivotAggFn, GridPivotValueField } from '../../types';
5
5
  import { getViewportWidth } from '../../utils/viewport';
6
+ import { getPanelPosition, type GridPanelPosition } from '../../utils/direction';
6
7
 
7
8
  const AGG_FNS: GridPivotAggFn[] = ['sum', 'avg', 'count', 'min', 'max'];
8
9
 
@@ -31,18 +32,14 @@ export interface PivotPanelProps {
31
32
 
32
33
  export function PivotPanel({ anchorRef, columns, model, onChange, onClose }: PivotPanelProps) {
33
34
  const panelRef = useRef<HTMLDivElement>(null);
34
- const [pos, setPos] = useState({ top: 0, right: 0 });
35
+ const [pos, setPos] = useState<GridPanelPosition>({ top: 0, right: 0 });
35
36
  const [portalContainer, setPortalContainer] = useState<Element | null>(null);
36
37
 
37
38
  useLayoutEffect(() => {
38
39
  setPortalContainer(anchorRef.current?.closest('.ogx-theme-provider') ?? document.body);
39
40
  }, [anchorRef]);
40
41
 
41
- const computePos = useCallback(() => {
42
- if (!anchorRef.current) return { top: 0, right: 0 };
43
- const rect = anchorRef.current.getBoundingClientRect();
44
- return { top: rect.bottom + 6, right: getViewportWidth() - rect.right };
45
- }, [anchorRef]);
42
+ const computePos = useCallback(() => getPanelPosition(anchorRef.current, getViewportWidth()), [anchorRef]);
46
43
 
47
44
  useEffect(() => {
48
45
  setPos(computePos());
@@ -132,7 +129,7 @@ export function PivotPanel({ anchorRef, columns, model, onChange, onClose }: Piv
132
129
  className="ogx-pivot-panel"
133
130
  role="dialog"
134
131
  aria-label="Pivot configuration"
135
- style={{ top: pos.top, right: pos.right }}
132
+ style={{ top: pos.top, right: pos.right, left: pos.left }}
136
133
  >
137
134
  { }
138
135
  <div className="ogx-pivot-panel__header">
@@ -27,3 +27,16 @@
27
27
  display: block;
28
28
  transition: transform 0.2s ease;
29
29
  }
30
+ /* The chevron points towards the inline end (right, or left in a right-to-left grid) and down when expanded. */
31
+ .ogx-expand-icon__chevron {
32
+ transform: rotate(0deg);
33
+ }
34
+ .ogx-expand-icon--expanded .ogx-expand-icon__chevron {
35
+ transform: rotate(90deg);
36
+ }
37
+ .ogx-expand-icon__chevron:dir(rtl) {
38
+ transform: scaleX(-1) rotate(0deg);
39
+ }
40
+ .ogx-expand-icon--expanded .ogx-expand-icon__chevron:dir(rtl) {
41
+ transform: scaleX(-1) rotate(90deg);
42
+ }
@@ -59,10 +59,8 @@ export function ExpandIcon({ isExpanded, onClick, variant = 'chevron', tabIndex
59
59
  viewBox="0 0 16 16"
60
60
  fill="none"
61
61
  xmlns="http://www.w3.org/2000/svg"
62
- style={{
63
- transform: isExpanded ? 'rotate(90deg)' : 'rotate(0deg)',
64
- transition: 'transform 0.2s ease'
65
- }}
62
+ className="ogx-expand-icon__chevron"
63
+ aria-hidden="true"
66
64
  >
67
65
  <path
68
66
  d="M6 4L10 8L6 12"
@@ -1,6 +1,7 @@
1
1
  import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
2
2
  import type { RefObject } from 'react';
3
3
  import { scrollColumnIntoView, scrollRowIntoView } from '../../utils/scroll';
4
+ import { isRtl } from '../../utils/direction';
4
5
  import { isSyntheticRowId } from '../../utils/syntheticRows';
5
6
  import { isTextEntryElement } from '../../utils/focus';
6
7
  import type { FocusedCell } from '../../utils/focus';
@@ -132,6 +133,11 @@ interface DragState {
132
133
  y: number;
133
134
  frame: number;
134
135
  moved: boolean;
136
+ /**
137
+ * Whether the viewport lays out right to left, read once per drag: `getComputedStyle` every frame
138
+ * forces a style recalculation, which slowed WebKit's auto-scroll enough to delay the release.
139
+ */
140
+ rtl?: boolean;
135
141
  /** Ends the drag; `cancelled` unless the button was released normally (tracked drags: Escape cancels). */
136
142
  stop: (cancelled?: boolean, altKey?: boolean) => void;
137
143
  /** A tracked drag (fill handle): told the cell under the pointer each frame instead of moving the range. */
@@ -448,11 +454,17 @@ export function useGridCellSelection<R extends GridRowModel>(params: UseGridCell
448
454
  const bodyTop = stickyTop ? stickyTop.getBoundingClientRect().bottom : box.top;
449
455
  const bodyBottom = Math.min(box.top + height, stickyBottom ? stickyBottom.getBoundingClientRect().top : box.top + height);
450
456
  const headerBottom = headerWrap ? headerWrap.getBoundingClientRect().bottom : bodyTop;
451
- const bodyLeft = box.left + layout.pinnedSystemColumnsWidth + layout.leftWidth;
452
- const bodyRight = box.left + width - layout.rightWidth;
457
+ // Left-pinned columns (and pinned system columns) sit at the inline start: on the right in a
458
+ // right-to-left grid, whose vertical scrollbar is on the left.
459
+ if (drag.rtl === undefined) drag.rtl = isRtl(viewport);
460
+ const rtl = drag.rtl;
461
+ const contentLeft = rtl ? box.right - width : box.left;
462
+ const startPinnedWidth = layout.pinnedSystemColumnsWidth + layout.leftWidth;
463
+ const bodyLeft = contentLeft + (rtl ? layout.rightWidth : startPinnedWidth);
464
+ const bodyRight = contentLeft + width - (rtl ? startPinnedWidth : layout.rightWidth);
453
465
 
454
466
  // The cell under the pointer; outside the viewport (or over the header) the nearest body cell.
455
- const x = clamp(drag.x, box.left + 1, box.left + width - 2);
467
+ const x = clamp(drag.x, contentLeft + 1, contentLeft + width - 2);
456
468
  let y = drag.y;
457
469
  if (y < box.top || y > box.top + height || y < headerBottom) y = clamp(y, bodyTop + 1, Math.max(bodyTop + 1, bodyBottom - 2));
458
470
  // During fast auto-scroll the rows under the pointer may not be rendered yet (virtualization
@@ -476,7 +488,7 @@ export function useGridCellSelection<R extends GridRowModel>(params: UseGridCell
476
488
  if (drag.onTrack) drag.onTrack(drag.head, drag.x, drag.y);
477
489
 
478
490
  if (!allowScroll) return;
479
- const outside = drag.y < bodyTop || drag.y > bodyBottom || drag.x < box.left || drag.x > box.left + width;
491
+ const outside = drag.y < bodyTop || drag.y > bodyBottom || drag.x < contentLeft || drag.x > contentLeft + width;
480
492
  if (!drag.moved && !outside) return;
481
493
  let dy = 0;
482
494
  let dx = 0;
@@ -485,7 +497,10 @@ export function useGridCellSelection<R extends GridRowModel>(params: UseGridCell
485
497
  if (drag.x < bodyLeft + AUTO_SCROLL_EDGE) dx = -autoScrollStep(bodyLeft + AUTO_SCROLL_EDGE - drag.x);
486
498
  else if (drag.x > bodyRight - AUTO_SCROLL_EDGE) dx = autoScrollStep(drag.x - (bodyRight - AUTO_SCROLL_EDGE));
487
499
  const canScrollY = (dy < 0 && viewport.scrollTop > 0) || (dy > 0 && viewport.scrollTop + height < viewport.scrollHeight - 1);
488
- const canScrollX = (dx < 0 && viewport.scrollLeft > 0) || (dx > 0 && viewport.scrollLeft + width < viewport.scrollWidth - 1);
500
+ // scrollLeft runs from 0 to the maximum, or from minus the maximum to 0 right to left.
501
+ const maxScroll = viewport.scrollWidth - width;
502
+ const minLeft = rtl ? -maxScroll : 0;
503
+ const canScrollX = (dx < 0 && viewport.scrollLeft > minLeft) || (dx > 0 && viewport.scrollLeft < minLeft + maxScroll - 1);
489
504
  if (!canScrollY && !canScrollX) return;
490
505
  if (canScrollY) viewport.scrollTop += dy;
491
506
  if (canScrollX) viewport.scrollLeft += dx;
@@ -282,8 +282,8 @@ export function useGridColumns<R extends GridRowModel>(
282
282
  const content = custom.useDefault ? defaultHierarchyContent(cellParams, isRowGrouping, isTreeData) : custom.content;
283
283
 
284
284
  return (
285
- <div style={{ display: 'flex', alignItems: 'center', paddingLeft: depth * 24, width: '100%', height: '100%' }}>
286
- <div style={{ marginRight: 4, width: 24, height: 24, display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: hasChildren ? 'pointer' : 'default', flexShrink: 0 }}>
285
+ <div style={{ display: 'flex', alignItems: 'center', paddingInlineStart: depth * 24, width: '100%', height: '100%' }}>
286
+ <div style={{ marginInlineEnd: 4, width: 24, height: 24, display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: hasChildren ? 'pointer' : 'default', flexShrink: 0 }}>
287
287
  {hasChildren ? (
288
288
  <div
289
289
  onClick={(e) => { e.stopPropagation(); e.preventDefault(); activeHierarchyHandlers?.toggleExpansion(getRowId(cellParams.row)); }}
@@ -87,7 +87,9 @@ export function useGridDevWarnings(params: UseGridDevWarningsParams): void {
87
87
  console.warn(
88
88
  `[OpenGridX] The grid content is ${Math.round(scrollHeight)}px tall, more than this browser lays out ` +
89
89
  '(about 17.9M px in Firefox, 33.5M px in Chrome and Safari). ' + reach +
90
- 'Use pagination, server-side or infinite loading, or a smaller rowHeight. See docs/features/virtualization.md.'
90
+ 'Load the rows on demand with a `dataSource` (server-side pages or infinite scroll, ' +
91
+ 'docs/features/data-source.md), use `pagination`, or a smaller rowHeight (density="compact"). ' +
92
+ 'See docs/features/virtualization.md#the-browser-height-limit.'
91
93
  );
92
94
  }, [scrollHeight, maxContentHeight, reachableRowCount, unpinnedRowCount]);
93
95
 
@@ -0,0 +1,55 @@
1
+ import type { GridColDef, GridRowId, GridRowModel } from '../../types';
2
+ import { useGridEditCommitChannel } from './useGridUndoRedo';
3
+ import { useGridBatchEdit, type UseGridBatchEditParams } from '../features/useGridBatchEdit';
4
+ import { useGridEditing } from '../features/useGridEditing';
5
+
6
+ export interface UseGridEditPipelineParams<R extends GridRowModel> {
7
+ /** The row store's lookup (rows stored since the last render are read through `batchEdit.getRow`). */
8
+ rowsLookup: ReadonlyMap<GridRowId, R>;
9
+ /** The stored rows, in store order. */
10
+ rows: R[];
11
+ getRowId: (row: R) => GridRowId;
12
+ columns: GridColDef<R>[];
13
+ processRowUpdate: UseGridBatchEditParams<R>['processRowUpdate'];
14
+ onProcessRowUpdateError: UseGridBatchEditParams<R>['onProcessRowUpdateError'];
15
+ replaceRows: UseGridBatchEditParams<R>['replaceRows'];
16
+ }
17
+
18
+ /**
19
+ * Steps 12a–13 of `DataGrid` (docs/architecture/datagrid-orchestration.md), in their call order: the edit
20
+ * commit channel, the batch edit and single-cell editing. Single edits store through `batchEdit.storeRow`
21
+ * and report to the channel; the undo/redo history (`useGridRangeEditing`, later) listens on it.
22
+ */
23
+ export function useGridEditPipeline<R extends GridRowModel>(params: UseGridEditPipelineParams<R>) {
24
+ const { rowsLookup, rows, getRowId, columns, processRowUpdate, onProcessRowUpdateError, replaceRows } = params;
25
+
26
+ // Commits (single edits and batch edits) reach the undo/redo history created further down.
27
+ const editCommitChannel = useGridEditCommitChannel();
28
+ // Batch edits (paste, range clear, undo, redo): one processRowUpdate per row, one store update.
29
+ const batchEdit = useGridBatchEdit<R>({
30
+ rowsLookup,
31
+ columns,
32
+ processRowUpdate,
33
+ onProcessRowUpdateError,
34
+ replaceRows,
35
+ onCommitted: editCommitChannel.emit,
36
+ });
37
+
38
+ const editingHandlers = useGridEditing({
39
+ rows,
40
+ getRowId,
41
+ columns,
42
+ processRowUpdate,
43
+ onProcessRowUpdateError,
44
+ onRowChange: (updatedRow, rowId) => {
45
+ // Replaces the edited row in the *current* store, under the edited row's id: this runs
46
+ // after an awaited processRowUpdate, by which time the rows prop (or a dataSource
47
+ // fetch) may have replaced the rows this render saw. The returned row is stored as is
48
+ // (the grid never writes an id onto it), even if it does not carry its key itself.
49
+ batchEdit.storeRow(rowId, updatedRow);
50
+ },
51
+ onCommitted: (changes) => editCommitChannel.emit(changes, 'edit'),
52
+ });
53
+
54
+ return { editCommitChannel, batchEdit, editingHandlers };
55
+ }
@@ -1,4 +1,5 @@
1
1
  import { useState, useCallback, useMemo, useRef, useLayoutEffect } from 'react';
2
+ import { getScrollStart, isRtl, logicalArrowKey, setScrollStart } from '../../utils/direction';
2
3
  import { scrollRowIntoView } from '../../utils/scroll';
3
4
  import { resolveCellEditable } from '../../utils/editing';
4
5
  import { getCellValue } from '../../utils/values';
@@ -256,12 +257,13 @@ export function useGridKeyboardNavigation<R extends GridRowModel>(
256
257
  const isPinned = field === REORDER_FIELD
257
258
  || (field === EXPAND_FIELD && pinExpandColumn)
258
259
  || (field === CHECKBOX_FIELD && pinCheckboxColumn);
259
- if (!isPinned && el.scrollLeft > 0) el.scrollLeft = 0;
260
+ if (!isPinned && getScrollStart(el) > 0) setScrollStart(el, 0);
260
261
  return;
261
262
  }
262
263
  if (!columnMetrics) return;
263
264
 
264
- const { clientWidth, scrollLeft } = el;
265
+ const { clientWidth } = el;
266
+ const scrollLeft = getScrollStart(el);
265
267
  const { leftPinnedWidth, rightPinnedWidth, unpinnedAccWidths, unpinnedCols, totalSpecialsWidth, pinnedSpecialsWidth } = columnMetrics;
266
268
  const unpinnedIndex = unpinnedCols.findIndex(c => c.field === field);
267
269
  if (unpinnedIndex === -1) return;
@@ -277,7 +279,7 @@ export function useGridKeyboardNavigation<R extends GridRowModel>(
277
279
  } else if (colRight > visibleEnd) {
278
280
  newScrollLeft = colRight - clientWidth + rightPinnedWidth;
279
281
  }
280
- if (newScrollLeft !== scrollLeft) el.scrollLeft = newScrollLeft;
282
+ if (newScrollLeft !== scrollLeft) setScrollStart(el, newScrollLeft);
281
283
  }, [viewportRef, virtualization, pinnedTopRowCount, rowHeight, pinCheckboxColumn, pinExpandColumn]);
282
284
 
283
285
  // Keep DOM focus on the element of the focused cell. This covers keyboard moves, a cell that
@@ -365,7 +367,8 @@ export function useGridKeyboardNavigation<R extends GridRowModel>(
365
367
  // While an IME is composing, Enter / Escape / arrows confirm or navigate candidates.
366
368
  if (event.nativeEvent?.isComposing || event.keyCode === 229) return;
367
369
 
368
- const { key } = event;
370
+ // In a right-to-left grid ArrowLeft moves to the column on the left: the next column.
371
+ const key = logicalArrowKey(event.key, isRtl(viewport));
369
372
  const isSpace = key === ' ' || key === 'Spacebar';
370
373
  const isEditing = Boolean(editingHandlers.editingCell);
371
374
 
@@ -0,0 +1,129 @@
1
+ import type { MutableRefObject, RefObject } from 'react';
2
+ import type { DataGridProps, GridApi, GridCellParams, GridCellSelectionModel, GridColDef, GridRowId, GridRowMeta, GridRowModel } from '../../types';
3
+ import type { UseGridCellSelectionReturn } from './useGridCellSelection';
4
+ import type { FocusedCell } from './useGridKeyboardNavigation';
5
+ import type { LayoutResult } from './useLayout';
6
+ import type { UseGridBatchEditReturn } from '../features/useGridBatchEdit';
7
+ import { useGridCellSelectionApi } from './useGridCellSelectionApi';
8
+ import { useGridClipboardPaste } from './useGridClipboardPaste';
9
+ import { useGridUndoRedo, UNDO_HISTORY_LIMIT, type GridEditCommitChannel } from './useGridUndoRedo';
10
+ import { useGridFillHandle } from './useGridFillHandle';
11
+
12
+ /** The `DataGrid` props the range editing hooks read. */
13
+ export type GridRangeEditingProps<R extends GridRowModel> = Pick<DataGridProps<R>,
14
+ 'disableClipboardPaste' | 'disableRangeClear' | 'onClipboardPaste' | 'onBeforeClipboardPaste' |
15
+ 'undoRedo' | 'onHistoryChange' | 'disableFillHandle' | 'onFill'>;
16
+
17
+ export interface UseGridRangeEditingParams<R extends GridRowModel> {
18
+ props: GridRangeEditingProps<R>;
19
+ apiRef: MutableRefObject<GridApi>;
20
+ containerRef: RefObject<HTMLDivElement | null>;
21
+ viewportRef: RefObject<HTMLDivElement | null>;
22
+ selection: UseGridCellSelectionReturn;
23
+ model: GridCellSelectionModel;
24
+ getPendingModel: () => GridCellSelectionModel | null;
25
+ focusedCell: FocusedCell | null;
26
+ setFocusedCell: React.Dispatch<React.SetStateAction<FocusedCell | null>>;
27
+ allRenderableRows: R[];
28
+ getRowId: (row: R) => GridRowId;
29
+ navigationColumns: GridColDef<R>[];
30
+ columnIndexMap: Map<string, number>;
31
+ isCellEditable?: (params: GridCellParams<R>) => boolean;
32
+ rowMetaMap: Map<GridRowId, GridRowMeta>;
33
+ getSpanOrigin?: (rowId: GridRowId, field: string) => { rowId: GridRowId; field: string } | null;
34
+ isEditing: boolean;
35
+ batchEdit: UseGridBatchEditReturn<R>;
36
+ channel: GridEditCommitChannel;
37
+ layout: LayoutResult<R>;
38
+ rowHeight: number;
39
+ pinnedTopRowCount: number;
40
+ }
41
+
42
+ /**
43
+ * Steps 36a–36d of `DataGrid` (docs/architecture/datagrid-orchestration.md), in their call order: the cell
44
+ * selection API (anchor and focus kept together), clipboard paste and range clear, undo/redo, and the fill
45
+ * handle. Returns the fill handle (its position goes to the row renderers).
46
+ */
47
+ export function useGridRangeEditing<R extends GridRowModel>(params: UseGridRangeEditingParams<R>) {
48
+ const {
49
+ props, apiRef, containerRef, viewportRef, selection, model, getPendingModel, focusedCell, setFocusedCell,
50
+ allRenderableRows, getRowId, navigationColumns, columnIndexMap, isCellEditable, rowMetaMap, getSpanOrigin,
51
+ isEditing, batchEdit, channel, layout, rowHeight, pinnedTopRowCount,
52
+ } = params;
53
+ const {
54
+ disableClipboardPaste = false,
55
+ disableRangeClear = false,
56
+ onClipboardPaste,
57
+ onBeforeClipboardPaste,
58
+ undoRedo = false,
59
+ disableFillHandle = false,
60
+ onFill,
61
+ onHistoryChange,
62
+ } = props;
63
+
64
+ // Keeps the range anchor and the focused cell together; installs the cell selection API.
65
+ useGridCellSelectionApi({
66
+ apiRef,
67
+ selection,
68
+ model,
69
+ getPendingModel,
70
+ focusedCell,
71
+ setFocusedCell,
72
+ containerRef,
73
+ });
74
+
75
+ // Ctrl/Cmd+V and Delete on a range (cellSelection); apiRef.pasteText.
76
+ useGridClipboardPaste<R>({
77
+ apiRef, containerRef, viewportRef,
78
+ selection,
79
+ disableClipboardPaste, disableRangeClear,
80
+ focusedCell,
81
+ allRenderableRows,
82
+ getRowId,
83
+ navigationColumns,
84
+ isCellEditable,
85
+ rowMetaMap,
86
+ getSpanOrigin,
87
+ isEditing,
88
+ batchEdit,
89
+ onClipboardPaste,
90
+ onBeforeClipboardPaste,
91
+ });
92
+
93
+ // Undo / redo (undoRedo): history of committed edits, keys and apiRef methods.
94
+ useGridUndoRedo<R>({
95
+ apiRef,
96
+ enabled: Boolean(undoRedo),
97
+ limit: (typeof undoRedo === 'object' && undoRedo.limit) || UNDO_HISTORY_LIMIT,
98
+ onHistoryChange,
99
+ channel,
100
+ batchEdit,
101
+ isEditing,
102
+ containerRef, viewportRef,
103
+ selection,
104
+ setFocusedCell,
105
+ allRenderableRows,
106
+ getRowId,
107
+ navigationColumns,
108
+ layout,
109
+ rowHeight,
110
+ pinnedTopRowCount,
111
+ });
112
+
113
+ // Fill handle (cellSelection): drag the range's corner, Ctrl/Cmd+D and Ctrl/Cmd+R.
114
+ return useGridFillHandle<R>({
115
+ containerRef, viewportRef,
116
+ selection,
117
+ disableFillHandle,
118
+ isEditing,
119
+ allRenderableRows,
120
+ getRowId,
121
+ navigationColumns,
122
+ columnIndexMap,
123
+ isCellEditable,
124
+ rowMetaMap,
125
+ getSpanOrigin,
126
+ batchEdit,
127
+ onFill,
128
+ });
129
+ }
@@ -0,0 +1,66 @@
1
+ import type { GridColDef, GridRowId, GridRowModel } from '../../types';
2
+ import type { GridAriaRowLayout } from '../../utils/aria';
3
+ import type { GridSpanningResult } from '../features/useGridSpanning';
4
+ import { useGridSpanRowWindow, useGridSpanColumnWindow } from '../features/useGridSpanRenderWindow';
5
+ import { useGridAriaRows, type UseGridAriaRowsParams } from './useGridAriaRows';
6
+ import { useGridVisibleRows, type GridVisibleRow } from './useGridVisibleRows';
7
+ import type { GridVirtualizationResult } from './useGridVirtualization';
8
+ import type { LayoutResult } from './useLayout';
9
+
10
+ export interface UseGridRenderWindowParams<R extends GridRowModel> {
11
+ aria: UseGridAriaRowsParams;
12
+ spanning: GridSpanningResult;
13
+ virtualization: GridVirtualizationResult<R>;
14
+ layout: LayoutResult<R>;
15
+ pinnedTopRows: R[];
16
+ pinnedBottomRows: R[];
17
+ paginatedUnpinnedRows: R[];
18
+ sortedUnpinnedRows: R[];
19
+ pagination: boolean;
20
+ getRowId: (row: R) => GridRowId;
21
+ }
22
+
23
+ export interface UseGridRenderWindowResult<R extends GridRowModel> {
24
+ ariaRows: GridAriaRowLayout;
25
+ spanRowWindow: ReturnType<typeof useGridSpanRowWindow>;
26
+ visibleRows: GridVisibleRow<R>[];
27
+ renderColumns: GridColDef<R>[];
28
+ }
29
+
30
+ /**
31
+ * Steps 38–41 of `DataGrid` (docs/architecture/datagrid-orchestration.md), in their call order: the
32
+ * `aria-rowcount` / `aria-rowindex` bases, the render window widened to whole row spans, the rows to
33
+ * render (pinned + virtual centre) and the columns to render, widened to whole column spans.
34
+ */
35
+ export function useGridRenderWindow<R extends GridRowModel>(params: UseGridRenderWindowParams<R>): UseGridRenderWindowResult<R> {
36
+ const {
37
+ aria, spanning, virtualization, layout, pinnedTopRows, pinnedBottomRows, paginatedUnpinnedRows, sortedUnpinnedRows,
38
+ pagination, getRowId,
39
+ } = params;
40
+
41
+ const ariaRows = useGridAriaRows(aria);
42
+
43
+ const spanRowWindow = useGridSpanRowWindow({
44
+ spanning,
45
+ renderContext: virtualization.renderContext,
46
+ offsetTop: virtualization.offsetTop,
47
+ cumulativeHeights: layout.cumulativeHeights,
48
+ });
49
+
50
+ const visibleRows = useGridVisibleRows<R>({
51
+ renderContext: spanRowWindow.renderContext,
52
+ pinnedTopRows, pinnedBottomRows, paginatedUnpinnedRows, sortedUnpinnedRows, pagination,
53
+ getRowId,
54
+ });
55
+
56
+ const renderColumns = useGridSpanColumnWindow<R>({
57
+ spanning,
58
+ firstColumnIndex: virtualization.renderContext.firstColumnIndex,
59
+ lastColumnIndex: virtualization.renderContext.lastColumnIndex,
60
+ virtualColumns: virtualization.virtualColumns,
61
+ layout,
62
+ rows: visibleRows,
63
+ });
64
+
65
+ return { ariaRows, spanRowWindow, visibleRows, renderColumns };
66
+ }