@opencorestack/opengridx 3.2.2 → 3.4.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 +53 -0
- package/README.md +16 -2
- package/dist/ai.cjs +1 -0
- package/dist/ai.cjs.map +1 -0
- package/dist/ai.d.ts +166 -0
- package/dist/ai.es.js +1 -0
- package/dist/ai.es.js.map +1 -0
- package/dist/index.d.ts +321 -3
- package/dist/opengridx.css +1 -1
- package/dist/opengridx.es.js +6483 -5291
- package/dist/opengridx.es.js.map +1 -1
- package/dist/opengridx.umd.js +16 -13
- package/dist/opengridx.umd.js.map +1 -1
- package/docs/API_REFERENCE.md +132 -3
- package/docs/README.md +4 -1
- package/docs/architecture/datagrid-orchestration.md +23 -6
- package/docs/components/header.md +25 -0
- package/docs/customization/theming.md +12 -1
- package/docs/features/ai-toolkit.md +119 -0
- package/docs/features/cell-selection.md +131 -0
- package/docs/features/clipboard.md +90 -0
- package/docs/features/editing-reordering.md +50 -0
- package/docs/features/keyboard-navigation.md +25 -2
- package/docs/features/undo-redo.md +79 -0
- package/docs/roadmap.md +64 -7
- package/lib/ai/coerce.ts +90 -0
- package/lib/ai/columns.ts +90 -0
- package/lib/ai/index.ts +16 -0
- package/lib/ai/schema.ts +214 -0
- package/lib/ai/types.ts +88 -0
- package/lib/ai/validate.ts +286 -0
- package/lib/components/Cell/Cell.css +35 -0
- package/lib/components/Cell/Cell.tsx +25 -13
- package/lib/components/Cell/CellErrorBoundary.tsx +3 -0
- package/lib/components/Cell/GridEditInputCell.css +12 -4
- package/lib/components/Cell/GridEditInputCell.tsx +19 -12
- package/lib/components/CellSelectionStats/CellSelectionStats.css +31 -0
- package/lib/components/CellSelectionStats/CellSelectionStats.tsx +32 -0
- package/lib/components/CellSelectionStats/GridCellSelectionStatsArea.tsx +24 -0
- package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.css +14 -0
- package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.tsx +3 -2
- package/lib/components/DataGrid/DataGrid.tsx +123 -3
- package/lib/components/DataGrid/GridLiveRegion.tsx +8 -2
- package/lib/components/DataGrid/GridPinnedRows.tsx +9 -0
- package/lib/components/DataGrid/GridToolbarSlot.tsx +5 -0
- package/lib/components/DataGrid/GridVirtualRows.tsx +9 -0
- package/lib/components/FilterPanel/FilterPanel.css +14 -0
- package/lib/components/FilterPanel/FilterPanel.tsx +5 -3
- package/lib/components/Header/Header.css +12 -0
- package/lib/components/Header/Header.tsx +17 -4
- package/lib/components/ListView/ListView.css +2 -4
- package/lib/components/ListView/ListViewRow.tsx +2 -2
- package/lib/components/Row/Row.tsx +38 -1
- package/lib/components/Toolbar/GlobalSearch.css +16 -0
- package/lib/components/Toolbar/GlobalSearch.tsx +3 -2
- package/lib/components/Toolbar/GridToolbar.tsx +10 -4
- package/lib/components/ui/Checkbox.tsx +26 -10
- package/lib/components/ui/Input.css +43 -0
- package/lib/components/ui/Input.tsx +24 -5
- package/lib/hooks/core/gridApiPlaceholder.ts +14 -1
- package/lib/hooks/core/useDataGrid.ts +27 -0
- package/lib/hooks/core/useGridCellSelection.ts +631 -0
- package/lib/hooks/core/useGridCellSelectionApi.ts +97 -0
- package/lib/hooks/core/useGridClipboardApi.ts +8 -2
- package/lib/hooks/core/useGridClipboardPaste.ts +283 -0
- package/lib/hooks/core/useGridControlledState.ts +38 -0
- package/lib/hooks/core/useGridKeyboardNavigation.ts +32 -0
- package/lib/hooks/core/useGridRowInteractions.ts +8 -4
- package/lib/hooks/core/useGridUndoRedo.ts +220 -0
- package/lib/hooks/features/useAggregation.ts +8 -6
- package/lib/hooks/features/useGridBatchEdit.ts +217 -0
- package/lib/hooks/features/useGridClipboard.ts +47 -2
- package/lib/hooks/features/useGridEditing.ts +39 -51
- package/lib/index.ts +16 -0
- package/lib/styles/base/variables.css +6 -1
- package/lib/styles/opengridx.css +3 -1
- package/lib/theme/DataGridThemeProvider.tsx +4 -0
- package/lib/theme/presets.ts +8 -0
- package/lib/theme/types.ts +4 -0
- package/lib/types/index.ts +254 -0
- package/lib/utils/attempt.ts +14 -0
- package/lib/utils/cellSelection.ts +238 -0
- package/lib/utils/editing/commit.ts +69 -0
- package/lib/utils/editing/targets.ts +109 -0
- package/lib/utils/headerWrap.ts +23 -0
- package/lib/utils/parsing.ts +252 -0
- package/llms.txt +71 -2
- package/package.json +14 -4
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useState, useMemo, useRef, useId } from 'react';
|
|
2
2
|
import { Checkbox } from '../ui/Checkbox';
|
|
3
|
+
import { Input } from '../ui/Input';
|
|
3
4
|
import type { GridColDef, GridRowModel, GridValidRowModel } from '../../types';
|
|
4
5
|
|
|
5
6
|
|
|
@@ -115,9 +116,9 @@ export function ColumnVisibilityPanel<R extends GridValidRowModel = GridRowModel
|
|
|
115
116
|
<div className="ogx-column-visibility-panel__search-section">
|
|
116
117
|
<div className="ogx-column-visibility-panel__search-container">
|
|
117
118
|
<SearchIcon />
|
|
118
|
-
<
|
|
119
|
+
<Input
|
|
119
120
|
autoComplete="off"
|
|
120
|
-
|
|
121
|
+
inputClassName="ogx-column-visibility-panel__search-input"
|
|
121
122
|
placeholder="Search"
|
|
122
123
|
value={searchQuery}
|
|
123
124
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
@@ -27,6 +27,9 @@ import { useDataGrid } from '../../hooks/core/useDataGrid';
|
|
|
27
27
|
import { useRowReorder } from '../../hooks/useRowReorder';
|
|
28
28
|
import { useGridHierarchy } from '../../hooks/core/useGridHierarchy';
|
|
29
29
|
import { useGridEditing } from '../../hooks/features/useGridEditing';
|
|
30
|
+
import { useGridBatchEdit } from '../../hooks/features/useGridBatchEdit';
|
|
31
|
+
import { useGridClipboardPaste } from '../../hooks/core/useGridClipboardPaste';
|
|
32
|
+
import { useGridUndoRedo, useGridEditCommitChannel, UNDO_HISTORY_LIMIT } from '../../hooks/core/useGridUndoRedo';
|
|
30
33
|
import { useGridSpanning } from '../../hooks/features/useGridSpanning';
|
|
31
34
|
import { useGridSpanRowWindow, useGridSpanColumnWindow } from '../../hooks/features/useGridSpanRenderWindow';
|
|
32
35
|
import { useColumnGroupReorderGuard } from '../../hooks/features/useColumnGroupReorderGuard';
|
|
@@ -52,6 +55,9 @@ import { useGridAggregationApi } from '../../hooks/core/useGridAggregationApi';
|
|
|
52
55
|
import { useGridClipboardApi } from '../../hooks/core/useGridClipboardApi';
|
|
53
56
|
import { useGridScrollToIndexesApi } from '../../hooks/core/useGridScrollToIndexesApi';
|
|
54
57
|
import { useGridColumnAutosize } from '../../hooks/core/useGridColumnAutosize';
|
|
58
|
+
import { useGridCellSelection } from '../../hooks/core/useGridCellSelection';
|
|
59
|
+
import { useGridCellSelectionApi } from '../../hooks/core/useGridCellSelectionApi';
|
|
60
|
+
import { GridCellSelectionStatsArea } from '../CellSelectionStats/GridCellSelectionStatsArea';
|
|
55
61
|
import { useGridSortHandlers, useGridColumnMenuHandlers } from '../../hooks/core/useGridHeaderHandlers';
|
|
56
62
|
import { useGridKeyboardMode, useGridPointerFocusHandlers } from '../../hooks/core/useGridFocusHandlers';
|
|
57
63
|
import { getPaginationRowCount, pickPaginationLocaleText } from '../../utils/pagination';
|
|
@@ -76,12 +82,23 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
76
82
|
getRowId,
|
|
77
83
|
rowHeight: rowHeightProp,
|
|
78
84
|
headerHeight: headerHeightProp,
|
|
85
|
+
wrapHeaderText = false,
|
|
79
86
|
autoHeight = false,
|
|
80
87
|
density,
|
|
81
88
|
checkboxSelection = false,
|
|
82
89
|
disableRowSelectionOnClick = false,
|
|
83
90
|
disableMultipleRowSelection = false,
|
|
84
91
|
disableClipboardCopy = false,
|
|
92
|
+
cellSelection: cellSelectionProp = false,
|
|
93
|
+
cellSelectionModel: propCellSelectionModel,
|
|
94
|
+
onCellSelectionModelChange,
|
|
95
|
+
showCellSelectionStats = false,
|
|
96
|
+
disableClipboardPaste = false,
|
|
97
|
+
disableRangeClear = false,
|
|
98
|
+
onClipboardPaste,
|
|
99
|
+
onBeforeClipboardPaste,
|
|
100
|
+
undoRedo = false,
|
|
101
|
+
onHistoryChange,
|
|
85
102
|
rowSelectionModel: propRowSelectionModel,
|
|
86
103
|
onRowSelectionModelChange: propOnRowSelectionModelChange,
|
|
87
104
|
onRowClick,
|
|
@@ -184,6 +201,8 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
184
201
|
rowSelectionModel: propRowSelectionModel,
|
|
185
202
|
onRowSelectionModelChange: propOnRowSelectionModelChange,
|
|
186
203
|
density,
|
|
204
|
+
cellSelectionModel: propCellSelectionModel,
|
|
205
|
+
onCellSelectionModelChange,
|
|
187
206
|
});
|
|
188
207
|
|
|
189
208
|
const {
|
|
@@ -261,7 +280,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
261
280
|
syncRows: !dataSource,
|
|
262
281
|
});
|
|
263
282
|
const {
|
|
264
|
-
state, apiRef, setRows,
|
|
283
|
+
state, apiRef, setRows, replaceRows, setColumns, setDimensions, setDataSourceLoading, setDataSourceError,
|
|
265
284
|
setRowCount
|
|
266
285
|
} = gridData;
|
|
267
286
|
|
|
@@ -285,6 +304,18 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
285
304
|
getAggregationPosition, columnLookup, filterMode, sortingMode,
|
|
286
305
|
});
|
|
287
306
|
|
|
307
|
+
// Commits (single edits and batch edits) reach the undo/redo history created further down.
|
|
308
|
+
const editCommitChannel = useGridEditCommitChannel();
|
|
309
|
+
// Batch edits (paste, range clear, undo, redo): one processRowUpdate per row, one store update.
|
|
310
|
+
const batchEdit = useGridBatchEdit<R>({
|
|
311
|
+
rowsLookup: state.rows.idRowsLookup as unknown as ReadonlyMap<GridRowId, R>,
|
|
312
|
+
columns: activeColumns,
|
|
313
|
+
processRowUpdate,
|
|
314
|
+
onProcessRowUpdateError,
|
|
315
|
+
replaceRows: replaceRows as unknown as (rows: ReadonlyMap<GridRowId, R>) => void,
|
|
316
|
+
onCommitted: editCommitChannel.emit,
|
|
317
|
+
});
|
|
318
|
+
|
|
288
319
|
const editingHandlers = useGridEditing({
|
|
289
320
|
rows: effectiveRows,
|
|
290
321
|
getRowId: getRowIdOf,
|
|
@@ -296,8 +327,9 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
296
327
|
// after an awaited processRowUpdate, by which time the rows prop (or a dataSource
|
|
297
328
|
// fetch) may have replaced the rows this render saw. The returned row is stored as is
|
|
298
329
|
// (the grid never writes an id onto it), even if it does not carry its key itself.
|
|
299
|
-
|
|
330
|
+
batchEdit.storeRow(rowId, updatedRow);
|
|
300
331
|
},
|
|
332
|
+
onCommitted: (changes) => editCommitChannel.emit(changes, 'edit'),
|
|
301
333
|
});
|
|
302
334
|
|
|
303
335
|
// The selection without ids of removed rows. Only client-owned rows are pruned: a server page
|
|
@@ -493,12 +525,34 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
493
525
|
[layout.leftPinnedCols, layout.unpinnedColsWithWidth, layout.rightPinnedCols]
|
|
494
526
|
);
|
|
495
527
|
|
|
528
|
+
// Cell range selection (cellSelection): the range, pointer drag and Shift-keys, copy text, totals.
|
|
529
|
+
const cellSelection = useGridCellSelection<R>({
|
|
530
|
+
enabled: cellSelectionProp && !listView,
|
|
531
|
+
model: controlledState.cellSelectionModel,
|
|
532
|
+
onModelChange: controlledState.handleCellSelectionModelChange,
|
|
533
|
+
getPendingModel: controlledState.getPendingCellSelectionModel,
|
|
534
|
+
allRenderableRows,
|
|
535
|
+
getRowId: getRowIdOf,
|
|
536
|
+
navigationColumns,
|
|
537
|
+
columnIndexMap,
|
|
538
|
+
spanning,
|
|
539
|
+
rowMetaMap,
|
|
540
|
+
layout,
|
|
541
|
+
rowHeight: effectiveRowHeight,
|
|
542
|
+
pinnedTopRowCount: pinnedTopRows.length,
|
|
543
|
+
viewportRef,
|
|
544
|
+
containerRef,
|
|
545
|
+
localeText,
|
|
546
|
+
showStats: showCellSelectionStats,
|
|
547
|
+
});
|
|
548
|
+
|
|
496
549
|
useGridClipboardApi({
|
|
497
550
|
apiRef,
|
|
498
551
|
columns: renderedDataColumns as unknown as GridColDef[],
|
|
499
552
|
getRowId: getRowIdOf,
|
|
500
553
|
containerRef,
|
|
501
554
|
disableKeyboardShortcut: disableClipboardCopy,
|
|
555
|
+
getCellRangeCopyText: cellSelection.enabled ? cellSelection.getCopyText : undefined,
|
|
502
556
|
});
|
|
503
557
|
useGridScrollToIndexesApi<R>({ apiRef, viewportRef, layout, rowHeight: effectiveRowHeight });
|
|
504
558
|
const handleColumnAutosize = useGridColumnAutosize<R>({ apiRef, containerRef, columns: orderedColumns, onColumnResize: handleColumnResize });
|
|
@@ -541,11 +595,61 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
541
595
|
pinCheckboxColumn,
|
|
542
596
|
pinExpandColumn,
|
|
543
597
|
rowHeight: effectiveRowHeight,
|
|
598
|
+
cellSelection: cellSelection.keyboard,
|
|
544
599
|
});
|
|
545
600
|
|
|
546
601
|
const { handleCellClick, handleHeaderClick } = useGridPointerFocusHandlers<R>({
|
|
547
602
|
ensureGridFocus, setKeyboardMode, setFocusedCell, getRowId: getRowIdOf, onCellClick,
|
|
548
603
|
});
|
|
604
|
+
// Keeps the range anchor and the focused cell together; installs the cell selection API.
|
|
605
|
+
useGridCellSelectionApi({
|
|
606
|
+
apiRef,
|
|
607
|
+
selection: cellSelection,
|
|
608
|
+
model: controlledState.cellSelectionModel,
|
|
609
|
+
getPendingModel: controlledState.getPendingCellSelectionModel,
|
|
610
|
+
focusedCell,
|
|
611
|
+
setFocusedCell,
|
|
612
|
+
containerRef,
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
// Ctrl/Cmd+V and Delete on a range (cellSelection); apiRef.pasteText.
|
|
616
|
+
useGridClipboardPaste<R>({
|
|
617
|
+
apiRef, containerRef, viewportRef,
|
|
618
|
+
selection: cellSelection,
|
|
619
|
+
disableClipboardPaste, disableRangeClear,
|
|
620
|
+
focusedCell,
|
|
621
|
+
allRenderableRows,
|
|
622
|
+
getRowId: getRowIdOf,
|
|
623
|
+
navigationColumns: navigationColumns as unknown as GridColDef<R>[],
|
|
624
|
+
isCellEditable,
|
|
625
|
+
rowMetaMap,
|
|
626
|
+
getSpanOrigin: spanning.getSpanOrigin,
|
|
627
|
+
isEditing: editingHandlers.editingCell !== null,
|
|
628
|
+
batchEdit,
|
|
629
|
+
onClipboardPaste,
|
|
630
|
+
onBeforeClipboardPaste,
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
// Undo / redo (undoRedo): history of committed edits, keys and apiRef methods.
|
|
634
|
+
useGridUndoRedo<R>({
|
|
635
|
+
apiRef,
|
|
636
|
+
enabled: Boolean(undoRedo),
|
|
637
|
+
limit: (typeof undoRedo === 'object' && undoRedo.limit) || UNDO_HISTORY_LIMIT,
|
|
638
|
+
onHistoryChange,
|
|
639
|
+
channel: editCommitChannel,
|
|
640
|
+
batchEdit,
|
|
641
|
+
isEditing: editingHandlers.editingCell !== null,
|
|
642
|
+
containerRef, viewportRef,
|
|
643
|
+
selection: cellSelection,
|
|
644
|
+
setFocusedCell,
|
|
645
|
+
allRenderableRows,
|
|
646
|
+
getRowId: getRowIdOf,
|
|
647
|
+
navigationColumns: navigationColumns as unknown as GridColDef<R>[],
|
|
648
|
+
layout,
|
|
649
|
+
rowHeight: effectiveRowHeight,
|
|
650
|
+
pinnedTopRowCount: pinnedTopRows.length,
|
|
651
|
+
});
|
|
652
|
+
|
|
549
653
|
const { handleHideColumn, handlePinColumn } = useGridColumnMenuHandlers({
|
|
550
654
|
columnVisibilityModel,
|
|
551
655
|
onColumnVisibilityModelChange: handleColumnVisibilityModelChange,
|
|
@@ -658,6 +762,8 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
658
762
|
rowSpanningCaches: spanning.rowSpanningCaches,
|
|
659
763
|
rowHeight: effectiveRowHeight,
|
|
660
764
|
rowMetaMap,
|
|
765
|
+
cellRange: cellSelection.displayRange,
|
|
766
|
+
cellRangeHasRowSpan: cellSelection.hasRowSpan,
|
|
661
767
|
};
|
|
662
768
|
|
|
663
769
|
return (
|
|
@@ -746,11 +852,13 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
746
852
|
aria-label={ariaLabel || 'Data grid'}
|
|
747
853
|
aria-rowcount={ariaRows.rowCount}
|
|
748
854
|
aria-colcount={navigationColumns.length}
|
|
749
|
-
aria-multiselectable={rowSelectionEnabled && !disableMultipleRowSelection}
|
|
855
|
+
aria-multiselectable={(rowSelectionEnabled && !disableMultipleRowSelection) || cellSelection.enabled}
|
|
750
856
|
aria-busy={effectiveLoading}
|
|
751
857
|
tabIndex={viewportTabIndex}
|
|
752
858
|
onKeyDownCapture={() => { setKeyboardMode(true); }}
|
|
753
859
|
onMouseDownCapture={handleMouseDownCapture}
|
|
860
|
+
onMouseDown={cellSelection.handleMouseDown}
|
|
861
|
+
onClickCapture={cellSelection.handleClickCapture}
|
|
754
862
|
onKeyDown={handleKeyDown}
|
|
755
863
|
onFocus={handleFocus}
|
|
756
864
|
onBlur={handleBlur}
|
|
@@ -802,6 +910,8 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
802
910
|
onHideColumn={handleHideColumn}
|
|
803
911
|
onManageColumns={columnsPanel.openColumnsPanel}
|
|
804
912
|
onPinColumn={handlePinColumn}
|
|
913
|
+
wrapHeaderText={wrapHeaderText}
|
|
914
|
+
headerHeight={headerHeight}
|
|
805
915
|
/>
|
|
806
916
|
|
|
807
917
|
<GridPinnedRows<R>
|
|
@@ -881,6 +991,15 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
881
991
|
</div>
|
|
882
992
|
)}
|
|
883
993
|
|
|
994
|
+
{cellSelection.enabled && showCellSelectionStats && (
|
|
995
|
+
<GridCellSelectionStatsArea
|
|
996
|
+
stats={cellSelection.stats}
|
|
997
|
+
apiRef={gridData.apiRef}
|
|
998
|
+
slot={slots?.cellSelectionStats}
|
|
999
|
+
slotProps={slotProps?.cellSelectionStats}
|
|
1000
|
+
/>
|
|
1001
|
+
)}
|
|
1002
|
+
|
|
884
1003
|
{FooterSlot && (
|
|
885
1004
|
<FooterSlot
|
|
886
1005
|
apiRef={gridData.apiRef}
|
|
@@ -915,6 +1034,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
915
1034
|
filteredRowCount={filteredRows.length}
|
|
916
1035
|
dataRowCount={dataRows.length}
|
|
917
1036
|
filterModel={filterModel}
|
|
1037
|
+
cellSelectionAnnouncement={cellSelection.announcement}
|
|
918
1038
|
/>
|
|
919
1039
|
|
|
920
1040
|
{showLoadingOverRows && (
|
|
@@ -11,10 +11,15 @@ interface GridLiveRegionProps {
|
|
|
11
11
|
/** Data rows that pass the filter: the "n rows found" count. */
|
|
12
12
|
dataRowCount: number;
|
|
13
13
|
filterModel: GridFilterModel | undefined;
|
|
14
|
+
/** The size of the selected cell range (`cellSelection`), already debounced. */
|
|
15
|
+
cellSelectionAnnouncement?: string;
|
|
14
16
|
}
|
|
15
17
|
|
|
16
|
-
/**
|
|
17
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Polite status announcements: loading, a load error, no rows, or the row count of an active filter;
|
|
20
|
+
* and the size of the selected cell range.
|
|
21
|
+
*/
|
|
22
|
+
export function GridLiveRegion({ loading, error, noRowsLabel, filteredRowCount, dataRowCount, filterModel, cellSelectionAnnouncement }: GridLiveRegionProps) {
|
|
18
23
|
return (
|
|
19
24
|
<div className="ogx-aria-live-status" role="status" aria-live="polite">
|
|
20
25
|
{loading ? 'Loading data...' : ''}
|
|
@@ -26,6 +31,7 @@ export function GridLiveRegion({ loading, error, noRowsLabel, filteredRowCount,
|
|
|
26
31
|
? `${dataRowCount} ${dataRowCount === 1 ? 'row' : 'rows'} found`
|
|
27
32
|
: ''
|
|
28
33
|
)}
|
|
34
|
+
{cellSelectionAnnouncement ? <span className="ogx-aria-live-status__cells"> {cellSelectionAnnouncement}</span> : null}
|
|
29
35
|
</div>
|
|
30
36
|
);
|
|
31
37
|
}
|
|
@@ -15,6 +15,8 @@ import type {
|
|
|
15
15
|
GridRowMeta,
|
|
16
16
|
} from '../../types';
|
|
17
17
|
import type { CellColSpanInfo, RowSpanningCaches } from '../../hooks/features/useGridSpanning';
|
|
18
|
+
import type { GridDisplayCellRange } from '../../hooks/core/useGridCellSelection';
|
|
19
|
+
import { getRowCellRange } from '../../utils/cellSelection';
|
|
18
20
|
import type { GridEditingState } from '../../hooks/features/useGridEditing';
|
|
19
21
|
|
|
20
22
|
interface PinnedRowEditingHandlers {
|
|
@@ -58,6 +60,10 @@ export interface GridPinnedRowsProps<R extends GridRowModel> {
|
|
|
58
60
|
/** `aria-rowindex` of the first of these rows. */
|
|
59
61
|
ariaRowIndexBase?: number;
|
|
60
62
|
columnIndexMap?: Map<string, number>;
|
|
63
|
+
/** The selected cell range to draw (`cellSelection`), in renderable-row / data-column indices. */
|
|
64
|
+
cellRange?: GridDisplayCellRange | null;
|
|
65
|
+
/** The grid has row spans: rows tell their cells how far the range bottom is. */
|
|
66
|
+
cellRangeHasRowSpan?: boolean;
|
|
61
67
|
/** Pinned rows are edited like any other row. */
|
|
62
68
|
editingHandlers?: PinnedRowEditingHandlers;
|
|
63
69
|
isCellEditable?: (params: GridCellParams<R>) => boolean;
|
|
@@ -103,6 +109,8 @@ export function GridPinnedRows<R extends GridRowModel>({
|
|
|
103
109
|
editingHandlers,
|
|
104
110
|
isCellEditable,
|
|
105
111
|
rowReordering = false,
|
|
112
|
+
cellRange,
|
|
113
|
+
cellRangeHasRowSpan = false,
|
|
106
114
|
}: GridPinnedRowsProps<R>) {
|
|
107
115
|
if (rows.length === 0) return null;
|
|
108
116
|
|
|
@@ -148,6 +156,7 @@ export function GridPinnedRows<R extends GridRowModel>({
|
|
|
148
156
|
onEditCellValueChange={editingHandlers?.setEditCellValue}
|
|
149
157
|
isCellEditable={isCellEditable}
|
|
150
158
|
rowReordering={rowReordering}
|
|
159
|
+
cellRange={getRowCellRange(cellRange, rowIndex, cellRangeHasRowSpan)}
|
|
151
160
|
/>
|
|
152
161
|
);
|
|
153
162
|
})}
|
|
@@ -25,8 +25,13 @@ function isPlainFunctionComponent(component: React.ComponentType<ToolbarProps>):
|
|
|
25
25
|
* Runs a plain function toolbar inside its own fiber. The caller keys this element on the
|
|
26
26
|
* function's source, so the fiber (and every hook in it) survives when the consumer redefines
|
|
27
27
|
* the same toolbar inline on each render, but remounts when a different toolbar is passed.
|
|
28
|
+
*
|
|
29
|
+
* `render` calls hooks, so it must run on every render. React Compiler would memoize the call
|
|
30
|
+
* like any other and skip it when `render` and `toolbarProps` are unchanged, which breaks the
|
|
31
|
+
* toolbar's hooks; `'use no memo'` keeps this wrapper out of the compiler.
|
|
28
32
|
*/
|
|
29
33
|
function InlineToolbar({ render, toolbarProps }: InlineToolbarProps) {
|
|
34
|
+
'use no memo';
|
|
30
35
|
return <>{render(toolbarProps)}</>;
|
|
31
36
|
}
|
|
32
37
|
|
|
@@ -18,6 +18,8 @@ import type { GridVisibleRow } from '../../hooks/core/useGridVisibleRows';
|
|
|
18
18
|
import type { UseRowReorderReturn } from '../../hooks/useRowReorder';
|
|
19
19
|
import type { GridEditingState } from '../../hooks/features/useGridEditing';
|
|
20
20
|
import type { CellColSpanInfo, RowSpanningCaches } from '../../hooks/features/useGridSpanning';
|
|
21
|
+
import type { GridDisplayCellRange } from '../../hooks/core/useGridCellSelection';
|
|
22
|
+
import { getRowCellRange } from '../../utils/cellSelection';
|
|
21
23
|
|
|
22
24
|
interface RowEditingHandlers {
|
|
23
25
|
editingCell: GridEditingState['editingCell'];
|
|
@@ -71,6 +73,10 @@ export interface GridVirtualRowsProps<R extends GridRowModel> {
|
|
|
71
73
|
/** Added to a row's rowIndex to get its `aria-rowindex` (header rows and earlier pages included). */
|
|
72
74
|
ariaRowIndexOffset?: number;
|
|
73
75
|
columnIndexMap?: Map<string, number>;
|
|
76
|
+
/** The selected cell range to draw (`cellSelection`), in renderable-row / data-column indices. */
|
|
77
|
+
cellRange?: GridDisplayCellRange | null;
|
|
78
|
+
/** The grid has row spans: rows tell their cells how far the range bottom is. */
|
|
79
|
+
cellRangeHasRowSpan?: boolean;
|
|
74
80
|
}
|
|
75
81
|
|
|
76
82
|
export function GridVirtualRows<R extends GridRowModel>({
|
|
@@ -115,6 +121,8 @@ export function GridVirtualRows<R extends GridRowModel>({
|
|
|
115
121
|
loadingOverlay,
|
|
116
122
|
ariaRowIndexOffset,
|
|
117
123
|
columnIndexMap,
|
|
124
|
+
cellRange,
|
|
125
|
+
cellRangeHasRowSpan = false,
|
|
118
126
|
}: GridVirtualRowsProps<R>) {
|
|
119
127
|
const skeletonColumns: GridColDef<R>[] = baseColumns.length > 0
|
|
120
128
|
? baseColumns
|
|
@@ -206,6 +214,7 @@ export function GridVirtualRows<R extends GridRowModel>({
|
|
|
206
214
|
rowSpanningCaches={rowSpanningCaches}
|
|
207
215
|
rowHeight={rowHeight}
|
|
208
216
|
rowMeta={rowMetaMap.get(id)}
|
|
217
|
+
cellRange={getRowCellRange(cellRange, actualIndex, cellRangeHasRowSpan)}
|
|
209
218
|
/>
|
|
210
219
|
))
|
|
211
220
|
)}
|
|
@@ -115,11 +115,25 @@
|
|
|
115
115
|
border-radius: var(--ogx-border-radius-sm);
|
|
116
116
|
font-family: var(--ogx-font-family);
|
|
117
117
|
font-size: var(--ogx-font-size-xs);
|
|
118
|
+
line-height: revert;
|
|
118
119
|
background: var(--ogx-grid-background);
|
|
119
120
|
color: var(--ogx-grid-row-text);
|
|
120
121
|
outline: none;
|
|
121
122
|
transition: border-color 0.15s, box-shadow 0.15s;
|
|
122
123
|
}
|
|
124
|
+
/* The text/date value box renders through the shared Input: the <input> keeps its own border,
|
|
125
|
+
so the wrapper draws nothing and only takes the input's place in the row. */
|
|
126
|
+
.ogx-filter__controls .ogx-input-wrapper {
|
|
127
|
+
flex: 1;
|
|
128
|
+
min-width: 60px;
|
|
129
|
+
border: none;
|
|
130
|
+
border-radius: 0;
|
|
131
|
+
background: transparent;
|
|
132
|
+
transition: none;
|
|
133
|
+
}
|
|
134
|
+
.ogx-filter__controls .ogx-input-wrapper:focus-within {
|
|
135
|
+
box-shadow: none;
|
|
136
|
+
}
|
|
123
137
|
.ogx-filter__value-input::placeholder {
|
|
124
138
|
color: var(--ogx-overlay-text, var(--ogx-color-gray-400));
|
|
125
139
|
opacity: 0.6;
|
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
} from '../../types';
|
|
9
9
|
import { getOperatorsForType, NO_VALUE_OPERATORS } from '../../utils/filtering';
|
|
10
10
|
import { toLocalDateString } from '../../utils/values';
|
|
11
|
+
import { Input } from '../ui/Input';
|
|
11
12
|
|
|
12
13
|
|
|
13
14
|
export interface FilterPanelProps {
|
|
@@ -107,7 +108,8 @@ const FilterRow: React.FC<{
|
|
|
107
108
|
onChange: (item: GridFilterItem | null) => void;
|
|
108
109
|
}> = ({ col, item, resetToken, onChange }) => {
|
|
109
110
|
const filterId = useId();
|
|
110
|
-
|
|
111
|
+
// Memoized so React Compiler treats the list as frozen and can keep the callbacks below memoized.
|
|
112
|
+
const typeOperators = useMemo(() => getOperatorsForType(col.type), [col.type]);
|
|
111
113
|
const defaultOperator = typeOperators[0];
|
|
112
114
|
const currentOperator: GridFilterOperator = item?.operator ?? defaultOperator;
|
|
113
115
|
// An operator set programmatically that the column type does not list is still shown, so the
|
|
@@ -250,10 +252,10 @@ const FilterRow: React.FC<{
|
|
|
250
252
|
);
|
|
251
253
|
} else if (showValue) {
|
|
252
254
|
valueControl = (
|
|
253
|
-
<
|
|
255
|
+
<Input
|
|
254
256
|
id={`${filterId}-val`}
|
|
255
257
|
name={`filter-val-${col.field}`}
|
|
256
|
-
|
|
258
|
+
inputClassName={valueClassName}
|
|
257
259
|
type={col.type === 'date' ? 'date' : 'text'}
|
|
258
260
|
value={draft ?? toInputText(item?.value, col)}
|
|
259
261
|
onChange={(e) => setDraft(e.target.value)}
|
|
@@ -68,6 +68,18 @@
|
|
|
68
68
|
font-weight: 600;
|
|
69
69
|
letter-spacing: -0.01em;
|
|
70
70
|
}
|
|
71
|
+
/* Wrapped header title (wrapHeaderText): as many lines as fit in --ogx-header-height, the line count
|
|
72
|
+
is set inline as -webkit-line-clamp (getHeaderLineClamp). The line-height must match
|
|
73
|
+
HEADER_WRAP_LINE_HEIGHT in utils/headerWrap.ts. max-height keeps the header at its height even when
|
|
74
|
+
a theme changes the font or padding. */
|
|
75
|
+
.ogx__header-cell--wrap .ogx__header-cell-title {
|
|
76
|
+
display: -webkit-box;
|
|
77
|
+
-webkit-box-orient: vertical;
|
|
78
|
+
white-space: normal;
|
|
79
|
+
overflow-wrap: break-word;
|
|
80
|
+
line-height: 16px;
|
|
81
|
+
max-height: calc(var(--ogx-header-height, 56px) - 2 * var(--ogx-grid-cell-padding-y, 8px));
|
|
82
|
+
}
|
|
71
83
|
.ogx__header-cell-aggregation {
|
|
72
84
|
font-size: 0.6rem;
|
|
73
85
|
font-weight: 500;
|
|
@@ -7,6 +7,7 @@ import { isColumnPinned, calculatePinnedPositions, getPinnedEdgeFields } from '.
|
|
|
7
7
|
import { getRenderedColumnWidth } from '../../utils/columnWidth';
|
|
8
8
|
import { buildColumnGroupRow, getColumnGroupDepth, getColumnGroupPaths } from '../../utils/columnGroups';
|
|
9
9
|
import { ColumnMenu } from './ColumnMenu';
|
|
10
|
+
import { getHeaderLineClamp, shouldWrapHeaderText } from '../../utils/headerWrap';
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
function MenuIcon() {
|
|
@@ -69,6 +70,10 @@ export interface HeaderProps<R extends GridRowModel = GridRowModel> {
|
|
|
69
70
|
onManageColumns?: () => void;
|
|
70
71
|
/** Position of each visible data column in render order, for `colIndex` and `aria-colindex`. */
|
|
71
72
|
columnIndexMap?: Map<string, number>;
|
|
73
|
+
/** Wraps header titles onto several lines; `GridColDef.wrapHeaderText` overrides it per column. */
|
|
74
|
+
wrapHeaderText?: boolean;
|
|
75
|
+
/** Header row height in pixels; sets how many lines a wrapped title may take. Defaults to 56. */
|
|
76
|
+
headerHeight?: number;
|
|
72
77
|
}
|
|
73
78
|
|
|
74
79
|
export function Header<R extends GridRowModel = GridRowModel>(props: HeaderProps<R>) {
|
|
@@ -105,6 +110,8 @@ export function Header<R extends GridRowModel = GridRowModel>(props: HeaderProps
|
|
|
105
110
|
onPinColumn,
|
|
106
111
|
onManageColumns,
|
|
107
112
|
columnIndexMap,
|
|
113
|
+
wrapHeaderText = false,
|
|
114
|
+
headerHeight = 56,
|
|
108
115
|
} = props;
|
|
109
116
|
|
|
110
117
|
const selectAllId = useId();
|
|
@@ -411,6 +418,8 @@ export function Header<R extends GridRowModel = GridRowModel>(props: HeaderProps
|
|
|
411
418
|
const isPinned = pinnedPosition !== null;
|
|
412
419
|
const isDragging = draggedColumn === colDef.field;
|
|
413
420
|
const isDragOver = dragOverColumn === colDef.field;
|
|
421
|
+
const wraps = shouldWrapHeaderText(colDef.wrapHeaderText, wrapHeaderText);
|
|
422
|
+
const aggregationLabel = aggregationModel?.[colDef.field];
|
|
414
423
|
|
|
415
424
|
const classNames = [
|
|
416
425
|
'ogx__header-cell',
|
|
@@ -423,6 +432,7 @@ export function Header<R extends GridRowModel = GridRowModel>(props: HeaderProps
|
|
|
423
432
|
pinnedPosition === 'right' && colDef.field === pinnedEdges.firstRight && 'ogx__header-cell--pinned-right-first',
|
|
424
433
|
isDragging && 'ogx__header-cell--dragging',
|
|
425
434
|
isDragOver && 'ogx__header-cell--drag-over',
|
|
435
|
+
wraps && 'ogx__header-cell--wrap',
|
|
426
436
|
isHeaderFocused(colDef.field) && 'ogx__header-cell--focused',
|
|
427
437
|
colDef.headerClassName
|
|
428
438
|
].filter(Boolean).join(' ');
|
|
@@ -476,7 +486,7 @@ export function Header<R extends GridRowModel = GridRowModel>(props: HeaderProps
|
|
|
476
486
|
key={colDef.field}
|
|
477
487
|
className={classNames}
|
|
478
488
|
style={style}
|
|
479
|
-
title={colDef.description}
|
|
489
|
+
title={colDef.description ?? (wraps && typeof headerContent === 'string' ? headerContent : undefined)}
|
|
480
490
|
onClick={handleHeaderClick}
|
|
481
491
|
role="columnheader"
|
|
482
492
|
draggable={!!handleDragStart}
|
|
@@ -498,12 +508,15 @@ export function Header<R extends GridRowModel = GridRowModel>(props: HeaderProps
|
|
|
498
508
|
</div>
|
|
499
509
|
)}
|
|
500
510
|
<div className="ogx__header-cell-title-wrapper">
|
|
501
|
-
<span
|
|
511
|
+
<span
|
|
512
|
+
className="ogx__header-cell-title"
|
|
513
|
+
style={wraps ? { WebkitLineClamp: getHeaderLineClamp(headerHeight, !!aggregationLabel) } : undefined}
|
|
514
|
+
>
|
|
502
515
|
{headerContent}
|
|
503
516
|
</span>
|
|
504
|
-
{
|
|
517
|
+
{aggregationLabel && (
|
|
505
518
|
<span className="ogx__header-cell-aggregation">
|
|
506
|
-
{
|
|
519
|
+
{aggregationLabel}
|
|
507
520
|
</span>
|
|
508
521
|
)}
|
|
509
522
|
</div>
|
|
@@ -65,11 +65,9 @@
|
|
|
65
65
|
border-right: var(--ogx-border-width-thin) solid var(--ogx-grid-border-color);
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
height: 16px;
|
|
68
|
+
/* The row checkbox is the shared Checkbox (its <input> is visually hidden). */
|
|
69
|
+
.ogx-list-view__checkbox .ogx-checkbox-wrapper {
|
|
71
70
|
cursor: pointer;
|
|
72
|
-
accent-color: var(--ogx-color-primary);
|
|
73
71
|
}
|
|
74
72
|
|
|
75
73
|
/* ─── Cell content area ───────────────────────────────────────────────────── */
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { formatValueByType, getCellValue, getFormattedValue } from '../../utils/values';
|
|
3
3
|
import { ExpandIcon } from '../ui/ExpandIcon';
|
|
4
|
+
import { Checkbox } from '../ui/Checkbox';
|
|
4
5
|
import { CellErrorBoundary } from '../Cell/CellErrorBoundary';
|
|
5
6
|
import { isSyntheticRowId } from '../../utils/syntheticRows';
|
|
6
7
|
import type { GridColDef, GridRowModel, GridRowId, GridRowMeta, GridListViewColDef, GridRenderCellParams } from '../../types';
|
|
@@ -92,8 +93,7 @@ export function ListViewRow<R extends GridRowModel = GridRowModel>({
|
|
|
92
93
|
>
|
|
93
94
|
{checkboxSelection && (
|
|
94
95
|
<div className="ogx-list-view__checkbox" onClick={(e) => e.stopPropagation()}>
|
|
95
|
-
{!isSyntheticRow && <
|
|
96
|
-
type="checkbox"
|
|
96
|
+
{!isSyntheticRow && <Checkbox
|
|
97
97
|
checked={isSelected}
|
|
98
98
|
onChange={handleCheckboxChange}
|
|
99
99
|
aria-label={`Select row ${id}`}
|
|
@@ -13,6 +13,8 @@ import { tryGetCellValue } from '../../utils/values';
|
|
|
13
13
|
import { isSyntheticRowId } from '../../utils/syntheticRows';
|
|
14
14
|
import { getRenderedColumnWidth } from '../../utils/columnWidth';
|
|
15
15
|
import { DEFAULT_DETAIL_PANEL_HEIGHT } from '../../utils/detailPanel';
|
|
16
|
+
import { GROUPING_COLUMN_FIELD, getCellRangeFlags, isSameRowCellRange } from '../../utils/cellSelection';
|
|
17
|
+
import type { GridRowCellRange } from '../../utils/cellSelection';
|
|
16
18
|
|
|
17
19
|
/** Sticky system cells (drag handle, detail toggle, checkbox) sit above scrolled, spanned and focused cells. */
|
|
18
20
|
const SYSTEM_CELL_Z_INDEX = 12;
|
|
@@ -96,9 +98,14 @@ export interface RowProps<R extends GridRowModel = GridRowModel> {
|
|
|
96
98
|
columnIndexMap?: Map<string, number>;
|
|
97
99
|
/** 1-based `aria-rowindex` of this row in the whole grid (header rows included). Defaults to `rowIndex + 2`. */
|
|
98
100
|
ariaRowIndex?: number;
|
|
101
|
+
/**
|
|
102
|
+
* The part of the selected cell range (`cellSelection`) in this row, or `null` / absent when the
|
|
103
|
+
* row is outside it. @since v3.3
|
|
104
|
+
*/
|
|
105
|
+
cellRange?: GridRowCellRange | null;
|
|
99
106
|
}
|
|
100
107
|
|
|
101
|
-
|
|
108
|
+
function RowImpl<R extends GridRowModel = GridRowModel>(props: RowProps<R>) {
|
|
102
109
|
const {
|
|
103
110
|
row,
|
|
104
111
|
rowId: rowIdProp,
|
|
@@ -144,6 +151,7 @@ export function Row<R extends GridRowModel = GridRowModel>(props: RowProps<R>) {
|
|
|
144
151
|
rowMeta,
|
|
145
152
|
columnIndexMap,
|
|
146
153
|
ariaRowIndex,
|
|
154
|
+
cellRange,
|
|
147
155
|
} = props;
|
|
148
156
|
|
|
149
157
|
const id = rowIdProp ?? row.id;
|
|
@@ -437,6 +445,14 @@ export function Row<R extends GridRowModel = GridRowModel>(props: RowProps<R>) {
|
|
|
437
445
|
|| (pinnedPosition === 'right' && colDef.field === pinnedEdges.firstRight);
|
|
438
446
|
const rowSpan = rowSpanningCaches?.spannedCells[id]?.[colDef.field];
|
|
439
447
|
const isHiddenByRowSpan = rowSpanningCaches?.hiddenCells[id]?.[colDef.field] || false;
|
|
448
|
+
const rangeFlags = cellRange && !isHiddenByRowSpan && colDef.field !== GROUPING_COLUMN_FIELD
|
|
449
|
+
? getCellRangeFlags(
|
|
450
|
+
cellRange,
|
|
451
|
+
colIndex,
|
|
452
|
+
colSpanInfo && !colSpanInfo.spannedByColSpan ? colSpanInfo.cellProps.colSpan : 1,
|
|
453
|
+
rowSpan ?? 1,
|
|
454
|
+
)
|
|
455
|
+
: undefined;
|
|
440
456
|
|
|
441
457
|
return (
|
|
442
458
|
<Cell
|
|
@@ -468,6 +484,7 @@ export function Row<R extends GridRowModel = GridRowModel>(props: RowProps<R>) {
|
|
|
468
484
|
isHiddenByRowSpan={isHiddenByRowSpan}
|
|
469
485
|
rowMeta={rowMeta}
|
|
470
486
|
valueError={valueError}
|
|
487
|
+
rangeFlags={rangeFlags}
|
|
471
488
|
/>
|
|
472
489
|
);
|
|
473
490
|
})}
|
|
@@ -490,3 +507,23 @@ export function Row<R extends GridRowModel = GridRowModel>(props: RowProps<R>) {
|
|
|
490
507
|
</>
|
|
491
508
|
);
|
|
492
509
|
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Shallow prop comparison, except `cellRange`, which is compared by value: a row inside a cell range
|
|
513
|
+
* gets a new range object on every render, but only re-renders when its part of the range changes.
|
|
514
|
+
*/
|
|
515
|
+
function areRowPropsEqual<R extends GridRowModel>(prev: RowProps<R>, next: RowProps<R>): boolean {
|
|
516
|
+
const prevKeys = Object.keys(prev) as (keyof RowProps<R>)[];
|
|
517
|
+
if (prevKeys.length !== Object.keys(next).length) return false;
|
|
518
|
+
for (const key of prevKeys) {
|
|
519
|
+
if (key === 'cellRange') {
|
|
520
|
+
if (!isSameRowCellRange(prev.cellRange, next.cellRange)) return false;
|
|
521
|
+
} else if (!Object.is(prev[key], next[key])) {
|
|
522
|
+
return false;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
return true;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/** One grid row. Memoised: rows whose props did not change (for example outside a dragged cell range) do not re-render. */
|
|
529
|
+
export const Row = React.memo(RowImpl, areRowPropsEqual) as typeof RowImpl;
|