@opencorestack/opengridx 3.5.1 → 3.6.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 (73) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +2 -0
  3. package/dist/ai.cjs +4 -2
  4. package/dist/ai.cjs.map +1 -1
  5. package/dist/ai.d.ts +2 -2
  6. package/dist/ai.es.js +4 -2
  7. package/dist/ai.es.js.map +1 -1
  8. package/dist/index.d.ts +32 -0
  9. package/dist/opengridx.css +1 -1
  10. package/dist/opengridx.es.js +6573 -6390
  11. package/dist/opengridx.es.js.map +1 -1
  12. package/dist/opengridx.umd.js +16 -16
  13. package/dist/opengridx.umd.js.map +1 -1
  14. package/docs/API_REFERENCE.md +14 -1
  15. package/docs/README.md +2 -0
  16. package/docs/features/ai-toolkit.md +2 -2
  17. package/docs/features/export-guide.md +2 -0
  18. package/docs/features/header-filters.md +34 -0
  19. package/docs/features/keyboard-navigation.md +2 -0
  20. package/docs/features/nextjs.md +241 -0
  21. package/docs/features/pdf-export.md +2 -0
  22. package/docs/features/pinning.md +2 -0
  23. package/docs/features/rtl.md +58 -0
  24. package/docs/performance.md +86 -118
  25. package/docs/roadmap.md +4 -2
  26. package/lib/ai/promptHandler.ts +12 -8
  27. package/lib/ai/validate.ts +5 -2
  28. package/lib/components/AiAssistant/GridAiAssistantPanel.css +1 -1
  29. package/lib/components/Cell/Cell.css +29 -9
  30. package/lib/components/Cell/Cell.tsx +2 -2
  31. package/lib/components/ColumnGroupHeader/ColumnGroupHeader.css +2 -2
  32. package/lib/components/ColumnResizeHandle/ColumnResizeHandle.css +3 -3
  33. package/lib/components/ColumnResizeHandle/ColumnResizeHandle.tsx +18 -2
  34. package/lib/components/DataGrid/DataGrid.css +12 -6
  35. package/lib/components/DataGrid/DataGrid.tsx +1 -0
  36. package/lib/components/DataGrid/GridAggregationFooter.tsx +3 -3
  37. package/lib/components/DataGrid/GridListView.tsx +3 -1
  38. package/lib/components/Header/ColumnMenu.css +1 -1
  39. package/lib/components/Header/ColumnMenu.tsx +14 -6
  40. package/lib/components/Header/Header.css +18 -10
  41. package/lib/components/Header/Header.tsx +16 -9
  42. package/lib/components/Header/headerColumnStyle.ts +3 -2
  43. package/lib/components/HeaderFilter/HeaderFilter.css +8 -2
  44. package/lib/components/HeaderFilter/HeaderFilterCell.tsx +21 -18
  45. package/lib/components/HeaderFilter/HeaderFilterOperatorMenu.tsx +5 -4
  46. package/lib/components/HeaderFilter/HeaderFilterRow.tsx +9 -3
  47. package/lib/components/ListView/ListViewRow.tsx +1 -1
  48. package/lib/components/Pagination/Pagination.css +4 -0
  49. package/lib/components/Pagination/Pagination.tsx +4 -4
  50. package/lib/components/Row/Row.tsx +3 -3
  51. package/lib/components/SkeletonRow/SkeletonRow.tsx +2 -2
  52. package/lib/components/Toolbar/GridToolbar.tsx +8 -28
  53. package/lib/components/Toolbar/PivotPanel.tsx +4 -7
  54. package/lib/components/ui/ExpandIcon.css +13 -0
  55. package/lib/components/ui/ExpandIcon.tsx +2 -4
  56. package/lib/hooks/core/useGridCellSelection.ts +20 -5
  57. package/lib/hooks/core/useGridColumns.tsx +2 -2
  58. package/lib/hooks/core/useGridKeyboardNavigation.ts +7 -4
  59. package/lib/hooks/core/useGridScrollSync.ts +28 -5
  60. package/lib/hooks/core/useLayout.ts +13 -6
  61. package/lib/hooks/useRowGrouping.ts +9 -9
  62. package/lib/types/index.ts +30 -0
  63. package/lib/utils/aggregation/index.ts +56 -1
  64. package/lib/utils/direction.ts +52 -0
  65. package/lib/utils/export/exportToExcelAdvanced.ts +9 -2
  66. package/lib/utils/export/exportToPdf.ts +2 -1
  67. package/lib/utils/filtering/index.ts +58 -6
  68. package/lib/utils/headerFilterLocale.ts +37 -0
  69. package/lib/utils/scroll/index.ts +5 -3
  70. package/lib/utils/sorting/index.ts +141 -26
  71. package/lib/utils/values.ts +10 -6
  72. package/llms.txt +4 -2
  73. package/package.json +1 -1
@@ -243,7 +243,7 @@
243
243
  justify-content: center;
244
244
  overflow: hidden;
245
245
  box-sizing: border-box;
246
- border-right: 1px solid var(--ogx-grid-border-color);
246
+ border-inline-end: 1px solid var(--ogx-grid-border-color);
247
247
  position: relative;
248
248
  }
249
249
  .ogx__aggregation-cell--pinned-left,
@@ -265,7 +265,7 @@
265
265
  background: var(--ogx-grid-header-background);
266
266
  }
267
267
  .ogx__aggregation-cell--pinned-right {
268
- border-left: 1px solid var(--ogx-grid-border-color);
268
+ border-inline-start: 1px solid var(--ogx-grid-border-color);
269
269
  }
270
270
  .ogx__aggregation-cell--pinned-left-last {
271
271
  overflow: visible;
@@ -274,26 +274,32 @@
274
274
  content: '';
275
275
  position: absolute;
276
276
  top: 0;
277
- right: -10px;
277
+ inset-inline-end: -10px;
278
278
  bottom: 0;
279
279
  width: 10px;
280
280
  pointer-events: none;
281
281
  box-shadow: inset 10px 0 8px -8px rgba(0, 0, 0, 0.1);
282
282
  }
283
283
  .ogx__aggregation-cell--pinned-right-first {
284
- margin-left: auto;
284
+ margin-inline-start: auto;
285
285
  overflow: visible;
286
286
  }
287
287
  .ogx__aggregation-cell--pinned-right-first::before {
288
288
  content: '';
289
289
  position: absolute;
290
290
  top: 0;
291
- left: -10px;
291
+ inset-inline-start: -10px;
292
292
  bottom: 0;
293
293
  width: 10px;
294
294
  pointer-events: none;
295
295
  box-shadow: inset -10px 0 8px -8px rgba(0, 0, 0, 0.1);
296
296
  }
297
+ .ogx__aggregation-cell--pinned-left-last:dir(rtl)::after {
298
+ box-shadow: inset -10px 0 8px -8px rgba(0, 0, 0, 0.1);
299
+ }
300
+ .ogx__aggregation-cell--pinned-right-first:dir(rtl)::before {
301
+ box-shadow: inset 10px 0 8px -8px rgba(0, 0, 0, 0.1);
302
+ }
297
303
  .ogx__aggregation-label {
298
304
  font-size: 0.7em;
299
305
  text-transform: uppercase;
@@ -321,7 +327,7 @@
321
327
  }
322
328
  .ogx__group-cell-content {
323
329
  font-weight: 500;
324
- text-align: left;
330
+ text-align: start;
325
331
  display: flex;
326
332
  align-items: center;
327
333
  gap: 4px;
@@ -991,6 +991,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
991
991
  filterModel={filterModel}
992
992
  onFilterModelChange={handleFilterModelChange}
993
993
  onOpenFilterPanel={columnsPanel.openFiltersPanel}
994
+ localeText={localeText}
994
995
  />
995
996
 
996
997
  <GridPinnedRows<R>
@@ -68,7 +68,7 @@ export function GridAggregationFooter({
68
68
  <div
69
69
  key={key}
70
70
  className={`ogx__aggregation-spacer${pinned ? ' ogx__aggregation-spacer--pinned' : ''}`}
71
- style={pinned ? { left } : undefined}
71
+ style={pinned ? { insetInlineStart: left } : undefined}
72
72
  aria-hidden="true"
73
73
  />
74
74
  );
@@ -116,8 +116,8 @@ export function GridAggregationFooter({
116
116
  maxWidth: colWidth,
117
117
  textAlign: (col.align as React.CSSProperties['textAlign']) || 'left',
118
118
  };
119
- if (pinnedPosition === 'left' && pinnedOffset !== undefined) style.left = pinnedOffset;
120
- if (pinnedPosition === 'right' && pinnedOffset !== undefined) style.right = pinnedOffset;
119
+ if (pinnedPosition === 'left' && pinnedOffset !== undefined) style.insetInlineStart = pinnedOffset;
120
+ if (pinnedPosition === 'right' && pinnedOffset !== undefined) style.insetInlineEnd = pinnedOffset;
121
121
 
122
122
  return (
123
123
  <div
@@ -1,4 +1,5 @@
1
1
  import React from 'react';
2
+ import { isRtl, logicalArrowKey } from '../../utils/direction';
2
3
  import { Pagination } from '../Pagination/Pagination';
3
4
  import { ListViewRow } from '../ListView/ListViewRow';
4
5
  import { getPageCount, normalizePageSize } from '../../utils/pagination';
@@ -138,7 +139,8 @@ export function GridListView<R extends GridRowModel>({
138
139
  const canToggle = Boolean(meta?.hasChildren && onToggleExpansion);
139
140
  if (event.altKey && (event.key === 'ArrowRight' || event.key === 'ArrowLeft')) {
140
141
  event.preventDefault();
141
- if (canToggle && (event.key === 'ArrowRight') !== (meta?.isExpanded === true)) onToggleExpansion?.(id);
142
+ const expands = logicalArrowKey(event.key, isRtl(event.currentTarget)) === 'ArrowRight';
143
+ if (canToggle && expands !== (meta?.isExpanded === true)) onToggleExpansion?.(id);
142
144
  return;
143
145
  }
144
146
  switch (event.key) {
@@ -28,7 +28,7 @@
28
28
  font-family: var(--ogx-font-family, system-ui, sans-serif);
29
29
  font-size: var(--ogx-font-size-xs);
30
30
  color: var(--ogx-overlay-text, #334155);
31
- text-align: left;
31
+ text-align: start;
32
32
  gap: 6px;
33
33
  transition: background 0.1s;
34
34
  }
@@ -2,6 +2,7 @@ import React, { useRef, useEffect, useState } from 'react';
2
2
  import ReactDOM from 'react-dom';
3
3
  import type { GridColDef, GridRowModel, GridSortDirection, GridColumnPinning } from '../../types';
4
4
  import { getViewportWidth, getViewportHeight } from '../../utils/viewport';
5
+ import { isRtl } from '../../utils/direction';
5
6
 
6
7
 
7
8
  interface ColumnMenuProps {
@@ -222,6 +223,13 @@ export function ColumnMenu({ colDef, sortModel, onSort, onHide, onPin, pinnedCol
222
223
  const isPinnedLeft = pinnedColumns?.left?.includes(colDef.field) ?? false;
223
224
  const isPinnedRight = pinnedColumns?.right?.includes(colDef.field) ?? false;
224
225
  const isUnpinned = !isPinnedLeft && !isPinnedRight;
226
+ // `pinnedColumns.left` / `.right` are the inline start / end: in a right-to-left grid the start
227
+ // is on the right, so the item labelled by screen side pins to the opposite list.
228
+ const rtl = isRtl(anchorEl);
229
+ const screenLeftSide = rtl ? 'right' : 'left';
230
+ const screenRightSide = rtl ? 'left' : 'right';
231
+ const isPinnedScreenLeft = rtl ? isPinnedRight : isPinnedLeft;
232
+ const isPinnedScreenRight = rtl ? isPinnedLeft : isPinnedRight;
225
233
 
226
234
  const colLabel = colDef.headerName || colDef.field;
227
235
 
@@ -257,15 +265,15 @@ export function ColumnMenu({ colDef, sortModel, onSort, onHide, onPin, pinnedCol
257
265
  {canPin && (
258
266
  <>
259
267
  <div className="ogx-menu-section-label" aria-hidden="true">Pin column</div>
260
- <button type="button" className={`ogx-menu-item${isPinnedLeft ? ' ogx-menu-item--active' : ''}`}
261
- onClick={() => handlePin('left')} role="menuitemradio" aria-checked={isPinnedLeft} aria-label="Pin to left">
262
- <span className="ogx-menu-item__indicator" aria-hidden="true">{isPinnedLeft ? <ActiveDotIcon /> : <InactiveDotIcon />}</span>
268
+ <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">
270
+ <span className="ogx-menu-item__indicator" aria-hidden="true">{isPinnedScreenLeft ? <ActiveDotIcon /> : <InactiveDotIcon />}</span>
263
271
  <span className="ogx-menu-item__icon" aria-hidden="true"><PinLeftIcon /></span>
264
272
  Pin to Left
265
273
  </button>
266
- <button type="button" className={`ogx-menu-item${isPinnedRight ? ' ogx-menu-item--active' : ''}`}
267
- onClick={() => handlePin('right')} role="menuitemradio" aria-checked={isPinnedRight} aria-label="Pin to right">
268
- <span className="ogx-menu-item__indicator" aria-hidden="true">{isPinnedRight ? <ActiveDotIcon /> : <InactiveDotIcon />}</span>
274
+ <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">
276
+ <span className="ogx-menu-item__indicator" aria-hidden="true">{isPinnedScreenRight ? <ActiveDotIcon /> : <InactiveDotIcon />}</span>
269
277
  <span className="ogx-menu-item__icon" aria-hidden="true"><PinRightIcon /></span>
270
278
  Pin to Right
271
279
  </button>
@@ -20,7 +20,7 @@
20
20
  width: 48px;
21
21
  min-width: 48px;
22
22
  flex-shrink: 0;
23
- border-right: 1px solid transparent;
23
+ border-inline-end: 1px solid transparent;
24
24
  }
25
25
  .ogx__header-cell {
26
26
  display: flex;
@@ -39,7 +39,7 @@
39
39
  .ogx__header-cell:not(:last-child)::after {
40
40
  content: '';
41
41
  position: absolute;
42
- right: 0;
42
+ inset-inline-end: 0;
43
43
  top: 25%;
44
44
  height: 50%;
45
45
  width: 1px;
@@ -106,11 +106,11 @@
106
106
  }
107
107
  .ogx__header-cell--align-right .ogx__header-cell-title-wrapper {
108
108
  align-items: flex-end;
109
- text-align: right;
109
+ text-align: end;
110
110
  }
111
111
  .ogx__header-cell--align-right .ogx__menu-icon-btn {
112
- margin-left: 0;
113
- margin-right: auto;
112
+ margin-inline-start: 0;
113
+ margin-inline-end: auto;
114
114
  }
115
115
  .ogx__header-cell--sortable {
116
116
  cursor: pointer;
@@ -131,7 +131,7 @@
131
131
  justify-content: center;
132
132
  width: 16px;
133
133
  height: 16px;
134
- margin-left: 4px;
134
+ margin-inline-start: 4px;
135
135
  flex-shrink: 0;
136
136
  font-size: 14px;
137
137
  line-height: 1;
@@ -161,11 +161,19 @@
161
161
  box-shadow: var(--ogx-grid-pinned-left-shadow, 4px 0 12px -4px rgba(0, 0, 0, 0.05));
162
162
  }
163
163
  .ogx__header-cell--pinned-right-first {
164
- margin-left: auto;
164
+ margin-inline-start: auto;
165
165
  }
166
166
  .ogx__header-cell--pinned-right {
167
167
  box-shadow: var(--ogx-grid-pinned-right-shadow, -4px 0 12px -4px rgba(0, 0, 0, 0.05));
168
168
  }
169
+ /* Right to left: the start-pinned section is on the right and casts its shadow leftwards, the
170
+ end-pinned one on the left casts it rightwards (the mirrored shadow tokens). */
171
+ .ogx__header-cell--pinned-left:dir(rtl) {
172
+ box-shadow: var(--ogx-grid-pinned-right-shadow, -4px 0 12px -4px rgba(0, 0, 0, 0.05));
173
+ }
174
+ .ogx__header-cell--pinned-right:dir(rtl) {
175
+ box-shadow: var(--ogx-grid-pinned-left-shadow, 4px 0 12px -4px rgba(0, 0, 0, 0.05));
176
+ }
169
177
  .ogx__header-cell--pinned::after {
170
178
  display: none;
171
179
  }
@@ -178,7 +186,7 @@
178
186
  display: flex;
179
187
  align-items: center;
180
188
  justify-content: center;
181
- margin-right: 8px;
189
+ margin-inline-end: 8px;
182
190
  color: var(--ogx-color-primary);
183
191
  animation: ogx-pulse 1.5s infinite ease-in-out;
184
192
  }
@@ -188,7 +196,7 @@
188
196
  100% { transform: scale(1); }
189
197
  }
190
198
  .ogx__header-cell--drag-over {
191
- border-left: 2px solid var(--ogx-color-primary) !important;
199
+ border-inline-start: 2px solid var(--ogx-color-primary) !important;
192
200
  background: var(--ogx-color-primary-light);
193
201
  }
194
202
  /* Show ring on header cells during keyboard navigation */
@@ -222,7 +230,7 @@
222
230
  cursor: pointer;
223
231
  opacity: 0;
224
232
  transition: opacity 0.15s ease, background 0.15s ease, color 0.15s ease;
225
- margin-left: auto;
233
+ margin-inline-start: auto;
226
234
  }
227
235
  .ogx__header-cell:hover .ogx__menu-icon-btn,
228
236
  .ogx__header-cell--sorted .ogx__menu-icon-btn {
@@ -2,7 +2,7 @@ import React, { useId } from 'react';
2
2
  import { Checkbox } from '../ui/Checkbox';
3
3
  import { ColumnResizeHandle } from '../ColumnResizeHandle/ColumnResizeHandle';
4
4
  import { clampResizeWidth } from '../ColumnResizeHandle/clampResizeWidth';
5
- import type { GridColDef, GridRowModel, GridSortDirection, GridColumnPinning, GridAggregationModel, GridColumnGroupingModel, GridFilterModel } from '../../types';
5
+ import type { GridColDef, GridRowModel, GridSortDirection, GridColumnPinning, GridAggregationModel, GridColumnGroupingModel, GridFilterModel, GridLocaleText } from '../../types';
6
6
  import { isColumnPinned, calculatePinnedPositions, getPinnedEdgeFields } from '../../utils/pinning';
7
7
  import { getRenderedColumnWidth } from '../../utils/columnWidth';
8
8
  import { buildColumnGroupRow, getColumnGroupDepth, getColumnGroupPaths } from '../../utils/columnGroups';
@@ -10,6 +10,7 @@ import { ColumnMenu } from './ColumnMenu';
10
10
  import { getHeaderLineClamp, shouldWrapHeaderText } from '../../utils/headerWrap';
11
11
  import { getHeaderColumnStyle } from './headerColumnStyle';
12
12
  import { HeaderFilterRow } from '../HeaderFilter/HeaderFilterRow';
13
+ import { isRtl, logicalArrowKey } from '../../utils/direction';
13
14
 
14
15
 
15
16
  function MenuIcon() {
@@ -87,6 +88,8 @@ export interface HeaderProps<R extends GridRowModel = GridRowModel> {
87
88
  onFilterModelChange?: (model: GridFilterModel) => void;
88
89
  /** Opens the filter panel; a header filter cell showing "Custom filter" calls it. */
89
90
  onOpenFilterPanel?: () => void;
91
+ /** Translations of the header filter row's strings. */
92
+ localeText?: GridLocaleText;
90
93
  }
91
94
 
92
95
  export function Header<R extends GridRowModel = GridRowModel>(props: HeaderProps<R>) {
@@ -129,6 +132,7 @@ export function Header<R extends GridRowModel = GridRowModel>(props: HeaderProps
129
132
  filterModel,
130
133
  onFilterModelChange,
131
134
  onOpenFilterPanel,
135
+ localeText,
132
136
  } = props;
133
137
 
134
138
  const selectAllId = useId();
@@ -153,7 +157,9 @@ export function Header<R extends GridRowModel = GridRowModel>(props: HeaderProps
153
157
  event.preventDefault();
154
158
  event.stopPropagation();
155
159
  const width = getRenderedColumnWidth(colDef, columnWidths);
156
- const step = (event.shiftKey ? 50 : 10) * (event.key === 'ArrowRight' ? 1 : -1);
160
+ // ArrowRight widens: towards the column's end edge, on the left in a right-to-left grid.
161
+ const key = logicalArrowKey(event.key, isRtl(event.currentTarget));
162
+ const step = (event.shiftKey ? 50 : 10) * (key === 'ArrowRight' ? 1 : -1);
157
163
  onColumnResize(colDef.field, clampResizeWidth(width + step, width, colDef.minWidth, colDef.maxWidth));
158
164
  return;
159
165
  }
@@ -285,7 +291,7 @@ export function Header<R extends GridRowModel = GridRowModel>(props: HeaderProps
285
291
  minWidth: prefixWidth,
286
292
  flexShrink: 0,
287
293
  position: isPrefixPinned ? 'sticky' : undefined,
288
- left: isPrefixPinned ? 0 : undefined,
294
+ insetInlineStart: isPrefixPinned ? 0 : undefined,
289
295
  }}
290
296
  aria-hidden="true"
291
297
  />
@@ -300,12 +306,12 @@ export function Header<R extends GridRowModel = GridRowModel>(props: HeaderProps
300
306
  };
301
307
  if (cell.pinned === 'left') {
302
308
  style.position = 'sticky';
303
- style.left = pinnedPositions[cell.fields[0]];
309
+ style.insetInlineStart = pinnedPositions[cell.fields[0]];
304
310
  } else if (cell.pinned === 'right') {
305
311
  style.position = 'sticky';
306
- style.right = pinnedPositions[cell.fields[cell.fields.length - 1]];
312
+ style.insetInlineEnd = pinnedPositions[cell.fields[cell.fields.length - 1]];
307
313
  // Free space goes before the right-pinned section, as in the column header row.
308
- if (cells[cellIndex - 1]?.pinned !== 'right') style.marginLeft = 'auto';
314
+ if (cells[cellIndex - 1]?.pinned !== 'right') style.marginInlineStart = 'auto';
309
315
  }
310
316
  if (!cell.isGroup) {
311
317
  return (
@@ -351,7 +357,7 @@ export function Header<R extends GridRowModel = GridRowModel>(props: HeaderProps
351
357
  minWidth: 48,
352
358
  maxWidth: 48,
353
359
  position: 'sticky',
354
- left: 0,
360
+ insetInlineStart: 0,
355
361
  }}
356
362
  />
357
363
  )}
@@ -370,7 +376,7 @@ export function Header<R extends GridRowModel = GridRowModel>(props: HeaderProps
370
376
  minWidth: 48,
371
377
  maxWidth: 48,
372
378
  position: pinExpandColumn ? 'sticky' : undefined,
373
- left: pinExpandColumn ? (rowReordering ? 48 : 0) : undefined,
379
+ insetInlineStart: pinExpandColumn ? (rowReordering ? 48 : 0) : undefined,
374
380
  }}
375
381
  >
376
382
  { }
@@ -387,7 +393,7 @@ export function Header<R extends GridRowModel = GridRowModel>(props: HeaderProps
387
393
  data-field="__checkbox_col__"
388
394
  style={{
389
395
  position: pinCheckboxColumn ? 'sticky' : undefined,
390
- left: pinCheckboxColumn ? ((rowReordering ? 48 : 0) + (hasDetailPanel && pinExpandColumn ? 48 : 0)) : undefined,
396
+ insetInlineStart: pinCheckboxColumn ? ((rowReordering ? 48 : 0) + (hasDetailPanel && pinExpandColumn ? 48 : 0)) : undefined,
391
397
 
392
398
  }}
393
399
  >
@@ -583,6 +589,7 @@ export function Header<R extends GridRowModel = GridRowModel>(props: HeaderProps
583
589
  filterModel={filterModel}
584
590
  onFilterModelChange={onFilterModelChange}
585
591
  onOpenFilterPanel={onOpenFilterPanel}
592
+ localeText={localeText}
586
593
  focusedField={focusedCell?.id === null && focusedCell.headerFilter ? focusedCell.field : null}
587
594
  />
588
595
  )}
@@ -21,8 +21,9 @@ export function getHeaderColumnStyle<R extends GridRowModel>(
21
21
  boxSizing: 'border-box',
22
22
  position: pinnedPosition ? 'sticky' : 'relative',
23
23
  };
24
- if (pinnedPosition === 'left') style.left = pinnedOffset;
25
- else if (pinnedPosition === 'right') style.right = pinnedOffset;
24
+ // Logical sides: `left` pinning sticks to the inline start (the right edge in a right-to-left grid).
25
+ if (pinnedPosition === 'left') style.insetInlineStart = pinnedOffset;
26
+ else if (pinnedPosition === 'right') style.insetInlineEnd = pinnedOffset;
26
27
  return style;
27
28
  }
28
29
 
@@ -39,9 +39,15 @@
39
39
  box-shadow: var(--ogx-grid-pinned-left-shadow, 4px 0 12px -4px rgba(0, 0, 0, 0.05));
40
40
  }
41
41
  .ogx__header-filter-cell--pinned-right-first {
42
- margin-left: auto;
42
+ margin-inline-start: auto;
43
43
  box-shadow: var(--ogx-grid-pinned-right-shadow, -4px 0 12px -4px rgba(0, 0, 0, 0.05));
44
44
  }
45
+ .ogx__header-filter-cell--pinned-left-last:dir(rtl) {
46
+ box-shadow: var(--ogx-grid-pinned-right-shadow, -4px 0 12px -4px rgba(0, 0, 0, 0.05));
47
+ }
48
+ .ogx__header-filter-cell--pinned-right-first:dir(rtl) {
49
+ box-shadow: var(--ogx-grid-pinned-left-shadow, 4px 0 12px -4px rgba(0, 0, 0, 0.05));
50
+ }
45
51
 
46
52
  /* Operator button: a compact symbol for the current operator; opens the operator menu. */
47
53
  .ogx__header-filter-operator {
@@ -152,7 +158,7 @@
152
158
  color: var(--ogx-color-primary);
153
159
  font: inherit;
154
160
  font-style: italic;
155
- text-align: left;
161
+ text-align: start;
156
162
  }
157
163
  button.ogx__header-filter-custom {
158
164
  cursor: pointer;
@@ -3,7 +3,6 @@ import type { GridColDef, GridFilterItem, GridFilterOperator } from '../../types
3
3
  import { filterValueToInputText, NO_VALUE_OPERATORS } from '../../utils/filtering';
4
4
  import {
5
5
  HEADER_FILTER_DEBOUNCE_MS,
6
- HEADER_FILTER_OPERATOR_LABELS,
7
6
  HEADER_FILTER_OPERATOR_SYMBOLS,
8
7
  buildHeaderFilterItem,
9
8
  convertHeaderFilterValue,
@@ -13,6 +12,7 @@ import {
13
12
  } from '../../utils/headerFilters';
14
13
  import { Input } from '../ui/Input';
15
14
  import { HeaderFilterOperatorMenu } from './HeaderFilterOperatorMenu';
15
+ import type { HeaderFilterLocale } from '../../utils/headerFilterLocale';
16
16
 
17
17
  export interface HeaderFilterCellProps {
18
18
  colDef: GridColDef;
@@ -25,6 +25,8 @@ export interface HeaderFilterCellProps {
25
25
  /** Stores (or removes, with null) the cell's own filter item. Must keep its identity. */
26
26
  onItemChange: (field: string, item: GridFilterItem | null) => void;
27
27
  onOpenFilterPanel?: () => void;
28
+ /** The row's strings (`resolveHeaderFilterLocale`). */
29
+ locale: HeaderFilterLocale;
28
30
  }
29
31
 
30
32
  interface ValueOption {
@@ -75,7 +77,7 @@ function ClearIcon() {
75
77
  }
76
78
 
77
79
  /** One cell of the header filter row: operator button, value control and clear button. */
78
- export function HeaderFilterCell({ colDef, state, style, className, ariaColIndex, onItemChange, onOpenFilterPanel }: HeaderFilterCellProps) {
80
+ export function HeaderFilterCell({ colDef, state, style, className, ariaColIndex, onItemChange, onOpenFilterPanel, locale }: HeaderFilterCellProps) {
79
81
  const label = colDef.headerName || colDef.field;
80
82
  const item = state.kind === 'editable' ? state.item : undefined;
81
83
  const operator: GridFilterOperator = item?.operator ?? getHeaderFilterDefaultOperator(colDef);
@@ -242,17 +244,17 @@ export function HeaderFilterCell({ colDef, state, style, className, ariaColIndex
242
244
  className="ogx__header-filter-custom"
243
245
  tabIndex={-1}
244
246
  onClick={onOpenFilterPanel}
245
- title="Set in the filter panel"
247
+ title={locale.customTooltip}
246
248
  >
247
- Custom filter
249
+ {locale.custom}
248
250
  </button>
249
251
  ) : (
250
- <span className="ogx__header-filter-custom" title="Set in the filter panel">Custom filter</span>
252
+ <span className="ogx__header-filter-custom" title={locale.customTooltip}>{locale.custom}</span>
251
253
  );
252
254
  } else {
253
255
  let valueControl: React.ReactNode;
254
256
  if (!showsValue) {
255
- valueControl = <span className="ogx__header-filter-operator-label">{HEADER_FILTER_OPERATOR_LABELS[operator] ?? operator}</span>;
257
+ valueControl = <span className="ogx__header-filter-operator-label">{locale.operator(operator)}</span>;
256
258
  } else if (colDef.type === 'boolean') {
257
259
  valueControl = (
258
260
  <select
@@ -261,11 +263,11 @@ export function HeaderFilterCell({ colDef, state, style, className, ariaColIndex
261
263
  tabIndex={-1}
262
264
  value={item?.value == null || item.value === '' ? '' : String(item.value)}
263
265
  onChange={(e) => setValue(e.target.value === '' ? '' : e.target.value === 'true')}
264
- aria-label={`Filter value for ${label}`}
266
+ aria-label={locale.valueLabel(label)}
265
267
  >
266
- <option value="">Any</option>
267
- <option value="true">Yes</option>
268
- <option value="false">No</option>
268
+ <option value="">{locale.any}</option>
269
+ <option value="true">{locale.yes}</option>
270
+ <option value="false">{locale.no}</option>
269
271
  </select>
270
272
  );
271
273
  } else if (usesSelect) {
@@ -280,9 +282,9 @@ export function HeaderFilterCell({ colDef, state, style, className, ariaColIndex
280
282
  const option = valueOptions.find(o => o.key === e.target.value);
281
283
  setValue(convertHeaderFilterValue(option ? option.value : '', operator));
282
284
  }}
283
- aria-label={`Filter value for ${label}`}
285
+ aria-label={locale.valueLabel(label)}
284
286
  >
285
- <option value="">Any</option>
287
+ <option value="">{locale.any}</option>
286
288
  {valueOptions.map(o => <option key={o.key} value={o.key}>{o.label}</option>)}
287
289
  </select>
288
290
  );
@@ -298,11 +300,11 @@ export function HeaderFilterCell({ colDef, state, style, className, ariaColIndex
298
300
  autoComplete="off"
299
301
  value={draft ?? filterValueToInputText(item?.value, colDef.type)}
300
302
  onChange={(e) => setDraft(e.target.value)}
301
- aria-label={`Filter value for ${label}`}
303
+ aria-label={locale.valueLabel(label)}
302
304
  />
303
305
  );
304
306
  }
305
- const operatorLabel = HEADER_FILTER_OPERATOR_LABELS[operator] ?? operator;
307
+ const operatorLabel = locale.operator(operator);
306
308
  content = (
307
309
  <>
308
310
  <button
@@ -312,7 +314,7 @@ export function HeaderFilterCell({ colDef, state, style, className, ariaColIndex
312
314
  tabIndex={-1}
313
315
  aria-haspopup="menu"
314
316
  aria-expanded={menuAnchor !== null}
315
- aria-label={`Filter operator for ${label}: ${operatorLabel}`}
317
+ aria-label={locale.operatorButtonLabel(label, operatorLabel)}
316
318
  title={operatorLabel}
317
319
  onClick={() => (menuAnchor ? setMenuAnchor(null) : openMenu())}
318
320
  >
@@ -324,8 +326,8 @@ export function HeaderFilterCell({ colDef, state, style, className, ariaColIndex
324
326
  type="button"
325
327
  className="ogx__header-filter-clear"
326
328
  tabIndex={-1}
327
- aria-label={`Clear filter for ${label}`}
328
- title="Clear"
329
+ aria-label={locale.clearLabel(label)}
330
+ title={locale.clearTooltip}
329
331
  onClick={() => { clear(); focusCell(); }}
330
332
  >
331
333
  <ClearIcon />
@@ -335,6 +337,7 @@ export function HeaderFilterCell({ colDef, state, style, className, ariaColIndex
335
337
  <HeaderFilterOperatorMenu
336
338
  anchorEl={menuAnchor}
337
339
  columnLabel={label}
340
+ locale={locale}
338
341
  operators={getHeaderFilterOperators(colDef)}
339
342
  current={operator}
340
343
  onSelect={selectOperator}
@@ -358,7 +361,7 @@ export function HeaderFilterCell({ colDef, state, style, className, ariaColIndex
358
361
  className={classNames}
359
362
  style={style}
360
363
  role="columnheader"
361
- aria-label={`Filter ${label}`}
364
+ aria-label={locale.cellLabel(label)}
362
365
  aria-colindex={ariaColIndex}
363
366
  data-field={colDef.field}
364
367
  tabIndex={-1}
@@ -1,7 +1,7 @@
1
1
  import React, { useEffect, useLayoutEffect, useRef, useState } from 'react';
2
2
  import ReactDOM from 'react-dom';
3
3
  import type { GridFilterOperator } from '../../types';
4
- import { HEADER_FILTER_OPERATOR_LABELS } from '../../utils/headerFilters';
4
+ import type { HeaderFilterLocale } from '../../utils/headerFilterLocale';
5
5
  import { getViewportHeight, getViewportWidth } from '../../utils/viewport';
6
6
 
7
7
  export interface HeaderFilterOperatorMenuProps {
@@ -13,6 +13,7 @@ export interface HeaderFilterOperatorMenuProps {
13
13
  onSelect: (operator: GridFilterOperator) => void;
14
14
  /** `restoreFocus`: the menu was left from the keyboard or by choosing an item. */
15
15
  onClose: (restoreFocus: boolean) => void;
16
+ locale: HeaderFilterLocale;
16
17
  }
17
18
 
18
19
  const HIDDEN_STYLE: React.CSSProperties = { position: 'fixed', zIndex: 1300, top: 0, left: 0, visibility: 'hidden' };
@@ -21,7 +22,7 @@ const itemsOf = (menu: HTMLElement | null): HTMLElement[] =>
21
22
  menu ? Array.from(menu.querySelectorAll<HTMLElement>('[role="menuitemradio"]')) : [];
22
23
 
23
24
  /** The operator menu of a header filter cell: a menu of radio items, portalled out of the header. */
24
- export function HeaderFilterOperatorMenu({ anchorEl, columnLabel, operators, current, onSelect, onClose }: HeaderFilterOperatorMenuProps) {
25
+ export function HeaderFilterOperatorMenu({ anchorEl, columnLabel, operators, current, onSelect, onClose, locale }: HeaderFilterOperatorMenuProps) {
25
26
  const menuRef = useRef<HTMLDivElement>(null);
26
27
  const [style, setStyle] = useState<React.CSSProperties>(HIDDEN_STYLE);
27
28
 
@@ -102,7 +103,7 @@ export function HeaderFilterOperatorMenu({ anchorEl, columnLabel, operators, cur
102
103
  className="ogx-column-menu ogx__header-filter-menu"
103
104
  style={style}
104
105
  role="menu"
105
- aria-label={`Filter operator for ${columnLabel}`}
106
+ aria-label={locale.operatorMenuLabel(columnLabel)}
106
107
  onKeyDown={handleKeyDown}
107
108
  >
108
109
  {operators.map(op => (
@@ -115,7 +116,7 @@ export function HeaderFilterOperatorMenu({ anchorEl, columnLabel, operators, cur
115
116
  className={`ogx-menu-item${op === current ? ' ogx-menu-item--active' : ''}`}
116
117
  onClick={() => onSelect(op)}
117
118
  >
118
- {HEADER_FILTER_OPERATOR_LABELS[op] ?? op}
119
+ {locale.operator(op)}
119
120
  </button>
120
121
  ))}
121
122
  </div>,
@@ -1,7 +1,8 @@
1
1
  import { useCallback, useLayoutEffect, useRef } from 'react';
2
- import type { GridColDef, GridColumnPinning, GridFilterItem, GridFilterModel } from '../../types';
2
+ import type { GridColDef, GridColumnPinning, GridFilterItem, GridFilterModel, GridLocaleText } from '../../types';
3
3
  import { isColumnPinned, SYSTEM_COLUMN_WIDTH } from '../../utils/pinning';
4
4
  import { CHECKBOX_FIELD, EXPAND_FIELD, REORDER_FIELD } from '../../utils/focus';
5
+ import { resolveHeaderFilterLocale } from '../../utils/headerFilterLocale';
5
6
  import { getHeaderFilterState, hasHeaderFilter, setHeaderFilterItem } from '../../utils/headerFilters';
6
7
  import { getHeaderColumnStyle } from '../Header/headerColumnStyle';
7
8
  import { HeaderFilterCell } from './HeaderFilterCell';
@@ -27,6 +28,8 @@ export interface HeaderFilterRowProps {
27
28
  onOpenFilterPanel?: () => void;
28
29
  /** Field of the focused filter cell, or null. */
29
30
  focusedField: string | null;
31
+ /** Translations of the row's strings. */
32
+ localeText?: GridLocaleText;
30
33
  }
31
34
 
32
35
  function SystemFilterCell({ field, ariaColIndex, sticky, left, focused }: {
@@ -49,7 +52,7 @@ function SystemFilterCell({ field, ariaColIndex, sticky, left, focused }: {
49
52
  minWidth: SYSTEM_COLUMN_WIDTH,
50
53
  maxWidth: SYSTEM_COLUMN_WIDTH,
51
54
  position: sticky ? 'sticky' : undefined,
52
- left: sticky ? left : undefined,
55
+ insetInlineStart: sticky ? left : undefined,
53
56
  }}
54
57
  role="columnheader"
55
58
  aria-colindex={ariaColIndex}
@@ -68,7 +71,7 @@ export function HeaderFilterRow(props: HeaderFilterRowProps) {
68
71
  const {
69
72
  columns, columnWidths, pinnedColumns, pinnedPositions, pinnedEdges, rowReordering, hasDetailPanel,
70
73
  checkboxSelection, pinExpandColumn, pinCheckboxColumn, columnIndexMap, ariaRowIndex, filterModel,
71
- onFilterModelChange, onOpenFilterPanel, focusedField,
74
+ onFilterModelChange, onOpenFilterPanel, focusedField, localeText,
72
75
  } = props;
73
76
 
74
77
  // Cells commit from timers and unmount cleanups: they read the newest model, and two commits
@@ -86,6 +89,8 @@ export function HeaderFilterRow(props: HeaderFilterRowProps) {
86
89
  emit(next);
87
90
  }, []);
88
91
 
92
+ const locale = resolveHeaderFilterLocale(localeText);
93
+
89
94
  const reorderColIndex = rowReordering ? 1 : 0;
90
95
  const expandColIndex = hasDetailPanel ? reorderColIndex + 1 : reorderColIndex;
91
96
  const checkboxColIndex = checkboxSelection ? expandColIndex + 1 : expandColIndex;
@@ -149,6 +154,7 @@ export function HeaderFilterRow(props: HeaderFilterRowProps) {
149
154
  ariaColIndex={ariaColIndex}
150
155
  onItemChange={handleItemChange}
151
156
  onOpenFilterPanel={onOpenFilterPanel}
157
+ locale={locale}
152
158
  />
153
159
  );
154
160
  })}
@@ -107,7 +107,7 @@ export function ListViewRow<R extends GridRowModel = GridRowModel>({
107
107
  // (not a Tab stop: Enter or Alt+Arrow toggle the focused row).
108
108
  <div
109
109
  className="ogx-list-view__expand"
110
- style={{ paddingLeft: (rowMeta.treeDepth ?? 0) * 16 }}
110
+ style={{ paddingInlineStart: (rowMeta.treeDepth ?? 0) * 16 }}
111
111
  onClick={(e) => e.stopPropagation()}
112
112
  onDoubleClick={(e) => e.stopPropagation()}
113
113
  >
@@ -89,3 +89,7 @@
89
89
  order: 2;
90
90
  }
91
91
  }
92
+ /* First / previous / next / last arrows point the way the pages run: mirrored right to left. */
93
+ .ogx-pagination__icon:dir(rtl) {
94
+ transform: scaleX(-1);
95
+ }