@opencorestack/opengridx 3.2.2 → 3.3.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 +22 -0
- package/README.md +12 -1
- package/dist/index.d.ts +173 -1
- package/dist/opengridx.css +1 -1
- package/dist/opengridx.es.js +5472 -4841
- package/dist/opengridx.es.js.map +1 -1
- package/dist/opengridx.umd.js +15 -14
- package/dist/opengridx.umd.js.map +1 -1
- package/docs/API_REFERENCE.md +34 -2
- package/docs/README.md +1 -0
- package/docs/architecture/datagrid-orchestration.md +13 -5
- package/docs/components/header.md +25 -0
- package/docs/customization/theming.md +3 -1
- package/docs/features/cell-selection.md +131 -0
- package/docs/features/clipboard.md +2 -0
- package/docs/features/keyboard-navigation.md +14 -2
- package/docs/roadmap.md +64 -7
- package/lib/components/Cell/Cell.css +30 -0
- package/lib/components/Cell/Cell.tsx +25 -13
- package/lib/components/Cell/CellErrorBoundary.tsx +3 -0
- 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/DataGrid/DataGrid.tsx +61 -1
- 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.tsx +2 -1
- package/lib/components/Header/Header.css +12 -0
- package/lib/components/Header/Header.tsx +17 -4
- package/lib/components/Row/Row.tsx +38 -1
- package/lib/components/Toolbar/GridToolbar.tsx +10 -4
- package/lib/components/ui/Checkbox.tsx +10 -5
- package/lib/hooks/core/gridApiPlaceholder.ts +6 -0
- package/lib/hooks/core/useGridCellSelection.ts +603 -0
- package/lib/hooks/core/useGridCellSelectionApi.ts +97 -0
- package/lib/hooks/core/useGridClipboardApi.ts +8 -2
- 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/features/useAggregation.ts +8 -6
- package/lib/hooks/features/useGridClipboard.ts +47 -2
- package/lib/hooks/features/useGridEditing.ts +16 -7
- package/lib/index.ts +9 -0
- package/lib/styles/base/variables.css +6 -1
- package/lib/styles/opengridx.css +1 -0
- 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 +130 -0
- package/lib/utils/attempt.ts +14 -0
- package/lib/utils/cellSelection.ts +238 -0
- package/lib/utils/headerWrap.ts +23 -0
- package/llms.txt +17 -2
- package/package.json +7 -3
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/* Status bar of showCellSelectionStats (v3.3). The area keeps its height while no range is selected. */
|
|
2
|
+
.ogx__cell-selection-stats-area {
|
|
3
|
+
flex: 0 0 auto;
|
|
4
|
+
min-height: 32px;
|
|
5
|
+
border-top: var(--ogx-border-width-thin, 1px) solid var(--ogx-grid-border-color);
|
|
6
|
+
background: var(--ogx-grid-header-background);
|
|
7
|
+
color: var(--ogx-grid-header-text);
|
|
8
|
+
font-size: var(--ogx-font-size-sm);
|
|
9
|
+
display: flex;
|
|
10
|
+
align-items: center;
|
|
11
|
+
justify-content: flex-end;
|
|
12
|
+
box-sizing: border-box;
|
|
13
|
+
}
|
|
14
|
+
.ogx__cell-selection-stats {
|
|
15
|
+
display: flex;
|
|
16
|
+
align-items: center;
|
|
17
|
+
gap: var(--ogx-spacing-lg, 16px);
|
|
18
|
+
padding: 4px 16px;
|
|
19
|
+
}
|
|
20
|
+
.ogx__cell-selection-stat {
|
|
21
|
+
display: inline-flex;
|
|
22
|
+
gap: 6px;
|
|
23
|
+
white-space: nowrap;
|
|
24
|
+
}
|
|
25
|
+
.ogx__cell-selection-stat-label {
|
|
26
|
+
opacity: 0.75;
|
|
27
|
+
}
|
|
28
|
+
.ogx__cell-selection-stat-value {
|
|
29
|
+
font-weight: 600;
|
|
30
|
+
font-variant-numeric: tabular-nums;
|
|
31
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { formatAggregationValue } from '../../utils/aggregation';
|
|
2
|
+
import type { GridCellSelectionStatsSlotProps } from '../../types';
|
|
3
|
+
|
|
4
|
+
export type CellSelectionStatsProps = GridCellSelectionStatsSlotProps & Record<string, unknown>;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The status bar of `showCellSelectionStats`: count of non-empty cells, and the sum and average of
|
|
8
|
+
* the numeric cells of the selected range. Replace it with `slots.cellSelectionStats`, or wrap it.
|
|
9
|
+
* @since v3.3
|
|
10
|
+
*/
|
|
11
|
+
export function CellSelectionStats({ count, sum, average }: CellSelectionStatsProps) {
|
|
12
|
+
return (
|
|
13
|
+
<div className="ogx__cell-selection-stats">
|
|
14
|
+
<span className="ogx__cell-selection-stat">
|
|
15
|
+
<span className="ogx__cell-selection-stat-label">Count</span>
|
|
16
|
+
<span className="ogx__cell-selection-stat-value" data-stat="count">{count.toLocaleString()}</span>
|
|
17
|
+
</span>
|
|
18
|
+
{sum !== null && (
|
|
19
|
+
<span className="ogx__cell-selection-stat">
|
|
20
|
+
<span className="ogx__cell-selection-stat-label">Sum</span>
|
|
21
|
+
<span className="ogx__cell-selection-stat-value" data-stat="sum">{formatAggregationValue(sum, 'sum')}</span>
|
|
22
|
+
</span>
|
|
23
|
+
)}
|
|
24
|
+
{average !== null && (
|
|
25
|
+
<span className="ogx__cell-selection-stat">
|
|
26
|
+
<span className="ogx__cell-selection-stat-label">Average</span>
|
|
27
|
+
<span className="ogx__cell-selection-stat-value" data-stat="average">{formatAggregationValue(average, 'avg')}</span>
|
|
28
|
+
</span>
|
|
29
|
+
)}
|
|
30
|
+
</div>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type React from 'react';
|
|
2
|
+
import { CellSelectionStats } from './CellSelectionStats';
|
|
3
|
+
import type { GridCellSelectionStats } from '../../hooks/core/useGridCellSelection';
|
|
4
|
+
import type { GridApi, GridCellSelectionStatsSlotProps } from '../../types';
|
|
5
|
+
|
|
6
|
+
interface GridCellSelectionStatsAreaProps {
|
|
7
|
+
stats: GridCellSelectionStats | null;
|
|
8
|
+
apiRef: React.MutableRefObject<GridApi>;
|
|
9
|
+
slot?: React.ComponentType<GridCellSelectionStatsSlotProps & Record<string, unknown>>;
|
|
10
|
+
slotProps?: Partial<GridCellSelectionStatsSlotProps> & Record<string, unknown>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The strip under the viewport that holds the status bar. It keeps its height while one cell or
|
|
15
|
+
* none is selected, so the viewport does not resize each time a range starts or ends.
|
|
16
|
+
*/
|
|
17
|
+
export function GridCellSelectionStatsArea({ stats, apiRef, slot, slotProps }: GridCellSelectionStatsAreaProps) {
|
|
18
|
+
const Component = slot ?? CellSelectionStats;
|
|
19
|
+
return (
|
|
20
|
+
<div className="ogx__cell-selection-stats-area" aria-hidden={stats ? undefined : true}>
|
|
21
|
+
{stats && <Component apiRef={apiRef} {...stats} {...slotProps} />}
|
|
22
|
+
</div>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
@@ -52,6 +52,9 @@ import { useGridAggregationApi } from '../../hooks/core/useGridAggregationApi';
|
|
|
52
52
|
import { useGridClipboardApi } from '../../hooks/core/useGridClipboardApi';
|
|
53
53
|
import { useGridScrollToIndexesApi } from '../../hooks/core/useGridScrollToIndexesApi';
|
|
54
54
|
import { useGridColumnAutosize } from '../../hooks/core/useGridColumnAutosize';
|
|
55
|
+
import { useGridCellSelection } from '../../hooks/core/useGridCellSelection';
|
|
56
|
+
import { useGridCellSelectionApi } from '../../hooks/core/useGridCellSelectionApi';
|
|
57
|
+
import { GridCellSelectionStatsArea } from '../CellSelectionStats/GridCellSelectionStatsArea';
|
|
55
58
|
import { useGridSortHandlers, useGridColumnMenuHandlers } from '../../hooks/core/useGridHeaderHandlers';
|
|
56
59
|
import { useGridKeyboardMode, useGridPointerFocusHandlers } from '../../hooks/core/useGridFocusHandlers';
|
|
57
60
|
import { getPaginationRowCount, pickPaginationLocaleText } from '../../utils/pagination';
|
|
@@ -76,12 +79,17 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
76
79
|
getRowId,
|
|
77
80
|
rowHeight: rowHeightProp,
|
|
78
81
|
headerHeight: headerHeightProp,
|
|
82
|
+
wrapHeaderText = false,
|
|
79
83
|
autoHeight = false,
|
|
80
84
|
density,
|
|
81
85
|
checkboxSelection = false,
|
|
82
86
|
disableRowSelectionOnClick = false,
|
|
83
87
|
disableMultipleRowSelection = false,
|
|
84
88
|
disableClipboardCopy = false,
|
|
89
|
+
cellSelection: cellSelectionProp = false,
|
|
90
|
+
cellSelectionModel: propCellSelectionModel,
|
|
91
|
+
onCellSelectionModelChange,
|
|
92
|
+
showCellSelectionStats = false,
|
|
85
93
|
rowSelectionModel: propRowSelectionModel,
|
|
86
94
|
onRowSelectionModelChange: propOnRowSelectionModelChange,
|
|
87
95
|
onRowClick,
|
|
@@ -184,6 +192,8 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
184
192
|
rowSelectionModel: propRowSelectionModel,
|
|
185
193
|
onRowSelectionModelChange: propOnRowSelectionModelChange,
|
|
186
194
|
density,
|
|
195
|
+
cellSelectionModel: propCellSelectionModel,
|
|
196
|
+
onCellSelectionModelChange,
|
|
187
197
|
});
|
|
188
198
|
|
|
189
199
|
const {
|
|
@@ -493,12 +503,34 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
493
503
|
[layout.leftPinnedCols, layout.unpinnedColsWithWidth, layout.rightPinnedCols]
|
|
494
504
|
);
|
|
495
505
|
|
|
506
|
+
// Cell range selection (cellSelection): the range, pointer drag and Shift-keys, copy text, totals.
|
|
507
|
+
const cellSelection = useGridCellSelection<R>({
|
|
508
|
+
enabled: cellSelectionProp && !listView,
|
|
509
|
+
model: controlledState.cellSelectionModel,
|
|
510
|
+
onModelChange: controlledState.handleCellSelectionModelChange,
|
|
511
|
+
getPendingModel: controlledState.getPendingCellSelectionModel,
|
|
512
|
+
allRenderableRows,
|
|
513
|
+
getRowId: getRowIdOf,
|
|
514
|
+
navigationColumns,
|
|
515
|
+
columnIndexMap,
|
|
516
|
+
spanning,
|
|
517
|
+
rowMetaMap,
|
|
518
|
+
layout,
|
|
519
|
+
rowHeight: effectiveRowHeight,
|
|
520
|
+
pinnedTopRowCount: pinnedTopRows.length,
|
|
521
|
+
viewportRef,
|
|
522
|
+
containerRef,
|
|
523
|
+
localeText,
|
|
524
|
+
showStats: showCellSelectionStats,
|
|
525
|
+
});
|
|
526
|
+
|
|
496
527
|
useGridClipboardApi({
|
|
497
528
|
apiRef,
|
|
498
529
|
columns: renderedDataColumns as unknown as GridColDef[],
|
|
499
530
|
getRowId: getRowIdOf,
|
|
500
531
|
containerRef,
|
|
501
532
|
disableKeyboardShortcut: disableClipboardCopy,
|
|
533
|
+
getCellRangeCopyText: cellSelection.enabled ? cellSelection.getCopyText : undefined,
|
|
502
534
|
});
|
|
503
535
|
useGridScrollToIndexesApi<R>({ apiRef, viewportRef, layout, rowHeight: effectiveRowHeight });
|
|
504
536
|
const handleColumnAutosize = useGridColumnAutosize<R>({ apiRef, containerRef, columns: orderedColumns, onColumnResize: handleColumnResize });
|
|
@@ -541,11 +573,23 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
541
573
|
pinCheckboxColumn,
|
|
542
574
|
pinExpandColumn,
|
|
543
575
|
rowHeight: effectiveRowHeight,
|
|
576
|
+
cellSelection: cellSelection.keyboard,
|
|
544
577
|
});
|
|
545
578
|
|
|
546
579
|
const { handleCellClick, handleHeaderClick } = useGridPointerFocusHandlers<R>({
|
|
547
580
|
ensureGridFocus, setKeyboardMode, setFocusedCell, getRowId: getRowIdOf, onCellClick,
|
|
548
581
|
});
|
|
582
|
+
// Keeps the range anchor and the focused cell together; installs the cell selection API.
|
|
583
|
+
useGridCellSelectionApi({
|
|
584
|
+
apiRef,
|
|
585
|
+
selection: cellSelection,
|
|
586
|
+
model: controlledState.cellSelectionModel,
|
|
587
|
+
getPendingModel: controlledState.getPendingCellSelectionModel,
|
|
588
|
+
focusedCell,
|
|
589
|
+
setFocusedCell,
|
|
590
|
+
containerRef,
|
|
591
|
+
});
|
|
592
|
+
|
|
549
593
|
const { handleHideColumn, handlePinColumn } = useGridColumnMenuHandlers({
|
|
550
594
|
columnVisibilityModel,
|
|
551
595
|
onColumnVisibilityModelChange: handleColumnVisibilityModelChange,
|
|
@@ -658,6 +702,8 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
658
702
|
rowSpanningCaches: spanning.rowSpanningCaches,
|
|
659
703
|
rowHeight: effectiveRowHeight,
|
|
660
704
|
rowMetaMap,
|
|
705
|
+
cellRange: cellSelection.displayRange,
|
|
706
|
+
cellRangeHasRowSpan: cellSelection.hasRowSpan,
|
|
661
707
|
};
|
|
662
708
|
|
|
663
709
|
return (
|
|
@@ -746,11 +792,13 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
746
792
|
aria-label={ariaLabel || 'Data grid'}
|
|
747
793
|
aria-rowcount={ariaRows.rowCount}
|
|
748
794
|
aria-colcount={navigationColumns.length}
|
|
749
|
-
aria-multiselectable={rowSelectionEnabled && !disableMultipleRowSelection}
|
|
795
|
+
aria-multiselectable={(rowSelectionEnabled && !disableMultipleRowSelection) || cellSelection.enabled}
|
|
750
796
|
aria-busy={effectiveLoading}
|
|
751
797
|
tabIndex={viewportTabIndex}
|
|
752
798
|
onKeyDownCapture={() => { setKeyboardMode(true); }}
|
|
753
799
|
onMouseDownCapture={handleMouseDownCapture}
|
|
800
|
+
onMouseDown={cellSelection.handleMouseDown}
|
|
801
|
+
onClickCapture={cellSelection.handleClickCapture}
|
|
754
802
|
onKeyDown={handleKeyDown}
|
|
755
803
|
onFocus={handleFocus}
|
|
756
804
|
onBlur={handleBlur}
|
|
@@ -802,6 +850,8 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
802
850
|
onHideColumn={handleHideColumn}
|
|
803
851
|
onManageColumns={columnsPanel.openColumnsPanel}
|
|
804
852
|
onPinColumn={handlePinColumn}
|
|
853
|
+
wrapHeaderText={wrapHeaderText}
|
|
854
|
+
headerHeight={headerHeight}
|
|
805
855
|
/>
|
|
806
856
|
|
|
807
857
|
<GridPinnedRows<R>
|
|
@@ -881,6 +931,15 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
881
931
|
</div>
|
|
882
932
|
)}
|
|
883
933
|
|
|
934
|
+
{cellSelection.enabled && showCellSelectionStats && (
|
|
935
|
+
<GridCellSelectionStatsArea
|
|
936
|
+
stats={cellSelection.stats}
|
|
937
|
+
apiRef={gridData.apiRef}
|
|
938
|
+
slot={slots?.cellSelectionStats}
|
|
939
|
+
slotProps={slotProps?.cellSelectionStats}
|
|
940
|
+
/>
|
|
941
|
+
)}
|
|
942
|
+
|
|
884
943
|
{FooterSlot && (
|
|
885
944
|
<FooterSlot
|
|
886
945
|
apiRef={gridData.apiRef}
|
|
@@ -915,6 +974,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
915
974
|
filteredRowCount={filteredRows.length}
|
|
916
975
|
dataRowCount={dataRows.length}
|
|
917
976
|
filterModel={filterModel}
|
|
977
|
+
cellSelectionAnnouncement={cellSelection.announcement}
|
|
918
978
|
/>
|
|
919
979
|
|
|
920
980
|
{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
|
)}
|
|
@@ -107,7 +107,8 @@ const FilterRow: React.FC<{
|
|
|
107
107
|
onChange: (item: GridFilterItem | null) => void;
|
|
108
108
|
}> = ({ col, item, resetToken, onChange }) => {
|
|
109
109
|
const filterId = useId();
|
|
110
|
-
|
|
110
|
+
// Memoized so React Compiler treats the list as frozen and can keep the callbacks below memoized.
|
|
111
|
+
const typeOperators = useMemo(() => getOperatorsForType(col.type), [col.type]);
|
|
111
112
|
const defaultOperator = typeOperators[0];
|
|
112
113
|
const currentOperator: GridFilterOperator = item?.operator ?? defaultOperator;
|
|
113
114
|
// An operator set programmatically that the column type does not list is still shown, so the
|
|
@@ -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>
|
|
@@ -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;
|
|
@@ -474,9 +474,11 @@ function FilterPanelWrapper({
|
|
|
474
474
|
}
|
|
475
475
|
|
|
476
476
|
const EMPTY_PIVOT: GridPivotModel = { rowFields: [], columnFields: [], valueFields: [] };
|
|
477
|
+
// Module-level default, so `columns` is the same frozen value on every render (React Compiler).
|
|
478
|
+
const NO_COLUMNS: GridColDef[] = [];
|
|
477
479
|
|
|
478
480
|
export function GridToolbar({
|
|
479
|
-
columns =
|
|
481
|
+
columns = NO_COLUMNS,
|
|
480
482
|
baseColumns,
|
|
481
483
|
aggregationModel = {},
|
|
482
484
|
onAggregationModelChange,
|
|
@@ -504,7 +506,7 @@ export function GridToolbar({
|
|
|
504
506
|
// At most one panel is open. Every way the Columns panel closes (its button, a custom
|
|
505
507
|
// button, another panel opening, click-outside, Escape) reports onColumnsPanelClose, so the
|
|
506
508
|
// grid can ask for it again with forceColumnsOpen.
|
|
507
|
-
const [openPanel, setOpenPanel] = useState<ToolbarPanel | null>(null);
|
|
509
|
+
const [openPanel, setOpenPanel] = useState<ToolbarPanel | null>(forceColumnsOpen ? 'columns' : null);
|
|
508
510
|
const aggOpen = openPanel === 'summaries';
|
|
509
511
|
const pivotOpen = openPanel === 'pivot';
|
|
510
512
|
const colsOpen = openPanel === 'columns';
|
|
@@ -517,9 +519,13 @@ export function GridToolbar({
|
|
|
517
519
|
const togglePanel = (panel: ToolbarPanel) => switchPanel(openPanel === panel ? null : panel);
|
|
518
520
|
const closePanel = (panel: ToolbarPanel) => { if (openPanel === panel) switchPanel(null); };
|
|
519
521
|
|
|
520
|
-
|
|
522
|
+
// forceColumnsOpen turning true opens the Columns panel. Adjusted during render (not in an
|
|
523
|
+
// effect) so the panel opens in the same commit.
|
|
524
|
+
const [seenForceColumnsOpen, setSeenForceColumnsOpen] = useState(forceColumnsOpen);
|
|
525
|
+
if (seenForceColumnsOpen !== forceColumnsOpen) {
|
|
526
|
+
setSeenForceColumnsOpen(forceColumnsOpen);
|
|
521
527
|
if (forceColumnsOpen) setOpenPanel('columns');
|
|
522
|
-
}
|
|
528
|
+
}
|
|
523
529
|
|
|
524
530
|
// Tell the grid this toolbar shows the Columns panel for the column menu; it can only do so
|
|
525
531
|
// when the grid's forceColumnsOpen reaches it and the Columns panel is enabled.
|
|
@@ -7,6 +7,15 @@ export interface CheckboxProps extends Omit<React.InputHTMLAttributes<HTMLInputE
|
|
|
7
7
|
inputRef?: React.Ref<HTMLInputElement>;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
/** Points a callback or object ref at `element`. Outside the component, so it does not mutate a prop in render scope. */
|
|
11
|
+
function assignRef(ref: React.Ref<HTMLInputElement> | undefined, element: HTMLInputElement | null): void {
|
|
12
|
+
if (typeof ref === 'function') {
|
|
13
|
+
ref(element);
|
|
14
|
+
} else if (ref) {
|
|
15
|
+
(ref as React.MutableRefObject<HTMLInputElement | null>).current = element;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
10
19
|
export const Checkbox: React.FC<CheckboxProps> = ({
|
|
11
20
|
indeterminate = false,
|
|
12
21
|
label,
|
|
@@ -20,11 +29,7 @@ export const Checkbox: React.FC<CheckboxProps> = ({
|
|
|
20
29
|
const setRef = React.useCallback((element: HTMLInputElement | null) => {
|
|
21
30
|
internalRef.current = element;
|
|
22
31
|
|
|
23
|
-
|
|
24
|
-
inputRef(element);
|
|
25
|
-
} else if (inputRef) {
|
|
26
|
-
(inputRef as React.MutableRefObject<HTMLInputElement | null>).current = element;
|
|
27
|
-
}
|
|
32
|
+
assignRef(inputRef, element);
|
|
28
33
|
}, [inputRef]);
|
|
29
34
|
|
|
30
35
|
React.useEffect(() => {
|
|
@@ -32,5 +32,11 @@ export function createGridApiPlaceholder<R extends GridValidRowModel = GridRowMo
|
|
|
32
32
|
autosizeColumn: noop,
|
|
33
33
|
autosizeColumns: noop,
|
|
34
34
|
copySelectedRows: () => Promise.resolve(),
|
|
35
|
+
getCellSelectionModel: () => [],
|
|
36
|
+
setCellSelectionModel: noop,
|
|
37
|
+
selectCellRange: noop,
|
|
38
|
+
clearCellSelection: noop,
|
|
39
|
+
getSelectedCells: () => [],
|
|
40
|
+
copySelectedCells: () => Promise.resolve(),
|
|
35
41
|
};
|
|
36
42
|
}
|