@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.
- package/CHANGELOG.md +47 -0
- package/dist/ai.cjs.map +1 -1
- package/dist/ai.es.js.map +1 -1
- package/dist/index.d.ts +342 -3
- package/dist/opengridx.css +1 -1
- package/dist/opengridx.es.js +6952 -6697
- package/dist/opengridx.es.js.map +1 -1
- package/dist/opengridx.umd.js +17 -17
- package/dist/opengridx.umd.js.map +1 -1
- package/docs/API_REFERENCE.md +121 -4
- package/docs/customization/theming.md +11 -2
- package/docs/features/ai-toolkit.md +1 -1
- package/lib/components/AiAssistant/GridAiAssistantPanel.css +2 -2
- package/lib/components/AiAssistant/GridAiAssistantPanel.tsx +16 -14
- package/lib/components/Cell/Cell.css +1 -3
- package/lib/components/Cell/CellErrorBoundary.tsx +17 -10
- package/lib/components/CellSelectionStats/CellSelectionStats.tsx +5 -3
- package/lib/components/ColumnGroupHeader/ColumnGroupHeader.css +1 -2
- package/lib/components/ColumnResizeHandle/ColumnResizeHandle.tsx +3 -1
- package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.tsx +6 -4
- package/lib/components/DataGrid/DataGrid.css +1 -28
- package/lib/components/DataGrid/GridAggregationFooter.tsx +3 -1
- package/lib/components/DataGrid/GridErrorOverlay.tsx +4 -2
- package/lib/components/DataGrid/GridListView.tsx +7 -5
- package/lib/components/DataGrid/GridLiveRegion.tsx +5 -3
- package/lib/components/DataGrid/GridLoadingOverlay.tsx +3 -1
- package/lib/components/DataGrid/GridPinnedRows.tsx +1 -1
- package/lib/components/DataGrid/GridRoot.tsx +8 -1
- package/lib/components/DataGrid/GridStandaloneColumnPanel.tsx +3 -1
- package/lib/components/DataGrid/GridViewport.tsx +3 -1
- package/lib/components/DataGrid/GridVirtualRows.tsx +3 -2
- package/lib/components/DataGrid/gridLocaleContext.ts +13 -0
- package/lib/components/FilterPanel/FilterPanel.tsx +25 -56
- package/lib/components/Header/ColumnMenu.css +5 -5
- package/lib/components/Header/ColumnMenu.tsx +17 -15
- package/lib/components/Header/Header.css +2 -11
- package/lib/components/Header/Header.tsx +8 -6
- package/lib/components/HeaderFilter/HeaderFilterCell.tsx +6 -37
- package/lib/components/HeaderFilter/HeaderFilterRow.tsx +3 -1
- package/lib/components/ListView/ListView.css +0 -16
- package/lib/components/ListView/ListViewRow.tsx +3 -1
- package/lib/components/Pagination/Pagination.tsx +15 -18
- package/lib/components/QuickFilter/QuickFilter.tsx +5 -2
- package/lib/components/Row/Row.tsx +9 -4
- package/lib/components/Toolbar/GlobalSearch.tsx +6 -3
- package/lib/components/Toolbar/GridToolbar.tsx +35 -29
- package/lib/components/Toolbar/PivotPanel.tsx +26 -24
- package/lib/components/ui/Checkbox.css +5 -5
- package/lib/components/ui/DragHandleIcon.tsx +3 -1
- package/lib/components/ui/ExpandIcon.css +3 -3
- package/lib/components/ui/ExpandIcon.tsx +3 -1
- package/lib/hooks/core/useGridRowInteractions.ts +13 -3
- package/lib/hooks/core/useGridToolbarStack.ts +2 -2
- package/lib/hooks/core/useGridVirtualization.ts +40 -25
- package/lib/hooks/core/useShallowStableValue.ts +29 -0
- package/lib/hooks/features/useGridAiAssistant.ts +52 -22
- package/lib/hooks/features/useGridSpanning.ts +1 -1
- package/lib/styles/base/reset.css +6 -7
- package/lib/theme/DataGridThemeProvider.tsx +9 -0
- package/lib/theme/__baseline__/default-theme.darwin.chromium.json +1 -1503
- package/lib/theme/__baseline__/default-theme.darwin.firefox.json +1 -1503
- package/lib/theme/__baseline__/default-theme.darwin.webkit.json +1 -1503
- package/lib/theme/types.ts +18 -0
- package/lib/types/locale.ts +344 -0
- package/lib/types/rows.ts +0 -2
- package/lib/utils/cellSelection.ts +0 -2
- package/lib/utils/editing/index.ts +1 -1
- package/lib/utils/filtering/filterInput.ts +45 -0
- package/lib/utils/filtering/index.ts +0 -28
- package/lib/utils/gridLocale.ts +241 -0
- package/lib/utils/pinning/index.ts +0 -107
- package/lib/utils/sorting/index.ts +42 -33
- package/lib/utils/values.ts +5 -1
- package/llms.txt +9 -3
- package/package.json +1 -1
- 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 ||
|
|
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}
|
|
202
|
-
{pagination ? ` ·
|
|
203
|
-
{selectedRowIds.size > 0 ? ` · ${selectedRowIds.size}
|
|
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=
|
|
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 ?
|
|
28
|
-
{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
|
-
?
|
|
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=
|
|
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=
|
|
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 ||
|
|
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}__
|
|
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 &&
|
|
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,
|
|
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 ?
|
|
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 =
|
|
156
|
+
const valueOptions = getFilterValueOptions(col);
|
|
187
157
|
const valueClassName = `ogx-filter__value-input${isActive ? ' ogx-filter__value-input--active' : ''}`;
|
|
188
|
-
const valueLabel =
|
|
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="">
|
|
205
|
-
<option value="true">
|
|
206
|
-
<option value="false">
|
|
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 =>
|
|
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 =>
|
|
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="">
|
|
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' ?
|
|
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={
|
|
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={
|
|
290
|
-
title=
|
|
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">
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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">
|
|
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={
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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">
|
|
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=
|
|
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
|
-
|
|
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=
|
|
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
|
-
|
|
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=
|
|
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
|
-
|
|
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={
|
|
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
|
-
|
|
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=
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
245
|
-
opacity: 1;
|
|
246
|
-
background: var(--ogx-color-primary-light);
|
|
247
|
-
color: var(--ogx-color-primary);
|
|
248
|
-
}
|
|
239
|
+
|