@opencorestack/opengridx 2.1.0 → 3.0.1
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 +399 -0
- package/README.md +95 -72
- package/dist/index.d.ts +590 -131
- package/dist/opengridx.css +1 -1
- package/dist/opengridx.es.js +7891 -5631
- 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 -166
- 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 +6 -7
- package/docs/components/datagrid.md +15 -13
- 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 +90 -38
- 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 +126 -39
- 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 +72 -19
- package/docs/features/pinning.md +24 -5
- package/docs/features/selection.md +50 -10
- package/docs/features/sorting-pagination.md +64 -14
- package/docs/features/state-persistence.md +78 -10
- package/docs/features/toolbar-customization.md +26 -17
- package/docs/features/tree-data-grouping.md +36 -8
- package/docs/features/virtualization.md +37 -14
- package/docs/migration/v1-to-v2.md +115 -0
- package/docs/migration/v2-to-v3.md +1233 -0
- package/docs/performance.md +124 -0
- package/docs/roadmap.md +5 -5
- package/lib/components/Cell/Cell.css +17 -1
- package/lib/components/Cell/Cell.tsx +129 -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 +15 -2
- 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 +85 -12
- package/lib/components/DataGrid/DataGrid.tsx +582 -876
- package/lib/components/DataGrid/GridAggregationFooter.tsx +53 -17
- package/lib/components/DataGrid/GridErrorOverlay.tsx +15 -12
- package/lib/components/DataGrid/GridListView.tsx +165 -24
- 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 +62 -11
- package/lib/components/DataGrid/GridStandaloneColumnPanel.tsx +40 -38
- package/lib/components/DataGrid/GridToolbarSlot.tsx +51 -0
- package/lib/components/DataGrid/GridVirtualRows.tsx +40 -24
- 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 +6 -3
- package/lib/components/Header/Header.tsx +181 -160
- package/lib/components/ListView/ListView.css +25 -0
- package/lib/components/ListView/ListViewRow.tsx +76 -10
- package/lib/components/Pagination/Pagination.tsx +15 -7
- package/lib/components/Row/Row.tsx +177 -77
- 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 +19 -4
- 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 +279 -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 +104 -5
- 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 +447 -243
- 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 +17 -5
- 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 +82 -51
- 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 +298 -160
- package/lib/hooks/useRowReorder.ts +62 -31
- package/lib/hooks/useTreeData.ts +287 -220
- package/lib/index.ts +28 -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 +313 -116
- package/lib/utils/aggregation/index.ts +119 -24
- 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 +278 -139
- 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 +55 -5
- 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 +261 -164
- package/package.json +13 -5
- package/lib/components/ColumnGroupHeader/ColumnGroupHeader.tsx +0 -220
- package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.test.tsx +0 -85
- package/lib/components/DataGrid/DataGrid.defects.test.tsx +0 -103
- 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/DataGrid/DataGrid.virtualization.browser.test.tsx +0 -183
- 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 -298
- package/lib/hooks/useTreeData.test.ts +0 -83
- package/lib/utils/aggregation/aggregation.test.ts +0 -16
- package/lib/utils/export/exportToExcelAdvanced.test.ts +0 -109
- 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,14 +1,34 @@
|
|
|
1
1
|
import { useEffect, useRef, useState } from 'react';
|
|
2
|
-
import type {
|
|
2
|
+
import type { KeyboardEvent } from 'react';
|
|
3
|
+
import type { GridColDef, GridRenderEditCellParams } from '../../types';
|
|
3
4
|
|
|
4
|
-
export
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
export type GridEditInputCellProps = GridRenderEditCellParams;
|
|
6
|
+
|
|
7
|
+
type ValueOption = NonNullable<GridColDef['valueOptions']>[number];
|
|
8
|
+
|
|
9
|
+
/** The accessible name of an editor: its column header, or the field when there is none. */
|
|
10
|
+
function editorLabel(colDef: GridColDef): string {
|
|
11
|
+
return colDef.headerName || colDef.field;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** An IME is composing (keyCode 229 covers Safari and older engines): Enter/Escape belong to the IME. */
|
|
15
|
+
function isComposing(e: KeyboardEvent): boolean {
|
|
16
|
+
return e.nativeEvent.isComposing || e.keyCode === 229;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function makeKeyDownHandler(onCommit: () => void, onCancel: () => void, commitOnEnter = true) {
|
|
20
|
+
return (e: KeyboardEvent) => {
|
|
21
|
+
if (isComposing(e)) {
|
|
22
|
+
e.stopPropagation();
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (commitOnEnter && e.key === 'Enter') { e.stopPropagation(); onCommit(); }
|
|
26
|
+
if (e.key === 'Escape') { e.stopPropagation(); onCancel(); }
|
|
27
|
+
};
|
|
8
28
|
}
|
|
9
29
|
|
|
10
30
|
// ─── Text / String ────────────────────────────────────────────────────────────
|
|
11
|
-
function TextEditor({ value, onValueChange, onCommit, onCancel }: GridEditInputCellProps) {
|
|
31
|
+
function TextEditor({ value, colDef, onValueChange, onCommit, onCancel }: GridEditInputCellProps) {
|
|
12
32
|
const [local, setLocal] = useState(String(value ?? ''));
|
|
13
33
|
const ref = useRef<HTMLInputElement>(null);
|
|
14
34
|
useEffect(() => { ref.current?.focus(); ref.current?.select(); }, []);
|
|
@@ -18,19 +38,17 @@ function TextEditor({ value, onValueChange, onCommit, onCancel }: GridEditInputC
|
|
|
18
38
|
ref={ref}
|
|
19
39
|
type="text"
|
|
20
40
|
className="ogx__edit-input"
|
|
41
|
+
aria-label={editorLabel(colDef)}
|
|
21
42
|
value={local}
|
|
22
43
|
onChange={e => { setLocal(e.target.value); onValueChange(e.target.value); }}
|
|
23
44
|
onBlur={onCommit}
|
|
24
|
-
onKeyDown={
|
|
25
|
-
if (e.key === 'Enter') { e.stopPropagation(); onCommit(); }
|
|
26
|
-
if (e.key === 'Escape') { e.stopPropagation(); onCancel(); }
|
|
27
|
-
}}
|
|
45
|
+
onKeyDown={makeKeyDownHandler(onCommit, onCancel)}
|
|
28
46
|
/>
|
|
29
47
|
);
|
|
30
48
|
}
|
|
31
49
|
|
|
32
50
|
// ─── Number ───────────────────────────────────────────────────────────────────
|
|
33
|
-
function NumberEditor({ value, onValueChange, onCommit, onCancel }: GridEditInputCellProps) {
|
|
51
|
+
function NumberEditor({ value, colDef, onValueChange, onCommit, onCancel }: GridEditInputCellProps) {
|
|
34
52
|
const [local, setLocal] = useState<string | number>(typeof value === 'number' ? value : '');
|
|
35
53
|
const ref = useRef<HTMLInputElement>(null);
|
|
36
54
|
useEffect(() => { ref.current?.focus(); ref.current?.select(); }, []);
|
|
@@ -40,6 +58,7 @@ function NumberEditor({ value, onValueChange, onCommit, onCancel }: GridEditInpu
|
|
|
40
58
|
ref={ref}
|
|
41
59
|
type="number"
|
|
42
60
|
className="ogx__edit-input ogx__edit-input--number"
|
|
61
|
+
aria-label={editorLabel(colDef)}
|
|
43
62
|
value={local}
|
|
44
63
|
onChange={e => {
|
|
45
64
|
const v = e.target.value === '' ? '' : Number(e.target.value);
|
|
@@ -47,24 +66,20 @@ function NumberEditor({ value, onValueChange, onCommit, onCancel }: GridEditInpu
|
|
|
47
66
|
onValueChange(v === '' ? null : v);
|
|
48
67
|
}}
|
|
49
68
|
onBlur={onCommit}
|
|
50
|
-
onKeyDown={
|
|
51
|
-
if (e.key === 'Enter') { e.stopPropagation(); onCommit(); }
|
|
52
|
-
if (e.key === 'Escape') { e.stopPropagation(); onCancel(); }
|
|
53
|
-
}}
|
|
69
|
+
onKeyDown={makeKeyDownHandler(onCommit, onCancel)}
|
|
54
70
|
/>
|
|
55
71
|
);
|
|
56
72
|
}
|
|
57
73
|
|
|
58
74
|
// ─── Boolean ──────────────────────────────────────────────────────────────────
|
|
59
|
-
function BooleanEditor({ value, onValueChange, onCommit, onCancel }: GridEditInputCellProps) {
|
|
75
|
+
function BooleanEditor({ value, colDef, onValueChange, onCommit, onCancel }: GridEditInputCellProps) {
|
|
60
76
|
const ref = useRef<HTMLInputElement>(null);
|
|
61
77
|
useEffect(() => { ref.current?.focus(); }, []);
|
|
62
78
|
|
|
63
79
|
const handleChange = () => {
|
|
64
|
-
|
|
65
|
-
onValueChange(
|
|
66
|
-
|
|
67
|
-
setTimeout(onCommit, 0);
|
|
80
|
+
// The grid records the new value synchronously, so the toggle can be committed straight away.
|
|
81
|
+
onValueChange(!value);
|
|
82
|
+
onCommit();
|
|
68
83
|
};
|
|
69
84
|
|
|
70
85
|
return (
|
|
@@ -73,47 +88,107 @@ function BooleanEditor({ value, onValueChange, onCommit, onCancel }: GridEditInp
|
|
|
73
88
|
ref={ref}
|
|
74
89
|
type="checkbox"
|
|
75
90
|
className="ogx__edit-checkbox"
|
|
91
|
+
aria-label={editorLabel(colDef)}
|
|
76
92
|
checked={!!value}
|
|
77
93
|
onChange={handleChange}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}}
|
|
94
|
+
onBlur={onCommit}
|
|
95
|
+
onKeyDown={makeKeyDownHandler(onCommit, onCancel, false)}
|
|
81
96
|
/>
|
|
82
97
|
</div>
|
|
83
98
|
);
|
|
84
99
|
}
|
|
85
100
|
|
|
86
101
|
// ─── SingleSelect / Enum ──────────────────────────────────────────────────────
|
|
102
|
+
const optionValue = (opt: ValueOption): unknown => (typeof opt === 'object' ? opt.value : opt);
|
|
103
|
+
const optionLabel = (opt: ValueOption): string => (typeof opt === 'object' ? opt.label : String(opt));
|
|
104
|
+
|
|
105
|
+
function sameOptionValue(a: unknown, b: unknown): boolean {
|
|
106
|
+
if (a instanceof Date && b instanceof Date) return a.getTime() === b.getTime();
|
|
107
|
+
return Object.is(a, b);
|
|
108
|
+
}
|
|
109
|
+
|
|
87
110
|
function SelectEditor({ value, colDef, onValueChange, onCommit, onCancel }: GridEditInputCellProps) {
|
|
88
111
|
const ref = useRef<HTMLSelectElement>(null);
|
|
89
112
|
useEffect(() => { ref.current?.focus(); }, []);
|
|
90
113
|
|
|
91
114
|
const options = colDef.valueOptions ?? [];
|
|
115
|
+
const keys = options.map(opt => String(optionValue(opt)));
|
|
116
|
+
let selectedIndex = options.findIndex(opt => sameOptionValue(optionValue(opt), value));
|
|
117
|
+
if (selectedIndex === -1 && value !== null && value !== undefined) {
|
|
118
|
+
// Loose match so a string '2' in the row still selects the numeric option 2.
|
|
119
|
+
selectedIndex = keys.indexOf(String(value));
|
|
120
|
+
}
|
|
121
|
+
// With no matching option the browser would show the first option as chosen while the value
|
|
122
|
+
// is still null, and re-picking that option fires no change event. Show an empty choice instead.
|
|
123
|
+
const showEmptyChoice = selectedIndex === -1;
|
|
92
124
|
|
|
93
125
|
return (
|
|
94
126
|
<select
|
|
95
127
|
ref={ref}
|
|
96
128
|
className="ogx__edit-select"
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
129
|
+
aria-label={editorLabel(colDef)}
|
|
130
|
+
value={showEmptyChoice ? '' : keys[selectedIndex]}
|
|
131
|
+
onChange={e => {
|
|
132
|
+
const index = keys.indexOf(e.target.value);
|
|
133
|
+
// Commit the option's own value (a number stays a number), not the DOM string.
|
|
134
|
+
onValueChange(index === -1 ? null : optionValue(options[index]));
|
|
103
135
|
}}
|
|
136
|
+
onBlur={onCommit}
|
|
137
|
+
onKeyDown={makeKeyDownHandler(onCommit, onCancel)}
|
|
104
138
|
>
|
|
105
|
-
{
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
})}
|
|
139
|
+
{showEmptyChoice && <option value="" />}
|
|
140
|
+
{options.map((opt, i) => (
|
|
141
|
+
<option key={i} value={keys[i]}>{optionLabel(opt)}</option>
|
|
142
|
+
))}
|
|
110
143
|
</select>
|
|
111
144
|
);
|
|
112
145
|
}
|
|
113
146
|
|
|
114
147
|
// ─── Date ─────────────────────────────────────────────────────────────────────
|
|
115
|
-
|
|
116
|
-
|
|
148
|
+
const pad2 = (n: number) => String(n).padStart(2, '0');
|
|
149
|
+
const ISO_DATE_PREFIX = /^(\d{4}-\d{2}-\d{2})/;
|
|
150
|
+
|
|
151
|
+
/** The `YYYY-MM-DD` an `<input type="date">` needs; Dates use their local calendar day. */
|
|
152
|
+
function toDateInputValue(value: unknown): string {
|
|
153
|
+
if (value instanceof Date) {
|
|
154
|
+
return Number.isNaN(value.getTime())
|
|
155
|
+
? ''
|
|
156
|
+
: `${value.getFullYear()}-${pad2(value.getMonth() + 1)}-${pad2(value.getDate())}`;
|
|
157
|
+
}
|
|
158
|
+
if (typeof value === 'string') {
|
|
159
|
+
const iso = ISO_DATE_PREFIX.exec(value);
|
|
160
|
+
if (iso) return iso[1];
|
|
161
|
+
return value === '' ? '' : toDateInputValue(new Date(value));
|
|
162
|
+
}
|
|
163
|
+
if (typeof value === 'number') return toDateInputValue(new Date(value));
|
|
164
|
+
return '';
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Turns the picked `YYYY-MM-DD` back into the kind of value the cell held: a Date stays a Date
|
|
169
|
+
* (keeping its time of day), a timestamp stays a timestamp, an ISO date-time string keeps its time
|
|
170
|
+
* part. A cleared input commits `null`.
|
|
171
|
+
*/
|
|
172
|
+
function fromDateInputValue(picked: string, original: unknown): unknown {
|
|
173
|
+
if (picked === '') return null;
|
|
174
|
+
const [year, month, day] = picked.split('-').map(Number);
|
|
175
|
+
const withDay = (base: Date) => {
|
|
176
|
+
const next = new Date(base.getTime());
|
|
177
|
+
next.setFullYear(year, month - 1, day);
|
|
178
|
+
return next;
|
|
179
|
+
};
|
|
180
|
+
if (original instanceof Date && !Number.isNaN(original.getTime())) return withDay(original);
|
|
181
|
+
if (typeof original === 'number' && Number.isFinite(original)) return withDay(new Date(original)).getTime();
|
|
182
|
+
if (typeof original === 'string' && ISO_DATE_PREFIX.test(original) && original.length > 10) {
|
|
183
|
+
return picked + original.slice(10);
|
|
184
|
+
}
|
|
185
|
+
return picked;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function DateEditor({ value, colDef, onValueChange, onCommit, onCancel }: GridEditInputCellProps) {
|
|
189
|
+
// The value the edit started from decides the type that is committed back.
|
|
190
|
+
const [initialValue] = useState(value);
|
|
191
|
+
const [local, setLocal] = useState(() => toDateInputValue(value));
|
|
117
192
|
const ref = useRef<HTMLInputElement>(null);
|
|
118
193
|
useEffect(() => { ref.current?.focus(); }, []);
|
|
119
194
|
|
|
@@ -122,13 +197,11 @@ function DateEditor({ value, onValueChange, onCommit, onCancel }: GridEditInputC
|
|
|
122
197
|
ref={ref}
|
|
123
198
|
type="date"
|
|
124
199
|
className="ogx__edit-input"
|
|
200
|
+
aria-label={editorLabel(colDef)}
|
|
125
201
|
value={local}
|
|
126
|
-
onChange={e => { setLocal(e.target.value); onValueChange(e.target.value); }}
|
|
202
|
+
onChange={e => { setLocal(e.target.value); onValueChange(fromDateInputValue(e.target.value, initialValue)); }}
|
|
127
203
|
onBlur={onCommit}
|
|
128
|
-
onKeyDown={
|
|
129
|
-
if (e.key === 'Enter') { e.stopPropagation(); onCommit(); }
|
|
130
|
-
if (e.key === 'Escape') { e.stopPropagation(); onCancel(); }
|
|
131
|
-
}}
|
|
204
|
+
onKeyDown={makeKeyDownHandler(onCommit, onCancel)}
|
|
132
205
|
/>
|
|
133
206
|
);
|
|
134
207
|
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
align-items: stretch;
|
|
10
10
|
background: var(--ogx-grid-header-background);
|
|
11
11
|
border-bottom: 1px solid var(--ogx-grid-border-color);
|
|
12
|
-
overflow:
|
|
12
|
+
/* No overflow clipping: it would make the row the scroll container of its sticky (pinned) cells. */
|
|
13
13
|
flex-shrink: 0;
|
|
14
14
|
min-height: var(--ogx-header-height, 48px);
|
|
15
15
|
}
|
|
@@ -46,6 +46,12 @@
|
|
|
46
46
|
background: transparent;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
/* ─── Pinned group cell (sticky over pinned columns) ────────────────────────*/
|
|
50
|
+
.ogx-col-group-cell--pinned {
|
|
51
|
+
background: var(--ogx-grid-header-background);
|
|
52
|
+
z-index: 12;
|
|
53
|
+
}
|
|
54
|
+
|
|
49
55
|
/* ─── Label text ─────────────────────────────────────────────────────────────*/
|
|
50
56
|
.ogx-col-group-cell__label {
|
|
51
57
|
font-size: 0.75rem;
|
|
@@ -1,15 +1,28 @@
|
|
|
1
1
|
.ogx-column-resize-handle {
|
|
2
2
|
position: absolute;
|
|
3
3
|
top: 0;
|
|
4
|
-
|
|
4
|
+
/* Inside the header cell: the cell clips overflow, so a handle straddling the border lost its outer half. */
|
|
5
|
+
right: 0;
|
|
5
6
|
width: 8px;
|
|
6
7
|
height: 100%;
|
|
7
8
|
cursor: col-resize;
|
|
8
9
|
z-index: 10;
|
|
9
10
|
display: flex;
|
|
10
11
|
align-items: center;
|
|
11
|
-
justify-content:
|
|
12
|
+
justify-content: flex-end;
|
|
12
13
|
user-select: none;
|
|
14
|
+
/* Touch and pen drags resize the column instead of scrolling the grid. */
|
|
15
|
+
touch-action: none;
|
|
16
|
+
}
|
|
17
|
+
/* Right-pinned columns grow leftwards: their handle sits on the left edge. */
|
|
18
|
+
.ogx-column-resize-handle--start {
|
|
19
|
+
right: auto;
|
|
20
|
+
left: 0;
|
|
21
|
+
justify-content: flex-start;
|
|
22
|
+
}
|
|
23
|
+
.ogx-column-resize-handle:focus-visible .ogx-column-resize-handle__line {
|
|
24
|
+
background: var(--ogx-color-primary);
|
|
25
|
+
opacity: 1;
|
|
13
26
|
}
|
|
14
27
|
.ogx-column-resize-handle:hover .ogx-column-resize-handle__line,
|
|
15
28
|
.ogx-column-resize-handle--dragging .ogx-column-resize-handle__line {
|
|
@@ -1,58 +1,113 @@
|
|
|
1
|
-
|
|
2
|
-
import
|
|
1
|
+
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { clampResizeWidth } from './clampResizeWidth';
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
export interface ColumnResizeHandleProps {
|
|
6
6
|
field: string;
|
|
7
7
|
currentWidth: number;
|
|
8
8
|
onResize: (field: string, newWidth: number) => void;
|
|
9
|
+
/** Defaults to 50 (the layout's default minimum), or the current width if that is smaller. */
|
|
9
10
|
minWidth?: number;
|
|
11
|
+
/** Defaults to no limit. */
|
|
10
12
|
maxWidth?: number;
|
|
13
|
+
/**
|
|
14
|
+
* Which edge of the column the handle sits on. `'start'` (the left edge) is for right-pinned
|
|
15
|
+
* columns, which are anchored on the right and grow leftwards. Default `'end'`.
|
|
16
|
+
*/
|
|
17
|
+
edge?: 'start' | 'end';
|
|
11
18
|
}
|
|
12
19
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
20
|
+
interface ActiveResize {
|
|
21
|
+
pointerId: number;
|
|
22
|
+
startX: number;
|
|
23
|
+
startWidth: number;
|
|
24
|
+
lastUpdate: number;
|
|
25
|
+
moved: boolean;
|
|
26
|
+
}
|
|
18
27
|
|
|
19
|
-
|
|
20
|
-
|
|
28
|
+
const THROTTLE_MS = 16;
|
|
29
|
+
|
|
30
|
+
function preventMouseDefault(event: React.MouseEvent): void {
|
|
31
|
+
event.preventDefault();
|
|
32
|
+
event.stopPropagation();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Swallows the click the browser synthesizes after a drag, so ending a resize never sorts. */
|
|
36
|
+
function suppressNextClick(): void {
|
|
37
|
+
const swallow = (event: MouseEvent) => {
|
|
21
38
|
event.stopPropagation();
|
|
39
|
+
event.preventDefault();
|
|
40
|
+
};
|
|
41
|
+
window.addEventListener('click', swallow, { capture: true, once: true });
|
|
42
|
+
setTimeout(() => window.removeEventListener('click', swallow, { capture: true }), 0);
|
|
43
|
+
}
|
|
22
44
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
let lastUpdateTime = 0;
|
|
29
|
-
const throttleMs = 16;
|
|
30
|
-
|
|
31
|
-
const handleMouseMove = (e: MouseEvent) => {
|
|
32
|
-
const deltaX = e.clientX - startXRef.current;
|
|
33
|
-
const newWidth = Math.max(minWidth, Math.min(maxWidth, startWidthRef.current + deltaX));
|
|
34
|
-
|
|
35
|
-
const now = Date.now();
|
|
36
|
-
if (now - lastUpdateTime >= throttleMs) {
|
|
37
|
-
onResize(field, newWidth);
|
|
38
|
-
lastUpdateTime = now;
|
|
39
|
-
}
|
|
40
|
-
};
|
|
45
|
+
export function ColumnResizeHandle(props: ColumnResizeHandleProps) {
|
|
46
|
+
const { field, currentWidth, onResize, minWidth, maxWidth, edge = 'end' } = props;
|
|
47
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
48
|
+
const activeRef = useRef<ActiveResize | null>(null);
|
|
41
49
|
|
|
42
|
-
|
|
43
|
-
|
|
50
|
+
// A resize in progress ends with the handle: nothing is reported after unmount.
|
|
51
|
+
useEffect(() => () => { activeRef.current = null; }, []);
|
|
44
52
|
|
|
45
|
-
|
|
46
|
-
|
|
53
|
+
const widthAt = useCallback((active: ActiveResize, clientX: number) => {
|
|
54
|
+
const deltaX = clientX - active.startX;
|
|
55
|
+
const raw = active.startWidth + (edge === 'start' ? -deltaX : deltaX);
|
|
56
|
+
return clampResizeWidth(raw, active.startWidth, minWidth, maxWidth);
|
|
57
|
+
}, [edge, minWidth, maxWidth]);
|
|
47
58
|
|
|
48
|
-
|
|
59
|
+
const handlePointerDown = useCallback((event: React.PointerEvent<HTMLDivElement>) => {
|
|
60
|
+
if (event.button !== 0) return;
|
|
61
|
+
event.stopPropagation();
|
|
49
62
|
|
|
50
|
-
|
|
51
|
-
|
|
63
|
+
activeRef.current = {
|
|
64
|
+
pointerId: event.pointerId,
|
|
65
|
+
startX: event.clientX,
|
|
66
|
+
// The logical stored width: reliable for pinned, flex, and normal columns.
|
|
67
|
+
startWidth: currentWidth,
|
|
68
|
+
lastUpdate: 0,
|
|
69
|
+
moved: false,
|
|
52
70
|
};
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
71
|
+
setIsDragging(true);
|
|
72
|
+
// Keeps pointermove / pointerup coming to the handle wherever the pointer goes (also touch and pen).
|
|
73
|
+
try {
|
|
74
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
75
|
+
} catch {
|
|
76
|
+
// The pointer is no longer active (synthetic events); moves over the handle still work.
|
|
77
|
+
}
|
|
78
|
+
}, [currentWidth]);
|
|
79
|
+
|
|
80
|
+
const handlePointerMove = useCallback((event: React.PointerEvent<HTMLDivElement>) => {
|
|
81
|
+
const active = activeRef.current;
|
|
82
|
+
if (!active || event.pointerId !== active.pointerId) return;
|
|
83
|
+
if (event.clientX !== active.startX) active.moved = true;
|
|
84
|
+
if (!active.moved) return;
|
|
85
|
+
|
|
86
|
+
const now = Date.now();
|
|
87
|
+
if (now - active.lastUpdate >= THROTTLE_MS) {
|
|
88
|
+
active.lastUpdate = now;
|
|
89
|
+
onResize(field, widthAt(active, event.clientX));
|
|
90
|
+
}
|
|
91
|
+
}, [field, onResize, widthAt]);
|
|
92
|
+
|
|
93
|
+
const finish = useCallback((event: React.PointerEvent<HTMLDivElement>, commit: boolean) => {
|
|
94
|
+
const active = activeRef.current;
|
|
95
|
+
if (!active || event.pointerId !== active.pointerId) return;
|
|
96
|
+
activeRef.current = null;
|
|
97
|
+
setIsDragging(false);
|
|
98
|
+
if (active.moved || event.clientX !== active.startX) {
|
|
99
|
+
// The drag may end over the header cell, which would take the synthesized click as a sort.
|
|
100
|
+
suppressNextClick();
|
|
101
|
+
if (commit) onResize(field, widthAt(active, event.clientX));
|
|
102
|
+
}
|
|
103
|
+
}, [field, onResize, widthAt]);
|
|
104
|
+
|
|
105
|
+
const handleKeyDown = useCallback((event: React.KeyboardEvent<HTMLDivElement>) => {
|
|
106
|
+
if (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight') return;
|
|
107
|
+
event.preventDefault();
|
|
108
|
+
event.stopPropagation();
|
|
109
|
+
const step = (event.shiftKey ? 50 : 10) * (event.key === 'ArrowRight' ? 1 : -1);
|
|
110
|
+
onResize(field, clampResizeWidth(currentWidth + step, currentWidth, minWidth, maxWidth));
|
|
56
111
|
}, [field, currentWidth, onResize, minWidth, maxWidth]);
|
|
57
112
|
|
|
58
113
|
const handleDoubleClick = useCallback((event: React.MouseEvent) => {
|
|
@@ -62,22 +117,33 @@ export function ColumnResizeHandle(props: ColumnResizeHandleProps) {
|
|
|
62
117
|
|
|
63
118
|
const classNames = [
|
|
64
119
|
'ogx-column-resize-handle',
|
|
120
|
+
edge === 'start' && 'ogx-column-resize-handle--start',
|
|
65
121
|
isDragging && 'ogx-column-resize-handle--dragging'
|
|
66
122
|
].filter(Boolean).join(' ');
|
|
67
123
|
|
|
68
124
|
return (
|
|
69
125
|
<div
|
|
70
126
|
className={classNames}
|
|
71
|
-
|
|
127
|
+
onPointerDown={handlePointerDown}
|
|
128
|
+
// The mouse's default action would start dragging the (draggable) header or select text.
|
|
129
|
+
onMouseDown={preventMouseDefault}
|
|
130
|
+
onPointerMove={handlePointerMove}
|
|
131
|
+
onPointerUp={(e) => finish(e, true)}
|
|
132
|
+
onPointerCancel={(e) => finish(e, false)}
|
|
72
133
|
onClick={(e) => e.stopPropagation()}
|
|
73
134
|
onDragStart={(e) => {
|
|
74
135
|
e.preventDefault();
|
|
75
136
|
e.stopPropagation();
|
|
76
137
|
}}
|
|
77
138
|
onDoubleClick={handleDoubleClick}
|
|
139
|
+
onKeyDown={handleKeyDown}
|
|
78
140
|
role="separator"
|
|
79
141
|
aria-orientation="vertical"
|
|
80
142
|
aria-label={`Resize ${field} column`}
|
|
143
|
+
aria-valuenow={Math.round(currentWidth)}
|
|
144
|
+
aria-valuemin={Math.round(Math.min(minWidth ?? 50, currentWidth))}
|
|
145
|
+
aria-valuemax={maxWidth}
|
|
146
|
+
tabIndex={-1}
|
|
81
147
|
>
|
|
82
148
|
<div className="ogx-column-resize-handle__line" />
|
|
83
149
|
</div>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** The layout's minimum for a column without `minWidth` (see useLayout's flex solver). */
|
|
2
|
+
export const DEFAULT_MIN_COLUMN_WIDTH = 50;
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Clamps a width a resize asks for to the column's `minWidth` / `maxWidth`. Without `minWidth` the
|
|
6
|
+
* floor is 50px, and without `maxWidth` there is no ceiling. A column that already sits outside
|
|
7
|
+
* those bounds (a 30px icon column) is never snapped to them: its own width stays reachable.
|
|
8
|
+
*/
|
|
9
|
+
export function clampResizeWidth(width: number, startWidth: number, minWidth?: number, maxWidth?: number): number {
|
|
10
|
+
const min = Math.min(minWidth ?? DEFAULT_MIN_COLUMN_WIDTH, startWidth);
|
|
11
|
+
const max = Math.max(maxWidth ?? Infinity, startWidth);
|
|
12
|
+
return Math.max(min, Math.min(max, width));
|
|
13
|
+
}
|
|
@@ -109,6 +109,12 @@
|
|
|
109
109
|
font-size: var(--ogx-font-size-xs);
|
|
110
110
|
color: var(--ogx-overlay-text, var(--ogx-color-gray-900));
|
|
111
111
|
user-select: none;
|
|
112
|
+
cursor: inherit;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/* The column name fills the row, so a click anywhere after the checkbox toggles it. */
|
|
116
|
+
.ogx-column-visibility-panel__item-label .ogx-column-visibility-panel__label {
|
|
117
|
+
flex: 1;
|
|
112
118
|
}
|
|
113
119
|
|
|
114
120
|
.ogx-column-visibility-panel__footer {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { useState, useMemo, useRef } from 'react';
|
|
1
|
+
import { useState, useMemo, useRef, useId } from 'react';
|
|
2
2
|
import { Checkbox } from '../ui/Checkbox';
|
|
3
|
-
import type { GridColDef, GridRowModel } from '../../types';
|
|
3
|
+
import type { GridColDef, GridRowModel, GridValidRowModel } from '../../types';
|
|
4
4
|
|
|
5
5
|
|
|
6
|
-
export interface ColumnVisibilityPanelProps<R extends
|
|
6
|
+
export interface ColumnVisibilityPanelProps<R extends GridValidRowModel = GridRowModel> {
|
|
7
7
|
columns: GridColDef<R>[];
|
|
8
8
|
visibleColumns: Set<string>;
|
|
9
9
|
onVisibilityChange: (field: string, isVisible: boolean) => void;
|
|
@@ -43,11 +43,15 @@ function DragHandleIcon() {
|
|
|
43
43
|
);
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
export function ColumnVisibilityPanel<R extends
|
|
46
|
+
export function ColumnVisibilityPanel<R extends GridValidRowModel = GridRowModel>(
|
|
47
47
|
props: ColumnVisibilityPanelProps<R>
|
|
48
48
|
) {
|
|
49
49
|
const { columns, visibleColumns, onVisibilityChange, onShowAll, onHideAll, onColumnReorder, onColumnOrderReset, showNonHideableColumns } = props;
|
|
50
50
|
const [searchQuery, setSearchQuery] = useState('');
|
|
51
|
+
// Each checkbox is named by a <label htmlFor> holding the column name. (Wrapping Checkbox,
|
|
52
|
+
// which renders its own <label>, in another <label> nested labels and left the name to a
|
|
53
|
+
// generic fallback.)
|
|
54
|
+
const idBase = useId();
|
|
51
55
|
const [dragOverField, setDragOverField] = useState<string | null>(null);
|
|
52
56
|
const dragFieldRef = useRef<string | null>(null);
|
|
53
57
|
|
|
@@ -122,7 +126,8 @@ export function ColumnVisibilityPanel<R extends GridRowModel = GridRowModel>(
|
|
|
122
126
|
</div>
|
|
123
127
|
|
|
124
128
|
<div className="ogx-column-visibility-panel__list">
|
|
125
|
-
{filteredColumns.map(col => {
|
|
129
|
+
{filteredColumns.map((col, index) => {
|
|
130
|
+
const checkboxId = `${idBase}-col-${index}`;
|
|
126
131
|
const isVisible = visibleColumns.has(col.field);
|
|
127
132
|
const isHideable = col.hideable !== false;
|
|
128
133
|
const isDragOver = dragOverField === col.field;
|
|
@@ -152,35 +157,38 @@ export function ColumnVisibilityPanel<R extends GridRowModel = GridRowModel>(
|
|
|
152
157
|
<DragHandleIcon />
|
|
153
158
|
</span>
|
|
154
159
|
)}
|
|
155
|
-
<
|
|
160
|
+
<div
|
|
156
161
|
className="ogx-column-visibility-panel__item-label"
|
|
157
162
|
style={{ display: 'flex', alignItems: 'center', gap: 'var(--ogx-spacing-md)', flex: 1, cursor: isHideable ? 'pointer' : 'not-allowed' }}
|
|
158
163
|
>
|
|
159
164
|
<Checkbox
|
|
165
|
+
id={checkboxId}
|
|
160
166
|
checked={isVisible}
|
|
161
167
|
onChange={(e) => onVisibilityChange(col.field, e.target.checked)}
|
|
162
168
|
disabled={!isHideable}
|
|
163
169
|
/>
|
|
164
|
-
<
|
|
170
|
+
<label htmlFor={checkboxId} className="ogx-column-visibility-panel__label">
|
|
165
171
|
{col.headerName || col.field}
|
|
166
|
-
</
|
|
167
|
-
</
|
|
172
|
+
</label>
|
|
173
|
+
</div>
|
|
168
174
|
</div>
|
|
169
175
|
);
|
|
170
176
|
})}
|
|
171
177
|
</div>
|
|
172
178
|
|
|
173
179
|
<div className="ogx-column-visibility-panel__footer">
|
|
174
|
-
<
|
|
180
|
+
<div className="ogx-column-visibility-panel__item">
|
|
175
181
|
<Checkbox
|
|
182
|
+
id={`${idBase}-all`}
|
|
176
183
|
checked={allVisible}
|
|
177
184
|
onChange={handleToggleAll}
|
|
178
185
|
/>
|
|
179
|
-
<
|
|
186
|
+
<label htmlFor={`${idBase}-all`} className="ogx-column-visibility-panel__label">
|
|
180
187
|
Show/Hide All
|
|
181
|
-
</
|
|
182
|
-
</
|
|
188
|
+
</label>
|
|
189
|
+
</div>
|
|
183
190
|
<button
|
|
191
|
+
type="button"
|
|
184
192
|
className="ogx-column-visibility-panel__reset-btn"
|
|
185
193
|
onClick={() => { onShowAll(); onColumnOrderReset?.(); }}
|
|
186
194
|
>
|