@opencorestack/opengridx 1.0.6 → 1.1.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 +18 -0
- package/dist/index.d.ts +40 -12
- package/dist/opengridx.css +1 -1
- package/dist/opengridx.es.js +3212 -3130
- package/dist/opengridx.es.js.map +1 -1
- package/dist/opengridx.umd.js +12 -12
- package/dist/opengridx.umd.js.map +1 -1
- package/docs/API_REFERENCE.md +35 -0
- package/docs/components/cell.md +14 -0
- package/docs/components/filter-panel.md +13 -0
- package/docs/components/pagination.md +26 -0
- package/docs/components/row.md +26 -0
- package/docs/roadmap.md +8 -0
- package/lib/components/Cell/Cell.tsx +44 -44
- package/lib/components/Cell/CellErrorBoundary.tsx +53 -0
- package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.tsx +0 -2
- package/lib/components/DataGrid/DataGrid.css +47 -0
- package/lib/components/DataGrid/DataGrid.tsx +34 -8
- package/lib/components/DataGrid/GridAggregationFooter.tsx +47 -9
- package/lib/components/DataGrid/GridListView.tsx +4 -0
- package/lib/components/DataGrid/GridPinnedRows.tsx +4 -0
- package/lib/components/DataGrid/GridStandaloneColumnPanel.tsx +3 -1
- package/lib/components/DataGrid/GridVirtualRows.tsx +4 -0
- package/lib/components/FilterPanel/FilterPanel.tsx +5 -4
- package/lib/components/Header/Header.tsx +4 -3
- package/lib/components/Pagination/Pagination.test.tsx +64 -0
- package/lib/components/Pagination/Pagination.tsx +19 -7
- package/lib/components/Row/Row.tsx +9 -5
- package/lib/components/Toolbar/GlobalSearch.tsx +0 -2
- package/lib/components/Toolbar/GridToolbar.tsx +0 -4
- package/lib/hooks/core/useGridColumns.tsx +11 -11
- package/lib/hooks/core/useGridControlledState.test.ts +86 -0
- package/lib/hooks/core/useGridKeyboardNavigation.test.ts +176 -0
- package/lib/hooks/core/useGridRowPipeline.test.ts +117 -0
- package/lib/hooks/useRowGrouping.test.ts +84 -0
- package/lib/hooks/useRowGrouping.ts +38 -13
- package/lib/hooks/useTreeData.ts +22 -2
- package/lib/index.ts +2 -0
- package/lib/types/index.ts +38 -12
- package/package.json +1 -1
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
GridListViewColDef,
|
|
9
9
|
GridPaginationModel,
|
|
10
10
|
GridDataSource,
|
|
11
|
+
GridLocaleText,
|
|
11
12
|
} from '../../types';
|
|
12
13
|
|
|
13
14
|
export interface GridListViewProps<R extends GridRowModel> {
|
|
@@ -27,6 +28,7 @@ export interface GridListViewProps<R extends GridRowModel> {
|
|
|
27
28
|
serverRowCount: number;
|
|
28
29
|
paginationSlot?: React.ComponentType<Record<string, unknown>>;
|
|
29
30
|
paginationSlotProps?: Record<string, unknown>;
|
|
31
|
+
localeText?: Pick<GridLocaleText, 'paginationRowsPerPage' | 'paginationOf' | 'paginationPage'>;
|
|
30
32
|
onRowClick: (params: GridRowParams<R>) => void;
|
|
31
33
|
onSelectionChange: (rowId: GridRowId, isSelected: boolean) => void;
|
|
32
34
|
onPaginationModelChange: (model: GridPaginationModel) => void;
|
|
@@ -49,6 +51,7 @@ export function GridListView<R extends GridRowModel>({
|
|
|
49
51
|
serverRowCount,
|
|
50
52
|
paginationSlot,
|
|
51
53
|
paginationSlotProps,
|
|
54
|
+
localeText,
|
|
52
55
|
onRowClick,
|
|
53
56
|
onSelectionChange,
|
|
54
57
|
onPaginationModelChange,
|
|
@@ -108,6 +111,7 @@ export function GridListView<R extends GridRowModel>({
|
|
|
108
111
|
pageSizeOptions={pageSizeOptions}
|
|
109
112
|
onPageChange={(newPage: number) => onPaginationModelChange({ ...effectivePaginationModel, page: newPage })}
|
|
110
113
|
onPageSizeChange={(newPageSize: number) => onPaginationModelChange({ ...effectivePaginationModel, pageSize: newPageSize, page: 0 })}
|
|
114
|
+
localeText={localeText}
|
|
111
115
|
{...paginationSlotProps}
|
|
112
116
|
/>
|
|
113
117
|
)}
|
|
@@ -10,6 +10,7 @@ import type {
|
|
|
10
10
|
GridRowPinning,
|
|
11
11
|
GridDetailPanelParams,
|
|
12
12
|
GridDetailPanelHeight,
|
|
13
|
+
GridRowMeta,
|
|
13
14
|
} from '../../types';
|
|
14
15
|
import type { CellColSpanInfo, RowSpanningCaches } from '../../hooks/features/useGridSpanning';
|
|
15
16
|
|
|
@@ -36,6 +37,7 @@ export interface GridPinnedRowsProps<R extends GridRowModel> {
|
|
|
36
37
|
colspanMap?: Map<GridRowId, Record<string, CellColSpanInfo>>;
|
|
37
38
|
rowSpanningCaches?: RowSpanningCaches;
|
|
38
39
|
rowHeight: number;
|
|
40
|
+
rowMetaMap: Map<GridRowId, GridRowMeta>;
|
|
39
41
|
}
|
|
40
42
|
|
|
41
43
|
export function GridPinnedRows<R extends GridRowModel>({
|
|
@@ -61,6 +63,7 @@ export function GridPinnedRows<R extends GridRowModel>({
|
|
|
61
63
|
colspanMap,
|
|
62
64
|
rowSpanningCaches,
|
|
63
65
|
rowHeight,
|
|
66
|
+
rowMetaMap,
|
|
64
67
|
}: GridPinnedRowsProps<R>) {
|
|
65
68
|
if (rows.length === 0) return null;
|
|
66
69
|
|
|
@@ -91,6 +94,7 @@ export function GridPinnedRows<R extends GridRowModel>({
|
|
|
91
94
|
colspanMap={colspanMap}
|
|
92
95
|
rowSpanningCaches={rowSpanningCaches}
|
|
93
96
|
rowHeight={rowHeight}
|
|
97
|
+
rowMeta={rowMetaMap.get(row.id)}
|
|
94
98
|
/>
|
|
95
99
|
))}
|
|
96
100
|
</div>
|
|
@@ -6,6 +6,7 @@ import type { GridColDef, GridRowModel, GridColumnOrderChangeParams } from '../.
|
|
|
6
6
|
export interface GridStandaloneColumnPanelProps<R extends GridRowModel> {
|
|
7
7
|
isOpen: boolean;
|
|
8
8
|
containerRef: React.RefObject<HTMLDivElement | null>;
|
|
9
|
+
panelRef?: React.RefObject<HTMLDivElement | null>;
|
|
9
10
|
effectiveColumns: GridColDef<R>[];
|
|
10
11
|
columnVisibilityModel: Record<string, boolean>;
|
|
11
12
|
effectiveColumnOrder: string[];
|
|
@@ -20,6 +21,7 @@ export interface GridStandaloneColumnPanelProps<R extends GridRowModel> {
|
|
|
20
21
|
export function GridStandaloneColumnPanel<R extends GridRowModel>({
|
|
21
22
|
isOpen,
|
|
22
23
|
containerRef,
|
|
24
|
+
panelRef,
|
|
23
25
|
effectiveColumns,
|
|
24
26
|
columnVisibilityModel,
|
|
25
27
|
effectiveColumnOrder,
|
|
@@ -64,7 +66,7 @@ export function GridStandaloneColumnPanel<R extends GridRowModel>({
|
|
|
64
66
|
|
|
65
67
|
return ReactDOM.createPortal(
|
|
66
68
|
<div
|
|
67
|
-
|
|
69
|
+
ref={panelRef}
|
|
68
70
|
style={{
|
|
69
71
|
position: 'fixed',
|
|
70
72
|
top: panelTop,
|
|
@@ -12,6 +12,7 @@ import type {
|
|
|
12
12
|
GridRowPinning,
|
|
13
13
|
GridDetailPanelParams,
|
|
14
14
|
GridDetailPanelHeight,
|
|
15
|
+
GridRowMeta,
|
|
15
16
|
} from '../../types';
|
|
16
17
|
import type { GridVisibleRow } from '../../hooks/core/useGridVisibleRows';
|
|
17
18
|
import type { UseRowReorderReturn } from '../../hooks/useRowReorder';
|
|
@@ -60,6 +61,7 @@ export interface GridVirtualRowsProps<R extends GridRowModel> {
|
|
|
60
61
|
sortedUnpinnedRowCount: number;
|
|
61
62
|
infiniteScrollSkeletonCount: number;
|
|
62
63
|
unpinnedRowsLength: number;
|
|
64
|
+
rowMetaMap: Map<GridRowId, GridRowMeta>;
|
|
63
65
|
}
|
|
64
66
|
|
|
65
67
|
export function GridVirtualRows<R extends GridRowModel>({
|
|
@@ -97,6 +99,7 @@ export function GridVirtualRows<R extends GridRowModel>({
|
|
|
97
99
|
sortedUnpinnedRowCount,
|
|
98
100
|
infiniteScrollSkeletonCount,
|
|
99
101
|
unpinnedRowsLength,
|
|
102
|
+
rowMetaMap,
|
|
100
103
|
}: GridVirtualRowsProps<R>) {
|
|
101
104
|
const skeletonColumns: GridColDef<R>[] = baseColumns.length > 0
|
|
102
105
|
? baseColumns
|
|
@@ -179,6 +182,7 @@ export function GridVirtualRows<R extends GridRowModel>({
|
|
|
179
182
|
colspanMap={colspanMap}
|
|
180
183
|
rowSpanningCaches={rowSpanningCaches}
|
|
181
184
|
rowHeight={rowHeight}
|
|
185
|
+
rowMeta={rowMetaMap.get(row.id)}
|
|
182
186
|
/>
|
|
183
187
|
))
|
|
184
188
|
)}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
|
1
|
+
import React, { useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
|
2
2
|
import type {
|
|
3
3
|
GridFilterModel,
|
|
4
4
|
GridFilterItem,
|
|
@@ -25,6 +25,7 @@ const FilterRow: React.FC<{
|
|
|
25
25
|
item: GridFilterItem | undefined;
|
|
26
26
|
onChange: (item: GridFilterItem | null) => void;
|
|
27
27
|
}> = ({ col, item, onChange }) => {
|
|
28
|
+
const filterId = useId();
|
|
28
29
|
const operators = getOperatorsForType(col.type);
|
|
29
30
|
const isActive = !!item;
|
|
30
31
|
const currentOperator = item?.operator ?? operators[0];
|
|
@@ -99,7 +100,7 @@ const FilterRow: React.FC<{
|
|
|
99
100
|
<div className="ogx-filter__controls">
|
|
100
101
|
{ }
|
|
101
102
|
<select
|
|
102
|
-
id={
|
|
103
|
+
id={`${filterId}-op`}
|
|
103
104
|
name={`filter-op-${col.field}`}
|
|
104
105
|
className={`ogx-filter__op-select${isActive ? ' ogx-filter__op-select--active' : ''}`}
|
|
105
106
|
value={String(currentOperator)}
|
|
@@ -114,7 +115,7 @@ const FilterRow: React.FC<{
|
|
|
114
115
|
{ }
|
|
115
116
|
{showValue && isBoolean && (
|
|
116
117
|
<select
|
|
117
|
-
id={
|
|
118
|
+
id={`${filterId}-val`}
|
|
118
119
|
name={`filter-val-${col.field}`}
|
|
119
120
|
className={`ogx-filter__value-input ogx-filter__value-select${isActive ? ' ogx-filter__value-input--active' : ''}`}
|
|
120
121
|
value={String(item?.value ?? 'true')}
|
|
@@ -136,7 +137,7 @@ const FilterRow: React.FC<{
|
|
|
136
137
|
|
|
137
138
|
{showValue && !isBoolean && (
|
|
138
139
|
<input
|
|
139
|
-
id={
|
|
140
|
+
id={`${filterId}-val`}
|
|
140
141
|
name={`filter-val-${col.field}`}
|
|
141
142
|
className={`ogx-filter__value-input${isActive ? ' ogx-filter__value-input--active' : ''}`}
|
|
142
143
|
type="text"
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React from 'react';
|
|
1
|
+
import React, { useId } from 'react';
|
|
2
2
|
import { Checkbox } from '../ui/Checkbox';
|
|
3
3
|
import { ColumnResizeHandle } from '../ColumnResizeHandle/ColumnResizeHandle';
|
|
4
4
|
import type { GridColDef, GridRowModel, GridSortDirection, GridColumnPinning, GridAggregationModel, GridColumnGroup, GridColumnGroupingModel } from '../../types';
|
|
@@ -171,6 +171,7 @@ export function Header<R extends GridRowModel = GridRowModel>(props: HeaderProps
|
|
|
171
171
|
onManageColumns,
|
|
172
172
|
} = props;
|
|
173
173
|
|
|
174
|
+
const selectAllId = useId();
|
|
174
175
|
const cellRefs = React.useRef<Record<string, HTMLElement | null>>({});
|
|
175
176
|
const [menuOpenParams, setMenuOpenParams] = React.useState<{ colDef: GridColDef<R>; anchorEl: HTMLElement } | null>(null);
|
|
176
177
|
|
|
@@ -340,8 +341,8 @@ export function Header<R extends GridRowModel = GridRowModel>(props: HeaderProps
|
|
|
340
341
|
}}
|
|
341
342
|
>
|
|
342
343
|
<Checkbox
|
|
343
|
-
id=
|
|
344
|
-
name=
|
|
344
|
+
id={selectAllId}
|
|
345
|
+
name={selectAllId}
|
|
345
346
|
checked={allSelected}
|
|
346
347
|
indeterminate={someSelected}
|
|
347
348
|
onChange={(e) => onSelectAll?.(e.target.checked)}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { render, screen } from '@testing-library/react';
|
|
3
|
+
import { Pagination } from './Pagination';
|
|
4
|
+
|
|
5
|
+
const NOOP = () => {};
|
|
6
|
+
|
|
7
|
+
describe('Pagination', () => {
|
|
8
|
+
it('renders "0 of 0" when rowCount is 0', () => {
|
|
9
|
+
render(
|
|
10
|
+
<Pagination
|
|
11
|
+
page={0}
|
|
12
|
+
pageSize={10}
|
|
13
|
+
rowCount={0}
|
|
14
|
+
onPageChange={NOOP}
|
|
15
|
+
onPageSizeChange={NOOP}
|
|
16
|
+
/>
|
|
17
|
+
);
|
|
18
|
+
expect(screen.getByText('0 of 0')).toBeTruthy();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('calls localeText.paginationOf with (0, 0, 0) when rowCount is 0', () => {
|
|
22
|
+
const paginationOf = vi.fn().mockReturnValue('custom empty');
|
|
23
|
+
render(
|
|
24
|
+
<Pagination
|
|
25
|
+
page={0}
|
|
26
|
+
pageSize={10}
|
|
27
|
+
rowCount={0}
|
|
28
|
+
onPageChange={NOOP}
|
|
29
|
+
onPageSizeChange={NOOP}
|
|
30
|
+
localeText={{ paginationOf }}
|
|
31
|
+
/>
|
|
32
|
+
);
|
|
33
|
+
expect(paginationOf).toHaveBeenCalledWith(0, 0, 0);
|
|
34
|
+
expect(screen.getByText('custom empty')).toBeTruthy();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('renders correct row range label for non-empty data', () => {
|
|
38
|
+
render(
|
|
39
|
+
<Pagination
|
|
40
|
+
page={0}
|
|
41
|
+
pageSize={10}
|
|
42
|
+
rowCount={25}
|
|
43
|
+
onPageChange={NOOP}
|
|
44
|
+
onPageSizeChange={NOOP}
|
|
45
|
+
/>
|
|
46
|
+
);
|
|
47
|
+
expect(screen.getByText('1–10 of 25')).toBeTruthy();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('calls localeText.paginationOf with correct args for non-empty data', () => {
|
|
51
|
+
const paginationOf = vi.fn().mockReturnValue('custom range');
|
|
52
|
+
render(
|
|
53
|
+
<Pagination
|
|
54
|
+
page={1}
|
|
55
|
+
pageSize={10}
|
|
56
|
+
rowCount={25}
|
|
57
|
+
onPageChange={NOOP}
|
|
58
|
+
onPageSizeChange={NOOP}
|
|
59
|
+
localeText={{ paginationOf }}
|
|
60
|
+
/>
|
|
61
|
+
);
|
|
62
|
+
expect(paginationOf).toHaveBeenCalledWith(11, 20, 25);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
|
|
2
|
-
import React from 'react';
|
|
2
|
+
import React, { useId } from 'react';
|
|
3
3
|
import { Button } from '../ui/Button';
|
|
4
|
+
import type { GridLocaleText } from '../../types';
|
|
4
5
|
|
|
5
6
|
export interface PaginationProps {
|
|
6
7
|
page: number;
|
|
@@ -11,9 +12,11 @@ export interface PaginationProps {
|
|
|
11
12
|
pageSizeOptions?: number[];
|
|
12
13
|
showFirstButton?: boolean;
|
|
13
14
|
showLastButton?: boolean;
|
|
15
|
+
localeText?: Pick<GridLocaleText, 'paginationRowsPerPage' | 'paginationOf' | 'paginationPage'>;
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
export function Pagination(props: PaginationProps) {
|
|
19
|
+
const pageSizeId = useId();
|
|
17
20
|
const {
|
|
18
21
|
page,
|
|
19
22
|
pageSize,
|
|
@@ -22,7 +25,8 @@ export function Pagination(props: PaginationProps) {
|
|
|
22
25
|
onPageSizeChange,
|
|
23
26
|
pageSizeOptions = [10, 25, 50, 100],
|
|
24
27
|
showFirstButton = true,
|
|
25
|
-
showLastButton = true
|
|
28
|
+
showLastButton = true,
|
|
29
|
+
localeText,
|
|
26
30
|
} = props;
|
|
27
31
|
|
|
28
32
|
const pageCount = Math.max(1, Math.ceil(rowCount / pageSize));
|
|
@@ -32,6 +36,14 @@ export function Pagination(props: PaginationProps) {
|
|
|
32
36
|
const lastRowIndex = Math.min(firstRowIndex + pageSize, rowCount);
|
|
33
37
|
const displayedRows = rowCount === 0 ? 0 : `${firstRowIndex + 1}–${lastRowIndex}`;
|
|
34
38
|
|
|
39
|
+
const rowsPerPageLabel = localeText?.paginationRowsPerPage ?? 'Rows per page:';
|
|
40
|
+
const ofLabel = localeText?.paginationOf
|
|
41
|
+
? localeText.paginationOf(rowCount === 0 ? 0 : firstRowIndex + 1, lastRowIndex, rowCount)
|
|
42
|
+
: `${displayedRows} of ${rowCount}`;
|
|
43
|
+
const pageLabel = localeText?.paginationPage
|
|
44
|
+
? localeText.paginationPage(currentPage + 1, pageCount)
|
|
45
|
+
: `Page ${currentPage + 1} of ${pageCount}`;
|
|
46
|
+
|
|
35
47
|
const handleFirstPage = () => {
|
|
36
48
|
onPageChange(0);
|
|
37
49
|
};
|
|
@@ -61,11 +73,11 @@ export function Pagination(props: PaginationProps) {
|
|
|
61
73
|
<div className="ogx-pagination" role="navigation" aria-label="Pagination">
|
|
62
74
|
{ }
|
|
63
75
|
<div className="ogx-pagination__page-size">
|
|
64
|
-
<label htmlFor=
|
|
65
|
-
|
|
76
|
+
<label htmlFor={pageSizeId} className="ogx-pagination__label">
|
|
77
|
+
{rowsPerPageLabel}
|
|
66
78
|
</label>
|
|
67
79
|
<select
|
|
68
|
-
id=
|
|
80
|
+
id={pageSizeId}
|
|
69
81
|
className="ogx-pagination__select"
|
|
70
82
|
value={pageSize}
|
|
71
83
|
onChange={handlePageSizeChange}
|
|
@@ -81,7 +93,7 @@ export function Pagination(props: PaginationProps) {
|
|
|
81
93
|
|
|
82
94
|
{ }
|
|
83
95
|
<div className="ogx-pagination__info" aria-live="polite" aria-atomic="true">
|
|
84
|
-
{
|
|
96
|
+
{ofLabel}
|
|
85
97
|
</div>
|
|
86
98
|
|
|
87
99
|
{ }
|
|
@@ -115,7 +127,7 @@ export function Pagination(props: PaginationProps) {
|
|
|
115
127
|
</Button>
|
|
116
128
|
|
|
117
129
|
<div className="ogx-pagination__page-info" aria-current="page">
|
|
118
|
-
|
|
130
|
+
{pageLabel}
|
|
119
131
|
</div>
|
|
120
132
|
|
|
121
133
|
<Button
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
|
|
2
|
-
import React from 'react';
|
|
2
|
+
import React, { useId } from 'react';
|
|
3
3
|
import { Cell } from '../Cell/Cell';
|
|
4
4
|
import { Checkbox } from '../ui/Checkbox';
|
|
5
5
|
import { ExpandIcon } from '../ui/ExpandIcon';
|
|
6
6
|
import { DragHandleIcon } from '../ui/DragHandleIcon';
|
|
7
7
|
import { DetailPanel } from '../DetailPanel/DetailPanel';
|
|
8
|
-
import type { GridRowModel, GridColDef, GridRowId, GridColumnPinning, GridRowPinning, GridRowParams, GridCellParams, GridDetailPanelHeight } from '../../types';
|
|
8
|
+
import type { GridRowModel, GridColDef, GridRowId, GridColumnPinning, GridRowPinning, GridRowParams, GridCellParams, GridDetailPanelHeight, GridRowMeta } from '../../types';
|
|
9
9
|
import type { CellColSpanInfo, RowSpanningCaches } from '../../hooks/features/useGridSpanning';
|
|
10
10
|
import { isColumnPinned, calculatePinnedPositions, isRowPinned } from '../../utils/pinning';
|
|
11
11
|
|
|
@@ -51,6 +51,7 @@ export interface RowProps<R extends GridRowModel = GridRowModel> {
|
|
|
51
51
|
colspanMap?: Map<GridRowId, Record<string, CellColSpanInfo>>;
|
|
52
52
|
rowSpanningCaches?: RowSpanningCaches;
|
|
53
53
|
rowHeight?: number;
|
|
54
|
+
rowMeta?: GridRowMeta;
|
|
54
55
|
}
|
|
55
56
|
|
|
56
57
|
export function Row<R extends GridRowModel = GridRowModel>(props: RowProps<R>) {
|
|
@@ -91,9 +92,11 @@ export function Row<R extends GridRowModel = GridRowModel>(props: RowProps<R>) {
|
|
|
91
92
|
|
|
92
93
|
colspanMap,
|
|
93
94
|
rowSpanningCaches,
|
|
94
|
-
rowHeight = 52
|
|
95
|
+
rowHeight = 52,
|
|
96
|
+
rowMeta
|
|
95
97
|
} = props;
|
|
96
98
|
|
|
99
|
+
const checkboxId = useId();
|
|
97
100
|
const expandCellRef = React.useRef<HTMLDivElement>(null);
|
|
98
101
|
const checkboxCellRef = React.useRef<HTMLDivElement>(null);
|
|
99
102
|
|
|
@@ -120,7 +123,7 @@ export function Row<R extends GridRowModel = GridRowModel>(props: RowProps<R>) {
|
|
|
120
123
|
);
|
|
121
124
|
}, [columns, columnWidths, pinnedColumns, checkboxSelection, pinCheckboxColumn, hasDetailPanel, pinExpandColumn, rowReordering]);
|
|
122
125
|
|
|
123
|
-
const isGroupRow =
|
|
126
|
+
const isGroupRow = rowMeta?.hasChildren === true;
|
|
124
127
|
|
|
125
128
|
const handleCellEditStart = React.useCallback((field: string, value: unknown) => {
|
|
126
129
|
if (!isGroupRow) {
|
|
@@ -291,7 +294,7 @@ export function Row<R extends GridRowModel = GridRowModel>(props: RowProps<R>) {
|
|
|
291
294
|
}}
|
|
292
295
|
>
|
|
293
296
|
<Checkbox
|
|
294
|
-
id={
|
|
297
|
+
id={checkboxId}
|
|
295
298
|
name={`ogx-select-row-${row.id}`}
|
|
296
299
|
checked={isSelected}
|
|
297
300
|
onChange={handleCheckboxChange}
|
|
@@ -357,6 +360,7 @@ export function Row<R extends GridRowModel = GridRowModel>(props: RowProps<R>) {
|
|
|
357
360
|
colSpanInfo={colSpanInfo}
|
|
358
361
|
rowSpan={rowSpan}
|
|
359
362
|
isHiddenByRowSpan={isHiddenByRowSpan}
|
|
363
|
+
rowMeta={rowMeta}
|
|
360
364
|
/>
|
|
361
365
|
);
|
|
362
366
|
})}
|
|
@@ -146,8 +146,6 @@ export function GlobalSearch({ value = '', onChange, placeholder = 'Search...',
|
|
|
146
146
|
</div>
|
|
147
147
|
<input
|
|
148
148
|
ref={inputRef}
|
|
149
|
-
id="ogx-global-search-input"
|
|
150
|
-
name="ogx-global-search"
|
|
151
149
|
className="ogx-global-search__input"
|
|
152
150
|
value={localValue}
|
|
153
151
|
onChange={(e) => setLocalValue(e.target.value)}
|
|
@@ -684,11 +684,9 @@ export function GridToolbar({
|
|
|
684
684
|
<GridTooltip title="Pivot">
|
|
685
685
|
<button
|
|
686
686
|
ref={pivotButtonRef}
|
|
687
|
-
id="ogx-pivot-btn"
|
|
688
687
|
className={`ogx-toolbar__icon-btn${pivotOpen ? ' ogx-toolbar__icon-btn--active' : ''}`}
|
|
689
688
|
aria-label="Configure pivot"
|
|
690
689
|
aria-expanded={pivotOpen}
|
|
691
|
-
aria-controls="ogx-pivot-panel"
|
|
692
690
|
onClick={() => {
|
|
693
691
|
setPivotOpen(!pivotOpen);
|
|
694
692
|
setAggOpen(false);
|
|
@@ -727,11 +725,9 @@ export function GridToolbar({
|
|
|
727
725
|
<GridTooltip title="Summaries">
|
|
728
726
|
<button
|
|
729
727
|
ref={aggButtonRef}
|
|
730
|
-
id="ogx-aggregation-btn"
|
|
731
728
|
className={`ogx-toolbar__icon-btn${aggOpen ? ' ogx-toolbar__icon-btn--active' : ''}`}
|
|
732
729
|
aria-label="Configure summaries"
|
|
733
730
|
aria-expanded={aggOpen}
|
|
734
|
-
aria-controls="ogx-aggregation-panel"
|
|
735
731
|
onClick={() => {
|
|
736
732
|
setAggOpen(!aggOpen);
|
|
737
733
|
setPivotOpen(false);
|
|
@@ -74,14 +74,14 @@ export function useGridColumns<R extends GridRowModel>(
|
|
|
74
74
|
return {
|
|
75
75
|
...col,
|
|
76
76
|
renderCell: (cellParams: GridRenderCellParams<R>) => {
|
|
77
|
-
const
|
|
78
|
-
const depth =
|
|
79
|
-
const hasChildren = Boolean(
|
|
80
|
-
const isExpanded = Boolean(
|
|
81
|
-
const groupingField =
|
|
82
|
-
const groupingValue =
|
|
83
|
-
const descendantCount =
|
|
84
|
-
const isGroupRow = Boolean(
|
|
77
|
+
const meta = cellParams.rowMeta;
|
|
78
|
+
const depth = meta?.treeDepth ?? 0;
|
|
79
|
+
const hasChildren = Boolean(meta?.hasChildren);
|
|
80
|
+
const isExpanded = Boolean(meta?.isExpanded);
|
|
81
|
+
const groupingField = meta?.groupingField;
|
|
82
|
+
const groupingValue = meta?.groupingValue;
|
|
83
|
+
const descendantCount = meta?.descendantCount;
|
|
84
|
+
const isGroupRow = Boolean(meta?.isGroupRow);
|
|
85
85
|
|
|
86
86
|
let content: React.ReactNode = col.renderCell ? col.renderCell(cellParams) : cellParams.value as React.ReactNode;
|
|
87
87
|
|
|
@@ -130,9 +130,9 @@ export function useGridColumns<R extends GridRowModel>(
|
|
|
130
130
|
return {
|
|
131
131
|
...col,
|
|
132
132
|
renderCell: (cellParams: GridRenderCellParams<R>) => {
|
|
133
|
-
const
|
|
134
|
-
const hasChildren = Boolean(
|
|
135
|
-
const groupingField =
|
|
133
|
+
const meta = cellParams.rowMeta;
|
|
134
|
+
const hasChildren = Boolean(meta?.hasChildren);
|
|
135
|
+
const groupingField = meta?.groupingField;
|
|
136
136
|
|
|
137
137
|
if (isRowGrouping && hasChildren) {
|
|
138
138
|
if (col.field === groupingField) return null;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { renderHook, act } from '@testing-library/react';
|
|
3
|
+
import { useGridControlledState } from './useGridControlledState';
|
|
4
|
+
|
|
5
|
+
const BASE_PARAMS = {
|
|
6
|
+
sortModel: undefined,
|
|
7
|
+
paginationModel: undefined,
|
|
8
|
+
rowSelectionModel: undefined,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
describe('useGridControlledState — sort', () => {
|
|
12
|
+
it('uncontrolled: setInternalSortModel updates sortModel', () => {
|
|
13
|
+
const { result } = renderHook(() => useGridControlledState(BASE_PARAMS));
|
|
14
|
+
expect(result.current.sortModel).toEqual([]);
|
|
15
|
+
act(() => {
|
|
16
|
+
result.current.setInternalSortModel([{ field: 'name', sort: 'asc' }]);
|
|
17
|
+
});
|
|
18
|
+
expect(result.current.sortModel).toEqual([{ field: 'name', sort: 'asc' }]);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('controlled: external sortModel prop always wins', () => {
|
|
22
|
+
const externalSort = [{ field: 'age', sort: 'desc' as const }];
|
|
23
|
+
const { result } = renderHook(() =>
|
|
24
|
+
useGridControlledState({ ...BASE_PARAMS, sortModel: externalSort })
|
|
25
|
+
);
|
|
26
|
+
expect(result.current.isSortControlled).toBe(true);
|
|
27
|
+
expect(result.current.sortModel).toEqual(externalSort);
|
|
28
|
+
act(() => {
|
|
29
|
+
result.current.setInternalSortModel([{ field: 'name', sort: 'asc' }]);
|
|
30
|
+
});
|
|
31
|
+
// External prop still wins
|
|
32
|
+
expect(result.current.sortModel).toEqual(externalSort);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('initialState.sorting seeds the internal sort model', () => {
|
|
36
|
+
const { result } = renderHook(() =>
|
|
37
|
+
useGridControlledState({
|
|
38
|
+
...BASE_PARAMS,
|
|
39
|
+
initialState: { sorting: { sortModel: [{ field: 'name', sort: 'asc' }] } },
|
|
40
|
+
})
|
|
41
|
+
);
|
|
42
|
+
expect(result.current.sortModel).toEqual([{ field: 'name', sort: 'asc' }]);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe('useGridControlledState — pagination', () => {
|
|
47
|
+
it('uncontrolled: handlePaginationModelChange updates effectivePaginationModel', () => {
|
|
48
|
+
const { result } = renderHook(() => useGridControlledState(BASE_PARAMS));
|
|
49
|
+
act(() => {
|
|
50
|
+
result.current.handlePaginationModelChange({ page: 2, pageSize: 25 });
|
|
51
|
+
});
|
|
52
|
+
expect(result.current.effectivePaginationModel).toEqual({ page: 2, pageSize: 25 });
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('controlled: external paginationModel always wins', () => {
|
|
56
|
+
const externalPagination = { page: 3, pageSize: 50 };
|
|
57
|
+
const { result } = renderHook(() =>
|
|
58
|
+
useGridControlledState({ ...BASE_PARAMS, paginationModel: externalPagination })
|
|
59
|
+
);
|
|
60
|
+
expect(result.current.effectivePaginationModel).toEqual(externalPagination);
|
|
61
|
+
act(() => {
|
|
62
|
+
result.current.handlePaginationModelChange({ page: 0, pageSize: 10 });
|
|
63
|
+
});
|
|
64
|
+
expect(result.current.effectivePaginationModel).toEqual(externalPagination);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('initialState.pagination seeds page and pageSize', () => {
|
|
68
|
+
const { result } = renderHook(() =>
|
|
69
|
+
useGridControlledState({
|
|
70
|
+
...BASE_PARAMS,
|
|
71
|
+
initialState: { pagination: { paginationModel: { page: 1, pageSize: 25 } } },
|
|
72
|
+
})
|
|
73
|
+
);
|
|
74
|
+
expect(result.current.effectivePaginationModel).toEqual({ page: 1, pageSize: 25 });
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe('useGridControlledState — selection', () => {
|
|
79
|
+
it('uncontrolled: setInternalRowSelectionModel updates selectedRowIds', () => {
|
|
80
|
+
const { result } = renderHook(() => useGridControlledState(BASE_PARAMS));
|
|
81
|
+
act(() => {
|
|
82
|
+
result.current.setInternalRowSelectionModel([1, 2, 3]);
|
|
83
|
+
});
|
|
84
|
+
expect(result.current.selectedRowIds).toEqual(new Set([1, 2, 3]));
|
|
85
|
+
});
|
|
86
|
+
});
|