@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,18 +1,30 @@
|
|
|
1
1
|
import React, { useMemo } from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { formatAggregateForColumn, isAggregationAllowed } from '../../utils/aggregation';
|
|
3
3
|
import type { GridColDef, GridAggregationModel, GridAggregationResult, GridColumnPinning } from '../../types';
|
|
4
|
-
import { calculatePinnedPositions, isColumnPinned } from '../../utils/pinning';
|
|
4
|
+
import { calculatePinnedPositions, getPinnedEdgeFields, isColumnPinned } from '../../utils/pinning';
|
|
5
|
+
import { getRenderedColumnWidth } from '../../utils/columnWidth';
|
|
6
|
+
|
|
7
|
+
const SYSTEM_COLUMN_WIDTH = 48;
|
|
5
8
|
|
|
6
9
|
interface GridAggregationFooterProps {
|
|
10
|
+
/**
|
|
11
|
+
* The columns exactly as the rows lay them out: visible only, pinned-left first and pinned-right
|
|
12
|
+
* last, with the horizontal-virtualization spacers (`virtualization.virtualColumns`).
|
|
13
|
+
*/
|
|
7
14
|
columns: GridColDef[];
|
|
8
15
|
aggregationModel: GridAggregationModel;
|
|
9
16
|
aggregationResult: GridAggregationResult;
|
|
17
|
+
/** User resize overrides, as passed to the rows. */
|
|
10
18
|
columnWidths: Record<string, number>;
|
|
11
19
|
rowHeight: number;
|
|
12
20
|
checkboxSelection: boolean;
|
|
13
21
|
hasDetailPanel: boolean;
|
|
14
22
|
rowReordering: boolean;
|
|
23
|
+
pinCheckboxColumn?: boolean;
|
|
24
|
+
pinExpandColumn?: boolean;
|
|
15
25
|
pinnedColumns?: GridColumnPinning;
|
|
26
|
+
/** A server aggregation request is in flight. */
|
|
27
|
+
loading?: boolean;
|
|
16
28
|
}
|
|
17
29
|
|
|
18
30
|
export function GridAggregationFooter({
|
|
@@ -24,44 +36,68 @@ export function GridAggregationFooter({
|
|
|
24
36
|
checkboxSelection,
|
|
25
37
|
hasDetailPanel,
|
|
26
38
|
rowReordering,
|
|
39
|
+
pinCheckboxColumn = true,
|
|
40
|
+
pinExpandColumn = true,
|
|
27
41
|
pinnedColumns,
|
|
42
|
+
loading = false,
|
|
28
43
|
}: GridAggregationFooterProps) {
|
|
29
|
-
//
|
|
30
|
-
// The footer always renders checkbox/detail/reorder spacers unconditionally,
|
|
31
|
-
// so treat pinCheckboxColumn and pinExpandColumn as always true.
|
|
44
|
+
// Same offsets as Row, so pinned totals sit exactly under their pinned body cells.
|
|
32
45
|
const pinnedOffsets = useMemo(
|
|
33
46
|
() => calculatePinnedPositions(
|
|
34
47
|
columns,
|
|
35
48
|
columnWidths,
|
|
36
49
|
pinnedColumns,
|
|
37
50
|
checkboxSelection,
|
|
38
|
-
|
|
51
|
+
pinCheckboxColumn,
|
|
39
52
|
hasDetailPanel,
|
|
40
|
-
|
|
53
|
+
pinExpandColumn,
|
|
41
54
|
rowReordering,
|
|
42
55
|
),
|
|
43
|
-
[columns, columnWidths, pinnedColumns, checkboxSelection, hasDetailPanel, rowReordering]
|
|
56
|
+
[columns, columnWidths, pinnedColumns, checkboxSelection, pinCheckboxColumn, hasDetailPanel, pinExpandColumn, rowReordering]
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const { lastLeft: lastLeftField, firstRight: firstRightField } = useMemo(
|
|
60
|
+
() => getPinnedEdgeFields(columns, pinnedColumns),
|
|
61
|
+
[columns, pinnedColumns]
|
|
44
62
|
);
|
|
45
63
|
|
|
46
|
-
|
|
47
|
-
const
|
|
64
|
+
// System-column spacers stick exactly where Row sticks its drag handle, expand and checkbox cells.
|
|
65
|
+
const expandLeft = rowReordering ? SYSTEM_COLUMN_WIDTH : 0;
|
|
66
|
+
const checkboxLeft = expandLeft + (hasDetailPanel && pinExpandColumn ? SYSTEM_COLUMN_WIDTH : 0);
|
|
67
|
+
const spacer = (key: string, pinned: boolean, left: number) => (
|
|
68
|
+
<div
|
|
69
|
+
key={key}
|
|
70
|
+
className={`ogx__aggregation-spacer${pinned ? ' ogx__aggregation-spacer--pinned' : ''}`}
|
|
71
|
+
style={pinned ? { left } : undefined}
|
|
72
|
+
aria-hidden="true"
|
|
73
|
+
/>
|
|
74
|
+
);
|
|
48
75
|
|
|
49
76
|
return (
|
|
50
77
|
<div
|
|
51
|
-
className=
|
|
78
|
+
className={`ogx__aggregation-footer${loading ? ' ogx__aggregation-footer--loading' : ''}`}
|
|
52
79
|
role="row"
|
|
53
80
|
aria-label="Aggregation totals"
|
|
54
81
|
aria-live="polite"
|
|
82
|
+
aria-busy={loading || undefined}
|
|
55
83
|
style={{ minHeight: `${rowHeight}px` }}
|
|
56
84
|
>
|
|
57
|
-
{
|
|
58
|
-
{hasDetailPanel
|
|
59
|
-
{
|
|
85
|
+
{rowReordering && spacer('__reorder_col__', true, 0)}
|
|
86
|
+
{hasDetailPanel && spacer('__expand_col__', pinExpandColumn, expandLeft)}
|
|
87
|
+
{checkboxSelection && spacer('__checkbox_col__', pinCheckboxColumn, checkboxLeft)}
|
|
60
88
|
|
|
61
89
|
{columns.map((col) => {
|
|
62
|
-
|
|
90
|
+
if (col.isSpacer) {
|
|
91
|
+
const width = typeof col.width === 'number' ? col.width : 0;
|
|
92
|
+
return <div key={col.field} style={{ width, minWidth: width, flexShrink: 0 }} aria-hidden="true" />;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// A model entry the column forbids (aggregable: false, or a function outside
|
|
96
|
+
// availableAggregationFunctions) shows nothing, as the engine computes nothing for it.
|
|
97
|
+
const modelFn = (aggregationModel as Record<string, string>)[col.field];
|
|
98
|
+
const fnName = modelFn && isAggregationAllowed(col, modelFn) ? modelFn : undefined;
|
|
63
99
|
const rawValue = aggregationResult[col.field];
|
|
64
|
-
const colWidth =
|
|
100
|
+
const colWidth = getRenderedColumnWidth(col, columnWidths, 120);
|
|
65
101
|
|
|
66
102
|
const pinnedPosition = isColumnPinned(col.field, pinnedColumns);
|
|
67
103
|
const pinnedOffset = pinnedPosition ? pinnedOffsets[col.field] : undefined;
|
|
@@ -94,7 +130,7 @@ export function GridAggregationFooter({
|
|
|
94
130
|
<>
|
|
95
131
|
<span className="ogx__aggregation-label">{fnName}</span>
|
|
96
132
|
<span className="ogx__aggregation-value">
|
|
97
|
-
{
|
|
133
|
+
{formatAggregateForColumn(rawValue, fnName, col, aggregationResult)}
|
|
98
134
|
</span>
|
|
99
135
|
</>
|
|
100
136
|
) : null}
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
+
import { getDataSourceErrorMessage } from '../../utils/dataSource';
|
|
2
|
+
|
|
1
3
|
interface GridErrorOverlayProps {
|
|
2
4
|
error: unknown;
|
|
5
|
+
/** Runs the failed request again. Without it, no Retry button is shown. */
|
|
6
|
+
onRetry?: () => void;
|
|
3
7
|
}
|
|
4
8
|
|
|
5
|
-
export function GridErrorOverlay({ error }: GridErrorOverlayProps) {
|
|
9
|
+
export function GridErrorOverlay({ error, onRetry }: GridErrorOverlayProps) {
|
|
6
10
|
if (!error) return null;
|
|
7
|
-
const message = error
|
|
8
|
-
? error.message
|
|
9
|
-
: typeof error === 'string'
|
|
10
|
-
? error
|
|
11
|
-
: 'An unexpected error occurred while loading the data.';
|
|
11
|
+
const message = getDataSourceErrorMessage(error);
|
|
12
12
|
return (
|
|
13
13
|
<div className="ogx-error-overlay" aria-live="assertive" role="alert">
|
|
14
14
|
<svg width="56" height="56" viewBox="0 0 24 24" fill="none" stroke="var(--ogx-color-error)" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
|
@@ -22,12 +22,15 @@ export function GridErrorOverlay({ error }: GridErrorOverlayProps) {
|
|
|
22
22
|
<div className="ogx-error-overlay__message">
|
|
23
23
|
{message}
|
|
24
24
|
</div>
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
25
|
+
{onRetry && (
|
|
26
|
+
<button
|
|
27
|
+
type="button"
|
|
28
|
+
onClick={onRetry}
|
|
29
|
+
className="ogx-button ogx-button--primary"
|
|
30
|
+
>
|
|
31
|
+
Retry
|
|
32
|
+
</button>
|
|
33
|
+
)}
|
|
31
34
|
</div>
|
|
32
35
|
);
|
|
33
36
|
}
|
|
@@ -1,54 +1,96 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { Pagination } from '../Pagination/Pagination';
|
|
3
3
|
import { ListViewRow } from '../ListView/ListViewRow';
|
|
4
|
+
import { getPageCount, normalizePageSize } from '../../utils/pagination';
|
|
5
|
+
import { useRowsScrollEndLatch } from '../../hooks/core/useRowsScrollEndLatch';
|
|
4
6
|
import type {
|
|
7
|
+
GridColDef,
|
|
5
8
|
GridRowModel,
|
|
6
9
|
GridRowId,
|
|
10
|
+
GridRowMeta,
|
|
7
11
|
GridRowParams,
|
|
8
12
|
GridListViewColDef,
|
|
9
13
|
GridPaginationModel,
|
|
10
|
-
GridDataSource,
|
|
11
14
|
GridLocaleText,
|
|
15
|
+
GridRowScrollEndParams,
|
|
16
|
+
GridSlots,
|
|
12
17
|
} from '../../types';
|
|
13
18
|
|
|
14
19
|
export interface GridListViewProps<R extends GridRowModel> {
|
|
15
20
|
ariaLabel?: string;
|
|
21
|
+
/** Pinned-top rows, the current page and pinned-bottom rows: what the grid view renders. */
|
|
16
22
|
allRenderableRows: R[];
|
|
17
|
-
|
|
23
|
+
/** Filtered data rows (no group, subtotal or auto-created parent rows): the "N items" count. */
|
|
24
|
+
dataRowCount: number;
|
|
25
|
+
/** Number of rows at the start of allRenderableRows that are pinned to the top. */
|
|
26
|
+
pinnedTopRowCount: number;
|
|
27
|
+
/** Number of rows at the end of allRenderableRows that are pinned to the bottom. */
|
|
28
|
+
pinnedBottomRowCount: number;
|
|
29
|
+
/** Filtered, unpinned rows across all pages (the rows pagination splits into pages). */
|
|
30
|
+
unpinnedRowCount: number;
|
|
18
31
|
pagination: boolean;
|
|
19
32
|
effectivePaginationModel: GridPaginationModel;
|
|
33
|
+
/** The page actually shown (the requested page clamped to the last page). */
|
|
34
|
+
currentPage: number;
|
|
20
35
|
pageSizeOptions: number[];
|
|
21
36
|
selectedRowIds: Set<GridRowId>;
|
|
22
37
|
listViewColumn: GridListViewColDef<R>;
|
|
38
|
+
/** Grid columns by field; a list field that names one gets its valueGetter / valueFormatter. */
|
|
39
|
+
columnsByField: ReadonlyMap<string, GridColDef>;
|
|
40
|
+
rowMetaMap: Map<GridRowId, GridRowMeta>;
|
|
23
41
|
noRowsLabel: string;
|
|
24
42
|
rowHeight: number;
|
|
25
43
|
checkboxSelection: boolean;
|
|
26
44
|
paginationMode: 'client' | 'server' | 'infinite';
|
|
27
|
-
|
|
45
|
+
/** The grid's pagination total; used in server mode. */
|
|
28
46
|
serverRowCount: number;
|
|
29
|
-
paginationSlot?:
|
|
47
|
+
paginationSlot?: GridSlots['pagination'];
|
|
30
48
|
paginationSlotProps?: Record<string, unknown>;
|
|
31
49
|
localeText?: Pick<GridLocaleText, 'paginationRowsPerPage' | 'paginationOf' | 'paginationPage'>;
|
|
32
50
|
onRowClick: (params: GridRowParams<R>) => void;
|
|
33
51
|
onRowDoubleClick?: (params: GridRowParams<R>) => void;
|
|
34
52
|
onSelectionChange: (rowId: GridRowId, isSelected: boolean) => void;
|
|
35
53
|
onPaginationModelChange: (model: GridPaginationModel) => void;
|
|
54
|
+
onRowsScrollEnd?: (params: GridRowScrollEndParams) => void;
|
|
55
|
+
/** Resolves a row's id; defaults to `row.id`. */
|
|
56
|
+
getRowId?: (row: R) => GridRowId;
|
|
57
|
+
/** Whether several rows can be selected (aria-multiselectable). */
|
|
58
|
+
multiselectable?: boolean;
|
|
59
|
+
/** The grid is loading (the `loading` prop or a data source fetch). */
|
|
60
|
+
loading?: boolean;
|
|
61
|
+
/** Rendered `slots.loadingOverlay`, shown instead of the rows while loading with no rows. */
|
|
62
|
+
loadingOverlay?: React.ReactNode;
|
|
63
|
+
/** Rendered `slots.noRowsOverlay`, shown instead of the default empty state. */
|
|
64
|
+
noRowsOverlay?: React.ReactNode;
|
|
65
|
+
/** False when `slots.footer` replaces the pagination area. Defaults to true. */
|
|
66
|
+
showPaginationControls?: boolean;
|
|
67
|
+
/** Whether rows can be selected (Shift+Space toggles the focused row). */
|
|
68
|
+
selectable?: boolean;
|
|
69
|
+
/** Expands / collapses a tree-data parent or group row (Enter, Space, Alt+ArrowRight / Alt+ArrowLeft). */
|
|
70
|
+
onToggleExpansion?: (id: GridRowId) => void;
|
|
36
71
|
}
|
|
37
72
|
|
|
73
|
+
const defaultGetRowId = <R extends GridRowModel>(row: R): GridRowId => row.id;
|
|
74
|
+
|
|
38
75
|
export function GridListView<R extends GridRowModel>({
|
|
39
76
|
ariaLabel,
|
|
40
77
|
allRenderableRows,
|
|
41
|
-
|
|
78
|
+
dataRowCount,
|
|
79
|
+
pinnedTopRowCount,
|
|
80
|
+
pinnedBottomRowCount,
|
|
81
|
+
unpinnedRowCount,
|
|
42
82
|
pagination,
|
|
43
83
|
effectivePaginationModel,
|
|
84
|
+
currentPage,
|
|
44
85
|
pageSizeOptions,
|
|
45
86
|
selectedRowIds,
|
|
46
87
|
listViewColumn,
|
|
88
|
+
columnsByField,
|
|
89
|
+
rowMetaMap,
|
|
47
90
|
noRowsLabel,
|
|
48
91
|
rowHeight,
|
|
49
92
|
checkboxSelection,
|
|
50
93
|
paginationMode,
|
|
51
|
-
dataSource,
|
|
52
94
|
serverRowCount,
|
|
53
95
|
paginationSlot,
|
|
54
96
|
paginationSlotProps,
|
|
@@ -57,31 +99,120 @@ export function GridListView<R extends GridRowModel>({
|
|
|
57
99
|
onRowDoubleClick,
|
|
58
100
|
onSelectionChange,
|
|
59
101
|
onPaginationModelChange,
|
|
102
|
+
onRowsScrollEnd,
|
|
103
|
+
getRowId = defaultGetRowId,
|
|
104
|
+
multiselectable = false,
|
|
105
|
+
loading = false,
|
|
106
|
+
loadingOverlay,
|
|
107
|
+
noRowsOverlay,
|
|
108
|
+
showPaginationControls = true,
|
|
109
|
+
selectable = false,
|
|
110
|
+
onToggleExpansion,
|
|
60
111
|
}: GridListViewProps<R>) {
|
|
61
112
|
const PaginationComponent = paginationSlot || Pagination;
|
|
62
113
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
114
|
+
// Roving tab stop: the list is one Tab stop, on the last focused row (the first row until then,
|
|
115
|
+
// or when that row is no longer rendered).
|
|
116
|
+
const rowsRef = React.useRef<HTMLDivElement>(null);
|
|
117
|
+
const [focusedRowId, setFocusedRowId] = React.useState<GridRowId | null>(null);
|
|
118
|
+
const focusedIndex = focusedRowId === null ? -1 : allRenderableRows.findIndex(row => getRowId(row) === focusedRowId);
|
|
119
|
+
const tabStopIndex = focusedIndex === -1 ? 0 : focusedIndex;
|
|
120
|
+
|
|
121
|
+
const focusRow = (index: number) => {
|
|
122
|
+
const row = allRenderableRows[index];
|
|
123
|
+
if (!row) return;
|
|
124
|
+
setFocusedRowId(getRowId(row));
|
|
125
|
+
rowsRef.current?.querySelector<HTMLElement>(`:scope > [role="row"][data-rowindex="${index}"]`)?.focus();
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
|
|
129
|
+
const target = event.target as HTMLElement;
|
|
130
|
+
// Keys pressed in content rendered inside a row (inputs, buttons) belong to that content.
|
|
131
|
+
if (target.getAttribute('role') !== 'row' || target.parentElement !== rowsRef.current) return;
|
|
132
|
+
const index = Number(target.getAttribute('data-rowindex'));
|
|
133
|
+
const row = allRenderableRows[index];
|
|
134
|
+
if (!row) return;
|
|
135
|
+
const id = getRowId(row);
|
|
136
|
+
const last = allRenderableRows.length - 1;
|
|
137
|
+
const meta = rowMetaMap.get(id);
|
|
138
|
+
const canToggle = Boolean(meta?.hasChildren && onToggleExpansion);
|
|
139
|
+
if (event.altKey && (event.key === 'ArrowRight' || event.key === 'ArrowLeft')) {
|
|
140
|
+
event.preventDefault();
|
|
141
|
+
if (canToggle && (event.key === 'ArrowRight') !== (meta?.isExpanded === true)) onToggleExpansion?.(id);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
switch (event.key) {
|
|
145
|
+
case 'ArrowDown': event.preventDefault(); focusRow(Math.min(index + 1, last)); return;
|
|
146
|
+
case 'ArrowUp': event.preventDefault(); focusRow(Math.max(index - 1, 0)); return;
|
|
147
|
+
case 'Home': event.preventDefault(); focusRow(0); return;
|
|
148
|
+
case 'End': event.preventDefault(); focusRow(last); return;
|
|
149
|
+
case 'PageDown': event.preventDefault(); focusRow(Math.min(index + 10, last)); return;
|
|
150
|
+
case 'PageUp': event.preventDefault(); focusRow(Math.max(index - 10, 0)); return;
|
|
151
|
+
case 'Enter':
|
|
152
|
+
case ' ':
|
|
153
|
+
event.preventDefault();
|
|
154
|
+
if (event.key === ' ' && event.shiftKey) {
|
|
155
|
+
// Synthetic group rows are not data, so they are not selectable.
|
|
156
|
+
if (selectable && !meta?.isGroupRow) onSelectionChange(id, !selectedRowIds.has(id));
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
// A row with children (tree-data parent or group row) expands / collapses, as Enter does in
|
|
160
|
+
// the grid view; any other row acts as clicked (onRowClick and click selection).
|
|
161
|
+
if (canToggle) onToggleExpansion?.(id);
|
|
162
|
+
else onRowClick({ row, id, rowIndex: index });
|
|
163
|
+
return;
|
|
164
|
+
default:
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
// Under server pagination only the current page is loaded; the server reports the total.
|
|
169
|
+
const isServerPaged = paginationMode === 'server';
|
|
170
|
+
// Rows split into pages (pinned rows stay on every page, as in the grid view).
|
|
171
|
+
const pagedRowCount = isServerPaged ? (serverRowCount || 0) : unpinnedRowCount;
|
|
172
|
+
const itemCount = isServerPaged ? (serverRowCount || 0) : dataRowCount;
|
|
173
|
+
const pageSize = normalizePageSize(effectivePaginationModel.pageSize);
|
|
174
|
+
const pageCount = getPageCount(pagedRowCount, pageSize);
|
|
175
|
+
|
|
176
|
+
// aria-rowindex is 1-based over the whole data set (the list view has no header row):
|
|
177
|
+
// pinned-top rows, then the unpinned rows across all pages, then pinned-bottom rows.
|
|
178
|
+
const pageOffset = pagination ? currentPage * pageSize : 0;
|
|
179
|
+
const centerRowsOnPage = allRenderableRows.length - pinnedTopRowCount - pinnedBottomRowCount;
|
|
180
|
+
const ariaRowIndex = (idx: number): number => {
|
|
181
|
+
if (idx < pinnedTopRowCount) return idx + 1;
|
|
182
|
+
const centerIdx = idx - pinnedTopRowCount;
|
|
183
|
+
if (centerIdx < centerRowsOnPage) return pinnedTopRowCount + pageOffset + centerIdx + 1;
|
|
184
|
+
return pinnedTopRowCount + pagedRowCount + (centerIdx - centerRowsOnPage) + 1;
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const handleScroll = useRowsScrollEndLatch(onRowsScrollEnd, allRenderableRows.length);
|
|
66
188
|
|
|
67
189
|
return (
|
|
68
190
|
<div
|
|
69
191
|
className="ogx-list-view"
|
|
70
192
|
role="grid"
|
|
71
193
|
aria-label={ariaLabel || 'Data grid list view'}
|
|
72
|
-
aria-rowcount={
|
|
194
|
+
aria-rowcount={itemCount}
|
|
195
|
+
aria-multiselectable={multiselectable}
|
|
73
196
|
>
|
|
74
197
|
<div className="ogx-list-view__toolbar">
|
|
75
198
|
<span>
|
|
76
|
-
{
|
|
77
|
-
{pagination ? ` · page ${
|
|
199
|
+
{itemCount} {itemCount === 1 ? 'item' : 'items'}
|
|
200
|
+
{pagination ? ` · page ${currentPage + 1} of ${pageCount}` : ''}
|
|
78
201
|
{selectedRowIds.size > 0 ? ` · ${selectedRowIds.size} selected` : ''}
|
|
79
202
|
</span>
|
|
80
203
|
</div>
|
|
81
204
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
205
|
+
{/* tabIndex -1: Firefox otherwise makes this scroll container a Tab stop of its own; the rows carry the roving tab stop. */}
|
|
206
|
+
<div className="ogx-list-view__rows" tabIndex={-1} onScroll={handleScroll} onKeyDown={handleKeyDown} ref={rowsRef}>
|
|
207
|
+
{allRenderableRows.length === 0 && loading ? (
|
|
208
|
+
// The grid's live region announces loading; the list shows no "No Data" meanwhile.
|
|
209
|
+
<div className="ogx-list-view__loading">
|
|
210
|
+
{loadingOverlay ?? <div className="ogx__loading-bar" role="progressbar" aria-label="Loading data" />}
|
|
211
|
+
</div>
|
|
212
|
+
) : allRenderableRows.length === 0 && noRowsOverlay ? (
|
|
213
|
+
<div className="ogx-list-view__empty">{noRowsOverlay}</div>
|
|
214
|
+
) : allRenderableRows.length === 0 ? (
|
|
215
|
+
<div className="ogx-list-view__empty">
|
|
85
216
|
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
|
|
86
217
|
<rect x="3" y="3" width="18" height="18" rx="2" />
|
|
87
218
|
<path d="M3 9h18M9 21V9" />
|
|
@@ -89,28 +220,38 @@ export function GridListView<R extends GridRowModel>({
|
|
|
89
220
|
{noRowsLabel}
|
|
90
221
|
</div>
|
|
91
222
|
) : (
|
|
92
|
-
allRenderableRows.map((row, idx) =>
|
|
223
|
+
allRenderableRows.map((row, idx) => {
|
|
224
|
+
const id = getRowId(row);
|
|
225
|
+
return (
|
|
93
226
|
<ListViewRow<R>
|
|
94
|
-
key={
|
|
227
|
+
key={id}
|
|
95
228
|
row={row}
|
|
229
|
+
rowId={id}
|
|
96
230
|
rowIndex={idx}
|
|
231
|
+
ariaRowIndex={ariaRowIndex(idx)}
|
|
97
232
|
listViewColumn={listViewColumn}
|
|
98
|
-
|
|
233
|
+
gridColumn={columnsByField.get(listViewColumn.field) as GridColDef<R> | undefined}
|
|
234
|
+
rowMeta={rowMetaMap.get(id)}
|
|
235
|
+
isSelected={selectedRowIds.has(id)}
|
|
99
236
|
checkboxSelection={checkboxSelection}
|
|
100
237
|
rowHeight={rowHeight}
|
|
101
|
-
onRowClick={(r) => onRowClick({ row: r, id
|
|
102
|
-
onRowDoubleClick={onRowDoubleClick ? (r) => onRowDoubleClick({ row: r, id
|
|
238
|
+
onRowClick={(r) => onRowClick({ row: r, id, rowIndex: idx })}
|
|
239
|
+
onRowDoubleClick={onRowDoubleClick ? (r) => onRowDoubleClick({ row: r, id, rowIndex: idx }) : undefined}
|
|
103
240
|
onSelectionChange={onSelectionChange}
|
|
241
|
+
tabIndex={idx === tabStopIndex ? 0 : -1}
|
|
242
|
+
onToggleExpansion={onToggleExpansion}
|
|
243
|
+
onFocus={(rowIndex) => setFocusedRowId(getRowId(allRenderableRows[rowIndex]))}
|
|
104
244
|
/>
|
|
105
|
-
|
|
245
|
+
);
|
|
246
|
+
})
|
|
106
247
|
)}
|
|
107
248
|
</div>
|
|
108
249
|
|
|
109
|
-
{pagination && (
|
|
250
|
+
{pagination && showPaginationControls && (
|
|
110
251
|
<PaginationComponent
|
|
111
|
-
page={
|
|
252
|
+
page={currentPage}
|
|
112
253
|
pageSize={effectivePaginationModel.pageSize}
|
|
113
|
-
rowCount={
|
|
254
|
+
rowCount={pagedRowCount}
|
|
114
255
|
pageSizeOptions={pageSizeOptions}
|
|
115
256
|
onPageChange={(newPage: number) => onPaginationModelChange({ ...effectivePaginationModel, page: newPage })}
|
|
116
257
|
onPageSizeChange={(newPageSize: number) => onPaginationModelChange({ ...effectivePaginationModel, pageSize: newPageSize, page: 0 })}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { getDataSourceErrorMessage } from '../../utils/dataSource';
|
|
2
|
+
import type { GridFilterModel } from '../../types';
|
|
3
|
+
|
|
4
|
+
interface GridLiveRegionProps {
|
|
5
|
+
loading: boolean;
|
|
6
|
+
/** The dataSource error, if any. */
|
|
7
|
+
error: unknown;
|
|
8
|
+
noRowsLabel: string;
|
|
9
|
+
/** Rows that pass the filter (grid rows, group rows included). */
|
|
10
|
+
filteredRowCount: number;
|
|
11
|
+
/** Data rows that pass the filter: the "n rows found" count. */
|
|
12
|
+
dataRowCount: number;
|
|
13
|
+
filterModel: GridFilterModel | undefined;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Polite status announcements: loading, a load error, no rows, or the row count of an active filter. */
|
|
17
|
+
export function GridLiveRegion({ loading, error, noRowsLabel, filteredRowCount, dataRowCount, filterModel }: GridLiveRegionProps) {
|
|
18
|
+
return (
|
|
19
|
+
<div className="ogx-aria-live-status" role="status" aria-live="polite">
|
|
20
|
+
{loading ? 'Loading data...' : ''}
|
|
21
|
+
{error ? `Error: ${getDataSourceErrorMessage(error)}` : ''}
|
|
22
|
+
{!loading && !error && (
|
|
23
|
+
filteredRowCount === 0
|
|
24
|
+
? noRowsLabel
|
|
25
|
+
: (filterModel && ((filterModel.quickFilterValues?.length || 0) > 0 || (filterModel.items?.length || 0) > 0))
|
|
26
|
+
? `${dataRowCount} ${dataRowCount === 1 ? 'row' : 'rows'} found`
|
|
27
|
+
: ''
|
|
28
|
+
)}
|
|
29
|
+
</div>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
interface GridLoadingOverlayProps {
|
|
4
|
+
/** `slots.loadingOverlay`, rendered centred over the rows on a dimmed backdrop. */
|
|
5
|
+
overlay?: React.ReactNode;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Shown while the grid is loading but already has rows (a reload, a server sort or filter): the
|
|
10
|
+
* rows stay visible and interactive, and a progress bar runs along the top of the grid. A custom
|
|
11
|
+
* `slots.loadingOverlay` is shown centred instead. With no rows yet, the body shows skeleton rows
|
|
12
|
+
* or the loading overlay in place of the rows, so this is not rendered.
|
|
13
|
+
*/
|
|
14
|
+
export function GridLoadingOverlay({ overlay }: GridLoadingOverlayProps) {
|
|
15
|
+
if (overlay) {
|
|
16
|
+
return (
|
|
17
|
+
<div className="ogx__loading-overlay ogx__loading-overlay--over-rows ogx__loading-overlay--custom">
|
|
18
|
+
{overlay}
|
|
19
|
+
</div>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
return (
|
|
23
|
+
<div className="ogx__loading-overlay ogx__loading-overlay--over-rows">
|
|
24
|
+
<div className="ogx__loading-bar" role="progressbar" aria-label="Loading data" />
|
|
25
|
+
</div>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Pagination } from '../Pagination/Pagination';
|
|
3
|
+
import { pickPaginationLocaleText } from '../../utils/pagination';
|
|
4
|
+
import type { GridLocaleText, GridPaginationModel, GridPaginationSlotProps } from '../../types';
|
|
5
|
+
|
|
6
|
+
interface GridPaginationAreaProps {
|
|
7
|
+
/** `slots.pagination`; the built-in `Pagination` otherwise. */
|
|
8
|
+
paginationSlot?: React.ComponentType<GridPaginationSlotProps & Record<string, unknown>>;
|
|
9
|
+
/** `slotProps.pagination`, merged over the grid's props. */
|
|
10
|
+
paginationSlotProps?: Partial<GridPaginationSlotProps> & Record<string, unknown>;
|
|
11
|
+
/** The page shown (clamped by the row pipeline). */
|
|
12
|
+
page: number;
|
|
13
|
+
paginationModel: GridPaginationModel;
|
|
14
|
+
rowCount: number;
|
|
15
|
+
pageSizeOptions: number[];
|
|
16
|
+
onPaginationModelChange: (model: GridPaginationModel) => void;
|
|
17
|
+
localeText?: GridLocaleText;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** The grid's pager below the viewport: a page change keeps the page size, a page-size change returns to page 0. */
|
|
21
|
+
export function GridPaginationArea({
|
|
22
|
+
paginationSlot,
|
|
23
|
+
paginationSlotProps,
|
|
24
|
+
page,
|
|
25
|
+
paginationModel,
|
|
26
|
+
rowCount,
|
|
27
|
+
pageSizeOptions,
|
|
28
|
+
onPaginationModelChange,
|
|
29
|
+
localeText,
|
|
30
|
+
}: GridPaginationAreaProps) {
|
|
31
|
+
const PaginationComponent = paginationSlot || Pagination;
|
|
32
|
+
return (
|
|
33
|
+
<PaginationComponent
|
|
34
|
+
page={page}
|
|
35
|
+
pageSize={paginationModel.pageSize}
|
|
36
|
+
rowCount={rowCount}
|
|
37
|
+
pageSizeOptions={pageSizeOptions}
|
|
38
|
+
onPageChange={(newPage: number) => onPaginationModelChange({ ...paginationModel, page: newPage })}
|
|
39
|
+
onPageSizeChange={(newPageSize: number) => onPaginationModelChange({ ...paginationModel, pageSize: newPageSize, page: 0 })}
|
|
40
|
+
localeText={pickPaginationLocaleText(localeText)}
|
|
41
|
+
{...paginationSlotProps}
|
|
42
|
+
/>
|
|
43
|
+
);
|
|
44
|
+
}
|