@opencorestack/opengridx 1.0.6 → 1.1.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 (40) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/index.d.ts +40 -12
  3. package/dist/opengridx.css +1 -1
  4. package/dist/opengridx.es.js +3212 -3130
  5. package/dist/opengridx.es.js.map +1 -1
  6. package/dist/opengridx.umd.js +12 -12
  7. package/dist/opengridx.umd.js.map +1 -1
  8. package/docs/API_REFERENCE.md +35 -0
  9. package/docs/components/cell.md +14 -0
  10. package/docs/components/filter-panel.md +13 -0
  11. package/docs/components/pagination.md +26 -0
  12. package/docs/components/row.md +26 -0
  13. package/docs/roadmap.md +8 -0
  14. package/lib/components/Cell/Cell.tsx +44 -44
  15. package/lib/components/Cell/CellErrorBoundary.tsx +53 -0
  16. package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.tsx +0 -2
  17. package/lib/components/DataGrid/DataGrid.css +47 -0
  18. package/lib/components/DataGrid/DataGrid.tsx +34 -8
  19. package/lib/components/DataGrid/GridAggregationFooter.tsx +47 -9
  20. package/lib/components/DataGrid/GridListView.tsx +4 -0
  21. package/lib/components/DataGrid/GridPinnedRows.tsx +4 -0
  22. package/lib/components/DataGrid/GridStandaloneColumnPanel.tsx +3 -1
  23. package/lib/components/DataGrid/GridVirtualRows.tsx +4 -0
  24. package/lib/components/FilterPanel/FilterPanel.tsx +5 -4
  25. package/lib/components/Header/Header.tsx +4 -3
  26. package/lib/components/Pagination/Pagination.test.tsx +64 -0
  27. package/lib/components/Pagination/Pagination.tsx +19 -7
  28. package/lib/components/Row/Row.tsx +9 -5
  29. package/lib/components/Toolbar/GlobalSearch.tsx +0 -2
  30. package/lib/components/Toolbar/GridToolbar.tsx +0 -4
  31. package/lib/hooks/core/useGridColumns.tsx +11 -11
  32. package/lib/hooks/core/useGridControlledState.test.ts +86 -0
  33. package/lib/hooks/core/useGridKeyboardNavigation.test.ts +176 -0
  34. package/lib/hooks/core/useGridRowPipeline.test.ts +117 -0
  35. package/lib/hooks/useRowGrouping.test.ts +84 -0
  36. package/lib/hooks/useRowGrouping.ts +38 -13
  37. package/lib/hooks/useTreeData.ts +22 -2
  38. package/lib/index.ts +2 -0
  39. package/lib/types/index.ts +38 -12
  40. package/package.json +1 -1
@@ -143,6 +143,12 @@ The main component for displaying and interacting with data.
143
143
  | `className` | `string` | — | Additional CSS class applied to the outermost grid wrapper element. |
144
144
  | `style` | `React.CSSProperties` | — | Inline styles applied to the grid wrapper element. |
145
145
 
146
+ #### Localization
147
+
148
+ | Prop | Type | Default | Description |
149
+ | :--- | :--- | :--- | :--- |
150
+ | `localeText` | `GridLocaleText` | — | Override user-visible strings in the Pagination component. See `GridLocaleText` for the full interface. |
151
+
146
152
  #### Imperative Ref
147
153
 
148
154
  | Prop | Type | Default | Description |
@@ -564,6 +570,35 @@ interface GridPivotModel {
564
570
  }
565
571
  ```
566
572
 
573
+ ### `GridLocaleText`
574
+
575
+ Override user-visible strings in the Pagination component. All fields are optional.
576
+
577
+ ```typescript
578
+ interface GridLocaleText {
579
+ paginationRowsPerPage?: string;
580
+ paginationOf?: (from: number, to: number, count: number) => string;
581
+ paginationPage?: (page: number, pageCount: number) => string;
582
+ noRowsLabel?: string;
583
+ }
584
+ ```
585
+
586
+ ### `GridRowMeta`
587
+
588
+ Available via `params.rowMeta` in `renderCell`. Undefined for non-hierarchy rows.
589
+
590
+ ```typescript
591
+ interface GridRowMeta {
592
+ hasChildren?: boolean;
593
+ treeDepth?: number;
594
+ groupingField?: string;
595
+ groupingValue?: unknown;
596
+ descendantCount?: number;
597
+ isExpanded?: boolean;
598
+ isGroupRow?: boolean;
599
+ }
600
+ ```
601
+
567
602
  ---
568
603
 
569
604
  ## 📡 Event Callback Types
@@ -42,3 +42,17 @@ const columns: GridColDef[] = [
42
42
  }
43
43
  ];
44
44
  ```
45
+
46
+ ## 🛡️ Error containment
47
+
48
+ Custom `renderCell` callbacks are wrapped in a `CellErrorBoundary`. If a renderCell throws during render, that single cell shows a `⚠` indicator (CSS class `ogx__cell-error`) with the error message as a tooltip. The rest of the grid continues rendering normally.
49
+
50
+ Default cell display (no `renderCell`) does not go through the boundary — it cannot throw.
51
+
52
+ To style the error indicator:
53
+
54
+ ```css
55
+ .ogx__cell-error {
56
+ color: red; /* override the default */
57
+ }
58
+ ```
@@ -9,6 +9,19 @@ The Filter Panel allows users to define a list of `GridFilterItem` objects. Each
9
9
  - **Operator**: How to compare (e.g., `contains`, `equals`, `>`, `is empty`).
10
10
  - **Value**: The target value for comparison.
11
11
 
12
+ ### Hierarchy-aware custom cells
13
+
14
+ When a column has a custom `renderCell` and the grid is in tree-data or row-grouping mode, use `params.rowMeta` to access hierarchy context:
15
+
16
+ ```tsx
17
+ renderCell: (params) => {
18
+ if (params.rowMeta?.hasChildren) {
19
+ return <strong>{params.value}</strong>;
20
+ }
21
+ return params.value;
22
+ }
23
+ ```
24
+
12
25
  ## 🔌 Integration
13
26
 
14
27
  The `<DataGrid />` manages the visibility of the Filter Panel via its internal state. It is usually triggered from the Column Menu or the Toolbar.
@@ -32,3 +32,29 @@ You can replace the internal pagination UI using the `pagination` slot:
32
32
  }}
33
33
  />
34
34
  ```
35
+
36
+ ## 🌍 Internationalisation
37
+
38
+ Pass `localeText` on `<DataGrid />` to override the text labels in the Pagination component (rows-per-page, range, and page-counter labels). Note: ARIA labels (`aria-label="Pagination"`, `aria-label="Rows per page"`) and button titles (`title="Previous page"`, `title="Next page"`) are currently hardcoded and not overrideable via `localeText`.
39
+
40
+ | Key | Type | Default |
41
+ | :--- | :--- | :--- |
42
+ | `paginationRowsPerPage` | `string` | `"Rows per page:"` |
43
+ | `paginationOf` | `(from, to, count) => string` | `` `${from}–${to} of ${count}` `` |
44
+ | `paginationPage` | `(page, pageCount) => string` | `` `Page ${page} of ${pageCount}` `` |
45
+ | `noRowsLabel` | `string` | `"No Data"` |
46
+
47
+ **Example — French locale:**
48
+
49
+ ```tsx
50
+ <DataGrid
51
+ localeText={{
52
+ paginationRowsPerPage: 'Lignes par page :',
53
+ paginationOf: (from, to, count) => `${from}–${to} sur ${count}`,
54
+ paginationPage: (page, pageCount) => `Page ${page} sur ${pageCount}`,
55
+ noRowsLabel: 'Aucune donnée',
56
+ }}
57
+ rows={rows}
58
+ columns={columns}
59
+ />
60
+ ```
@@ -29,3 +29,29 @@ The grid supports "Zebra" striping via CSS:
29
29
  ## 📋 Detail Panel
30
30
 
31
31
  When `getDetailPanelContent` is provided, the `<Row />` renders an expandable container below itself to show supplemental data.
32
+
33
+ ## ⚠️ v1.1 — Hierarchy Field Deprecation
34
+
35
+ The internal hierarchy fields (`_hasChildren`, `_treeDepth`, `_isExpanded`, `_groupingField`, `_groupingValue`, `_descendantCount`, `_isGroupRow`) are no longer typed on `GridRowModel`. They will be removed from the runtime row object in v2.0.
36
+
37
+ **Before (still works at runtime in v1.1 — `params.row._hasChildren` is `unknown`, not `boolean`; assignment to typed variables and arithmetic will error, truthiness checks will not):**
38
+
39
+ ```tsx
40
+ renderCell: (params) => {
41
+ // v1.1 accepts this at runtime; _hasChildren resolves to unknown via [key: string]: unknown
42
+ const hasChildren = (params.row as Record<string, unknown>)._hasChildren;
43
+ return hasChildren ? <GroupIcon /> : params.value;
44
+ }
45
+ ```
46
+
47
+ **After (v1.1+):**
48
+
49
+ ```tsx
50
+ renderCell: (params) => {
51
+ const hasChildren = params.rowMeta?.hasChildren;
52
+ return hasChildren ? <GroupIcon /> : params.value;
53
+ }
54
+ ```
55
+
56
+ `params.rowMeta` is `undefined` for flat rows with no active tree-data or row-grouping.
57
+ See [`GridRowMeta` architecture doc](../architecture/grid-row-meta.md) for the full data flow.
package/docs/roadmap.md CHANGED
@@ -86,5 +86,13 @@ This document tracks the current status of features in `OpenGridX` and outlines
86
86
  * **GitHub Pages deployment**: CI/CD via GitHub Actions (`base: '/OpenGridX/'` already configured in `vite.config.js`).
87
87
 
88
88
  ## ✅ Implemented Features (Recent)
89
+
90
+ ### Library Hardening *(completed 2026-09-02)*
91
+ - **`GridRowMeta`**: Hierarchy metadata (`hasChildren`, `treeDepth`, `groupingField`, `groupingValue`, `descendantCount`, `isExpanded`, `isGroupRow`) moved from `GridRowModel` into a separate `Map<GridRowId, GridRowMeta>`. Exposed as `params.rowMeta` in `renderCell`. Runtime shim maintains backward compatibility; removed in v2.0.
92
+ - **`CellErrorBoundary`**: Custom `renderCell` errors are now caught per-cell. Failing cells show a `⚠` indicator with the error as a tooltip; the rest of the grid renders normally.
93
+ - **`GridLocaleText` / `localeText` prop**: All user-visible pagination strings (`paginationRowsPerPage`, `paginationOf`, `paginationPage`, `noRowsLabel`) are now overrideable for internationalisation.
94
+ - **Core hook tests**: `useGridRowPipeline`, `useGridControlledState`, `useGridKeyboardNavigation` now have full test coverage (25 new tests).
95
+ - **`CLAUDE.md`**: Root-level AI context file auto-loaded by Claude Code and other AI coding assistants.
96
+
89
97
  * **Demo app (partial)**: 32 interactive examples across 6 categories (Main features, Advanced features, Components, Customization, Tutorials, Resources) with categorized sidebar navigation and per-page Table of Contents. Source code viewer present on 6 of 32 examples. Syntax highlighting and public deployment are in the upcoming Demo Site section above.
90
98
  * **Quickstart & Installation Guides**: Comprehensive onboarding documentation for developers.
@@ -1,9 +1,10 @@
1
1
 
2
2
  import React from 'react';
3
- import type { GridColDef, GridRowModel, GridPinnedPosition, GridCellParams } from '../../types';
3
+ import type { GridColDef, GridRowModel, GridPinnedPosition, GridCellParams, GridRowMeta } from '../../types';
4
4
  import type { CellColSpanInfo } from '../../hooks/features/useGridSpanning';
5
5
 
6
6
  import { GridEditInputCell } from './GridEditInputCell';
7
+ import { CellErrorBoundary } from './CellErrorBoundary';
7
8
 
8
9
  export interface CellProps<R extends GridRowModel = GridRowModel> {
9
10
  onCellClick?: (params: GridCellParams<R>) => void;
@@ -32,6 +33,7 @@ export interface CellProps<R extends GridRowModel = GridRowModel> {
32
33
  colSpanInfo?: CellColSpanInfo;
33
34
  rowSpan?: number;
34
35
  isHiddenByRowSpan?: boolean;
36
+ rowMeta?: GridRowMeta;
35
37
  }
36
38
 
37
39
  function CellImpl<R extends GridRowModel = GridRowModel>(props: CellProps<R>) {
@@ -59,7 +61,8 @@ function CellImpl<R extends GridRowModel = GridRowModel>(props: CellProps<R>) {
59
61
 
60
62
  colSpanInfo,
61
63
  rowSpan: rowSpanProp,
62
- isHiddenByRowSpan
64
+ isHiddenByRowSpan,
65
+ rowMeta
63
66
  } = props;
64
67
 
65
68
  // All hooks must come before any early returns (Rules of Hooks)
@@ -87,47 +90,6 @@ function CellImpl<R extends GridRowModel = GridRowModel>(props: CellProps<R>) {
87
90
  return String(value);
88
91
  }, [value, row, colDef]);
89
92
 
90
- const cellContent = React.useMemo(() => {
91
- if (isEditing && (onCellValueChange || onValueChange) && onEditStop) {
92
- if (colDef.renderEditCell) {
93
- return colDef.renderEditCell({
94
- value,
95
- row,
96
- field: colDef.field,
97
- colDef,
98
- rowIndex,
99
- colIndex
100
- });
101
- }
102
- return (
103
- <GridEditInputCell
104
- value={value}
105
- row={row}
106
- field={colDef.field}
107
- colDef={colDef as unknown as GridColDef}
108
- rowIndex={rowIndex}
109
- colIndex={colIndex}
110
- onValueChange={handleValueChange}
111
- onCommit={() => onEditStop()}
112
- onCancel={() => onEditStop(true)}
113
- />
114
- );
115
- }
116
-
117
- if (colDef.renderCell) {
118
- return colDef.renderCell({
119
- value,
120
- row,
121
- field: colDef.field,
122
- colDef,
123
- rowIndex,
124
- colIndex
125
- });
126
- }
127
-
128
- return formattedValue;
129
- }, [value, row, colDef, rowIndex, colIndex, formattedValue, isEditing, handleValueChange, onEditStop, onCellValueChange, onValueChange]);
130
-
131
93
  const resolvedCellClassName = React.useMemo(() => {
132
94
  if (!colDef.cellClassName) return '';
133
95
  if (typeof colDef.cellClassName === 'function') {
@@ -252,7 +214,45 @@ function CellImpl<R extends GridRowModel = GridRowModel>(props: CellProps<R>) {
252
214
  {...(rowSpan > 1 ? { 'aria-rowspan': rowSpan } : {})}
253
215
  >
254
216
  <div className="ogx__cell-content">
255
- {cellContent}
217
+ {isEditing && (onCellValueChange || onValueChange) && onEditStop ? (
218
+ colDef.renderEditCell ? (
219
+ colDef.renderEditCell({
220
+ value,
221
+ row,
222
+ field: colDef.field,
223
+ colDef,
224
+ rowIndex,
225
+ colIndex,
226
+ rowMeta,
227
+ })
228
+ ) : (
229
+ <GridEditInputCell
230
+ value={value}
231
+ row={row}
232
+ field={colDef.field}
233
+ colDef={colDef as unknown as GridColDef}
234
+ rowIndex={rowIndex}
235
+ colIndex={colIndex}
236
+ onValueChange={handleValueChange}
237
+ onCommit={() => onEditStop()}
238
+ onCancel={() => onEditStop(true)}
239
+ />
240
+ )
241
+ ) : colDef.renderCell ? (
242
+ <CellErrorBoundary field={colDef.field} resetKey={value}>
243
+ {colDef.renderCell({
244
+ value,
245
+ row,
246
+ field: colDef.field,
247
+ colDef,
248
+ rowIndex,
249
+ colIndex,
250
+ rowMeta,
251
+ })}
252
+ </CellErrorBoundary>
253
+ ) : (
254
+ formattedValue
255
+ )}
256
256
  </div>
257
257
  </div>
258
258
  );
@@ -0,0 +1,53 @@
1
+ import React from 'react';
2
+
3
+ interface CellErrorBoundaryProps {
4
+ children: React.ReactNode;
5
+ field: string;
6
+ resetKey?: unknown;
7
+ }
8
+
9
+ interface CellErrorBoundaryState {
10
+ hasError: boolean;
11
+ error: Error | null;
12
+ resetKey?: unknown;
13
+ }
14
+
15
+ export class CellErrorBoundary extends React.Component<CellErrorBoundaryProps, CellErrorBoundaryState> {
16
+ constructor(props: CellErrorBoundaryProps) {
17
+ super(props);
18
+ this.state = { hasError: false, error: null, resetKey: props.resetKey };
19
+ }
20
+
21
+ static getDerivedStateFromProps(
22
+ props: CellErrorBoundaryProps,
23
+ state: CellErrorBoundaryState
24
+ ): Partial<CellErrorBoundaryState> | null {
25
+ if (state.hasError && props.resetKey !== state.resetKey) {
26
+ return { hasError: false, error: null, resetKey: props.resetKey };
27
+ }
28
+ if (props.resetKey !== state.resetKey) {
29
+ return { resetKey: props.resetKey };
30
+ }
31
+ return null;
32
+ }
33
+
34
+ static getDerivedStateFromError(error: Error): Partial<CellErrorBoundaryState> {
35
+ return { hasError: true, error };
36
+ }
37
+
38
+ override render() {
39
+ if (this.state.hasError) {
40
+ return (
41
+ <div
42
+ className="ogx__cell-error"
43
+ role="status"
44
+ aria-label={`Error in cell: ${this.props.field}`}
45
+ title={this.state.error?.message ?? 'Render error'}
46
+ >
47
+ ⚠
48
+ </div>
49
+ );
50
+ }
51
+ return this.props.children;
52
+ }
53
+ }
@@ -109,8 +109,6 @@ export function ColumnVisibilityPanel<R extends GridRowModel = GridRowModel>(
109
109
  <div className="ogx-column-visibility-panel__search-container">
110
110
  <SearchIcon />
111
111
  <input
112
- id="ogx-column-search"
113
- name="ogx-column-search"
114
112
  autoComplete="off"
115
113
  className="ogx-column-visibility-panel__search-input"
116
114
  placeholder="Search"
@@ -177,6 +177,42 @@
177
177
  overflow: hidden;
178
178
  box-sizing: border-box;
179
179
  border-right: 1px solid var(--ogx-grid-border-color);
180
+ position: relative;
181
+ }
182
+ .ogx__aggregation-cell--pinned-left,
183
+ .ogx__aggregation-cell--pinned-right {
184
+ position: sticky;
185
+ z-index: 3;
186
+ background: var(--ogx-grid-header-background);
187
+ }
188
+ .ogx__aggregation-cell--pinned-right {
189
+ border-left: 1px solid var(--ogx-grid-border-color);
190
+ }
191
+ .ogx__aggregation-cell--pinned-left-last {
192
+ overflow: visible;
193
+ }
194
+ .ogx__aggregation-cell--pinned-left-last::after {
195
+ content: '';
196
+ position: absolute;
197
+ top: 0;
198
+ right: -10px;
199
+ bottom: 0;
200
+ width: 10px;
201
+ pointer-events: none;
202
+ box-shadow: inset 10px 0 8px -8px rgba(0, 0, 0, 0.1);
203
+ }
204
+ .ogx__aggregation-cell--pinned-right-first {
205
+ overflow: visible;
206
+ }
207
+ .ogx__aggregation-cell--pinned-right-first::before {
208
+ content: '';
209
+ position: absolute;
210
+ top: 0;
211
+ left: -10px;
212
+ bottom: 0;
213
+ width: 10px;
214
+ pointer-events: none;
215
+ box-shadow: inset -10px 0 8px -8px rgba(0, 0, 0, 0.1);
180
216
  }
181
217
  .ogx__aggregation-label {
182
218
  font-size: 0.7em;
@@ -222,3 +258,14 @@
222
258
  white-space: nowrap;
223
259
  border: 0;
224
260
  }
261
+
262
+ .ogx__cell-error {
263
+ display: flex;
264
+ align-items: center;
265
+ justify-content: center;
266
+ width: 100%;
267
+ height: 100%;
268
+ color: var(--ogx-color-error, #d32f2f);
269
+ font-size: 1rem;
270
+ cursor: help;
271
+ }
@@ -27,7 +27,9 @@ import { GridListView } from './GridListView';
27
27
  import { GridPinnedRows } from './GridPinnedRows';
28
28
  import { GridVirtualRows } from './GridVirtualRows';
29
29
  import { GridStandaloneColumnPanel } from './GridStandaloneColumnPanel';
30
- import type { DataGridProps, GridRowModel, GridRowId, GridSortDirection, GridColDef, GridRowParams, GridCellParams, GridDataSource, GridAggregationResult, GridFilterModel, GridTreeNode, GridSortItem } from '../../types';
30
+ import type { DataGridProps, GridRowModel, GridRowId, GridSortDirection, GridColDef, GridRowParams, GridCellParams, GridDataSource, GridAggregationResult, GridFilterModel, GridTreeNode, GridSortItem, GridRowMeta } from '../../types';
31
+
32
+ const EMPTY_ROW_META_MAP: Map<GridRowId, GridRowMeta> = new Map();
31
33
 
32
34
  export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridProps<R>) {
33
35
  const {
@@ -109,9 +111,12 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
109
111
  listViewColumn,
110
112
  columnGroupingModel,
111
113
  noRowsLabel = 'No Data',
114
+ localeText,
112
115
  apiRef: propApiRef,
113
116
  } = props;
114
117
 
118
+ const effectiveNoRowsLabel = localeText?.noRowsLabel ?? noRowsLabel;
119
+
115
120
  // Stable defaults
116
121
  const defaultFilterModel: GridFilterModel = useMemo(() => ({ items: [] }), []);
117
122
  const filterModel = (propFilterModel || defaultFilterModel) as GridFilterModel;
@@ -333,18 +338,24 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
333
338
  const isHierarchyEnabled = isTreeData || isRowGrouping;
334
339
  const activeHierarchyHandlers = isTreeData ? treeDataHandlers : (isRowGrouping ? rowGroupingHandlers : null);
335
340
 
341
+ const rowMetaMap = useMemo<Map<GridRowId, GridRowMeta>>(() => {
342
+ if (isTreeData) return treeDataHandlers.rowMetaMap;
343
+ if (isRowGrouping) return rowGroupingHandlers.rowMetaMap;
344
+ return EMPTY_ROW_META_MAP;
345
+ }, [isTreeData, treeDataHandlers.rowMetaMap, isRowGrouping, rowGroupingHandlers.rowMetaMap]);
346
+
336
347
  const pagination = propPagination && !isRowGrouping;
337
348
 
338
349
  const handleRowClick = useCallback((params: GridRowParams<R>) => {
339
350
  const { row, id } = params;
340
351
 
341
- if (isHierarchyEnabled && row._hasChildren) {
352
+ if (isHierarchyEnabled && rowMetaMap.get(row.id)?.hasChildren) {
342
353
  activeHierarchyHandlers?.toggleExpansion(id);
343
354
  return;
344
355
  }
345
356
 
346
357
  onRowClick?.(params);
347
- }, [isHierarchyEnabled, activeHierarchyHandlers, onRowClick]);
358
+ }, [isHierarchyEnabled, activeHierarchyHandlers, onRowClick, rowMetaMap]);
348
359
 
349
360
  const dataSourceHandlers = useGridDataSource({
350
361
  dataSource,
@@ -685,6 +696,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
685
696
  const hasRowSpanning = React.useMemo(() => effectiveColumns.some(c => !!c.rowSpan), [effectiveColumns]);
686
697
  const [columnsPanelOpen, setColumnsPanelOpen] = React.useState(false);
687
698
  const containerRef = React.useRef<HTMLDivElement>(null);
699
+ const standalonePanelRef = React.useRef<HTMLDivElement>(null);
688
700
 
689
701
  const toolbarProps = React.useMemo(() => {
690
702
  if (!slots?.toolbar) return null;
@@ -735,8 +747,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
735
747
  useEffect(() => {
736
748
  if (!columnsPanelOpen || slots?.toolbar) return;
737
749
  function handleClickOutside(e: MouseEvent) {
738
- const panel = document.getElementById('ogx-standalone-col-panel');
739
- if (panel && !panel.contains(e.target as Node)) {
750
+ if (standalonePanelRef.current && !standalonePanelRef.current.contains(e.target as Node)) {
740
751
  setColumnsPanelOpen(false);
741
752
  }
742
753
  }
@@ -762,6 +773,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
762
773
  <GridStandaloneColumnPanel<R>
763
774
  isOpen={columnsPanelOpen}
764
775
  containerRef={containerRef}
776
+ panelRef={standalonePanelRef}
765
777
  effectiveColumns={effectiveColumns}
766
778
  columnVisibilityModel={columnVisibilityModel}
767
779
  effectiveColumnOrder={effectiveColumnOrder}
@@ -784,7 +796,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
784
796
  pageSizeOptions={pageSizeOptions}
785
797
  selectedRowIds={selectedRowIds}
786
798
  listViewColumn={listViewColumn}
787
- noRowsLabel={noRowsLabel}
799
+ noRowsLabel={effectiveNoRowsLabel}
788
800
  rowHeight={rowHeight}
789
801
  checkboxSelection={checkboxSelection}
790
802
  paginationMode={paginationMode}
@@ -792,6 +804,11 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
792
804
  serverRowCount={state.pagination.rowCount || 0}
793
805
  paginationSlot={slots?.pagination}
794
806
  paginationSlotProps={slotProps?.pagination}
807
+ localeText={localeText ? {
808
+ paginationRowsPerPage: localeText.paginationRowsPerPage,
809
+ paginationOf: localeText.paginationOf,
810
+ paginationPage: localeText.paginationPage,
811
+ } : undefined}
795
812
  onRowClick={handleRowClick}
796
813
  onSelectionChange={handleSelectionChange}
797
814
  onPaginationModelChange={handlePaginationModelChange}
@@ -892,7 +909,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
892
909
 
893
910
  {/* Empty State Overlay (Standard View) — showing after header */}
894
911
  {!effectiveLoading && !state.dataSource.error && filteredRows.length === 0 && (
895
- <GridEmptyState noRowsLabel={noRowsLabel} width={virtualization.totalWidth} />
912
+ <GridEmptyState noRowsLabel={effectiveNoRowsLabel} width={virtualization.totalWidth} />
896
913
  )}
897
914
 
898
915
  <GridPinnedRows<R>
@@ -918,6 +935,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
918
935
  colspanMap={spanning.colspanMap}
919
936
  rowSpanningCaches={spanning.rowSpanningState.caches}
920
937
  rowHeight={rowHeight}
938
+ rowMetaMap={rowMetaMap}
921
939
  />
922
940
 
923
941
  <GridVirtualRows<R>
@@ -955,6 +973,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
955
973
  sortedUnpinnedRowCount={sortedUnpinnedRows.length}
956
974
  infiniteScrollSkeletonCount={Math.min(effectivePaginationModel.pageSize, 20)}
957
975
  unpinnedRowsLength={layout.unpinnedRowsLength}
976
+ rowMetaMap={rowMetaMap}
958
977
  />
959
978
 
960
979
  <GridPinnedRows<R>
@@ -980,6 +999,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
980
999
  colspanMap={spanning.colspanMap}
981
1000
  rowSpanningCaches={spanning.rowSpanningState.caches}
982
1001
  rowHeight={rowHeight}
1002
+ rowMetaMap={rowMetaMap}
983
1003
  />
984
1004
 
985
1005
  {/* Aggregation Footer Row — suppressed in pivot mode because pivot rows
@@ -995,6 +1015,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
995
1015
  checkboxSelection={checkboxSelection}
996
1016
  hasDetailPanel={hasDetailPanel}
997
1017
  rowReordering={rowReordering}
1018
+ pinnedColumns={pinnedColumns}
998
1019
  />
999
1020
  )}
1000
1021
 
@@ -1012,6 +1033,11 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
1012
1033
  pageSizeOptions={pageSizeOptions}
1013
1034
  onPageChange={(newPage: number) => handlePaginationModelChange({ ...effectivePaginationModel, page: newPage })}
1014
1035
  onPageSizeChange={(newPageSize: number) => handlePaginationModelChange({ ...effectivePaginationModel, pageSize: newPageSize, page: 0 })}
1036
+ localeText={localeText ? {
1037
+ paginationRowsPerPage: localeText.paginationRowsPerPage,
1038
+ paginationOf: localeText.paginationOf,
1039
+ paginationPage: localeText.paginationPage,
1040
+ } : undefined}
1015
1041
  {...slotProps?.pagination}
1016
1042
  />
1017
1043
  );
@@ -1023,7 +1049,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
1023
1049
  {state.dataSource.error ? `Error: ${state.dataSource.error instanceof Error ? state.dataSource.error.message : 'Unknown error'}` : ''}
1024
1050
  {!loading && !state.dataSource.error && (
1025
1051
  filteredRows.length === 0
1026
- ? noRowsLabel
1052
+ ? effectiveNoRowsLabel
1027
1053
  : (filterModel && ((filterModel.quickFilterValues?.length || 0) > 0 || (filterModel.items?.length || 0) > 0))
1028
1054
  ? `${filteredRows.length} ${filteredRows.length === 1 ? 'row' : 'rows'} found`
1029
1055
  : ''
@@ -1,6 +1,7 @@
1
- import React from 'react';
1
+ import React, { useMemo } from 'react';
2
2
  import { formatAggregationValue } from '../../hooks/features/useAggregation';
3
- import type { GridColDef, GridAggregationModel, GridAggregationResult } from '../../types';
3
+ import type { GridColDef, GridAggregationModel, GridAggregationResult, GridColumnPinning } from '../../types';
4
+ import { calculatePinnedPositions, isColumnPinned } from '../../utils/pinning';
4
5
 
5
6
  interface GridAggregationFooterProps {
6
7
  columns: GridColDef[];
@@ -11,6 +12,7 @@ interface GridAggregationFooterProps {
11
12
  checkboxSelection: boolean;
12
13
  hasDetailPanel: boolean;
13
14
  rowReordering: boolean;
15
+ pinnedColumns?: GridColumnPinning;
14
16
  }
15
17
 
16
18
  export function GridAggregationFooter({
@@ -22,7 +24,28 @@ export function GridAggregationFooter({
22
24
  checkboxSelection,
23
25
  hasDetailPanel,
24
26
  rowReordering,
27
+ pinnedColumns,
25
28
  }: GridAggregationFooterProps) {
29
+ // Compute sticky left/right pixel offsets for each pinned column.
30
+ // The footer always renders checkbox/detail/reorder spacers unconditionally,
31
+ // so treat pinCheckboxColumn and pinExpandColumn as always true.
32
+ const pinnedOffsets = useMemo(
33
+ () => calculatePinnedPositions(
34
+ columns,
35
+ columnWidths,
36
+ pinnedColumns,
37
+ checkboxSelection,
38
+ true,
39
+ hasDetailPanel,
40
+ true,
41
+ rowReordering,
42
+ ),
43
+ [columns, columnWidths, pinnedColumns, checkboxSelection, hasDetailPanel, rowReordering]
44
+ );
45
+
46
+ const lastLeftField = pinnedColumns?.left?.[pinnedColumns.left.length - 1];
47
+ const firstRightField = pinnedColumns?.right?.[0];
48
+
26
49
  return (
27
50
  <div
28
51
  className="ogx__aggregation-footer"
@@ -40,17 +63,32 @@ export function GridAggregationFooter({
40
63
  const rawValue = aggregationResult[col.field];
41
64
  const colWidth = columnWidths[col.field] ?? (typeof col.width === 'number' ? col.width : 120);
42
65
 
66
+ const pinnedPosition = isColumnPinned(col.field, pinnedColumns);
67
+ const pinnedOffset = pinnedPosition ? pinnedOffsets[col.field] : undefined;
68
+
69
+ const className = [
70
+ 'ogx__aggregation-cell',
71
+ pinnedPosition === 'left' && 'ogx__aggregation-cell--pinned-left',
72
+ pinnedPosition === 'right' && 'ogx__aggregation-cell--pinned-right',
73
+ pinnedPosition === 'left' && col.field === lastLeftField && 'ogx__aggregation-cell--pinned-left-last',
74
+ pinnedPosition === 'right' && col.field === firstRightField && 'ogx__aggregation-cell--pinned-right-first',
75
+ ].filter(Boolean).join(' ');
76
+
77
+ const style: React.CSSProperties = {
78
+ width: colWidth,
79
+ minWidth: colWidth,
80
+ maxWidth: colWidth,
81
+ textAlign: (col.align as React.CSSProperties['textAlign']) || 'left',
82
+ };
83
+ if (pinnedPosition === 'left' && pinnedOffset !== undefined) style.left = pinnedOffset;
84
+ if (pinnedPosition === 'right' && pinnedOffset !== undefined) style.right = pinnedOffset;
85
+
43
86
  return (
44
87
  <div
45
88
  key={col.field}
46
- className="ogx__aggregation-cell"
89
+ className={className}
47
90
  role="gridcell"
48
- style={{
49
- width: colWidth,
50
- minWidth: colWidth,
51
- maxWidth: colWidth,
52
- textAlign: (col.align as React.CSSProperties['textAlign']) || 'left',
53
- }}
91
+ style={style}
54
92
  >
55
93
  {fnName ? (
56
94
  <>