@opencorestack/opengridx 1.0.7 → 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 (33) 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 +2953 -2885
  5. package/dist/opengridx.es.js.map +1 -1
  6. package/dist/opengridx.umd.js +11 -11
  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/DataGrid/DataGrid.css +11 -0
  17. package/lib/components/DataGrid/DataGrid.tsx +30 -6
  18. package/lib/components/DataGrid/GridListView.tsx +4 -0
  19. package/lib/components/DataGrid/GridPinnedRows.tsx +4 -0
  20. package/lib/components/DataGrid/GridVirtualRows.tsx +4 -0
  21. package/lib/components/Pagination/Pagination.test.tsx +64 -0
  22. package/lib/components/Pagination/Pagination.tsx +15 -4
  23. package/lib/components/Row/Row.tsx +6 -3
  24. package/lib/hooks/core/useGridColumns.tsx +11 -11
  25. package/lib/hooks/core/useGridControlledState.test.ts +86 -0
  26. package/lib/hooks/core/useGridKeyboardNavigation.test.ts +176 -0
  27. package/lib/hooks/core/useGridRowPipeline.test.ts +117 -0
  28. package/lib/hooks/useRowGrouping.test.ts +84 -0
  29. package/lib/hooks/useRowGrouping.ts +38 -13
  30. package/lib/hooks/useTreeData.ts +22 -2
  31. package/lib/index.ts +2 -0
  32. package/lib/types/index.ts +38 -12
  33. 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
+ }
@@ -258,3 +258,14 @@
258
258
  white-space: nowrap;
259
259
  border: 0;
260
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,
@@ -785,7 +796,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
785
796
  pageSizeOptions={pageSizeOptions}
786
797
  selectedRowIds={selectedRowIds}
787
798
  listViewColumn={listViewColumn}
788
- noRowsLabel={noRowsLabel}
799
+ noRowsLabel={effectiveNoRowsLabel}
789
800
  rowHeight={rowHeight}
790
801
  checkboxSelection={checkboxSelection}
791
802
  paginationMode={paginationMode}
@@ -793,6 +804,11 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
793
804
  serverRowCount={state.pagination.rowCount || 0}
794
805
  paginationSlot={slots?.pagination}
795
806
  paginationSlotProps={slotProps?.pagination}
807
+ localeText={localeText ? {
808
+ paginationRowsPerPage: localeText.paginationRowsPerPage,
809
+ paginationOf: localeText.paginationOf,
810
+ paginationPage: localeText.paginationPage,
811
+ } : undefined}
796
812
  onRowClick={handleRowClick}
797
813
  onSelectionChange={handleSelectionChange}
798
814
  onPaginationModelChange={handlePaginationModelChange}
@@ -893,7 +909,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
893
909
 
894
910
  {/* Empty State Overlay (Standard View) — showing after header */}
895
911
  {!effectiveLoading && !state.dataSource.error && filteredRows.length === 0 && (
896
- <GridEmptyState noRowsLabel={noRowsLabel} width={virtualization.totalWidth} />
912
+ <GridEmptyState noRowsLabel={effectiveNoRowsLabel} width={virtualization.totalWidth} />
897
913
  )}
898
914
 
899
915
  <GridPinnedRows<R>
@@ -919,6 +935,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
919
935
  colspanMap={spanning.colspanMap}
920
936
  rowSpanningCaches={spanning.rowSpanningState.caches}
921
937
  rowHeight={rowHeight}
938
+ rowMetaMap={rowMetaMap}
922
939
  />
923
940
 
924
941
  <GridVirtualRows<R>
@@ -956,6 +973,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
956
973
  sortedUnpinnedRowCount={sortedUnpinnedRows.length}
957
974
  infiniteScrollSkeletonCount={Math.min(effectivePaginationModel.pageSize, 20)}
958
975
  unpinnedRowsLength={layout.unpinnedRowsLength}
976
+ rowMetaMap={rowMetaMap}
959
977
  />
960
978
 
961
979
  <GridPinnedRows<R>
@@ -981,6 +999,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
981
999
  colspanMap={spanning.colspanMap}
982
1000
  rowSpanningCaches={spanning.rowSpanningState.caches}
983
1001
  rowHeight={rowHeight}
1002
+ rowMetaMap={rowMetaMap}
984
1003
  />
985
1004
 
986
1005
  {/* Aggregation Footer Row — suppressed in pivot mode because pivot rows
@@ -1014,6 +1033,11 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
1014
1033
  pageSizeOptions={pageSizeOptions}
1015
1034
  onPageChange={(newPage: number) => handlePaginationModelChange({ ...effectivePaginationModel, page: newPage })}
1016
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}
1017
1041
  {...slotProps?.pagination}
1018
1042
  />
1019
1043
  );
@@ -1025,7 +1049,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
1025
1049
  {state.dataSource.error ? `Error: ${state.dataSource.error instanceof Error ? state.dataSource.error.message : 'Unknown error'}` : ''}
1026
1050
  {!loading && !state.dataSource.error && (
1027
1051
  filteredRows.length === 0
1028
- ? noRowsLabel
1052
+ ? effectiveNoRowsLabel
1029
1053
  : (filterModel && ((filterModel.quickFilterValues?.length || 0) > 0 || (filterModel.items?.length || 0) > 0))
1030
1054
  ? `${filteredRows.length} ${filteredRows.length === 1 ? 'row' : 'rows'} found`
1031
1055
  : ''
@@ -8,6 +8,7 @@ import type {
8
8
  GridListViewColDef,
9
9
  GridPaginationModel,
10
10
  GridDataSource,
11
+ GridLocaleText,
11
12
  } from '../../types';
12
13
 
13
14
  export interface GridListViewProps<R extends GridRowModel> {
@@ -27,6 +28,7 @@ export interface GridListViewProps<R extends GridRowModel> {
27
28
  serverRowCount: number;
28
29
  paginationSlot?: React.ComponentType<Record<string, unknown>>;
29
30
  paginationSlotProps?: Record<string, unknown>;
31
+ localeText?: Pick<GridLocaleText, 'paginationRowsPerPage' | 'paginationOf' | 'paginationPage'>;
30
32
  onRowClick: (params: GridRowParams<R>) => void;
31
33
  onSelectionChange: (rowId: GridRowId, isSelected: boolean) => void;
32
34
  onPaginationModelChange: (model: GridPaginationModel) => void;
@@ -49,6 +51,7 @@ export function GridListView<R extends GridRowModel>({
49
51
  serverRowCount,
50
52
  paginationSlot,
51
53
  paginationSlotProps,
54
+ localeText,
52
55
  onRowClick,
53
56
  onSelectionChange,
54
57
  onPaginationModelChange,
@@ -108,6 +111,7 @@ export function GridListView<R extends GridRowModel>({
108
111
  pageSizeOptions={pageSizeOptions}
109
112
  onPageChange={(newPage: number) => onPaginationModelChange({ ...effectivePaginationModel, page: newPage })}
110
113
  onPageSizeChange={(newPageSize: number) => onPaginationModelChange({ ...effectivePaginationModel, pageSize: newPageSize, page: 0 })}
114
+ localeText={localeText}
111
115
  {...paginationSlotProps}
112
116
  />
113
117
  )}
@@ -10,6 +10,7 @@ import type {
10
10
  GridRowPinning,
11
11
  GridDetailPanelParams,
12
12
  GridDetailPanelHeight,
13
+ GridRowMeta,
13
14
  } from '../../types';
14
15
  import type { CellColSpanInfo, RowSpanningCaches } from '../../hooks/features/useGridSpanning';
15
16
 
@@ -36,6 +37,7 @@ export interface GridPinnedRowsProps<R extends GridRowModel> {
36
37
  colspanMap?: Map<GridRowId, Record<string, CellColSpanInfo>>;
37
38
  rowSpanningCaches?: RowSpanningCaches;
38
39
  rowHeight: number;
40
+ rowMetaMap: Map<GridRowId, GridRowMeta>;
39
41
  }
40
42
 
41
43
  export function GridPinnedRows<R extends GridRowModel>({
@@ -61,6 +63,7 @@ export function GridPinnedRows<R extends GridRowModel>({
61
63
  colspanMap,
62
64
  rowSpanningCaches,
63
65
  rowHeight,
66
+ rowMetaMap,
64
67
  }: GridPinnedRowsProps<R>) {
65
68
  if (rows.length === 0) return null;
66
69
 
@@ -91,6 +94,7 @@ export function GridPinnedRows<R extends GridRowModel>({
91
94
  colspanMap={colspanMap}
92
95
  rowSpanningCaches={rowSpanningCaches}
93
96
  rowHeight={rowHeight}
97
+ rowMeta={rowMetaMap.get(row.id)}
94
98
  />
95
99
  ))}
96
100
  </div>
@@ -12,6 +12,7 @@ import type {
12
12
  GridRowPinning,
13
13
  GridDetailPanelParams,
14
14
  GridDetailPanelHeight,
15
+ GridRowMeta,
15
16
  } from '../../types';
16
17
  import type { GridVisibleRow } from '../../hooks/core/useGridVisibleRows';
17
18
  import type { UseRowReorderReturn } from '../../hooks/useRowReorder';
@@ -60,6 +61,7 @@ export interface GridVirtualRowsProps<R extends GridRowModel> {
60
61
  sortedUnpinnedRowCount: number;
61
62
  infiniteScrollSkeletonCount: number;
62
63
  unpinnedRowsLength: number;
64
+ rowMetaMap: Map<GridRowId, GridRowMeta>;
63
65
  }
64
66
 
65
67
  export function GridVirtualRows<R extends GridRowModel>({
@@ -97,6 +99,7 @@ export function GridVirtualRows<R extends GridRowModel>({
97
99
  sortedUnpinnedRowCount,
98
100
  infiniteScrollSkeletonCount,
99
101
  unpinnedRowsLength,
102
+ rowMetaMap,
100
103
  }: GridVirtualRowsProps<R>) {
101
104
  const skeletonColumns: GridColDef<R>[] = baseColumns.length > 0
102
105
  ? baseColumns
@@ -179,6 +182,7 @@ export function GridVirtualRows<R extends GridRowModel>({
179
182
  colspanMap={colspanMap}
180
183
  rowSpanningCaches={rowSpanningCaches}
181
184
  rowHeight={rowHeight}
185
+ rowMeta={rowMetaMap.get(row.id)}
182
186
  />
183
187
  ))
184
188
  )}
@@ -0,0 +1,64 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { render, screen } from '@testing-library/react';
3
+ import { Pagination } from './Pagination';
4
+
5
+ const NOOP = () => {};
6
+
7
+ describe('Pagination', () => {
8
+ it('renders "0 of 0" when rowCount is 0', () => {
9
+ render(
10
+ <Pagination
11
+ page={0}
12
+ pageSize={10}
13
+ rowCount={0}
14
+ onPageChange={NOOP}
15
+ onPageSizeChange={NOOP}
16
+ />
17
+ );
18
+ expect(screen.getByText('0 of 0')).toBeTruthy();
19
+ });
20
+
21
+ it('calls localeText.paginationOf with (0, 0, 0) when rowCount is 0', () => {
22
+ const paginationOf = vi.fn().mockReturnValue('custom empty');
23
+ render(
24
+ <Pagination
25
+ page={0}
26
+ pageSize={10}
27
+ rowCount={0}
28
+ onPageChange={NOOP}
29
+ onPageSizeChange={NOOP}
30
+ localeText={{ paginationOf }}
31
+ />
32
+ );
33
+ expect(paginationOf).toHaveBeenCalledWith(0, 0, 0);
34
+ expect(screen.getByText('custom empty')).toBeTruthy();
35
+ });
36
+
37
+ it('renders correct row range label for non-empty data', () => {
38
+ render(
39
+ <Pagination
40
+ page={0}
41
+ pageSize={10}
42
+ rowCount={25}
43
+ onPageChange={NOOP}
44
+ onPageSizeChange={NOOP}
45
+ />
46
+ );
47
+ expect(screen.getByText('1–10 of 25')).toBeTruthy();
48
+ });
49
+
50
+ it('calls localeText.paginationOf with correct args for non-empty data', () => {
51
+ const paginationOf = vi.fn().mockReturnValue('custom range');
52
+ render(
53
+ <Pagination
54
+ page={1}
55
+ pageSize={10}
56
+ rowCount={25}
57
+ onPageChange={NOOP}
58
+ onPageSizeChange={NOOP}
59
+ localeText={{ paginationOf }}
60
+ />
61
+ );
62
+ expect(paginationOf).toHaveBeenCalledWith(11, 20, 25);
63
+ });
64
+ });