@opencorestack/opengridx 2.0.4 → 3.0.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 +408 -3
- package/README.md +93 -72
- package/dist/index.d.ts +611 -121
- package/dist/opengridx.css +1 -1
- package/dist/opengridx.es.js +7992 -5650
- package/dist/opengridx.es.js.map +1 -1
- package/dist/opengridx.umd.js +17 -23
- package/dist/opengridx.umd.js.map +1 -1
- package/docs/API_REFERENCE.md +348 -161
- package/docs/README.md +6 -1
- package/docs/architecture/datagrid-orchestration.md +81 -0
- package/docs/architecture/grid-row-meta.md +102 -0
- package/docs/architecture/use-grid-columns.md +128 -0
- package/docs/architecture/use-grid-controlled-state.md +157 -0
- package/docs/architecture/use-grid-row-pipeline.md +120 -0
- package/docs/architecture/use-grid-scroll-sync.md +111 -0
- package/docs/architecture/use-grid-state-snapshot.md +90 -0
- package/docs/architecture/use-grid-virtualization.md +115 -0
- package/docs/architecture/use-grid-visible-rows.md +85 -0
- package/docs/assets/banner-alt.png +0 -0
- package/docs/assets/banner.png +0 -0
- package/docs/assets/hero.png +0 -0
- package/docs/assets/logo.png +0 -0
- package/docs/components/aggregation-footer.md +15 -10
- package/docs/components/cell.md +16 -6
- package/docs/components/column-group-header.md +55 -28
- package/docs/components/column-resize.md +17 -13
- package/docs/components/column-visibility.md +45 -22
- package/docs/components/datagrid.md +14 -12
- package/docs/components/empty-state.md +2 -1
- package/docs/components/error-overlay.md +12 -7
- package/docs/components/filter-panel.md +28 -8
- package/docs/components/header.md +28 -9
- package/docs/components/pagination.md +14 -8
- package/docs/components/row.md +21 -5
- package/docs/components/toolbar.md +27 -9
- package/docs/components/tooltip.md +16 -7
- package/docs/customization/slots-api.md +100 -36
- package/docs/customization/theming.md +66 -18
- package/docs/features/aggregation-pivot.md +75 -9
- package/docs/features/cell-spanning.md +80 -125
- package/docs/features/clipboard.md +41 -16
- package/docs/features/custom-pagination.md +38 -18
- package/docs/features/data-source.md +36 -13
- package/docs/features/editing-reordering.md +125 -16
- package/docs/features/export-guide.md +147 -41
- package/docs/features/filtering.md +44 -6
- package/docs/features/infinite-scroll.md +54 -46
- package/docs/features/keyboard-navigation.md +83 -0
- package/docs/features/list-view.md +58 -5
- package/docs/features/loading-states.md +18 -20
- package/docs/features/master-detail.md +18 -5
- package/docs/features/pdf-export.md +56 -19
- package/docs/features/pinning.md +23 -5
- package/docs/features/selection.md +50 -10
- package/docs/features/sorting-pagination.md +66 -14
- package/docs/features/state-persistence.md +78 -10
- package/docs/features/toolbar-customization.md +26 -17
- package/docs/features/tree-data-grouping.md +44 -9
- package/docs/features/virtualization.md +64 -15
- package/docs/migration/v1-to-v2.md +115 -0
- package/docs/migration/v2-to-v3.md +1222 -0
- package/docs/performance.md +124 -0
- package/docs/roadmap.md +16 -10
- package/lib/components/Cell/Cell.css +13 -1
- package/lib/components/Cell/Cell.tsx +130 -57
- package/lib/components/Cell/CellErrorBoundary.tsx +12 -6
- package/lib/components/Cell/GridEditInputCell.tsx +114 -41
- package/lib/components/ColumnGroupHeader/ColumnGroupHeader.css +7 -1
- package/lib/components/ColumnResizeHandle/ColumnResizeHandle.css +11 -0
- package/lib/components/ColumnResizeHandle/ColumnResizeHandle.tsx +104 -38
- package/lib/components/ColumnResizeHandle/clampResizeWidth.ts +13 -0
- package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.css +6 -0
- package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.tsx +21 -13
- package/lib/components/DataGrid/DataGrid.css +84 -12
- package/lib/components/DataGrid/DataGrid.tsx +606 -872
- package/lib/components/DataGrid/GridAggregationFooter.tsx +53 -17
- package/lib/components/DataGrid/GridEmptyState.tsx +7 -1
- package/lib/components/DataGrid/GridErrorOverlay.tsx +15 -12
- package/lib/components/DataGrid/GridListView.tsx +166 -23
- package/lib/components/DataGrid/GridLiveRegion.tsx +31 -0
- package/lib/components/DataGrid/GridLoadingOverlay.tsx +27 -0
- package/lib/components/DataGrid/GridPaginationArea.tsx +44 -0
- package/lib/components/DataGrid/GridPinnedRows.tsx +65 -11
- package/lib/components/DataGrid/GridStandaloneColumnPanel.tsx +40 -38
- package/lib/components/DataGrid/GridToolbarSlot.tsx +51 -0
- package/lib/components/DataGrid/GridVirtualRows.tsx +48 -25
- package/lib/components/DetailPanel/DetailPanel.css +0 -6
- package/lib/components/DetailPanel/DetailPanel.tsx +34 -5
- package/lib/components/DetailPanel/DetailPanelContent.tsx +17 -0
- package/lib/components/FilterPanel/FilterPanel.css +13 -2
- package/lib/components/FilterPanel/FilterPanel.tsx +272 -99
- package/lib/components/Header/ColumnMenu.tsx +91 -39
- package/lib/components/Header/Header.css +3 -3
- package/lib/components/Header/Header.tsx +179 -160
- package/lib/components/ListView/ListView.css +25 -0
- package/lib/components/ListView/ListViewRow.tsx +79 -10
- package/lib/components/Pagination/Pagination.tsx +15 -7
- package/lib/components/Row/Row.tsx +185 -75
- package/lib/components/Toolbar/GlobalSearch.css +6 -3
- package/lib/components/Toolbar/GlobalSearch.tsx +26 -27
- package/lib/components/Toolbar/GridToolbar.css +12 -11
- package/lib/components/Toolbar/GridToolbar.tsx +133 -89
- package/lib/components/Toolbar/PivotPanel.tsx +89 -49
- package/lib/components/Tooltip/Tooltip.css +20 -0
- package/lib/components/Tooltip/Tooltip.tsx +68 -35
- package/lib/components/ui/Button.tsx +3 -0
- package/lib/components/ui/Checkbox.tsx +2 -1
- package/lib/components/ui/ExpandIcon.css +0 -9
- package/lib/components/ui/ExpandIcon.tsx +4 -1
- package/lib/hooks/core/gridApiPlaceholder.ts +34 -0
- package/lib/hooks/core/gridToolbarHostContext.ts +14 -0
- package/lib/hooks/core/useDataGrid.ts +115 -173
- package/lib/hooks/core/useGridAggregationApi.ts +72 -0
- package/lib/hooks/core/useGridApiMethods.ts +115 -0
- package/lib/hooks/core/useGridApiRef.ts +15 -3
- package/lib/hooks/core/useGridApiRefBinding.ts +18 -0
- package/lib/hooks/core/useGridAriaRows.ts +38 -0
- package/lib/hooks/core/useGridClipboardApi.ts +38 -0
- package/lib/hooks/core/useGridColumnLookup.ts +30 -0
- package/lib/hooks/core/useGridColumns.tsx +284 -76
- package/lib/hooks/core/useGridColumnsPanel.ts +77 -0
- package/lib/hooks/core/useGridControlledState.ts +99 -6
- package/lib/hooks/core/useGridDetailPanel.ts +51 -0
- package/lib/hooks/core/useGridDevWarnings.ts +123 -0
- package/lib/hooks/core/useGridFocusHandlers.ts +50 -0
- package/lib/hooks/core/useGridGroupingColumn.ts +62 -0
- package/lib/hooks/core/useGridHeaderHandlers.ts +92 -0
- package/lib/hooks/core/useGridHierarchy.ts +81 -0
- package/lib/hooks/core/useGridKeyboardNavigation.ts +452 -250
- package/lib/hooks/core/useGridLiveRowSelection.ts +67 -0
- package/lib/hooks/core/useGridPageCorrection.ts +55 -0
- package/lib/hooks/core/useGridRowIdOf.ts +32 -0
- package/lib/hooks/core/useGridRowInteractions.ts +81 -0
- package/lib/hooks/core/useGridRowPipeline.ts +69 -39
- package/lib/hooks/core/useGridRowSelection.ts +91 -0
- package/lib/hooks/core/useGridScrollSync.ts +80 -32
- package/lib/hooks/core/useGridScrollToIndexesApi.ts +45 -0
- package/lib/hooks/core/useGridStateSnapshot.ts +40 -5
- package/lib/hooks/core/useGridThemeDimensions.ts +49 -0
- package/lib/hooks/core/useGridToolbarProps.ts +97 -0
- package/lib/hooks/core/useGridViewport.ts +38 -0
- package/lib/hooks/core/useGridViewportSize.ts +26 -0
- package/lib/hooks/core/useGridVisibleRows.ts +20 -7
- package/lib/hooks/core/useLayout.ts +215 -195
- package/lib/hooks/core/useRowsScrollEndLatch.ts +41 -0
- package/lib/hooks/core/useStableColumns.ts +34 -0
- package/lib/hooks/features/useAggregation.ts +86 -90
- package/lib/hooks/features/useColumnGroupReorderGuard.ts +34 -0
- package/lib/hooks/features/useDetailPanelHeights.ts +29 -0
- package/lib/hooks/features/useGridClipboard.ts +133 -73
- package/lib/hooks/features/useGridDataSource.ts +354 -128
- package/lib/hooks/features/useGridEditing.ts +211 -51
- package/lib/hooks/features/useGridPivot.ts +92 -0
- package/lib/hooks/features/useGridSpanRenderWindow.ts +104 -0
- package/lib/hooks/features/useGridSpanning.ts +410 -306
- package/lib/hooks/features/usePivot.ts +0 -0
- package/lib/hooks/features/useServerTreeChildren.ts +50 -0
- package/lib/hooks/useColumnReorder.ts +38 -44
- package/lib/hooks/useRowGrouping.ts +335 -214
- package/lib/hooks/useRowReorder.ts +62 -31
- package/lib/hooks/useTreeData.ts +330 -262
- package/lib/index.ts +30 -2
- package/lib/state/index.ts +1 -0
- package/lib/state/types.ts +7 -1
- package/lib/state/useGridStateStorage.ts +88 -29
- package/lib/theme/DataGridThemeProvider.tsx +143 -38
- package/lib/theme/gridThemeContext.ts +5 -0
- package/lib/theme/index.ts +5 -0
- package/lib/theme/presets.ts +5 -4
- package/lib/theme/types.ts +40 -2
- package/lib/types/index.ts +311 -108
- package/lib/utils/aggregation/index.ts +157 -0
- package/lib/utils/aria/index.ts +40 -0
- package/lib/utils/columnGroups.ts +110 -0
- package/lib/utils/columnLookup.ts +33 -0
- package/lib/utils/columnWidth.ts +22 -0
- package/lib/utils/dataSource/index.ts +46 -0
- package/lib/utils/detailPanel.ts +66 -0
- package/lib/utils/dragData.ts +44 -0
- package/lib/utils/editing/index.ts +30 -0
- package/lib/utils/export/exportShared.ts +197 -0
- package/lib/utils/export/exportToExcelAdvanced.ts +344 -121
- package/lib/utils/export/exportToPdf.ts +143 -92
- package/lib/utils/export/groupLabel.ts +14 -0
- package/lib/utils/export/index.ts +278 -272
- package/lib/utils/export/pdfText.ts +50 -0
- package/lib/utils/filtering/index.ts +277 -148
- package/lib/utils/focus/index.ts +115 -0
- package/lib/utils/gridApiRows.ts +35 -0
- package/lib/utils/gridModes.ts +77 -0
- package/lib/utils/grouping/groupedExportRows.ts +91 -0
- package/lib/utils/pagination/index.ts +56 -0
- package/lib/utils/pinning/index.ts +77 -33
- package/lib/utils/pivot/index.ts +368 -0
- package/lib/utils/scroll/index.ts +78 -0
- package/lib/utils/scroll/scrollEnd.ts +18 -0
- package/lib/utils/sorting/index.ts +154 -30
- package/lib/utils/syntheticRows.ts +12 -0
- package/lib/utils/values.ts +206 -0
- package/lib/utils/viewport.ts +14 -0
- package/llms.txt +272 -166
- package/package.json +18 -6
- package/lib/components/ColumnGroupHeader/ColumnGroupHeader.tsx +0 -220
- package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.test.tsx +0 -85
- package/lib/components/DataGrid/DataGrid.regression.test.tsx +0 -132
- package/lib/components/DataGrid/DataGrid.smoke.test.tsx +0 -58
- package/lib/components/DataGrid/DataGrid.v2.test.tsx +0 -127
- package/lib/components/Header/Header.test.tsx +0 -135
- package/lib/components/Pagination/Pagination.test.tsx +0 -64
- package/lib/hooks/core/useDataGrid.regression.test.ts +0 -66
- package/lib/hooks/core/useGridControlledState.test.ts +0 -86
- package/lib/hooks/core/useGridKeyboardNavigation.test.ts +0 -176
- package/lib/hooks/core/useGridRowPipeline.test.ts +0 -117
- package/lib/hooks/features/useAggregation.test.ts +0 -175
- package/lib/hooks/features/usePivot.test.ts +0 -202
- package/lib/hooks/useRowGrouping.test.ts +0 -137
- package/lib/utils/export/exportToPdf.test.ts +0 -120
- package/lib/utils/export/groupedExport.test.ts +0 -144
- package/lib/utils/filtering/filtering.test.ts +0 -178
- package/lib/utils/sorting/sorting.test.ts +0 -88
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import React, { useId } from 'react';
|
|
2
2
|
import { Checkbox } from '../ui/Checkbox';
|
|
3
3
|
import { ColumnResizeHandle } from '../ColumnResizeHandle/ColumnResizeHandle';
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
4
|
+
import { clampResizeWidth } from '../ColumnResizeHandle/clampResizeWidth';
|
|
5
|
+
import type { GridColDef, GridRowModel, GridSortDirection, GridColumnPinning, GridAggregationModel, GridColumnGroupingModel } from '../../types';
|
|
6
|
+
import { isColumnPinned, calculatePinnedPositions, getPinnedEdgeFields } from '../../utils/pinning';
|
|
7
|
+
import { getRenderedColumnWidth } from '../../utils/columnWidth';
|
|
8
|
+
import { buildColumnGroupRow, getColumnGroupDepth, getColumnGroupPaths } from '../../utils/columnGroups';
|
|
6
9
|
import { ColumnMenu } from './ColumnMenu';
|
|
7
10
|
|
|
8
11
|
|
|
@@ -23,92 +26,14 @@ function SwapIcon() {
|
|
|
23
26
|
}
|
|
24
27
|
|
|
25
28
|
|
|
26
|
-
// ─── Group header helpers ─────────────────────────────────────────────────────
|
|
27
|
-
|
|
28
|
-
/** Collect all leaf field names from a group node (recursive). */
|
|
29
|
-
function collectGroupFields(group: GridColumnGroup): string[] {
|
|
30
|
-
const out: string[] = [];
|
|
31
|
-
for (const child of group.children) {
|
|
32
|
-
if (typeof child === 'string') out.push(child);
|
|
33
|
-
else out.push(...collectGroupFields(child));
|
|
34
|
-
}
|
|
35
|
-
return out;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/** Max nesting depth of the group tree (1 = single level). */
|
|
39
|
-
function groupTreeDepth(groups: GridColumnGroupingModel): number {
|
|
40
|
-
let d = 0;
|
|
41
|
-
function walk(g: GridColumnGroup, depth: number) {
|
|
42
|
-
d = Math.max(d, depth);
|
|
43
|
-
for (const c of g.children) if (typeof c !== 'string') walk(c, depth + 1);
|
|
44
|
-
}
|
|
45
|
-
groups.forEach(g => walk(g, 1));
|
|
46
|
-
return d;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
interface GroupCell { key: string; label: string; width: number; isGroup: boolean; }
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Builds the flat array of GroupCell descriptors for one row (level).
|
|
53
|
-
* level 0 = top-most group row.
|
|
54
|
-
* allFields is the ORDERED list of all visible columns (including spacers).
|
|
55
|
-
*/
|
|
56
|
-
function buildGroupRow(
|
|
57
|
-
groups: GridColumnGroupingModel,
|
|
58
|
-
allFields: { field: string; width: number }[],
|
|
59
|
-
level: number,
|
|
60
|
-
): GroupCell[] {
|
|
61
|
-
// Map each field → the group that owns it at `level`
|
|
62
|
-
function findAtLevel(g: GridColumnGroup, field: string, cur: number): GridColumnGroup | null {
|
|
63
|
-
if (!collectGroupFields(g).includes(field)) return null;
|
|
64
|
-
if (cur === level) return g;
|
|
65
|
-
for (const child of g.children) {
|
|
66
|
-
if (typeof child !== 'string') {
|
|
67
|
-
const found = findAtLevel(child, field, cur + 1);
|
|
68
|
-
if (found) return found;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
return null;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const fieldGroup = new Map<string, GridColumnGroup | null>();
|
|
75
|
-
for (const f of allFields) {
|
|
76
|
-
let found: GridColumnGroup | null = null;
|
|
77
|
-
for (const g of groups) {
|
|
78
|
-
found = findAtLevel(g, f.field, 0);
|
|
79
|
-
if (found) break;
|
|
80
|
-
}
|
|
81
|
-
fieldGroup.set(f.field, found);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const cells: GroupCell[] = [];
|
|
85
|
-
let i = 0;
|
|
86
|
-
while (i < allFields.length) {
|
|
87
|
-
const item = allFields[i];
|
|
88
|
-
const group = fieldGroup.get(item.field) ?? null;
|
|
89
|
-
const w = item.width;
|
|
90
|
-
|
|
91
|
-
if (!group) {
|
|
92
|
-
cells.push({ key: `fg-${item.field}-${level}`, label: '', width: w, isGroup: false });
|
|
93
|
-
i++;
|
|
94
|
-
} else {
|
|
95
|
-
let totalW = 0;
|
|
96
|
-
let j = i;
|
|
97
|
-
while (j < allFields.length && fieldGroup.get(allFields[j].field) === group) {
|
|
98
|
-
totalW += allFields[j].width;
|
|
99
|
-
j++;
|
|
100
|
-
}
|
|
101
|
-
cells.push({ key: `${group.groupId}-${level}`, label: group.headerName, width: totalW, isGroup: true });
|
|
102
|
-
i = j;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
return cells;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
29
|
// ─── Props ────────────────────────────────────────────────────────────────────
|
|
109
30
|
|
|
110
31
|
export interface HeaderProps<R extends GridRowModel = GridRowModel> {
|
|
111
32
|
columns: GridColDef<R>[];
|
|
33
|
+
/**
|
|
34
|
+
* Every rendered data column in render order (left-pinned, unpinned, right-pinned) with its
|
|
35
|
+
* resolved width. Column group header rows are laid out over these; defaults to `columns`.
|
|
36
|
+
*/
|
|
112
37
|
allColumns?: GridColDef<R>[];
|
|
113
38
|
columnGroupingModel?: GridColumnGroupingModel;
|
|
114
39
|
checkboxSelection?: boolean;
|
|
@@ -132,13 +57,16 @@ export interface HeaderProps<R extends GridRowModel = GridRowModel> {
|
|
|
132
57
|
onDragOver?: (field: string) => (event: React.DragEvent) => void;
|
|
133
58
|
onDragEnd?: () => void;
|
|
134
59
|
onDrop?: (targetField: string) => (event: React.DragEvent) => void;
|
|
135
|
-
|
|
60
|
+
/** The grid's focus position; `id: null` means a header cell is focused. */
|
|
61
|
+
focusedCell?: { id: string | number | null; field: string } | null;
|
|
136
62
|
onHeaderClick?: (field: string) => void;
|
|
137
63
|
onSortAdd?: (field: string, direction: GridSortDirection) => void;
|
|
138
64
|
multiSort?: boolean;
|
|
139
65
|
onHideColumn?: (field: string) => void;
|
|
140
66
|
onPinColumn?: (field: string, side: 'left' | 'right' | null) => void;
|
|
141
67
|
onManageColumns?: () => void;
|
|
68
|
+
/** Position of each visible data column in render order, for `colIndex` and `aria-colindex`. */
|
|
69
|
+
columnIndexMap?: Map<string, number>;
|
|
142
70
|
}
|
|
143
71
|
|
|
144
72
|
export function Header<R extends GridRowModel = GridRowModel>(props: HeaderProps<R>) {
|
|
@@ -173,10 +101,10 @@ export function Header<R extends GridRowModel = GridRowModel>(props: HeaderProps
|
|
|
173
101
|
onHideColumn,
|
|
174
102
|
onPinColumn,
|
|
175
103
|
onManageColumns,
|
|
104
|
+
columnIndexMap,
|
|
176
105
|
} = props;
|
|
177
106
|
|
|
178
107
|
const selectAllId = useId();
|
|
179
|
-
const cellRefs = React.useRef<Record<string, HTMLElement | null>>({});
|
|
180
108
|
const [menuOpenParams, setMenuOpenParams] = React.useState<{ colDef: GridColDef<R>; anchorEl: HTMLElement } | null>(null);
|
|
181
109
|
|
|
182
110
|
const handleMenuOpen = (colDef: GridColDef<R>) => (event: React.MouseEvent) => {
|
|
@@ -184,18 +112,38 @@ export function Header<R extends GridRowModel = GridRowModel>(props: HeaderProps
|
|
|
184
112
|
setMenuOpenParams({ colDef, anchorEl: event.currentTarget as HTMLElement });
|
|
185
113
|
};
|
|
186
114
|
|
|
187
|
-
const handleMenuClose = () => {
|
|
115
|
+
const handleMenuClose = React.useCallback(() => {
|
|
188
116
|
setMenuOpenParams(null);
|
|
117
|
+
}, []);
|
|
118
|
+
|
|
119
|
+
// Alt+ArrowDown or Ctrl/Cmd+Enter on a focused header cell opens its column menu.
|
|
120
|
+
// DOM focus is moved onto the focused header cell by useGridKeyboardNavigation.
|
|
121
|
+
const handleHeaderKeyDown = (colDef: GridColDef<R>) => (event: React.KeyboardEvent<HTMLDivElement>) => {
|
|
122
|
+
// Alt+ArrowLeft / Alt+ArrowRight resize the column (Shift for bigger steps).
|
|
123
|
+
const resizes = event.altKey && (event.key === 'ArrowLeft' || event.key === 'ArrowRight');
|
|
124
|
+
if (resizes && event.target === event.currentTarget) {
|
|
125
|
+
if (colDef.resizable === false || !onColumnResize) return;
|
|
126
|
+
event.preventDefault();
|
|
127
|
+
event.stopPropagation();
|
|
128
|
+
const width = getRenderedColumnWidth(colDef, columnWidths);
|
|
129
|
+
const step = (event.shiftKey ? 50 : 10) * (event.key === 'ArrowRight' ? 1 : -1);
|
|
130
|
+
onColumnResize(colDef.field, clampResizeWidth(width + step, width, colDef.minWidth, colDef.maxWidth));
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
const opensMenu = (event.altKey && event.key === 'ArrowDown') || ((event.ctrlKey || event.metaKey) && event.key === 'Enter');
|
|
134
|
+
if (!opensMenu || colDef.disableColumnMenu || event.target !== event.currentTarget) return;
|
|
135
|
+
event.preventDefault();
|
|
136
|
+
event.stopPropagation();
|
|
137
|
+
const anchor = event.currentTarget.querySelector<HTMLElement>('.ogx__menu-icon-btn') ?? event.currentTarget;
|
|
138
|
+
setMenuOpenParams({ colDef, anchorEl: anchor });
|
|
189
139
|
};
|
|
190
140
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
}
|
|
198
|
-
}, [focusedCell]);
|
|
141
|
+
const isHeaderFocused = (field: string) => focusedCell?.id === null && focusedCell.field === field;
|
|
142
|
+
|
|
143
|
+
// aria-colindex: system columns first (reorder, expand, checkbox), then the data columns.
|
|
144
|
+
const reorderColIndex = rowReordering ? 1 : 0;
|
|
145
|
+
const expandColIndex = hasDetailPanel ? reorderColIndex + 1 : reorderColIndex;
|
|
146
|
+
const checkboxColIndex = checkboxSelection ? expandColIndex + 1 : expandColIndex;
|
|
199
147
|
|
|
200
148
|
const handleColumnClick = (colDef: GridColDef<R>) => (e: React.MouseEvent) => {
|
|
201
149
|
if (colDef.sortable === false) return;
|
|
@@ -218,6 +166,19 @@ export function Header<R extends GridRowModel = GridRowModel>(props: HeaderProps
|
|
|
218
166
|
}
|
|
219
167
|
};
|
|
220
168
|
|
|
169
|
+
// Column menu: act on this column only. "Unsort" removes just this key, and picking a direction
|
|
170
|
+
// for a column that is already sorted keeps the other keys. A new sort replaces the model
|
|
171
|
+
// unless multiSort is on, matching a plain header click.
|
|
172
|
+
const handleMenuSort = (field: string, direction: GridSortDirection) => {
|
|
173
|
+
const isSorted = sortModel.some(item => item.field === field);
|
|
174
|
+
const keepOtherKeys = direction === null || isSorted || multiSort;
|
|
175
|
+
if (onSortAdd && (keepOtherKeys || !onSort)) {
|
|
176
|
+
onSortAdd(field, direction);
|
|
177
|
+
} else {
|
|
178
|
+
onSort?.(field, direction);
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
|
|
221
182
|
const getSortIcon = (field: string) => {
|
|
222
183
|
const sortItem = sortModel.find(item => item.field === field);
|
|
223
184
|
if (!sortItem) return null;
|
|
@@ -246,16 +207,23 @@ export function Header<R extends GridRowModel = GridRowModel>(props: HeaderProps
|
|
|
246
207
|
);
|
|
247
208
|
}, [columns, columnWidths, pinnedColumns, checkboxSelection, pinCheckboxColumn, hasDetailPanel, pinExpandColumn, rowReordering]);
|
|
248
209
|
|
|
249
|
-
const
|
|
210
|
+
const pinnedEdges = React.useMemo(() => getPinnedEdgeFields(columns, pinnedColumns), [columns, pinnedColumns]);
|
|
250
211
|
|
|
251
|
-
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
212
|
+
const groupDepth = getColumnGroupDepth(columnGroupingModel);
|
|
213
|
+
const groupPaths = React.useMemo(() => getColumnGroupPaths(columnGroupingModel), [columnGroupingModel]);
|
|
214
|
+
|
|
215
|
+
// Group rows are laid out over every rendered data column (not just the horizontal render
|
|
216
|
+
// window), in render order and with the layout-resolved widths, so they line up with the headers.
|
|
217
|
+
const groupColumns = React.useMemo(() => {
|
|
218
|
+
if (groupDepth === 0) return [];
|
|
219
|
+
return (allColumns ?? columns).map(c => ({
|
|
256
220
|
field: c.field,
|
|
257
|
-
width:
|
|
221
|
+
width: getRenderedColumnWidth(c, columnWidths),
|
|
222
|
+
flex: c.isSpacer ? 0 : c.flex,
|
|
223
|
+
pinned: c.isSpacer ? null : isColumnPinned(c.field, pinnedColumns),
|
|
224
|
+
isSpacer: c.isSpacer,
|
|
258
225
|
}));
|
|
226
|
+
}, [groupDepth, allColumns, columns, columnWidths, pinnedColumns]);
|
|
259
227
|
|
|
260
228
|
// --- CHECKBOX / DETAIL / REORDER prefix width (for group filler) ---
|
|
261
229
|
const CHECKBOX_W = 48;
|
|
@@ -265,6 +233,12 @@ export function Header<R extends GridRowModel = GridRowModel>(props: HeaderProps
|
|
|
265
233
|
(rowReordering ? REORDER_W : 0) +
|
|
266
234
|
(hasDetailPanel ? EXPAND_W : 0) +
|
|
267
235
|
(checkboxSelection ? CHECKBOX_W : 0);
|
|
236
|
+
// The prefix filler sticks with the system columns when all of them are pinned.
|
|
237
|
+
const isPrefixPinned =
|
|
238
|
+
(rowReordering || hasDetailPanel || checkboxSelection) &&
|
|
239
|
+
(!hasDetailPanel || pinExpandColumn) &&
|
|
240
|
+
(!checkboxSelection || pinCheckboxColumn);
|
|
241
|
+
const systemColumnCount = (rowReordering ? 1 : 0) + (hasDetailPanel ? 1 : 0) + (checkboxSelection ? 1 : 0);
|
|
268
242
|
|
|
269
243
|
return (
|
|
270
244
|
// Outer sticky wrapper — both group rows AND column header row live here
|
|
@@ -272,53 +246,83 @@ export function Header<R extends GridRowModel = GridRowModel>(props: HeaderProps
|
|
|
272
246
|
<div className="ogx__header-wrap">
|
|
273
247
|
{/* ── Group header rows ── */}
|
|
274
248
|
{groupDepth > 0 && Array.from({ length: groupDepth }, (_, level) => {
|
|
275
|
-
const
|
|
276
|
-
columnGroupingModel!,
|
|
277
|
-
allLeafFields,
|
|
278
|
-
level,
|
|
279
|
-
);
|
|
249
|
+
const cells = buildColumnGroupRow(groupColumns, groupPaths, level);
|
|
280
250
|
return (
|
|
281
|
-
<div key={`ogx-grp-${level}`} className="ogx-col-group-row" role="row">
|
|
251
|
+
<div key={`ogx-grp-${level}`} className="ogx-col-group-row" role="row" aria-rowindex={level + 1}>
|
|
282
252
|
{/* Filler for checkbox / reorder / detailPanel prefix */}
|
|
283
253
|
{prefixWidth > 0 && (
|
|
284
254
|
<div
|
|
285
|
-
className=
|
|
286
|
-
style={{
|
|
287
|
-
|
|
255
|
+
className={`ogx-col-group-cell ogx-col-group-cell--filler${isPrefixPinned ? ' ogx-col-group-cell--pinned ogx-col-group-cell--pinned-left' : ''}`}
|
|
256
|
+
style={{
|
|
257
|
+
width: prefixWidth,
|
|
258
|
+
minWidth: prefixWidth,
|
|
259
|
+
flexShrink: 0,
|
|
260
|
+
position: isPrefixPinned ? 'sticky' : undefined,
|
|
261
|
+
left: isPrefixPinned ? 0 : undefined,
|
|
262
|
+
}}
|
|
263
|
+
aria-hidden="true"
|
|
288
264
|
/>
|
|
289
265
|
)}
|
|
290
|
-
{
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
266
|
+
{cells.map(cell => {
|
|
267
|
+
const pinnedClass = cell.pinned ? ` ogx-col-group-cell--pinned ogx-col-group-cell--pinned-${cell.pinned}` : '';
|
|
268
|
+
const style: React.CSSProperties = {
|
|
269
|
+
width: cell.width,
|
|
270
|
+
minWidth: cell.width,
|
|
271
|
+
flexShrink: 0,
|
|
272
|
+
flexGrow: cell.flexGrow,
|
|
273
|
+
};
|
|
274
|
+
if (cell.pinned === 'left') {
|
|
275
|
+
style.position = 'sticky';
|
|
276
|
+
style.left = pinnedPositions[cell.fields[0]];
|
|
277
|
+
} else if (cell.pinned === 'right') {
|
|
278
|
+
style.position = 'sticky';
|
|
279
|
+
style.right = pinnedPositions[cell.fields[cell.fields.length - 1]];
|
|
280
|
+
}
|
|
281
|
+
if (!cell.isGroup) {
|
|
282
|
+
return (
|
|
283
|
+
<div
|
|
284
|
+
key={cell.key}
|
|
285
|
+
className={`ogx-col-group-cell ogx-col-group-cell--filler${pinnedClass}`}
|
|
286
|
+
style={style}
|
|
287
|
+
aria-hidden="true"
|
|
288
|
+
/>
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
return (
|
|
292
|
+
<div
|
|
293
|
+
key={cell.key}
|
|
294
|
+
role="columnheader"
|
|
295
|
+
aria-colspan={cell.fields.length}
|
|
296
|
+
aria-colindex={systemColumnCount + cell.firstDataIndex + 1}
|
|
297
|
+
className={`ogx-col-group-cell ogx-col-group-cell--group${pinnedClass}${cell.headerClassName ? ` ${cell.headerClassName}` : ''}`}
|
|
298
|
+
style={style}
|
|
299
|
+
title={cell.label}
|
|
300
|
+
>
|
|
299
301
|
<span className="ogx-col-group-cell__label">{cell.label}</span>
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
)
|
|
302
|
+
</div>
|
|
303
|
+
);
|
|
304
|
+
})}
|
|
303
305
|
</div>
|
|
304
306
|
);
|
|
305
307
|
})}
|
|
306
308
|
|
|
307
309
|
{/* ── Column header row ── */}
|
|
308
|
-
<div className="ogx__header" role="row">
|
|
310
|
+
<div className="ogx__header" role="row" aria-rowindex={groupDepth + 1}>
|
|
309
311
|
{ }
|
|
310
312
|
{rowReordering && (
|
|
311
313
|
<div
|
|
312
|
-
className=
|
|
314
|
+
className={`ogx__header-cell ogx__header-cell--pinned ogx__header-cell--pinned-left ${isHeaderFocused('__reorder_col__') ? 'ogx__header-cell--focused' : ''}`}
|
|
313
315
|
role="columnheader"
|
|
314
316
|
aria-label="Row reorder handle"
|
|
317
|
+
aria-colindex={reorderColIndex}
|
|
318
|
+
data-field="__reorder_col__"
|
|
319
|
+
tabIndex={-1}
|
|
315
320
|
title="Drag to reorder rows"
|
|
316
321
|
style={{
|
|
317
322
|
minWidth: 48,
|
|
318
323
|
maxWidth: 48,
|
|
319
324
|
position: 'sticky',
|
|
320
325
|
left: 0,
|
|
321
|
-
zIndex: 4
|
|
322
326
|
}}
|
|
323
327
|
/>
|
|
324
328
|
)}
|
|
@@ -327,15 +331,17 @@ export function Header<R extends GridRowModel = GridRowModel>(props: HeaderProps
|
|
|
327
331
|
{hasDetailPanel && (
|
|
328
332
|
<div
|
|
329
333
|
className={`ogx__header-cell ogx__header-cell--expand ${pinExpandColumn ? 'ogx__header-cell--pinned ogx__header-cell--pinned-left' : ''
|
|
330
|
-
}`}
|
|
334
|
+
} ${isHeaderFocused('__expand_col__') ? 'ogx__header-cell--focused' : ''}`}
|
|
331
335
|
role="columnheader"
|
|
332
336
|
aria-label="Expand row details"
|
|
337
|
+
aria-colindex={expandColIndex}
|
|
338
|
+
data-field="__expand_col__"
|
|
339
|
+
tabIndex={-1}
|
|
333
340
|
style={{
|
|
334
341
|
minWidth: 48,
|
|
335
342
|
maxWidth: 48,
|
|
336
343
|
position: pinExpandColumn ? 'sticky' : undefined,
|
|
337
344
|
left: pinExpandColumn ? (rowReordering ? 48 : 0) : undefined,
|
|
338
|
-
zIndex: pinExpandColumn ? 5 : undefined
|
|
339
345
|
}}
|
|
340
346
|
>
|
|
341
347
|
{ }
|
|
@@ -346,25 +352,29 @@ export function Header<R extends GridRowModel = GridRowModel>(props: HeaderProps
|
|
|
346
352
|
{checkboxSelection && (
|
|
347
353
|
<div
|
|
348
354
|
className={`ogx__header-cell ogx__header-cell--checkbox ${pinCheckboxColumn ? 'ogx__header-cell--pinned ogx__header-cell--pinned-left' : ''
|
|
349
|
-
}`}
|
|
355
|
+
} ${isHeaderFocused('__checkbox_col__') ? 'ogx__header-cell--focused' : ''}`}
|
|
350
356
|
role="columnheader"
|
|
357
|
+
aria-colindex={checkboxColIndex}
|
|
358
|
+
data-field="__checkbox_col__"
|
|
351
359
|
style={{
|
|
352
360
|
position: pinCheckboxColumn ? 'sticky' : undefined,
|
|
353
361
|
left: pinCheckboxColumn ? ((rowReordering ? 48 : 0) + (hasDetailPanel && pinExpandColumn ? 48 : 0)) : undefined,
|
|
354
362
|
|
|
355
363
|
}}
|
|
356
364
|
>
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
365
|
+
{/* No select-all without a handler (single-row selection has none). */}
|
|
366
|
+
{onSelectAll && (
|
|
367
|
+
<Checkbox
|
|
368
|
+
id={selectAllId}
|
|
369
|
+
name={selectAllId}
|
|
370
|
+
checked={allSelected}
|
|
371
|
+
indeterminate={someSelected}
|
|
372
|
+
onChange={(e) => onSelectAll(e.target.checked)}
|
|
373
|
+
aria-label={allSelected ? 'Deselect all rows' : 'Select all rows'}
|
|
374
|
+
tabIndex={-1}
|
|
375
|
+
onMouseDown={(e) => e.preventDefault()}
|
|
376
|
+
/>
|
|
377
|
+
)}
|
|
368
378
|
</div>
|
|
369
379
|
)}
|
|
370
380
|
|
|
@@ -379,7 +389,18 @@ export function Header<R extends GridRowModel = GridRowModel>(props: HeaderProps
|
|
|
379
389
|
);
|
|
380
390
|
}
|
|
381
391
|
const isSortable = colDef.sortable !== false;
|
|
382
|
-
const
|
|
392
|
+
const sortPosition = sortModel.findIndex(item => item.field === colDef.field);
|
|
393
|
+
const isSorted = sortPosition !== -1;
|
|
394
|
+
const sortDirection = isSorted ? sortModel[sortPosition].sort : null;
|
|
395
|
+
const colIndex = columnIndexMap?.get(colDef.field) ?? index;
|
|
396
|
+
// Only the primary sort column carries aria-sort (WAI-ARIA allows one per grid);
|
|
397
|
+
// with multi-sort every sorted column describes its direction and priority.
|
|
398
|
+
const ariaSort = !isSortable ? undefined
|
|
399
|
+
: sortPosition === 0 ? (sortDirection === 'asc' ? 'ascending' : 'descending')
|
|
400
|
+
: 'none';
|
|
401
|
+
const sortDescription = isSorted && sortModel.length > 1
|
|
402
|
+
? `Sorted ${sortDirection === 'asc' ? 'ascending' : 'descending'}, sort priority ${sortPosition + 1} of ${sortModel.length}`
|
|
403
|
+
: undefined;
|
|
383
404
|
const isResizable = colDef.resizable !== false;
|
|
384
405
|
const pinnedPosition = isColumnPinned(colDef.field, pinnedColumns);
|
|
385
406
|
const isPinned = pinnedPosition !== null;
|
|
@@ -393,9 +414,11 @@ export function Header<R extends GridRowModel = GridRowModel>(props: HeaderProps
|
|
|
393
414
|
isSorted && 'ogx__header-cell--sorted',
|
|
394
415
|
isPinned && `ogx__header-cell--pinned`,
|
|
395
416
|
isPinned && `ogx__header-cell--pinned-${pinnedPosition}`,
|
|
417
|
+
pinnedPosition === 'left' && colDef.field === pinnedEdges.lastLeft && 'ogx__header-cell--pinned-left-last',
|
|
418
|
+
pinnedPosition === 'right' && colDef.field === pinnedEdges.firstRight && 'ogx__header-cell--pinned-right-first',
|
|
396
419
|
isDragging && 'ogx__header-cell--dragging',
|
|
397
420
|
isDragOver && 'ogx__header-cell--drag-over',
|
|
398
|
-
(
|
|
421
|
+
isHeaderFocused(colDef.field) && 'ogx__header-cell--focused',
|
|
399
422
|
colDef.headerClassName
|
|
400
423
|
].filter(Boolean).join(' ');
|
|
401
424
|
|
|
@@ -424,7 +447,7 @@ export function Header<R extends GridRowModel = GridRowModel>(props: HeaderProps
|
|
|
424
447
|
}
|
|
425
448
|
|
|
426
449
|
const headerContent = colDef.renderHeader
|
|
427
|
-
? colDef.renderHeader({ field: colDef.field, colDef: colDef as unknown as GridColDef, colIndex
|
|
450
|
+
? colDef.renderHeader({ field: colDef.field, colDef: colDef as unknown as GridColDef, colIndex })
|
|
428
451
|
: colDef.headerName || colDef.field;
|
|
429
452
|
|
|
430
453
|
const handleHeaderClick = (e: React.MouseEvent) => {
|
|
@@ -438,9 +461,10 @@ export function Header<R extends GridRowModel = GridRowModel>(props: HeaderProps
|
|
|
438
461
|
}
|
|
439
462
|
};
|
|
440
463
|
|
|
441
|
-
|
|
442
|
-
const
|
|
443
|
-
const
|
|
464
|
+
// Pinned columns keep their place: they are neither dragged nor drop targets.
|
|
465
|
+
const handleDragStart = onDragStart && !isPinned ? onDragStart(colDef.field) : undefined;
|
|
466
|
+
const handleDragOver = onDragOver && !isPinned ? onDragOver(colDef.field) : undefined;
|
|
467
|
+
const handleDrop = onDrop && !isPinned ? onDrop(colDef.field) : undefined;
|
|
444
468
|
|
|
445
469
|
return (
|
|
446
470
|
<div
|
|
@@ -450,24 +474,17 @@ export function Header<R extends GridRowModel = GridRowModel>(props: HeaderProps
|
|
|
450
474
|
title={colDef.description}
|
|
451
475
|
onClick={handleHeaderClick}
|
|
452
476
|
role="columnheader"
|
|
453
|
-
draggable={!!
|
|
477
|
+
draggable={!!handleDragStart}
|
|
454
478
|
onDragStart={handleDragStart}
|
|
455
479
|
onDragOver={handleDragOver}
|
|
456
480
|
onDragEnd={onDragEnd}
|
|
457
481
|
onDrop={handleDrop}
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
? 'ascending'
|
|
463
|
-
: 'descending'
|
|
464
|
-
: 'none')
|
|
465
|
-
: undefined
|
|
466
|
-
}
|
|
467
|
-
aria-colindex={index + 1 + (checkboxSelection ? 1 : 0)}
|
|
482
|
+
onKeyDown={handleHeaderKeyDown(colDef)}
|
|
483
|
+
aria-sort={ariaSort}
|
|
484
|
+
aria-description={sortDescription}
|
|
485
|
+
aria-colindex={systemColumnCount + colIndex + 1}
|
|
468
486
|
data-field={colDef.field}
|
|
469
487
|
tabIndex={-1}
|
|
470
|
-
ref={(el) => { cellRefs.current[colDef.field] = el; }}
|
|
471
488
|
>
|
|
472
489
|
<div className="ogx__header-cell-content">
|
|
473
490
|
{isDragging && (
|
|
@@ -488,6 +505,7 @@ export function Header<R extends GridRowModel = GridRowModel>(props: HeaderProps
|
|
|
488
505
|
{isSortable && getSortIcon(colDef.field)}
|
|
489
506
|
{!colDef.disableColumnMenu && (
|
|
490
507
|
<button
|
|
508
|
+
type="button"
|
|
491
509
|
className="ogx__menu-icon-btn"
|
|
492
510
|
onClick={handleMenuOpen(colDef)}
|
|
493
511
|
aria-label={`Open column menu for ${colDef.headerName || colDef.field}`}
|
|
@@ -503,10 +521,11 @@ export function Header<R extends GridRowModel = GridRowModel>(props: HeaderProps
|
|
|
503
521
|
{isResizable && onColumnResize && (
|
|
504
522
|
<ColumnResizeHandle
|
|
505
523
|
field={colDef.field}
|
|
506
|
-
currentWidth={
|
|
524
|
+
currentWidth={getRenderedColumnWidth(colDef, columnWidths)}
|
|
507
525
|
onResize={onColumnResize}
|
|
508
526
|
minWidth={colDef.minWidth}
|
|
509
527
|
maxWidth={colDef.maxWidth}
|
|
528
|
+
edge={pinnedPosition === 'right' ? 'start' : 'end'}
|
|
510
529
|
/>
|
|
511
530
|
)}
|
|
512
531
|
</div>
|
|
@@ -517,7 +536,7 @@ export function Header<R extends GridRowModel = GridRowModel>(props: HeaderProps
|
|
|
517
536
|
<ColumnMenu
|
|
518
537
|
colDef={menuOpenParams.colDef as unknown as GridColDef<GridRowModel>}
|
|
519
538
|
sortModel={sortModel}
|
|
520
|
-
onSort={onSort}
|
|
539
|
+
onSort={onSort || onSortAdd ? handleMenuSort : undefined}
|
|
521
540
|
onHide={onHideColumn}
|
|
522
541
|
onPin={onPinColumn}
|
|
523
542
|
pinnedColumns={pinnedColumns}
|
|
@@ -31,6 +31,15 @@
|
|
|
31
31
|
background: var(--ogx-color-gray-50, #f8fafc);
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
/* Keyboard focus (roving tab stop between list rows). */
|
|
35
|
+
.ogx-list-view__row:focus {
|
|
36
|
+
outline: none;
|
|
37
|
+
}
|
|
38
|
+
.ogx-list-view__row:focus-visible {
|
|
39
|
+
outline: 2px solid var(--ogx-color-primary);
|
|
40
|
+
outline-offset: -2px;
|
|
41
|
+
}
|
|
42
|
+
|
|
34
43
|
.ogx-list-view__row--selected {
|
|
35
44
|
background: var(--ogx-selection-bg, rgba(59, 130, 246, 0.08));
|
|
36
45
|
}
|
|
@@ -40,6 +49,13 @@
|
|
|
40
49
|
}
|
|
41
50
|
|
|
42
51
|
/* ─── Checkbox column inside list row ────────────────────────────────────── */
|
|
52
|
+
.ogx-list-view__expand {
|
|
53
|
+
display: flex;
|
|
54
|
+
align-items: center;
|
|
55
|
+
flex-shrink: 0;
|
|
56
|
+
min-width: 32px;
|
|
57
|
+
}
|
|
58
|
+
|
|
43
59
|
.ogx-list-view__checkbox {
|
|
44
60
|
display: flex;
|
|
45
61
|
align-items: center;
|
|
@@ -110,3 +126,12 @@
|
|
|
110
126
|
display: none;
|
|
111
127
|
}
|
|
112
128
|
}
|
|
129
|
+
|
|
130
|
+
/* Loading with no rows yet: a progress bar, or slots.loadingOverlay centred. */
|
|
131
|
+
.ogx-list-view__loading {
|
|
132
|
+
position: relative;
|
|
133
|
+
display: flex;
|
|
134
|
+
align-items: center;
|
|
135
|
+
justify-content: center;
|
|
136
|
+
min-height: 120px;
|
|
137
|
+
}
|