@opencorestack/opengridx 3.8.1 → 3.9.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 (53) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/index.d.ts +340 -1
  3. package/dist/opengridx.css +1 -1
  4. package/dist/opengridx.es.js +7529 -7285
  5. package/dist/opengridx.es.js.map +1 -1
  6. package/dist/opengridx.umd.js +16 -16
  7. package/dist/opengridx.umd.js.map +1 -1
  8. package/docs/API_REFERENCE.md +93 -4
  9. package/docs/customization/theming.md +11 -2
  10. package/docs/features/ai-toolkit.md +1 -1
  11. package/lib/components/AiAssistant/GridAiAssistantPanel.tsx +16 -14
  12. package/lib/components/Cell/CellErrorBoundary.tsx +17 -10
  13. package/lib/components/CellSelectionStats/CellSelectionStats.tsx +5 -3
  14. package/lib/components/ColumnResizeHandle/ColumnResizeHandle.tsx +3 -1
  15. package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.tsx +6 -4
  16. package/lib/components/DataGrid/GridAggregationFooter.tsx +3 -1
  17. package/lib/components/DataGrid/GridErrorOverlay.tsx +4 -2
  18. package/lib/components/DataGrid/GridListView.tsx +7 -5
  19. package/lib/components/DataGrid/GridLiveRegion.tsx +5 -3
  20. package/lib/components/DataGrid/GridLoadingOverlay.tsx +3 -1
  21. package/lib/components/DataGrid/GridPinnedRows.tsx +1 -1
  22. package/lib/components/DataGrid/GridRoot.tsx +8 -1
  23. package/lib/components/DataGrid/GridStandaloneColumnPanel.tsx +3 -1
  24. package/lib/components/DataGrid/GridViewport.tsx +3 -1
  25. package/lib/components/DataGrid/GridVirtualRows.tsx +1 -1
  26. package/lib/components/DataGrid/gridLocaleContext.ts +13 -0
  27. package/lib/components/FilterPanel/FilterPanel.tsx +18 -17
  28. package/lib/components/Header/ColumnMenu.css +5 -5
  29. package/lib/components/Header/ColumnMenu.tsx +17 -15
  30. package/lib/components/Header/Header.tsx +8 -6
  31. package/lib/components/HeaderFilter/HeaderFilterRow.tsx +3 -1
  32. package/lib/components/ListView/ListViewRow.tsx +3 -1
  33. package/lib/components/Pagination/Pagination.tsx +15 -18
  34. package/lib/components/QuickFilter/QuickFilter.tsx +5 -2
  35. package/lib/components/Row/Row.tsx +5 -3
  36. package/lib/components/Toolbar/GlobalSearch.tsx +6 -3
  37. package/lib/components/Toolbar/GridToolbar.tsx +35 -29
  38. package/lib/components/Toolbar/PivotPanel.tsx +26 -24
  39. package/lib/components/ui/Checkbox.css +5 -5
  40. package/lib/components/ui/DragHandleIcon.tsx +3 -1
  41. package/lib/components/ui/ExpandIcon.css +3 -3
  42. package/lib/components/ui/ExpandIcon.tsx +3 -1
  43. package/lib/hooks/core/useGridRowInteractions.ts +13 -3
  44. package/lib/hooks/core/useGridToolbarStack.ts +2 -2
  45. package/lib/hooks/core/useGridVirtualization.ts +40 -25
  46. package/lib/hooks/core/useShallowStableValue.ts +29 -0
  47. package/lib/hooks/features/useGridAiAssistant.ts +52 -22
  48. package/lib/theme/DataGridThemeProvider.tsx +9 -0
  49. package/lib/theme/types.ts +18 -0
  50. package/lib/types/locale.ts +344 -0
  51. package/lib/utils/gridLocale.ts +241 -0
  52. package/llms.txt +3 -1
  53. package/package.json +1 -1
@@ -1,5 +1,6 @@
1
1
  import React, { useState, useEffect, useRef, useCallback, useLayoutEffect } from 'react';
2
2
  import { Input } from '../ui/Input';
3
+ import { useGridLocale } from '../DataGrid/gridLocaleContext';
3
4
 
4
5
 
5
6
  interface GlobalSearchProps {
@@ -32,7 +33,9 @@ function normalizeSearch(text: string): string {
32
33
  return text.trim().split(/\s+/).filter(Boolean).join(' ');
33
34
  }
34
35
 
35
- export function GlobalSearch({ value = '', onChange, placeholder = 'Search...', debounceMs = 250 }: GlobalSearchProps) {
36
+ export function GlobalSearch({ value = '', onChange, placeholder: placeholderProp, debounceMs = 250 }: GlobalSearchProps) {
37
+ const locale = useGridLocale();
38
+ const placeholder = placeholderProp ?? locale.toolbarSearchPlaceholder;
36
39
  const [isExpanded, setIsExpanded] = useState(() => value.length > 0);
37
40
  // Fully uncontrolled: localValue is the single source of truth.
38
41
  const [localValue, setLocalValue] = useState(value);
@@ -152,11 +155,11 @@ export function GlobalSearch({ value = '', onChange, placeholder = 'Search...',
152
155
  onFocus={handleFocus}
153
156
  onBlur={handleBlur}
154
157
  placeholder={placeholder}
155
- aria-label="Global Search"
158
+ aria-label={locale.toolbarSearchLabel}
156
159
  autoComplete="off"
157
160
  />
158
161
  {localValue && (
159
- <button type="button" className="ogx-global-search__clear" onClick={handleClear} aria-label="Clear search">
162
+ <button type="button" className="ogx-global-search__clear" onClick={handleClear} aria-label={locale.toolbarSearchClear}>
160
163
  <CloseIcon />
161
164
  </button>
162
165
  )}
@@ -12,6 +12,7 @@ import { ColumnVisibilityPanel } from '../ColumnVisibilityPanel/ColumnVisibility
12
12
  import { FilterPanel } from '../FilterPanel/FilterPanel';
13
13
  import { GridTooltip } from '../Tooltip/Tooltip';
14
14
  import { AiSparkleIcon } from '../AiAssistant/GridAiAssistantPanel';
15
+ import { useGridLocale } from '../DataGrid/gridLocaleContext';
15
16
 
16
17
  /** Props passed to a custom toolbar button renderer. */
17
18
  export interface ToolbarButtonRenderProps {
@@ -169,6 +170,7 @@ function AggregationPanel({
169
170
  onClose: () => void;
170
171
  activeCount: number;
171
172
  }) {
173
+ const locale = useGridLocale();
172
174
  const panelRef = useRef<HTMLDivElement>(null);
173
175
  const [pos, setPos] = useState<PanelPosition>({ top: 0, right: 0 });
174
176
  const [portalContainer, setPortalContainer] = useState<Element | null>(null);
@@ -214,22 +216,22 @@ function AggregationPanel({
214
216
  ref={panelRef}
215
217
  className="ogx-toolbar__panel"
216
218
  role="dialog"
217
- aria-label="Summaries configuration"
219
+ aria-label={locale.toolbarSummariesPanelLabel}
218
220
  style={{ top: pos.top, right: pos.right, left: pos.left, width: 400, maxWidth: 'calc(100vw - 24px)' }}
219
221
  >
220
222
  <div className="ogx-toolbar__panel-header">
221
223
  <span className="ogx-toolbar__panel-title">
222
224
  <SigmaIcon />
223
- Summaries
225
+ {locale.toolbarSummaries}
224
226
  </span>
225
227
  {activeCount > 0 && (
226
228
  <button
227
229
  type="button"
228
230
  className="ogx-toolbar__clear-btn"
229
231
  onClick={onClearAll}
230
- title="Clear all summaries"
232
+ title={locale.toolbarSummariesClearAllTooltip}
231
233
  >
232
- Clear all
234
+ {locale.toolbarSummariesClearAll}
233
235
  </button>
234
236
  )}
235
237
  </div>
@@ -237,8 +239,8 @@ function AggregationPanel({
237
239
  <div className="ogx-toolbar__panel-body">
238
240
  {aggregableColumns.length === 0 ? (
239
241
  <p className="ogx-toolbar__empty">
240
- No aggregable columns.<br />
241
- Set <code>aggregable: true</code> on a column to enable.
242
+ {locale.toolbarSummariesEmpty}<br />
243
+ {`${locale.toolbarSummariesEmptyHintBefore} `}<code>aggregable: true</code>{` ${locale.toolbarSummariesEmptyHintAfter}`}
242
244
  </p>
243
245
  ) : (
244
246
  aggregableColumns.map((col) => {
@@ -257,7 +259,7 @@ function AggregationPanel({
257
259
  onClick={() => onFunctionChange(col.field, fn)}
258
260
  aria-pressed={currentFn === fn}
259
261
  >
260
- {fn}
262
+ {locale.toolbarAggregationFunction(fn)}
261
263
  </button>
262
264
  ))}
263
265
  </div>
@@ -295,6 +297,7 @@ function ColumnsPanelWrapper({
295
297
  onClose: () => void;
296
298
  showNonHideableColumns?: boolean;
297
299
  }) {
300
+ const locale = useGridLocale();
298
301
  const panelRef = useRef<HTMLDivElement>(null);
299
302
  const [pos, setPos] = useState<PanelPosition>({ top: 0, right: 0 });
300
303
  const [portalContainer, setPortalContainer] = useState<Element | null>(null);
@@ -340,7 +343,7 @@ function ColumnsPanelWrapper({
340
343
  ref={panelRef}
341
344
  className="ogx-toolbar__panel"
342
345
  role="dialog"
343
- aria-label="Column visibility"
346
+ aria-label={locale.toolbarColumnsPanelLabel}
344
347
  style={{ top: pos.top, right: pos.right, left: pos.left, width: 280, padding: 0 }}
345
348
  >
346
349
  <ColumnVisibilityPanel
@@ -372,6 +375,7 @@ function FilterPanelWrapper({
372
375
  onFilterModelChange: (model: GridFilterModel) => void;
373
376
  onClose: () => void;
374
377
  }) {
378
+ const locale = useGridLocale();
375
379
  const panelRef = useRef<HTMLDivElement>(null);
376
380
  const [pos, setPos] = useState<PanelPosition>({ top: 0, right: 0 });
377
381
  // Stable ref for onClose — prevents the mousedown effect from
@@ -421,13 +425,13 @@ function FilterPanelWrapper({
421
425
  ref={panelRef}
422
426
  className="ogx-toolbar__panel ogx-toolbar__panel--filter"
423
427
  role="dialog"
424
- aria-label="Advanced filters"
428
+ aria-label={locale.toolbarFiltersLabel}
425
429
  style={{ top: pos.top, right: pos.right, left: pos.left, width: 540, maxWidth: 'calc(100vw - 24px)' }}
426
430
  >
427
431
  <div className="ogx-toolbar__panel-header">
428
432
  <span className="ogx-toolbar__panel-title">
429
433
  <FilterIcon />
430
- Filters
434
+ {locale.toolbarFilters}
431
435
  {activeFilterCount > 0 && (
432
436
  <span className="ogx-toolbar__panel-count">{activeFilterCount}</span>
433
437
  )}
@@ -438,19 +442,19 @@ function FilterPanelWrapper({
438
442
  type="button"
439
443
  className="ogx-toolbar__clear-btn"
440
444
  onClick={handleClearAll}
441
- title="Clear all filters"
445
+ title={locale.toolbarFiltersClearAllTooltip}
442
446
  >
443
- Clear all
447
+ {locale.toolbarFiltersClearAll}
444
448
  </button>
445
449
  )}
446
450
  <button
447
451
  type="button"
448
452
  className="ogx-toolbar__close-btn"
449
453
  onClick={() => onCloseRef.current()}
450
- title="Close filters"
451
- aria-label="Close filters panel"
454
+ title={locale.toolbarFiltersCloseTooltip}
455
+ aria-label={locale.toolbarFiltersCloseLabel}
452
456
  >
453
- Close
457
+ {locale.toolbarFiltersClose}
454
458
  </button>
455
459
  </div>
456
460
  </div>
@@ -501,8 +505,10 @@ export function GridToolbar({
501
505
  showNonHideableColumns,
502
506
  onAiAssistantToggle,
503
507
  aiAssistantOpen = false,
504
- aiAssistantLabel = 'Ask AI',
508
+ aiAssistantLabel: aiAssistantLabelProp,
505
509
  }: GridToolbarProps) {
510
+ const locale = useGridLocale();
511
+ const aiAssistantLabel = aiAssistantLabelProp ?? locale.aiAssistantButton;
506
512
  // At most one panel is open. Every way the Columns panel closes (its button, a custom
507
513
  // button, another panel opening, click-outside, Escape) reports onColumnsPanelClose, so the
508
514
  // grid can ask for it again with forceColumnsOpen.
@@ -646,7 +652,7 @@ export function GridToolbar({
646
652
  }, [columns, columnVisibilityModel, onColumnVisibilityModelChange]);
647
653
 
648
654
  return (
649
- <div className={['ogx-toolbar', className].filter(Boolean).join(' ')} role="toolbar" aria-label="Grid toolbar" style={style}>
655
+ <div className={['ogx-toolbar', className].filter(Boolean).join(' ')} role="toolbar" aria-label={locale.toolbarLabel} style={style}>
650
656
  {/* Left slot — custom content */}
651
657
  {children && <div className="ogx-toolbar__left">{children}</div>}
652
658
 
@@ -681,7 +687,7 @@ export function GridToolbar({
681
687
  <GlobalSearch
682
688
  value={searchValue}
683
689
  onChange={handleSearchChange}
684
- placeholder="Search..."
690
+ placeholder={locale.toolbarSearchPlaceholder}
685
691
  />
686
692
  )
687
693
  }
@@ -698,12 +704,12 @@ export function GridToolbar({
698
704
  activeCount: columns.length - visibleColumns.size,
699
705
  })
700
706
  : (
701
- <GridTooltip title="Columns">
707
+ <GridTooltip title={locale.toolbarColumns}>
702
708
  <button
703
709
  type="button"
704
710
  ref={colsButtonRef}
705
711
  className={`ogx-toolbar__icon-btn${colsOpen ? ' ogx-toolbar__icon-btn--active' : ''}`}
706
- aria-label="Manage columns"
712
+ aria-label={locale.toolbarColumnsLabel}
707
713
  aria-haspopup="dialog"
708
714
  aria-expanded={colsOpen}
709
715
  onClick={() => togglePanel('columns')}
@@ -740,19 +746,19 @@ export function GridToolbar({
740
746
  activeCount: activeFilterCount,
741
747
  })
742
748
  : (
743
- <GridTooltip title="Filters">
749
+ <GridTooltip title={locale.toolbarFilters}>
744
750
  <button
745
751
  type="button"
746
752
  ref={filterButtonRef}
747
753
  className={`ogx-toolbar__icon-btn${filterOpen ? ' ogx-toolbar__icon-btn--active' : ''}`}
748
- aria-label="Advanced filters"
754
+ aria-label={locale.toolbarFiltersLabel}
749
755
  aria-haspopup="dialog"
750
756
  aria-expanded={filterOpen}
751
757
  onClick={() => togglePanel('filters')}
752
758
  >
753
759
  <FilterIcon />
754
760
  {activeFilterCount > 0 && (
755
- <span className="ogx-toolbar__dot" aria-label="Filters active" />
761
+ <span className="ogx-toolbar__dot" aria-label={locale.toolbarFiltersActive} />
756
762
  )}
757
763
  </button>
758
764
  </GridTooltip>
@@ -773,19 +779,19 @@ export function GridToolbar({
773
779
  {/* Pivot button */}
774
780
  {onPivotModelChange && (
775
781
  <div className="ogx-toolbar__dropdown-wrapper" style={{ marginRight: 4 }}>
776
- <GridTooltip title="Pivot">
782
+ <GridTooltip title={locale.toolbarPivot}>
777
783
  <button
778
784
  type="button"
779
785
  ref={pivotButtonRef}
780
786
  className={`ogx-toolbar__icon-btn${pivotOpen ? ' ogx-toolbar__icon-btn--active' : ''}`}
781
- aria-label="Configure pivot"
787
+ aria-label={locale.toolbarPivotLabel}
782
788
  aria-haspopup="dialog"
783
789
  aria-expanded={pivotOpen}
784
790
  onClick={() => togglePanel('pivot')}
785
791
  >
786
792
  <PivotIcon />
787
793
  {pivotActive && (
788
- <span className="ogx-toolbar__dot" aria-label="Pivot active" />
794
+ <span className="ogx-toolbar__dot" aria-label={locale.toolbarPivotActive} />
789
795
  )}
790
796
  </button>
791
797
  </GridTooltip>
@@ -812,19 +818,19 @@ export function GridToolbar({
812
818
  activeCount,
813
819
  })
814
820
  : (
815
- <GridTooltip title="Summaries">
821
+ <GridTooltip title={locale.toolbarSummaries}>
816
822
  <button
817
823
  type="button"
818
824
  ref={aggButtonRef}
819
825
  className={`ogx-toolbar__icon-btn${aggOpen ? ' ogx-toolbar__icon-btn--active' : ''}`}
820
- aria-label="Configure summaries"
826
+ aria-label={locale.toolbarSummariesLabel}
821
827
  aria-haspopup="dialog"
822
828
  aria-expanded={aggOpen}
823
829
  onClick={() => togglePanel('summaries')}
824
830
  >
825
831
  <SigmaIcon />
826
832
  {activeCount > 0 && (
827
- <span className="ogx-toolbar__dot" aria-label={`${activeCount} active summaries`} />
833
+ <span className="ogx-toolbar__dot" aria-label={locale.toolbarSummariesActive(activeCount)} />
828
834
  )}
829
835
  </button>
830
836
  </GridTooltip>
@@ -4,6 +4,7 @@ import type { GridColDef } from '../../types';
4
4
  import type { GridPivotModel, GridPivotAggFn, GridPivotValueField } from '../../types';
5
5
  import { getViewportWidth } from '../../utils/viewport';
6
6
  import { getPanelPosition, type GridPanelPosition } from '../../utils/direction';
7
+ import { useGridLocale } from '../DataGrid/gridLocaleContext';
7
8
 
8
9
  const AGG_FNS: GridPivotAggFn[] = ['sum', 'avg', 'count', 'min', 'max'];
9
10
 
@@ -31,6 +32,7 @@ export interface PivotPanelProps {
31
32
  }
32
33
 
33
34
  export function PivotPanel({ anchorRef, columns, model, onChange, onClose }: PivotPanelProps) {
35
+ const locale = useGridLocale();
34
36
  const panelRef = useRef<HTMLDivElement>(null);
35
37
  const [pos, setPos] = useState<GridPanelPosition>({ top: 0, right: 0 });
36
38
  const [portalContainer, setPortalContainer] = useState<Element | null>(null);
@@ -128,18 +130,18 @@ export function PivotPanel({ anchorRef, columns, model, onChange, onClose }: Piv
128
130
  ref={panelRef}
129
131
  className="ogx-pivot-panel"
130
132
  role="dialog"
131
- aria-label="Pivot configuration"
133
+ aria-label={locale.pivotPanelLabel}
132
134
  style={{ top: pos.top, right: pos.right, left: pos.left }}
133
135
  >
134
136
  { }
135
137
  <div className="ogx-pivot-panel__header">
136
138
  <span className="ogx-pivot-panel__title">
137
139
  <PivotIcon />
138
- Pivot Mode
140
+ {locale.pivotMode}
139
141
  </span>
140
142
  {totalActive > 0 && (
141
- <button type="button" className="ogx-pivot-panel__reset-btn" onClick={reset} title="Reset pivot">
142
- Reset
143
+ <button type="button" className="ogx-pivot-panel__reset-btn" onClick={reset} title={locale.pivotResetTooltip}>
144
+ {locale.pivotReset}
143
145
  </button>
144
146
  )}
145
147
  </div>
@@ -150,11 +152,11 @@ export function PivotPanel({ anchorRef, columns, model, onChange, onClose }: Piv
150
152
  <div className="ogx-pivot-zone">
151
153
  <span className="ogx-pivot-zone__label">
152
154
  <span className="ogx-pivot-zone__dot ogx-pivot-zone__dot--row" />
153
- Row Fields
155
+ {locale.pivotRowFields}
154
156
  </span>
155
157
  <div
156
158
  className={`ogx-pivot-chips${model.rowFields.length === 0 ? ' ogx-pivot-chips--empty' : ''}`}
157
- data-placeholder="Add fields here — each becomes a row label"
159
+ data-placeholder={locale.pivotRowFieldsPlaceholder}
158
160
  >
159
161
  {model.rowFields.map((f, i) => (
160
162
  <span key={`${i}:${f}`} className="ogx-pivot-chip ogx-pivot-chip--row">
@@ -163,8 +165,8 @@ export function PivotPanel({ anchorRef, columns, model, onChange, onClose }: Piv
163
165
  type="button"
164
166
  className="ogx-pivot-chip__remove"
165
167
  onClick={() => removeRow(i)}
166
- title={`Remove ${colLabel(f)}`}
167
- aria-label={`Remove ${colLabel(f)} from Row Fields`}
168
+ title={locale.pivotRemoveTooltip(colLabel(f))}
169
+ aria-label={locale.pivotRemoveRowLabel(colLabel(f))}
168
170
  >×</button>
169
171
  </span>
170
172
  ))}
@@ -175,11 +177,11 @@ export function PivotPanel({ anchorRef, columns, model, onChange, onClose }: Piv
175
177
  <div className="ogx-pivot-zone">
176
178
  <span className="ogx-pivot-zone__label">
177
179
  <span className="ogx-pivot-zone__dot ogx-pivot-zone__dot--col" />
178
- Column Fields
180
+ {locale.pivotColumnFields}
179
181
  </span>
180
182
  <div
181
183
  className={`ogx-pivot-chips${model.columnFields.length === 0 ? ' ogx-pivot-chips--empty' : ''}`}
182
- data-placeholder="Add fields here — unique values become column headers"
184
+ data-placeholder={locale.pivotColumnFieldsPlaceholder}
183
185
  >
184
186
  {model.columnFields.map((f, i) => (
185
187
  <span key={`${i}:${f}`} className="ogx-pivot-chip ogx-pivot-chip--col">
@@ -188,8 +190,8 @@ export function PivotPanel({ anchorRef, columns, model, onChange, onClose }: Piv
188
190
  type="button"
189
191
  className="ogx-pivot-chip__remove"
190
192
  onClick={() => removeCol(i)}
191
- title={`Remove ${colLabel(f)}`}
192
- aria-label={`Remove ${colLabel(f)} from Column Fields`}
193
+ title={locale.pivotRemoveTooltip(colLabel(f))}
194
+ aria-label={locale.pivotRemoveColumnLabel(colLabel(f))}
193
195
  >×</button>
194
196
  </span>
195
197
  ))}
@@ -200,11 +202,11 @@ export function PivotPanel({ anchorRef, columns, model, onChange, onClose }: Piv
200
202
  <div className="ogx-pivot-zone">
201
203
  <span className="ogx-pivot-zone__label">
202
204
  <span className="ogx-pivot-zone__dot ogx-pivot-zone__dot--value" />
203
- Value Fields (aggregated)
205
+ {locale.pivotValueFields}
204
206
  </span>
205
207
  <div
206
208
  className={`ogx-pivot-chips${model.valueFields.length === 0 ? ' ogx-pivot-chips--empty' : ''}`}
207
- data-placeholder="Add numeric fields here — values are aggregated"
209
+ data-placeholder={locale.pivotValueFieldsPlaceholder}
208
210
  >
209
211
  {model.valueFields.map((vf: GridPivotValueField, i) => {
210
212
  const allowed = allowedFns(vf.field);
@@ -216,19 +218,19 @@ export function PivotPanel({ anchorRef, columns, model, onChange, onClose }: Piv
216
218
  className="ogx-pivot-chip__fn-select"
217
219
  value={vf.aggFn}
218
220
  onChange={(e) => changeValueFn(i, e.target.value as GridPivotAggFn)}
219
- title="Aggregation function"
220
- aria-label={`Aggregation function for ${colLabel(vf.field)}`}
221
+ title={locale.pivotAggregationTooltip}
222
+ aria-label={locale.pivotAggregationLabel(colLabel(vf.field))}
221
223
  >
222
224
  {fnOptions.map((fn) => (
223
- <option key={fn} value={fn}>{fn.toUpperCase()}</option>
225
+ <option key={fn} value={fn}>{locale.pivotAggregationFunction(fn)}</option>
224
226
  ))}
225
227
  </select>
226
228
  <button
227
229
  type="button"
228
230
  className="ogx-pivot-chip__remove"
229
231
  onClick={() => removeValue(i)}
230
- title={`Remove ${colLabel(vf.field)}`}
231
- aria-label={`Remove ${colLabel(vf.field)} (${vf.aggFn.toUpperCase()}) from Value Fields`}
232
+ title={locale.pivotRemoveTooltip(colLabel(vf.field))}
233
+ aria-label={locale.pivotRemoveValueLabel(colLabel(vf.field), locale.pivotAggregationFunction(vf.aggFn))}
232
234
  >×</button>
233
235
  </span>
234
236
  );
@@ -240,7 +242,7 @@ export function PivotPanel({ anchorRef, columns, model, onChange, onClose }: Piv
240
242
 
241
243
  { }
242
244
  <div>
243
- <div className="ogx-pivot-panel__fields-label">Available Fields</div>
245
+ <div className="ogx-pivot-panel__fields-label">{locale.pivotAvailableFields}</div>
244
246
  <div className="ogx-pivot-fields">
245
247
  {availableColumns.map((col) => {
246
248
  const isNum = canBeValue(col);
@@ -253,13 +255,13 @@ export function PivotPanel({ anchorRef, columns, model, onChange, onClose }: Piv
253
255
  </span>
254
256
  <div className="ogx-pivot-field-actions">
255
257
  {isDimension && (
256
- <button type="button" className="ogx-pivot-add-btn ogx-pivot-add-btn--row" onClick={() => addRow(col.field)} title="Add to Row Fields" aria-label={`Add ${label} to Row Fields`}>Row</button>
258
+ <button type="button" className="ogx-pivot-add-btn ogx-pivot-add-btn--row" onClick={() => addRow(col.field)} title={locale.pivotAddRowTooltip} aria-label={locale.pivotAddRowLabel(label)}>{locale.pivotAddRow}</button>
257
259
  )}
258
260
  {isDimension && (
259
- <button type="button" className="ogx-pivot-add-btn ogx-pivot-add-btn--col" onClick={() => addCol(col.field)} title="Add to Column Fields" aria-label={`Add ${label} to Column Fields`}>Col</button>
261
+ <button type="button" className="ogx-pivot-add-btn ogx-pivot-add-btn--col" onClick={() => addCol(col.field)} title={locale.pivotAddColumnTooltip} aria-label={locale.pivotAddColumnLabel(label)}>{locale.pivotAddColumn}</button>
260
262
  )}
261
263
  {isNum && (
262
- <button type="button" className="ogx-pivot-add-btn ogx-pivot-add-btn--value" onClick={() => addValue(col.field)} title="Add to Value Fields" aria-label={`Add ${label} to Value Fields`}>Val</button>
264
+ <button type="button" className="ogx-pivot-add-btn ogx-pivot-add-btn--value" onClick={() => addValue(col.field)} title={locale.pivotAddValueTooltip} aria-label={locale.pivotAddValueLabel(label)}>{locale.pivotAddValue}</button>
263
265
  )}
264
266
  </div>
265
267
  </div>
@@ -267,7 +269,7 @@ export function PivotPanel({ anchorRef, columns, model, onChange, onClose }: Piv
267
269
  })}
268
270
  {availableColumns.length === 0 && (
269
271
  <span style={{ fontSize: 12, color: 'var(--ogx-color-gray-400, #94a3b8)' }}>
270
- All fields are in use.
272
+ {locale.pivotAllFieldsInUse}
271
273
  </span>
272
274
  )}
273
275
  </div>
@@ -26,7 +26,7 @@
26
26
  border-radius: 4px;
27
27
  background: var(--ogx-checkbox-bg, var(--ogx-color-white));
28
28
  transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
29
- color: white;
29
+ color: var(--ogx-checkbox-checkmark, white);
30
30
  flex-shrink: 0;
31
31
  }
32
32
  .ogx-checkbox__icon {
@@ -43,15 +43,15 @@
43
43
  line-height: 1;
44
44
  }
45
45
  .ogx-checkbox__input:checked + .ogx-checkbox__box {
46
- background: var(--ogx-color-primary);
47
- border-color: var(--ogx-color-primary);
46
+ background: var(--ogx-checkbox-checked-bg, var(--ogx-color-primary));
47
+ border-color: var(--ogx-checkbox-checked-bg, var(--ogx-color-primary));
48
48
  }
49
49
  .ogx-checkbox__input:checked + .ogx-checkbox__box .ogx-checkbox__icon {
50
50
  transform: scale(1);
51
51
  }
52
52
  .ogx-checkbox__input:indeterminate + .ogx-checkbox__box {
53
- background: var(--ogx-color-primary);
54
- border-color: var(--ogx-color-primary);
53
+ background: var(--ogx-checkbox-checked-bg, var(--ogx-color-primary));
54
+ border-color: var(--ogx-checkbox-checked-bg, var(--ogx-color-primary));
55
55
  }
56
56
  .ogx-checkbox__input:indeterminate + .ogx-checkbox__box .ogx-checkbox__icon {
57
57
  transform: scale(1);
@@ -1,13 +1,15 @@
1
1
  import React from 'react';
2
+ import { useGridLocale } from '../DataGrid/gridLocaleContext';
2
3
 
3
4
  export type DragHandleIconProps = React.ButtonHTMLAttributes<HTMLButtonElement>;
4
5
 
5
6
  export const DragHandleIcon: React.FC<DragHandleIconProps> = ({ className = '', ...props }) => {
7
+ const locale = useGridLocale();
6
8
  return (
7
9
  <button
8
10
  type="button"
9
11
  className={`ogx-drag-handle ${className}`}
10
- aria-label="Drag to reorder row"
12
+ aria-label={locale.rowReorderLabel}
11
13
  {...props}
12
14
  >
13
15
  <svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor" aria-hidden="true">
@@ -8,13 +8,13 @@
8
8
  border: none;
9
9
  background: transparent;
10
10
  cursor: pointer;
11
- color: var(--ogx-grid-header-text);
11
+ color: var(--ogx-expand-icon-color, var(--ogx-grid-header-text));
12
12
  transition: color 0.2s ease;
13
13
  border-radius: 4px;
14
14
  }
15
15
  .ogx-expand-icon:hover {
16
- background: transparent;
17
- color: var(--ogx-grid-row-text);
16
+ background: transparent;
17
+ color: var(--ogx-expand-icon-hover-color, var(--ogx-grid-row-text));
18
18
  }
19
19
  .ogx-expand-icon:focus {
20
20
  outline: none;
@@ -1,5 +1,6 @@
1
1
 
2
2
  import React from 'react';
3
+ import { useGridLocale } from '../DataGrid/gridLocaleContext';
3
4
 
4
5
  export interface ExpandIconProps {
5
6
  isExpanded: boolean;
@@ -10,6 +11,7 @@ export interface ExpandIconProps {
10
11
  }
11
12
 
12
13
  export function ExpandIcon({ isExpanded, onClick, variant = 'chevron', tabIndex }: ExpandIconProps) {
14
+ const locale = useGridLocale();
13
15
  return (
14
16
  <button
15
17
  className={`ogx-expand-icon ${isExpanded ? 'ogx-expand-icon--expanded' : ''}`}
@@ -18,7 +20,7 @@ export function ExpandIcon({ isExpanded, onClick, variant = 'chevron', tabIndex
18
20
  onClick(e);
19
21
  }
20
22
  }}
21
- aria-label={isExpanded ? 'Collapse row' : 'Expand row'}
23
+ aria-label={isExpanded ? locale.rowCollapseLabel : locale.rowExpandLabel}
22
24
  type="button"
23
25
  tabIndex={tabIndex}
24
26
  >
@@ -1,4 +1,4 @@
1
- import { useCallback, useMemo } from 'react';
1
+ import { useCallback, useLayoutEffect, useMemo, useRef } from 'react';
2
2
  import { useGridRowSelection, type UseGridRowSelectionReturn } from './useGridRowSelection';
3
3
  import { PIVOT_GRAND_TOTAL_ID } from '../../utils/pivot';
4
4
  import { isSyntheticRowId } from '../../utils/syntheticRows';
@@ -56,7 +56,7 @@ export function useGridRowInteractions<R extends GridRowModel>(
56
56
  // handlers keep their identity and memoised rows do not re-render.
57
57
  const { clickRow, toggleRow } = rowSelection;
58
58
 
59
- const handleRowClick = useCallback((rowParams: GridRowParams<R>) => {
59
+ const rowClick = useCallback((rowParams: GridRowParams<R>) => {
60
60
  const { id } = rowParams;
61
61
 
62
62
  // Synthetic group rows toggle on click and are never selected. Tree-data parents are real rows:
@@ -77,10 +77,20 @@ export function useGridRowInteractions<R extends GridRowModel>(
77
77
 
78
78
  // Honors disableMultipleRowSelection like a row click does. Synthetic group / subtotal rows
79
79
  // and the pivot Grand Total are not data: their ids never enter the selection model.
80
- const handleSelectionChange = useCallback((rowId: GridRowId, isSelected: boolean) => {
80
+ const selectionChange = useCallback((rowId: GridRowId, isSelected: boolean) => {
81
81
  if (isSyntheticRowId(rowId, rowMetaMap)) return;
82
82
  toggleRow(rowId, isSelected);
83
83
  }, [rowMetaMap, toggleRow]);
84
84
 
85
+ // Rows receive these handlers and are memoised on their props: stable identities (reading the
86
+ // latest handlers at event time) keep a selection change from re-rendering every visible row.
87
+ const latestRef = useRef({ rowClick, selectionChange });
88
+ useLayoutEffect(() => { latestRef.current = { rowClick, selectionChange }; });
89
+ const handleRowClick = useCallback((rowParams: GridRowParams<R>) => latestRef.current.rowClick(rowParams), []);
90
+ const handleSelectionChange = useCallback(
91
+ (rowId: GridRowId, isSelected: boolean) => latestRef.current.selectionChange(rowId, isSelected),
92
+ []
93
+ );
94
+
85
95
  return { rowSelection, handleRowClick, handleSelectionChange };
86
96
  }
@@ -13,7 +13,7 @@ export type GridToolbarStackToolbarParams<R extends GridRowModel> = Omit<UseGrid
13
13
  export interface UseGridToolbarStackParams<R extends GridRowModel> {
14
14
  apiRef: MutableRefObject<GridApi>;
15
15
  containerRef: RefObject<HTMLDivElement | null>;
16
- aiAssistant: Omit<UseGridAiAssistantParams, 'apiRef'>;
16
+ aiAssistant: Omit<UseGridAiAssistantParams, 'apiRef' | 'localeText'>;
17
17
  toolbar: GridToolbarStackToolbarParams<R>;
18
18
  localeText: GridLocaleText | undefined;
19
19
  }
@@ -30,7 +30,7 @@ export function useGridToolbarStack<R extends GridRowModel>(params: UseGridToolb
30
30
  useGridStylesheetWarning(containerRef);
31
31
 
32
32
  // The aiAssistant prompt panel (v3.5): applies validated replies through apiRef; apiRef.openAiAssistant.
33
- const aiAssistantPanel = useGridAiAssistant({ apiRef, ...aiAssistant });
33
+ const aiAssistantPanel = useGridAiAssistant({ apiRef, localeText, ...aiAssistant });
34
34
 
35
35
  const toolbarProps = useGridToolbarProps<R>({
36
36
  ...toolbar,