@opencorestack/opengridx 3.8.0 → 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 (76) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/dist/ai.cjs.map +1 -1
  3. package/dist/ai.es.js.map +1 -1
  4. package/dist/index.d.ts +342 -3
  5. package/dist/opengridx.css +1 -1
  6. package/dist/opengridx.es.js +6952 -6697
  7. package/dist/opengridx.es.js.map +1 -1
  8. package/dist/opengridx.umd.js +17 -17
  9. package/dist/opengridx.umd.js.map +1 -1
  10. package/docs/API_REFERENCE.md +121 -4
  11. package/docs/customization/theming.md +11 -2
  12. package/docs/features/ai-toolkit.md +1 -1
  13. package/lib/components/AiAssistant/GridAiAssistantPanel.css +2 -2
  14. package/lib/components/AiAssistant/GridAiAssistantPanel.tsx +16 -14
  15. package/lib/components/Cell/Cell.css +1 -3
  16. package/lib/components/Cell/CellErrorBoundary.tsx +17 -10
  17. package/lib/components/CellSelectionStats/CellSelectionStats.tsx +5 -3
  18. package/lib/components/ColumnGroupHeader/ColumnGroupHeader.css +1 -2
  19. package/lib/components/ColumnResizeHandle/ColumnResizeHandle.tsx +3 -1
  20. package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.tsx +6 -4
  21. package/lib/components/DataGrid/DataGrid.css +1 -28
  22. package/lib/components/DataGrid/GridAggregationFooter.tsx +3 -1
  23. package/lib/components/DataGrid/GridErrorOverlay.tsx +4 -2
  24. package/lib/components/DataGrid/GridListView.tsx +7 -5
  25. package/lib/components/DataGrid/GridLiveRegion.tsx +5 -3
  26. package/lib/components/DataGrid/GridLoadingOverlay.tsx +3 -1
  27. package/lib/components/DataGrid/GridPinnedRows.tsx +1 -1
  28. package/lib/components/DataGrid/GridRoot.tsx +8 -1
  29. package/lib/components/DataGrid/GridStandaloneColumnPanel.tsx +3 -1
  30. package/lib/components/DataGrid/GridViewport.tsx +3 -1
  31. package/lib/components/DataGrid/GridVirtualRows.tsx +3 -2
  32. package/lib/components/DataGrid/gridLocaleContext.ts +13 -0
  33. package/lib/components/FilterPanel/FilterPanel.tsx +25 -56
  34. package/lib/components/Header/ColumnMenu.css +5 -5
  35. package/lib/components/Header/ColumnMenu.tsx +17 -15
  36. package/lib/components/Header/Header.css +2 -11
  37. package/lib/components/Header/Header.tsx +8 -6
  38. package/lib/components/HeaderFilter/HeaderFilterCell.tsx +6 -37
  39. package/lib/components/HeaderFilter/HeaderFilterRow.tsx +3 -1
  40. package/lib/components/ListView/ListView.css +0 -16
  41. package/lib/components/ListView/ListViewRow.tsx +3 -1
  42. package/lib/components/Pagination/Pagination.tsx +15 -18
  43. package/lib/components/QuickFilter/QuickFilter.tsx +5 -2
  44. package/lib/components/Row/Row.tsx +9 -4
  45. package/lib/components/Toolbar/GlobalSearch.tsx +6 -3
  46. package/lib/components/Toolbar/GridToolbar.tsx +35 -29
  47. package/lib/components/Toolbar/PivotPanel.tsx +26 -24
  48. package/lib/components/ui/Checkbox.css +5 -5
  49. package/lib/components/ui/DragHandleIcon.tsx +3 -1
  50. package/lib/components/ui/ExpandIcon.css +3 -3
  51. package/lib/components/ui/ExpandIcon.tsx +3 -1
  52. package/lib/hooks/core/useGridRowInteractions.ts +13 -3
  53. package/lib/hooks/core/useGridToolbarStack.ts +2 -2
  54. package/lib/hooks/core/useGridVirtualization.ts +40 -25
  55. package/lib/hooks/core/useShallowStableValue.ts +29 -0
  56. package/lib/hooks/features/useGridAiAssistant.ts +52 -22
  57. package/lib/hooks/features/useGridSpanning.ts +1 -1
  58. package/lib/styles/base/reset.css +6 -7
  59. package/lib/theme/DataGridThemeProvider.tsx +9 -0
  60. package/lib/theme/__baseline__/default-theme.darwin.chromium.json +1 -1503
  61. package/lib/theme/__baseline__/default-theme.darwin.firefox.json +1 -1503
  62. package/lib/theme/__baseline__/default-theme.darwin.webkit.json +1 -1503
  63. package/lib/theme/types.ts +18 -0
  64. package/lib/types/locale.ts +344 -0
  65. package/lib/types/rows.ts +0 -2
  66. package/lib/utils/cellSelection.ts +0 -2
  67. package/lib/utils/editing/index.ts +1 -1
  68. package/lib/utils/filtering/filterInput.ts +45 -0
  69. package/lib/utils/filtering/index.ts +0 -28
  70. package/lib/utils/gridLocale.ts +241 -0
  71. package/lib/utils/pinning/index.ts +0 -107
  72. package/lib/utils/sorting/index.ts +42 -33
  73. package/lib/utils/values.ts +5 -1
  74. package/llms.txt +9 -3
  75. package/package.json +1 -1
  76. package/lib/theme/defaultStyles.ts +0 -0
@@ -4,6 +4,7 @@ import { Pagination } from '../Pagination/Pagination';
4
4
  import { ListViewRow } from '../ListView/ListViewRow';
5
5
  import { getPageCount, normalizePageSize } from '../../utils/pagination';
6
6
  import { useRowsScrollEndLatch } from '../../hooks/core/useRowsScrollEndLatch';
7
+ import { useGridLocale } from './gridLocaleContext';
7
8
  import type {
8
9
  GridColDef,
9
10
  GridRowModel,
@@ -110,6 +111,7 @@ export function GridListView<R extends GridRowModel>({
110
111
  selectable = false,
111
112
  onToggleExpansion,
112
113
  }: GridListViewProps<R>) {
114
+ const locale = useGridLocale();
113
115
  const PaginationComponent = paginationSlot || Pagination;
114
116
 
115
117
  // Roving tab stop: the list is one Tab stop, on the last focused row (the first row until then,
@@ -192,15 +194,15 @@ export function GridListView<R extends GridRowModel>({
192
194
  <div
193
195
  className="ogx-list-view"
194
196
  role="grid"
195
- aria-label={ariaLabel || 'Data grid list view'}
197
+ aria-label={ariaLabel || locale.listViewLabel}
196
198
  aria-rowcount={itemCount}
197
199
  aria-multiselectable={multiselectable}
198
200
  >
199
201
  <div className="ogx-list-view__toolbar">
200
202
  <span>
201
- {itemCount} {itemCount === 1 ? 'item' : 'items'}
202
- {pagination ? ` · page ${currentPage + 1} of ${pageCount}` : ''}
203
- {selectedRowIds.size > 0 ? ` · ${selectedRowIds.size} selected` : ''}
203
+ {locale.listViewItemCount(itemCount)}
204
+ {pagination ? ` · ${locale.listViewPage(currentPage + 1, pageCount)}` : ''}
205
+ {selectedRowIds.size > 0 ? ` · ${locale.listViewSelectedCount(selectedRowIds.size)}` : ''}
204
206
  </span>
205
207
  </div>
206
208
 
@@ -209,7 +211,7 @@ export function GridListView<R extends GridRowModel>({
209
211
  {allRenderableRows.length === 0 && loading ? (
210
212
  // The grid's live region announces loading; the list shows no "No Data" meanwhile.
211
213
  <div className="ogx-list-view__loading">
212
- {loadingOverlay ?? <div className="ogx__loading-bar" role="progressbar" aria-label="Loading data" />}
214
+ {loadingOverlay ?? <div className="ogx__loading-bar" role="progressbar" aria-label={locale.loadingLabel} />}
213
215
  </div>
214
216
  ) : allRenderableRows.length === 0 && noRowsOverlay ? (
215
217
  <div className="ogx-list-view__empty">{noRowsOverlay}</div>
@@ -1,5 +1,6 @@
1
1
  import { getDataSourceErrorMessage } from '../../utils/dataSource';
2
2
  import type { GridFilterModel } from '../../types';
3
+ import { useGridLocale } from './gridLocaleContext';
3
4
 
4
5
  interface GridLiveRegionProps {
5
6
  loading: boolean;
@@ -22,15 +23,16 @@ interface GridLiveRegionProps {
22
23
  * the size of the selected cell range; and the AI assistant's status.
23
24
  */
24
25
  export function GridLiveRegion({ loading, error, noRowsLabel, filteredRowCount, dataRowCount, filterModel, cellSelectionAnnouncement, aiAssistantAnnouncement }: GridLiveRegionProps) {
26
+ const locale = useGridLocale();
25
27
  return (
26
28
  <div className="ogx-aria-live-status" role="status" aria-live="polite">
27
- {loading ? 'Loading data...' : ''}
28
- {error ? `Error: ${getDataSourceErrorMessage(error)}` : ''}
29
+ {loading ? locale.liveRegionLoading : ''}
30
+ {error ? locale.liveRegionError(getDataSourceErrorMessage(error)) : ''}
29
31
  {!loading && !error && (
30
32
  filteredRowCount === 0
31
33
  ? noRowsLabel
32
34
  : (filterModel && ((filterModel.quickFilterValues?.length || 0) > 0 || (filterModel.items?.length || 0) > 0))
33
- ? `${dataRowCount} ${dataRowCount === 1 ? 'row' : 'rows'} found`
35
+ ? locale.liveRegionRowsFound(dataRowCount)
34
36
  : ''
35
37
  )}
36
38
  {cellSelectionAnnouncement ? <span className="ogx-aria-live-status__cells"> {cellSelectionAnnouncement}</span> : null}
@@ -1,4 +1,5 @@
1
1
  import React from 'react';
2
+ import { useGridLocale } from './gridLocaleContext';
2
3
 
3
4
  interface GridLoadingOverlayProps {
4
5
  /** `slots.loadingOverlay`, rendered centred over the rows on a dimmed backdrop. */
@@ -12,6 +13,7 @@ interface GridLoadingOverlayProps {
12
13
  * or the loading overlay in place of the rows, so this is not rendered.
13
14
  */
14
15
  export function GridLoadingOverlay({ overlay }: GridLoadingOverlayProps) {
16
+ const locale = useGridLocale();
15
17
  if (overlay) {
16
18
  return (
17
19
  <div className="ogx__loading-overlay ogx__loading-overlay--over-rows ogx__loading-overlay--custom">
@@ -21,7 +23,7 @@ export function GridLoadingOverlay({ overlay }: GridLoadingOverlayProps) {
21
23
  }
22
24
  return (
23
25
  <div className="ogx__loading-overlay ogx__loading-overlay--over-rows">
24
- <div className="ogx__loading-bar" role="progressbar" aria-label="Loading data" />
26
+ <div className="ogx__loading-bar" role="progressbar" aria-label={locale.loadingLabel} />
25
27
  </div>
26
28
  );
27
29
  }
@@ -154,7 +154,7 @@ export function GridPinnedRows<R extends GridRowModel>({
154
154
  rowSpanningCaches={rowSpanningCaches}
155
155
  rowHeight={rowHeight}
156
156
  rowMeta={rowMetaMap.get(id)}
157
- editingCell={editingHandlers?.editingCell}
157
+ editingCell={editingHandlers?.editingCell?.id === id ? editingHandlers.editingCell : null}
158
158
  onEditStart={editingHandlers?.startCellEdit}
159
159
  onEditStop={editingHandlers?.stopCellEdit}
160
160
  onEditCellValueChange={editingHandlers?.setEditCellValue}
@@ -1,4 +1,7 @@
1
- import React from 'react';
1
+ import React, { useMemo } from 'react';
2
+ import { GridLocaleContext } from './gridLocaleContext';
3
+ import { resolveGridLocale } from '../../utils/gridLocale';
4
+ import { useShallowStableValue } from '../../hooks/core/useShallowStableValue';
2
5
  import { GridToolbarHostContext } from '../../hooks/core/gridToolbarHostContext';
3
6
  import { GridToolbarSlot } from './GridToolbarSlot';
4
7
  import { GridAiAssistantArea } from '../AiAssistant/GridAiAssistantArea';
@@ -95,8 +98,11 @@ export function GridRoot<R extends GridRowModel>(props: GridRootProps<R>) {
95
98
  // body shows skeletons or the loading overlay instead, and infinite scroll appends skeleton rows.
96
99
  const showLoadingOverRows = effectiveLoading && allRenderableRows.length > 0 && paginationMode !== 'infinite';
97
100
  const ToolbarSlot = slots?.toolbar;
101
+ const stableLocaleText = useShallowStableValue(localeText);
102
+ const locale = useMemo(() => resolveGridLocale(stableLocaleText), [stableLocaleText]);
98
103
 
99
104
  return (
105
+ <GridLocaleContext.Provider value={locale}>
100
106
  <div
101
107
  ref={containerRef}
102
108
  className={['ogx', className, autoHeight && 'ogx--auto-height', !autoHeight && height === undefined && style?.height === undefined && 'ogx--fill', hasRowSpanning && 'ogx--row-spanning', listView && 'ogx--list-view'].filter(Boolean).join(' ')}
@@ -235,5 +241,6 @@ export function GridRoot<R extends GridRowModel>(props: GridRootProps<R>) {
235
241
 
236
242
  <GridErrorOverlay error={dataSourceError} onRetry={onRetry} />
237
243
  </div>
244
+ </GridLocaleContext.Provider>
238
245
  );
239
246
  }
@@ -4,6 +4,7 @@ import { ColumnVisibilityPanel } from '../ColumnVisibilityPanel/ColumnVisibility
4
4
  import type { GridColDef, GridRowModel, GridColumnGroupingModel } from '../../types';
5
5
  import { canReorderWithinColumnGroups } from '../../utils/columnGroups';
6
6
  import { getViewportWidth } from '../../utils/viewport';
7
+ import { useGridLocale } from './gridLocaleContext';
7
8
 
8
9
  export interface GridStandaloneColumnPanelProps<R extends GridRowModel> {
9
10
  isOpen: boolean;
@@ -42,6 +43,7 @@ export function GridStandaloneColumnPanel<R extends GridRowModel>({
42
43
  onColumnOrderReset,
43
44
  columnGroupingModel,
44
45
  }: GridStandaloneColumnPanelProps<R>) {
46
+ const locale = useGridLocale();
45
47
  // Resolved from the DOM after mount, never during render, so the grid renders on the server.
46
48
  const [placement, setPlacement] = useState<PanelPlacement | null>(null);
47
49
 
@@ -97,7 +99,7 @@ export function GridStandaloneColumnPanel<R extends GridRowModel>({
97
99
  zIndex: 1,
98
100
  boxShadow: '0 1px 4px rgba(0,0,0,0.2)',
99
101
  }}
100
- aria-label="Close"
102
+ aria-label={locale.columnsPanelClose}
101
103
  >×</button>
102
104
  <ColumnVisibilityPanel<R>
103
105
  columns={columns}
@@ -4,6 +4,7 @@ import { GridPinnedRows, type GridPinnedRowsProps } from './GridPinnedRows';
4
4
  import { GridVirtualRows, type GridVirtualRowsProps } from './GridVirtualRows';
5
5
  import { GridEmptyState } from './GridEmptyState';
6
6
  import { GridAggregationFooter } from './GridAggregationFooter';
7
+ import { useGridLocale } from './gridLocaleContext';
7
8
  import type { GridAggregationModel, GridAggregationResult, GridColDef, GridColumnPinning, GridRowId, GridRowModel } from '../../types';
8
9
  import type { GridAriaRowLayout } from '../../utils/aria';
9
10
  import type { UseGridCellSelectionReturn } from '../../hooks/core/useGridCellSelection';
@@ -120,6 +121,7 @@ export interface GridViewportProps<R extends GridRowModel> {
120
121
  * empty state, the virtual centre rows and the aggregation footer.
121
122
  */
122
123
  export function GridViewport<R extends GridRowModel>(props: GridViewportProps<R>) {
124
+ const locale = useGridLocale();
123
125
  const {
124
126
  setViewportElement, handleScroll, ariaLabel, ariaRows, navigationColumnCount, rowSelectionEnabled,
125
127
  disableMultipleRowSelection, cellSelection, effectiveLoading, viewportTabIndex, setKeyboardMode,
@@ -165,7 +167,7 @@ export function GridViewport<R extends GridRowModel>(props: GridViewportProps<R>
165
167
  className="ogx__viewport"
166
168
  onScroll={handleScroll}
167
169
  role="grid"
168
- aria-label={ariaLabel || 'Data grid'}
170
+ aria-label={ariaLabel || locale.gridLabel}
169
171
  aria-rowcount={ariaRows.rowCount}
170
172
  aria-colcount={navigationColumnCount}
171
173
  aria-multiselectable={(rowSelectionEnabled && !disableMultipleRowSelection) || cellSelection.enabled}
@@ -208,7 +208,7 @@ export function GridVirtualRows<R extends GridRowModel>({
208
208
  onDrop={rowReorderHandlers.onDrop}
209
209
  isDragging={rowReorderHandlers.draggedRowId === id}
210
210
  isDragOver={rowReorderHandlers.dragOverRowId === id}
211
- editingCell={editingHandlers.editingCell}
211
+ editingCell={editingHandlers.editingCell?.id === id ? editingHandlers.editingCell : null}
212
212
  onEditStart={editingHandlers.startCellEdit}
213
213
  onEditStop={editingHandlers.stopCellEdit}
214
214
  onEditCellValueChange={editingHandlers.setEditCellValue}
@@ -228,7 +228,8 @@ export function GridVirtualRows<R extends GridRowModel>({
228
228
  {Array.from({ length: infiniteScrollSkeletonCount }).map((_, i) => (
229
229
  <Row<R>
230
230
  key={`__skeleton_${i}__`}
231
- row={{ id: `__skeleton_${i}__`, _isSkeleton: true } as unknown as R}
231
+ row={{ id: `__skeleton_${i}__` } as unknown as R}
232
+ skeleton
232
233
  columns={virtualColumns}
233
234
  rowIndex={unpinnedRowsLength + i}
234
235
  rowHeight={rowHeight}
@@ -0,0 +1,13 @@
1
+ import { createContext, useContext } from 'react';
2
+ import { resolveGridLocale, type ResolvedGridLocale } from '../../utils/gridLocale';
3
+
4
+ /**
5
+ * The grid's resolved `localeText`, provided once by `GridRoot`. Components rendered outside a grid
6
+ * (the exported `ColumnVisibilityPanel`, `GridToolbar`, `FilterPanel`, …) get the English default.
7
+ */
8
+ export const GridLocaleContext = createContext<ResolvedGridLocale>(resolveGridLocale(undefined));
9
+
10
+ /** The strings of the enclosing grid, English outside one. */
11
+ export function useGridLocale(): ResolvedGridLocale {
12
+ return useContext(GridLocaleContext);
13
+ }
@@ -7,8 +7,9 @@ import type {
7
7
  GridFilterOperator
8
8
  } from '../../types';
9
9
  import { filterValueToInputText, getOperatorsForType, NO_VALUE_OPERATORS } from '../../utils/filtering';
10
+ import { filterValueFromInputText, getFilterValueOptions, isSameFilterValue, matchesFilterValueOption } from '../../utils/filtering/filterInput';
10
11
  import { Input } from '../ui/Input';
11
-
12
+ import { useGridLocale } from '../DataGrid/gridLocaleContext';
12
13
 
13
14
  export interface FilterPanelProps {
14
15
  filterModel: GridFilterModel;
@@ -29,17 +30,10 @@ function getActiveFilter(items: readonly FilterEntry[], field: string): GridFilt
29
30
  return items.find((entry): entry is GridFilterItem => isFilterItem(entry) && entry.field === field);
30
31
  }
31
32
 
32
- function sameValue(a: unknown, b: unknown): boolean {
33
- if (Object.is(a, b)) return true;
34
- if (a instanceof Date && b instanceof Date) return a.getTime() === b.getTime();
35
- if (Array.isArray(a) && Array.isArray(b)) return a.length === b.length && a.every((v, i) => sameValue(v, b[i]));
36
- return false;
37
- }
38
-
39
33
  function sameEntry(a: FilterEntry, b: FilterEntry): boolean {
40
34
  if (a === b) return true;
41
35
  if (isFilterItem(a) && isFilterItem(b)) {
42
- return a.id === b.id && a.field === b.field && a.operator === b.operator && sameValue(a.value, b.value);
36
+ return a.id === b.id && a.field === b.field && a.operator === b.operator && isSameFilterValue(a.value, b.value);
43
37
  }
44
38
  if (!isFilterItem(a) && !isFilterItem(b)) {
45
39
  return a.id === b.id && a.logicOperator === b.logicOperator && sameEntries(a.items, b.items);
@@ -52,36 +46,11 @@ function sameEntries(a: readonly FilterEntry[], b: readonly FilterEntry[]): bool
52
46
  return a.length === b.length && a.every((entry, i) => sameEntry(entry, b[i]));
53
47
  }
54
48
 
55
- interface ValueOption {
56
- key: string;
57
- value: unknown;
58
- label: string;
59
- }
60
-
61
- function getValueOptions(col: GridColDef): ValueOption[] {
62
- return (col.valueOptions ?? []).map((option, index) => (
63
- option !== null && typeof option === 'object'
64
- ? { key: String(index), value: option.value, label: option.label }
65
- : { key: String(index), value: option, label: String(option) }
66
- ));
67
- }
68
-
69
- function matchesOption(option: ValueOption, value: unknown): boolean {
70
- if (Object.is(option.value, value)) return true;
71
- return value != null && String(option.value).toLowerCase() === String(value).toLowerCase();
72
- }
73
-
74
49
  /** Text shown in the value input for a committed value. Never used to emit anything. */
75
50
  function toInputText(value: unknown, col: GridColDef): string {
76
51
  return filterValueToInputText(value, col.type);
77
52
  }
78
53
 
79
- /** The item value for typed text. isAnyOf takes a comma-separated list. */
80
- function fromInputText(text: string, operator: GridFilterOperator): unknown {
81
- if (operator === 'isAnyOf') return text.split(',').map(part => part.trim()).filter(part => part !== '');
82
- return text;
83
- }
84
-
85
54
  /** Carry a value over to a newly picked operator: lists for isAnyOf, scalars for the rest. */
86
55
  function convertValueForOperator(value: unknown, operator: GridFilterOperator): unknown {
87
56
  if (NO_VALUE_OPERATORS.has(operator)) return undefined;
@@ -101,6 +70,7 @@ const FilterRow: React.FC<{
101
70
  onChange: (item: GridFilterItem | null) => void;
102
71
  }> = ({ col, item, resetToken, onChange }) => {
103
72
  const filterId = useId();
73
+ const locale = useGridLocale();
104
74
  // Memoized so React Compiler treats the list as frozen and can keep the callbacks below memoized.
105
75
  const typeOperators = useMemo(() => getOperatorsForType(col.type), [col.type]);
106
76
  const defaultOperator = typeOperators[0];
@@ -144,7 +114,7 @@ const FilterRow: React.FC<{
144
114
  const unchanged = current ? text === toInputText(current.value, colRef.current) : text.trim() === '';
145
115
  if (unchanged) return;
146
116
  const operator = current?.operator ?? defaultOperator;
147
- onChangeRef.current(buildItem(operator, fromInputText(text, operator)));
117
+ onChangeRef.current(buildItem(operator, filterValueFromInputText(text, operator)));
148
118
  }, [defaultOperator, buildItem]);
149
119
 
150
120
  // Text still waiting for the debounce; flushed when the panel closes (unmounts) meanwhile.
@@ -172,7 +142,7 @@ const FilterRow: React.FC<{
172
142
 
173
143
  const handleOperatorChange = (op: GridFilterOperator) => {
174
144
  // Keep what the user typed even if the debounce has not committed it yet.
175
- const value = draft !== null ? fromInputText(draft, op) : item?.value;
145
+ const value = draft !== null ? filterValueFromInputText(draft, op) : item?.value;
176
146
  setDraft(null);
177
147
  onChange(buildItem(op, convertValueForOperator(value, op)));
178
148
  };
@@ -183,9 +153,9 @@ const FilterRow: React.FC<{
183
153
  };
184
154
 
185
155
  const showValue = !NO_VALUE_OPERATORS.has(currentOperator);
186
- const valueOptions = getValueOptions(col);
156
+ const valueOptions = getFilterValueOptions(col);
187
157
  const valueClassName = `ogx-filter__value-input${isActive ? ' ogx-filter__value-input--active' : ''}`;
188
- const valueLabel = `Filter value for ${label}`;
158
+ const valueLabel = locale.filterPanelValueLabel(label);
189
159
 
190
160
  let valueControl: React.ReactNode = null;
191
161
  if (showValue && col.type === 'boolean') {
@@ -201,9 +171,9 @@ const FilterRow: React.FC<{
201
171
  }}
202
172
  aria-label={valueLabel}
203
173
  >
204
- <option value="">Any</option>
205
- <option value="true">true</option>
206
- <option value="false">false</option>
174
+ <option value="">{locale.filterPanelAny}</option>
175
+ <option value="true">{locale.filterPanelTrue}</option>
176
+ <option value="false">{locale.filterPanelFalse}</option>
207
177
  </select>
208
178
  );
209
179
  } else if (showValue && col.type === 'singleSelect' && valueOptions.length > 0 && currentOperator === 'isAnyOf') {
@@ -215,7 +185,7 @@ const FilterRow: React.FC<{
215
185
  className={`${valueClassName} ogx-filter__value-multiselect`}
216
186
  multiple
217
187
  size={Math.min(valueOptions.length, 4)}
218
- value={valueOptions.filter(o => selected.some(v => matchesOption(o, v))).map(o => o.key)}
188
+ value={valueOptions.filter(o => selected.some(v => matchesFilterValueOption(o, v))).map(o => o.key)}
219
189
  onChange={(e) => {
220
190
  const keys = new Set(Array.from(e.target.selectedOptions, option => option.value));
221
191
  onChange(buildItem('isAnyOf', valueOptions.filter(o => keys.has(o.key)).map(o => o.value)));
@@ -226,7 +196,7 @@ const FilterRow: React.FC<{
226
196
  </select>
227
197
  );
228
198
  } else if (showValue && col.type === 'singleSelect' && valueOptions.length > 0 && (currentOperator === 'is' || currentOperator === 'not')) {
229
- const current = valueOptions.find(o => matchesOption(o, item?.value));
199
+ const current = valueOptions.find(o => matchesFilterValueOption(o, item?.value));
230
200
  valueControl = (
231
201
  <select
232
202
  id={`${filterId}-val`}
@@ -239,7 +209,7 @@ const FilterRow: React.FC<{
239
209
  }}
240
210
  aria-label={valueLabel}
241
211
  >
242
- <option value="">Any</option>
212
+ <option value="">{locale.filterPanelAny}</option>
243
213
  {valueOptions.map(o => <option key={o.key} value={o.key}>{o.label}</option>)}
244
214
  </select>
245
215
  );
@@ -252,7 +222,7 @@ const FilterRow: React.FC<{
252
222
  type={col.type === 'date' ? 'date' : 'text'}
253
223
  value={draft ?? toInputText(item?.value, col)}
254
224
  onChange={(e) => setDraft(e.target.value)}
255
- placeholder={col.type === 'number' ? 'Number…' : 'Value…'}
225
+ placeholder={col.type === 'number' ? locale.filterPanelNumberPlaceholder : locale.filterPanelValuePlaceholder}
256
226
  aria-label={valueLabel}
257
227
  />
258
228
  );
@@ -271,10 +241,10 @@ const FilterRow: React.FC<{
271
241
  className={`ogx-filter__op-select${isActive ? ' ogx-filter__op-select--active' : ''}`}
272
242
  value={currentOperator}
273
243
  onChange={(e) => handleOperatorChange(e.target.value as GridFilterOperator)}
274
- aria-label={`Filter operator for ${label}`}
244
+ aria-label={locale.filterPanelOperatorLabel(label)}
275
245
  >
276
246
  {operators.map(op => (
277
- <option key={op} value={op}>{op}</option>
247
+ <option key={op} value={op}>{locale.filterPanelOperator(op)}</option>
278
248
  ))}
279
249
  </select>
280
250
 
@@ -286,8 +256,8 @@ const FilterRow: React.FC<{
286
256
  type="button"
287
257
  className="ogx-filter__row-clear"
288
258
  onClick={handleClear}
289
- aria-label={`Clear filter for ${label}`}
290
- title="Clear"
259
+ aria-label={locale.filterPanelClearLabel(label)}
260
+ title={locale.filterPanelClearTooltip}
291
261
  >
292
262
  ×
293
263
  </button>
@@ -300,6 +270,7 @@ const FilterRow: React.FC<{
300
270
  // ── Root panel ────────────────────────────────────────────────────────────────
301
271
 
302
272
  export const FilterPanel: React.FC<FilterPanelProps> = ({ filterModel, columns, onFilterModelChange }) => {
273
+ const locale = useGridLocale();
303
274
  const items = useMemo<FilterEntry[]>(() => filterModel.items ?? [], [filterModel.items]);
304
275
  const logicOperator = filterModel.logicOperator ?? 'and';
305
276
 
@@ -345,14 +316,14 @@ export const FilterPanel: React.FC<FilterPanelProps> = ({ filterModel, columns,
345
316
  <>
346
317
  {items.length > 1 && (
347
318
  <div className="ogx-filter__logic-bar">
348
- <span className="ogx-filter__logic-label">Match:</span>
319
+ <span className="ogx-filter__logic-label">{locale.filterPanelMatch}</span>
349
320
  <button
350
321
  type="button"
351
322
  className={`ogx-filter__logic-pill${logicOperator === 'and' ? ' ogx-filter__logic-pill--active' : ''}`}
352
323
  onClick={() => handleLogicChange('and')}
353
324
  aria-pressed={logicOperator === 'and'}
354
325
  >
355
- ALL (AND)
326
+ {locale.filterPanelAnd}
356
327
  </button>
357
328
  <button
358
329
  type="button"
@@ -360,21 +331,19 @@ export const FilterPanel: React.FC<FilterPanelProps> = ({ filterModel, columns,
360
331
  onClick={() => handleLogicChange('or')}
361
332
  aria-pressed={logicOperator === 'or'}
362
333
  >
363
- ANY (OR)
334
+ {locale.filterPanelOr}
364
335
  </button>
365
336
  </div>
366
337
  )}
367
338
 
368
339
  {notShownCount > 0 && (
369
340
  <div className="ogx-filter__hidden-note" role="note">
370
- {notShownCount === 1
371
- ? '1 more condition (a filter group or a second condition on a column) is applied and not shown here.'
372
- : `${notShownCount} more conditions (filter groups or further conditions on a column) are applied and not shown here.`}
341
+ {locale.filterPanelHiddenConditions(notShownCount)}
373
342
  </div>
374
343
  )}
375
344
 
376
345
  {filterableColumns.length === 0 ? (
377
- <div className="ogx-filter__empty">No filterable columns.</div>
346
+ <div className="ogx-filter__empty">{locale.filterPanelNoColumns}</div>
378
347
  ) : (
379
348
  filterableColumns.map(col => (
380
349
  <FilterRow
@@ -14,7 +14,7 @@
14
14
  font-weight: 600;
15
15
  text-transform: uppercase;
16
16
  letter-spacing: 0.06em;
17
- color: var(--ogx-color-gray-400, #94a3b8);
17
+ color: var(--ogx-overlay-section-label-text, var(--ogx-color-gray-400, #94a3b8));
18
18
  user-select: none;
19
19
  }
20
20
  .ogx-menu-item {
@@ -49,21 +49,21 @@
49
49
  display: flex;
50
50
  align-items: center;
51
51
  justify-content: center;
52
- color: var(--ogx-overlay-item-selected-text, #1976d2);
52
+ color: var(--ogx-overlay-item-indicator, var(--ogx-overlay-item-selected-text, #1976d2));
53
53
  }
54
54
  .ogx-menu-item__icon {
55
55
  flex: 0 0 16px;
56
56
  display: flex;
57
57
  align-items: center;
58
58
  justify-content: center;
59
- color: var(--ogx-color-gray-400, #94a3b8);
59
+ color: var(--ogx-overlay-icon, var(--ogx-color-gray-400, #94a3b8));
60
60
  transition: color 0.1s;
61
61
  }
62
62
  .ogx-menu-item--active .ogx-menu-item__icon {
63
- color: var(--ogx-color-primary, #1976d2);
63
+ color: var(--ogx-overlay-item-selected-icon, var(--ogx-color-primary, #1976d2));
64
64
  }
65
65
  .ogx-menu-divider {
66
66
  height: 1px;
67
- background: var(--ogx-overlay-border, #e2e8f0);
67
+ background: var(--ogx-overlay-divider, var(--ogx-overlay-border, #e2e8f0));
68
68
  margin: 4px 0;
69
69
  }
@@ -3,6 +3,7 @@ import ReactDOM from 'react-dom';
3
3
  import type { GridColDef, GridRowModel, GridSortDirection, GridColumnPinning } from '../../types';
4
4
  import { getViewportWidth, getViewportHeight } from '../../utils/viewport';
5
5
  import { isRtl } from '../../utils/direction';
6
+ import { useGridLocale } from '../DataGrid/gridLocaleContext';
6
7
 
7
8
 
8
9
  interface ColumnMenuProps {
@@ -123,6 +124,7 @@ const menuItemsOf = (menu: HTMLElement | null): HTMLElement[] =>
123
124
 
124
125
  export function ColumnMenu({ colDef, sortModel, onSort, onHide, onPin, pinnedColumns, onManageColumns, onClose, anchorEl }: ColumnMenuProps) {
125
126
  const menuRef = useRef<HTMLDivElement>(null);
127
+ const locale = useGridLocale();
126
128
 
127
129
  // Closes the menu; when it was left from the keyboard or by choosing an item, focus goes back
128
130
  // to the header cell that owns it (WAI-ARIA menu button pattern).
@@ -234,7 +236,7 @@ export function ColumnMenu({ colDef, sortModel, onSort, onHide, onPin, pinnedCol
234
236
  const colLabel = colDef.headerName || colDef.field;
235
237
 
236
238
  return ReactDOM.createPortal(
237
- <div ref={menuRef} className="ogx-column-menu" style={style} role="menu" aria-label={`Column options for ${colLabel}`} onKeyDown={handleMenuKeyDown}>
239
+ <div ref={menuRef} className="ogx-column-menu" style={style} role="menu" aria-label={locale.columnMenuLabel(colLabel)} onKeyDown={handleMenuKeyDown}>
238
240
 
239
241
  { }
240
242
  {onSort && colDef.sortable !== false && (
@@ -243,19 +245,19 @@ export function ColumnMenu({ colDef, sortModel, onSort, onHide, onPin, pinnedCol
243
245
  onClick={() => handleSort('asc')} role="menuitemradio" aria-checked={currentSort === 'asc'}>
244
246
  <span className="ogx-menu-item__indicator" aria-hidden="true">{currentSort === 'asc' ? <ActiveDotIcon /> : <InactiveDotIcon />}</span>
245
247
  <span className="ogx-menu-item__icon" aria-hidden="true"><AscIcon /></span>
246
- Sort Ascending
248
+ {locale.columnMenuSortAscending}
247
249
  </button>
248
250
  <button type="button" className={`ogx-menu-item${currentSort === 'desc' ? ' ogx-menu-item--active' : ''}`}
249
251
  onClick={() => handleSort('desc')} role="menuitemradio" aria-checked={currentSort === 'desc'}>
250
252
  <span className="ogx-menu-item__indicator" aria-hidden="true">{currentSort === 'desc' ? <ActiveDotIcon /> : <InactiveDotIcon />}</span>
251
253
  <span className="ogx-menu-item__icon" aria-hidden="true"><DescIcon /></span>
252
- Sort Descending
254
+ {locale.columnMenuSortDescending}
253
255
  </button>
254
256
  <button type="button" className={`ogx-menu-item${isUnsorted ? ' ogx-menu-item--active' : ''}`}
255
257
  onClick={() => handleSort(null)} role="menuitemradio" aria-checked={isUnsorted}>
256
258
  <span className="ogx-menu-item__indicator" aria-hidden="true">{isUnsorted ? <ActiveDotIcon /> : <InactiveDotIcon />}</span>
257
259
  <span className="ogx-menu-item__icon" aria-hidden="true"><UnsortIcon /></span>
258
- Unsort
260
+ {locale.columnMenuUnsort}
259
261
  </button>
260
262
  <div className="ogx-menu-divider" />
261
263
  </>
@@ -264,24 +266,24 @@ export function ColumnMenu({ colDef, sortModel, onSort, onHide, onPin, pinnedCol
264
266
  { }
265
267
  {canPin && (
266
268
  <>
267
- <div className="ogx-menu-section-label" aria-hidden="true">Pin column</div>
269
+ <div className="ogx-menu-section-label" aria-hidden="true">{locale.columnMenuPinColumn}</div>
268
270
  <button type="button" className={`ogx-menu-item${isPinnedScreenLeft ? ' ogx-menu-item--active' : ''}`}
269
- onClick={() => handlePin(screenLeftSide)} role="menuitemradio" aria-checked={isPinnedScreenLeft} aria-label="Pin to left">
271
+ onClick={() => handlePin(screenLeftSide)} role="menuitemradio" aria-checked={isPinnedScreenLeft} aria-label={locale.columnMenuPinLeftLabel}>
270
272
  <span className="ogx-menu-item__indicator" aria-hidden="true">{isPinnedScreenLeft ? <ActiveDotIcon /> : <InactiveDotIcon />}</span>
271
273
  <span className="ogx-menu-item__icon" aria-hidden="true"><PinLeftIcon /></span>
272
- Pin to Left
274
+ {locale.columnMenuPinLeft}
273
275
  </button>
274
276
  <button type="button" className={`ogx-menu-item${isPinnedScreenRight ? ' ogx-menu-item--active' : ''}`}
275
- onClick={() => handlePin(screenRightSide)} role="menuitemradio" aria-checked={isPinnedScreenRight} aria-label="Pin to right">
277
+ onClick={() => handlePin(screenRightSide)} role="menuitemradio" aria-checked={isPinnedScreenRight} aria-label={locale.columnMenuPinRightLabel}>
276
278
  <span className="ogx-menu-item__indicator" aria-hidden="true">{isPinnedScreenRight ? <ActiveDotIcon /> : <InactiveDotIcon />}</span>
277
279
  <span className="ogx-menu-item__icon" aria-hidden="true"><PinRightIcon /></span>
278
- Pin to Right
280
+ {locale.columnMenuPinRight}
279
281
  </button>
280
282
  <button type="button" className={`ogx-menu-item${isUnpinned ? ' ogx-menu-item--active' : ''}`}
281
- onClick={() => handlePin(null)} role="menuitemradio" aria-checked={isUnpinned} aria-label="Unpin column">
283
+ onClick={() => handlePin(null)} role="menuitemradio" aria-checked={isUnpinned} aria-label={locale.columnMenuUnpinLabel}>
282
284
  <span className="ogx-menu-item__indicator" aria-hidden="true">{isUnpinned ? <ActiveDotIcon /> : <InactiveDotIcon />}</span>
283
285
  <span className="ogx-menu-item__icon" aria-hidden="true"><UnpinIcon /></span>
284
- Unpin
286
+ {locale.columnMenuUnpin}
285
287
  </button>
286
288
  <div className="ogx-menu-divider" />
287
289
  </>
@@ -289,19 +291,19 @@ export function ColumnMenu({ colDef, sortModel, onSort, onHide, onPin, pinnedCol
289
291
 
290
292
  {canHide && (
291
293
  <>
292
- <button type="button" className="ogx-menu-item" onClick={handleHide} role="menuitem" aria-label={`Hide ${colLabel} column`}>
294
+ <button type="button" className="ogx-menu-item" onClick={handleHide} role="menuitem" aria-label={locale.columnMenuHideColumnLabel(colLabel)}>
293
295
  <span className="ogx-menu-item__indicator" aria-hidden="true" />
294
296
  <span className="ogx-menu-item__icon" aria-hidden="true"><HideIcon /></span>
295
- Hide Column
297
+ {locale.columnMenuHideColumn}
296
298
  </button>
297
299
  <div className="ogx-menu-divider" />
298
300
  </>
299
301
  )}
300
302
 
301
- <button type="button" className="ogx-menu-item" onClick={() => { onManageColumns?.(); close(false); }} role="menuitem" aria-label="Manage all columns">
303
+ <button type="button" className="ogx-menu-item" onClick={() => { onManageColumns?.(); close(false); }} role="menuitem" aria-label={locale.columnMenuManageColumnsLabel}>
302
304
  <span className="ogx-menu-item__indicator" aria-hidden="true" />
303
305
  <span className="ogx-menu-item__icon" aria-hidden="true"><ManageColumnsIcon /></span>
304
- Manage columns
306
+ {locale.columnMenuManageColumns}
305
307
  </button>
306
308
  </div>,
307
309
  (anchorEl?.closest('.ogx-theme-provider') || document.body)
@@ -207,12 +207,7 @@
207
207
  background-color: var(--ogx-grid-header-hover-background);
208
208
  z-index: 6;
209
209
  }
210
- .ogx__header-cell--focus-visible {
211
- outline: 2px solid var(--ogx-grid-cell-focus-border, var(--ogx-color-primary));
212
- outline-offset: -2px;
213
- background-color: var(--ogx-grid-header-hover-background);
214
- z-index: 6;
215
- }
210
+
216
211
  .ogx__header-cell:focus, .ogx__header-cell:focus-visible {
217
212
  outline: none !important;
218
213
  }
@@ -241,8 +236,4 @@
241
236
  background: var(--ogx-grid-header-hover-background);
242
237
  color: var(--ogx-grid-header-text);
243
238
  }
244
- .ogx__menu-icon-btn--active {
245
- opacity: 1;
246
- background: var(--ogx-color-primary-light);
247
- color: var(--ogx-color-primary);
248
- }
239
+