@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.
- package/CHANGELOG.md +36 -0
- package/README.md +2 -0
- package/dist/ai.cjs +4 -2
- package/dist/ai.cjs.map +1 -1
- package/dist/ai.d.ts +2 -2
- package/dist/ai.es.js +4 -2
- package/dist/ai.es.js.map +1 -1
- package/dist/index.d.ts +32 -0
- package/dist/opengridx.css +1 -1
- package/dist/opengridx.es.js +6573 -6390
- package/dist/opengridx.es.js.map +1 -1
- package/dist/opengridx.umd.js +16 -16
- package/dist/opengridx.umd.js.map +1 -1
- package/docs/API_REFERENCE.md +14 -1
- package/docs/README.md +2 -0
- package/docs/features/ai-toolkit.md +2 -2
- package/docs/features/export-guide.md +2 -0
- package/docs/features/header-filters.md +34 -0
- package/docs/features/keyboard-navigation.md +2 -0
- package/docs/features/nextjs.md +241 -0
- package/docs/features/pdf-export.md +2 -0
- package/docs/features/pinning.md +2 -0
- package/docs/features/rtl.md +58 -0
- package/docs/performance.md +86 -118
- package/docs/roadmap.md +4 -2
- package/lib/ai/promptHandler.ts +12 -8
- package/lib/ai/validate.ts +5 -2
- package/lib/components/AiAssistant/GridAiAssistantPanel.css +1 -1
- package/lib/components/Cell/Cell.css +29 -9
- package/lib/components/Cell/Cell.tsx +2 -2
- package/lib/components/ColumnGroupHeader/ColumnGroupHeader.css +2 -2
- package/lib/components/ColumnResizeHandle/ColumnResizeHandle.css +3 -3
- package/lib/components/ColumnResizeHandle/ColumnResizeHandle.tsx +18 -2
- package/lib/components/DataGrid/DataGrid.css +12 -6
- package/lib/components/DataGrid/DataGrid.tsx +1 -0
- package/lib/components/DataGrid/GridAggregationFooter.tsx +3 -3
- package/lib/components/DataGrid/GridListView.tsx +3 -1
- package/lib/components/Header/ColumnMenu.css +1 -1
- package/lib/components/Header/ColumnMenu.tsx +14 -6
- package/lib/components/Header/Header.css +18 -10
- package/lib/components/Header/Header.tsx +16 -9
- package/lib/components/Header/headerColumnStyle.ts +3 -2
- package/lib/components/HeaderFilter/HeaderFilter.css +8 -2
- package/lib/components/HeaderFilter/HeaderFilterCell.tsx +21 -18
- package/lib/components/HeaderFilter/HeaderFilterOperatorMenu.tsx +5 -4
- package/lib/components/HeaderFilter/HeaderFilterRow.tsx +9 -3
- package/lib/components/ListView/ListViewRow.tsx +1 -1
- package/lib/components/Pagination/Pagination.css +4 -0
- package/lib/components/Pagination/Pagination.tsx +4 -4
- package/lib/components/Row/Row.tsx +3 -3
- package/lib/components/SkeletonRow/SkeletonRow.tsx +2 -2
- package/lib/components/Toolbar/GridToolbar.tsx +8 -28
- package/lib/components/Toolbar/PivotPanel.tsx +4 -7
- package/lib/components/ui/ExpandIcon.css +13 -0
- package/lib/components/ui/ExpandIcon.tsx +2 -4
- package/lib/hooks/core/useGridCellSelection.ts +20 -5
- package/lib/hooks/core/useGridColumns.tsx +2 -2
- package/lib/hooks/core/useGridKeyboardNavigation.ts +7 -4
- package/lib/hooks/core/useGridScrollSync.ts +28 -5
- package/lib/hooks/core/useLayout.ts +13 -6
- package/lib/hooks/useRowGrouping.ts +9 -9
- package/lib/types/index.ts +30 -0
- package/lib/utils/aggregation/index.ts +56 -1
- package/lib/utils/direction.ts +52 -0
- package/lib/utils/export/exportToExcelAdvanced.ts +9 -2
- package/lib/utils/export/exportToPdf.ts +2 -1
- package/lib/utils/filtering/index.ts +58 -6
- package/lib/utils/headerFilterLocale.ts +37 -0
- package/lib/utils/scroll/index.ts +5 -3
- package/lib/utils/sorting/index.ts +141 -26
- package/lib/utils/values.ts +10 -6
- package/llms.txt +4 -2
- 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-
|
|
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-
|
|
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
|
-
|
|
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-
|
|
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
|
-
|
|
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:
|
|
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.
|
|
120
|
-
if (pinnedPosition === 'right' && pinnedOffset !== undefined) style.
|
|
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
|
-
|
|
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) {
|
|
@@ -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${
|
|
261
|
-
onClick={() => handlePin(
|
|
262
|
-
<span className="ogx-menu-item__indicator" aria-hidden="true">{
|
|
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${
|
|
267
|
-
onClick={() => handlePin(
|
|
268
|
-
<span className="ogx-menu-item__indicator" aria-hidden="true">{
|
|
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-
|
|
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
|
-
|
|
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:
|
|
109
|
+
text-align: end;
|
|
110
110
|
}
|
|
111
111
|
.ogx__header-cell--align-right .ogx__menu-icon-btn {
|
|
112
|
-
margin-
|
|
113
|
-
margin-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
309
|
+
style.insetInlineStart = pinnedPositions[cell.fields[0]];
|
|
304
310
|
} else if (cell.pinned === 'right') {
|
|
305
311
|
style.position = 'sticky';
|
|
306
|
-
style.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
25
|
-
|
|
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-
|
|
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:
|
|
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=
|
|
247
|
+
title={locale.customTooltip}
|
|
246
248
|
>
|
|
247
|
-
|
|
249
|
+
{locale.custom}
|
|
248
250
|
</button>
|
|
249
251
|
) : (
|
|
250
|
-
<span className="ogx__header-filter-custom" title=
|
|
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">{
|
|
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={
|
|
266
|
+
aria-label={locale.valueLabel(label)}
|
|
265
267
|
>
|
|
266
|
-
<option value="">
|
|
267
|
-
<option value="true">
|
|
268
|
-
<option value="false">
|
|
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={
|
|
285
|
+
aria-label={locale.valueLabel(label)}
|
|
284
286
|
>
|
|
285
|
-
<option value="">
|
|
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={
|
|
303
|
+
aria-label={locale.valueLabel(label)}
|
|
302
304
|
/>
|
|
303
305
|
);
|
|
304
306
|
}
|
|
305
|
-
const operatorLabel =
|
|
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={
|
|
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={
|
|
328
|
-
title=
|
|
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={
|
|
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 {
|
|
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={
|
|
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
|
-
{
|
|
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
|
-
|
|
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={{
|
|
110
|
+
style={{ paddingInlineStart: (rowMeta.treeDepth ?? 0) * 16 }}
|
|
111
111
|
onClick={(e) => e.stopPropagation()}
|
|
112
112
|
onDoubleClick={(e) => e.stopPropagation()}
|
|
113
113
|
>
|