@versini/ui-datagrid 0.8.1 → 0.8.2
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/dist/{DataGridBody/useColumnMeasurement.js → 131.js} +14 -7
- package/dist/{utilities/classes.js → 298.js} +4 -9
- package/dist/{DataGridConstants/DataGridConstants.js → 46.js} +1 -3
- package/dist/511.js +9 -0
- package/dist/926.js +15 -0
- package/dist/DataGrid/index.js +165 -7
- package/dist/DataGridAnimated/index.js +159 -6
- package/dist/DataGridBody/index.js +33 -4
- package/dist/DataGridCell/index.js +37 -4
- package/dist/DataGridCellSort/index.js +138 -4
- package/dist/DataGridConstants/index.js +2 -6
- package/dist/DataGridFooter/index.js +79 -4
- package/dist/DataGridHeader/index.js +110 -4
- package/dist/DataGridInfinite/index.js +312 -4
- package/dist/DataGridRow/index.js +89 -4
- package/dist/DataGridSorting/index.js +225 -7
- package/package.json +3 -3
- package/dist/DataGrid/DataGrid.js +0 -183
- package/dist/DataGrid/DataGridContext.js +0 -16
- package/dist/DataGrid/DataGridTypes.js +0 -9
- package/dist/DataGridAnimated/AnimatedWrapper.js +0 -53
- package/dist/DataGridAnimated/useAnimatedHeight.js +0 -131
- package/dist/DataGridBody/DataGridBody.js +0 -55
- package/dist/DataGridBody/getBodyClass.js +0 -23
- package/dist/DataGridCell/DataGridCell.js +0 -51
- package/dist/DataGridCellSort/ButtonSort.js +0 -67
- package/dist/DataGridCellSort/DataGridCellSort.js +0 -111
- package/dist/DataGridFooter/DataGridFooter.js +0 -98
- package/dist/DataGridHeader/DataGridHeader.js +0 -129
- package/dist/DataGridInfinite/DataGridInfiniteBody.js +0 -334
- package/dist/DataGridRow/DataGridRow.js +0 -108
- package/dist/DataGridSorting/sortingUtils.js +0 -234
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
/*!
|
|
2
|
-
@versini/ui-datagrid v0.8.1
|
|
3
|
-
© 2026 gizmette.com
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
7
|
-
import { IconSort, IconSortDown, IconSortUp } from "@versini/ui-icons";
|
|
8
|
-
import clsx from "clsx";
|
|
9
|
-
import { DataGridContext } from "../DataGrid/DataGridContext.js";
|
|
10
|
-
import { getCellClasses } from "../utilities/classes.js";
|
|
11
|
-
import { ButtonSort } from "./ButtonSort.js";
|
|
12
|
-
|
|
13
|
-
;// CONCATENATED MODULE: external "react/jsx-runtime"
|
|
14
|
-
|
|
15
|
-
;// CONCATENATED MODULE: external "@versini/ui-icons"
|
|
16
|
-
|
|
17
|
-
;// CONCATENATED MODULE: external "clsx"
|
|
18
|
-
|
|
19
|
-
;// CONCATENATED MODULE: external "../DataGrid/DataGridContext.js"
|
|
20
|
-
|
|
21
|
-
;// CONCATENATED MODULE: external "../utilities/classes.js"
|
|
22
|
-
|
|
23
|
-
;// CONCATENATED MODULE: external "./ButtonSort.js"
|
|
24
|
-
|
|
25
|
-
;// CONCATENATED MODULE: ./src/DataGridCellSort/DataGridCellSort.tsx
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
/* =============================================================================
|
|
33
|
-
* DataGridCellSort
|
|
34
|
-
* ========================================================================== */ const DataGridCellSort = ({ className, children, cellId, onSort, sortDirection, sortedCell, slotLeft, slotRight, buttonClassName, align, ...rest })=>{
|
|
35
|
-
const isSorted = sortedCell === cellId;
|
|
36
|
-
const handleSort = ()=>{
|
|
37
|
-
if (onSort) {
|
|
38
|
-
// Convert false to undefined for the callback.
|
|
39
|
-
const currentDirection = isSorted && sortDirection ? sortDirection : undefined;
|
|
40
|
-
onSort(cellId, currentDirection);
|
|
41
|
-
}
|
|
42
|
-
};
|
|
43
|
-
const getSortIcon = ()=>{
|
|
44
|
-
if (isSorted && sortDirection === "asc") {
|
|
45
|
-
return /*#__PURE__*/ jsx(IconSortUp, {
|
|
46
|
-
size: "size-4",
|
|
47
|
-
monotone: true
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
if (isSorted && sortDirection === "desc") {
|
|
51
|
-
return /*#__PURE__*/ jsx(IconSortDown, {
|
|
52
|
-
size: "size-4",
|
|
53
|
-
monotone: true
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
return /*#__PURE__*/ jsx(IconSort, {
|
|
57
|
-
size: "size-4",
|
|
58
|
-
monotone: true
|
|
59
|
-
});
|
|
60
|
-
};
|
|
61
|
-
const getAriaSort = ()=>{
|
|
62
|
-
if (isSorted && sortDirection === "asc") {
|
|
63
|
-
return "ascending";
|
|
64
|
-
}
|
|
65
|
-
if (isSorted && sortDirection === "desc") {
|
|
66
|
-
return "descending";
|
|
67
|
-
}
|
|
68
|
-
return "other";
|
|
69
|
-
};
|
|
70
|
-
return /*#__PURE__*/ jsx(DataGridContext.Consumer, {
|
|
71
|
-
children: ({ mode, compact, cellWrapper, blurEffect })=>{
|
|
72
|
-
const mainClasses = getCellClasses({
|
|
73
|
-
cellWrapper,
|
|
74
|
-
className,
|
|
75
|
-
mode,
|
|
76
|
-
compact,
|
|
77
|
-
align
|
|
78
|
-
});
|
|
79
|
-
// Flex container for alignment of button within the cell.
|
|
80
|
-
const contentClasses = clsx("flex", {
|
|
81
|
-
"justify-start": align === "left" || !align,
|
|
82
|
-
"justify-center": align === "center",
|
|
83
|
-
"justify-end": align === "right"
|
|
84
|
-
});
|
|
85
|
-
return /*#__PURE__*/ jsx("div", {
|
|
86
|
-
className: mainClasses,
|
|
87
|
-
role: "columnheader",
|
|
88
|
-
"aria-sort": getAriaSort(),
|
|
89
|
-
...rest,
|
|
90
|
-
children: /*#__PURE__*/ jsxs("div", {
|
|
91
|
-
className: contentClasses,
|
|
92
|
-
children: [
|
|
93
|
-
slotLeft,
|
|
94
|
-
/*#__PURE__*/ jsx(ButtonSort, {
|
|
95
|
-
mode: mode,
|
|
96
|
-
blurEffect: blurEffect,
|
|
97
|
-
active: isSorted,
|
|
98
|
-
className: buttonClassName,
|
|
99
|
-
onClick: handleSort,
|
|
100
|
-
labelRight: children,
|
|
101
|
-
children: getSortIcon()
|
|
102
|
-
}),
|
|
103
|
-
slotRight
|
|
104
|
-
]
|
|
105
|
-
})
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
|
-
});
|
|
109
|
-
};
|
|
110
|
-
|
|
111
|
-
export { DataGridCellSort };
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
/*!
|
|
2
|
-
@versini/ui-datagrid v0.8.1
|
|
3
|
-
© 2026 gizmette.com
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { jsx } from "react/jsx-runtime";
|
|
7
|
-
import { useEffect, useLayoutEffect, useRef } from "react";
|
|
8
|
-
import { DataGridContext } from "../DataGrid/DataGridContext.js";
|
|
9
|
-
import { CellWrapper } from "../DataGridConstants/DataGridConstants.js";
|
|
10
|
-
import { getFooterClasses } from "../utilities/classes.js";
|
|
11
|
-
|
|
12
|
-
;// CONCATENATED MODULE: external "react/jsx-runtime"
|
|
13
|
-
|
|
14
|
-
;// CONCATENATED MODULE: external "react"
|
|
15
|
-
|
|
16
|
-
;// CONCATENATED MODULE: external "../DataGrid/DataGridContext.js"
|
|
17
|
-
|
|
18
|
-
;// CONCATENATED MODULE: external "../DataGridConstants/DataGridConstants.js"
|
|
19
|
-
|
|
20
|
-
;// CONCATENATED MODULE: external "../utilities/classes.js"
|
|
21
|
-
|
|
22
|
-
;// CONCATENATED MODULE: ./src/DataGridFooter/DataGridFooter.tsx
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
/* =============================================================================
|
|
29
|
-
* DataGridFooter
|
|
30
|
-
* ========================================================================== */ const DataGridFooter = ({ className, children, ...rest })=>{
|
|
31
|
-
return /*#__PURE__*/ jsx(DataGridContext.Consumer, {
|
|
32
|
-
children: (ctx)=>/*#__PURE__*/ jsx(DataGridFooterInner, {
|
|
33
|
-
className: className,
|
|
34
|
-
ctx: ctx,
|
|
35
|
-
...rest,
|
|
36
|
-
children: children
|
|
37
|
-
})
|
|
38
|
-
});
|
|
39
|
-
};
|
|
40
|
-
DataGridFooter.displayName = "DataGridFooter";
|
|
41
|
-
/**
|
|
42
|
-
* Inner component to handle the useEffect for registering footer. Separated to
|
|
43
|
-
* avoid hooks inside Consumer render prop.
|
|
44
|
-
*/ const DataGridFooterInner = ({ className, ctx, children, ...rest })=>{
|
|
45
|
-
const footerRef = useRef(null);
|
|
46
|
-
/**
|
|
47
|
-
* Register this footer with the parent DataGrid on mount. This enables sticky
|
|
48
|
-
* footer behavior regardless of nesting depth.
|
|
49
|
-
*/ useEffect(()=>{
|
|
50
|
-
ctx.registerFooter?.();
|
|
51
|
-
return ()=>{
|
|
52
|
-
ctx.unregisterFooter?.();
|
|
53
|
-
};
|
|
54
|
-
}, [
|
|
55
|
-
ctx.registerFooter,
|
|
56
|
-
ctx.unregisterFooter
|
|
57
|
-
]);
|
|
58
|
-
/**
|
|
59
|
-
* Measure and report footer height for dynamic padding calculation. Uses
|
|
60
|
-
* ResizeObserver to handle dynamic content changes.
|
|
61
|
-
*/ useLayoutEffect(()=>{
|
|
62
|
-
const element = footerRef.current;
|
|
63
|
-
if (!element || !ctx.stickyFooter || !ctx.setFooterHeight) {
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
const observer = new ResizeObserver((entries)=>{
|
|
67
|
-
const height = entries[0]?.borderBoxSize?.[0]?.blockSize;
|
|
68
|
-
if (height !== undefined) {
|
|
69
|
-
ctx.setFooterHeight?.(height);
|
|
70
|
-
}
|
|
71
|
-
});
|
|
72
|
-
observer.observe(element);
|
|
73
|
-
return ()=>observer.disconnect();
|
|
74
|
-
}, [
|
|
75
|
-
ctx.stickyFooter,
|
|
76
|
-
ctx.setFooterHeight
|
|
77
|
-
]);
|
|
78
|
-
return /*#__PURE__*/ jsx(DataGridContext.Provider, {
|
|
79
|
-
value: {
|
|
80
|
-
...ctx,
|
|
81
|
-
cellWrapper: CellWrapper.FOOTER
|
|
82
|
-
},
|
|
83
|
-
children: /*#__PURE__*/ jsx("div", {
|
|
84
|
-
ref: footerRef,
|
|
85
|
-
role: "rowgroup",
|
|
86
|
-
className: getFooterClasses({
|
|
87
|
-
className,
|
|
88
|
-
stickyFooter: ctx.stickyFooter,
|
|
89
|
-
mode: ctx.mode,
|
|
90
|
-
blurEffect: ctx.blurEffect
|
|
91
|
-
}),
|
|
92
|
-
...rest,
|
|
93
|
-
children: children
|
|
94
|
-
})
|
|
95
|
-
});
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
export { DataGridFooter };
|
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
/*!
|
|
2
|
-
@versini/ui-datagrid v0.8.1
|
|
3
|
-
© 2026 gizmette.com
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
7
|
-
import { useEffect, useId, useLayoutEffect, useRef } from "react";
|
|
8
|
-
import { DataGridContext } from "../DataGrid/DataGridContext.js";
|
|
9
|
-
import { CellWrapper } from "../DataGridConstants/DataGridConstants.js";
|
|
10
|
-
import { getCaptionClasses, getHeaderClasses } from "../utilities/classes.js";
|
|
11
|
-
|
|
12
|
-
;// CONCATENATED MODULE: external "react/jsx-runtime"
|
|
13
|
-
|
|
14
|
-
;// CONCATENATED MODULE: external "react"
|
|
15
|
-
|
|
16
|
-
;// CONCATENATED MODULE: external "../DataGrid/DataGridContext.js"
|
|
17
|
-
|
|
18
|
-
;// CONCATENATED MODULE: external "../DataGridConstants/DataGridConstants.js"
|
|
19
|
-
|
|
20
|
-
;// CONCATENATED MODULE: external "../utilities/classes.js"
|
|
21
|
-
|
|
22
|
-
;// CONCATENATED MODULE: ./src/DataGridHeader/DataGridHeader.tsx
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
/* =============================================================================
|
|
29
|
-
* DataGridHeader
|
|
30
|
-
* ========================================================================== */ const DataGridHeader = ({ caption, captionClassName, className, children, ...rest })=>{
|
|
31
|
-
const captionId = useId();
|
|
32
|
-
return /*#__PURE__*/ jsx(DataGridContext.Consumer, {
|
|
33
|
-
children: (ctx)=>/*#__PURE__*/ jsx(DataGridHeaderInner, {
|
|
34
|
-
caption: caption,
|
|
35
|
-
captionClassName: captionClassName,
|
|
36
|
-
captionId: captionId,
|
|
37
|
-
className: className,
|
|
38
|
-
ctx: ctx,
|
|
39
|
-
...rest,
|
|
40
|
-
children: children
|
|
41
|
-
})
|
|
42
|
-
});
|
|
43
|
-
};
|
|
44
|
-
DataGridHeader.displayName = "DataGridHeader";
|
|
45
|
-
/**
|
|
46
|
-
* Inner component to handle the useEffect for registering captionId and header.
|
|
47
|
-
* Separated to avoid hooks inside Consumer render prop.
|
|
48
|
-
*/ const DataGridHeaderInner = ({ caption, captionClassName, captionId, className, ctx, children, ...rest })=>{
|
|
49
|
-
const headerRef = useRef(null);
|
|
50
|
-
/**
|
|
51
|
-
* Register this header with the parent DataGrid on mount. This enables sticky
|
|
52
|
-
* header behavior regardless of nesting depth.
|
|
53
|
-
*/ useEffect(()=>{
|
|
54
|
-
ctx.registerHeader?.();
|
|
55
|
-
return ()=>{
|
|
56
|
-
ctx.unregisterHeader?.();
|
|
57
|
-
};
|
|
58
|
-
}, [
|
|
59
|
-
ctx.registerHeader,
|
|
60
|
-
ctx.unregisterHeader
|
|
61
|
-
]);
|
|
62
|
-
// Register the caption ID with the parent DataGrid for aria-labelledby.
|
|
63
|
-
useEffect(()=>{
|
|
64
|
-
if (caption && ctx.setCaptionId) {
|
|
65
|
-
ctx.setCaptionId(captionId);
|
|
66
|
-
}
|
|
67
|
-
return ()=>{
|
|
68
|
-
if (ctx.setCaptionId) {
|
|
69
|
-
ctx.setCaptionId(undefined);
|
|
70
|
-
}
|
|
71
|
-
};
|
|
72
|
-
}, [
|
|
73
|
-
caption,
|
|
74
|
-
captionId,
|
|
75
|
-
ctx.setCaptionId
|
|
76
|
-
]);
|
|
77
|
-
/**
|
|
78
|
-
* Measure and report header height for dynamic padding calculation. Uses
|
|
79
|
-
* ResizeObserver to handle dynamic content changes (text wrapping, etc.).
|
|
80
|
-
*/ useLayoutEffect(()=>{
|
|
81
|
-
const element = headerRef.current;
|
|
82
|
-
if (!element || !ctx.stickyHeader || !ctx.setHeaderHeight) {
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
85
|
-
const observer = new ResizeObserver((entries)=>{
|
|
86
|
-
const height = entries[0]?.borderBoxSize?.[0]?.blockSize;
|
|
87
|
-
if (height !== undefined) {
|
|
88
|
-
ctx.setHeaderHeight?.(height);
|
|
89
|
-
}
|
|
90
|
-
});
|
|
91
|
-
observer.observe(element);
|
|
92
|
-
return ()=>observer.disconnect();
|
|
93
|
-
}, [
|
|
94
|
-
ctx.stickyHeader,
|
|
95
|
-
ctx.setHeaderHeight
|
|
96
|
-
]);
|
|
97
|
-
return /*#__PURE__*/ jsx(DataGridContext.Provider, {
|
|
98
|
-
value: {
|
|
99
|
-
...ctx,
|
|
100
|
-
cellWrapper: CellWrapper.HEADER
|
|
101
|
-
},
|
|
102
|
-
children: /*#__PURE__*/ jsxs("div", {
|
|
103
|
-
ref: headerRef,
|
|
104
|
-
role: "rowgroup",
|
|
105
|
-
className: getHeaderClasses({
|
|
106
|
-
className,
|
|
107
|
-
stickyHeader: ctx.stickyHeader,
|
|
108
|
-
mode: ctx.mode,
|
|
109
|
-
blurEffect: ctx.blurEffect
|
|
110
|
-
}),
|
|
111
|
-
...rest,
|
|
112
|
-
children: [
|
|
113
|
-
caption && /*#__PURE__*/ jsx("div", {
|
|
114
|
-
id: captionId,
|
|
115
|
-
className: getCaptionClasses({
|
|
116
|
-
captionClassName,
|
|
117
|
-
mode: ctx.mode,
|
|
118
|
-
hasBlur: Boolean(ctx.blurEffect && ctx.blurEffect !== "none"),
|
|
119
|
-
stickyHeader: Boolean(ctx.stickyHeader)
|
|
120
|
-
}),
|
|
121
|
-
children: caption
|
|
122
|
-
}),
|
|
123
|
-
children
|
|
124
|
-
]
|
|
125
|
-
})
|
|
126
|
-
});
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
export { DataGridHeader };
|
|
@@ -1,334 +0,0 @@
|
|
|
1
|
-
/*!
|
|
2
|
-
@versini/ui-datagrid v0.8.1
|
|
3
|
-
© 2026 gizmette.com
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { jsx } from "react/jsx-runtime";
|
|
7
|
-
import { forwardRef, useCallback, useContext, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react";
|
|
8
|
-
import { DataGridContext } from "../DataGrid/DataGridContext.js";
|
|
9
|
-
import { getBodyClass } from "../DataGridBody/getBodyClass.js";
|
|
10
|
-
import { useColumnMeasurement } from "../DataGridBody/useColumnMeasurement.js";
|
|
11
|
-
import { CellWrapper } from "../DataGridConstants/index.js";
|
|
12
|
-
|
|
13
|
-
;// CONCATENATED MODULE: external "react/jsx-runtime"
|
|
14
|
-
|
|
15
|
-
;// CONCATENATED MODULE: external "react"
|
|
16
|
-
|
|
17
|
-
;// CONCATENATED MODULE: external "../DataGrid/DataGridContext.js"
|
|
18
|
-
|
|
19
|
-
;// CONCATENATED MODULE: external "../DataGridBody/getBodyClass.js"
|
|
20
|
-
|
|
21
|
-
;// CONCATENATED MODULE: external "../DataGridBody/useColumnMeasurement.js"
|
|
22
|
-
|
|
23
|
-
;// CONCATENATED MODULE: external "../DataGridConstants/index.js"
|
|
24
|
-
|
|
25
|
-
;// CONCATENATED MODULE: ./src/DataGridInfinite/DataGridInfiniteBody.tsx
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const DEFAULT_BATCH_SIZE = 20;
|
|
33
|
-
const DEFAULT_THRESHOLD = 5;
|
|
34
|
-
const DEFAULT_ROOT_MARGIN = "20px";
|
|
35
|
-
const ROW_INDEX_DATA_ATTR = "data-row-index";
|
|
36
|
-
/**
|
|
37
|
-
* Finds the nearest scrollable ancestor of an element. Returns null if no
|
|
38
|
-
* scrollable ancestor is found (uses viewport).
|
|
39
|
-
*/ function findScrollableAncestor(element) {
|
|
40
|
-
let parent = element.parentElement;
|
|
41
|
-
while(parent){
|
|
42
|
-
const style = getComputedStyle(parent);
|
|
43
|
-
const overflowY = style.overflowY;
|
|
44
|
-
if (overflowY === "auto" || overflowY === "scroll") {
|
|
45
|
-
return parent;
|
|
46
|
-
}
|
|
47
|
-
parent = parent.parentElement;
|
|
48
|
-
}
|
|
49
|
-
return null;
|
|
50
|
-
}
|
|
51
|
-
/**
|
|
52
|
-
* A DataGridBody variant that handles infinite scroll internally.
|
|
53
|
-
*
|
|
54
|
-
* This component manages all the complexity of infinite scroll:
|
|
55
|
-
* - Progressive data loading with IntersectionObserver
|
|
56
|
-
* - Correct marker placement for seamless scrolling
|
|
57
|
-
* - Automatic data slicing and memoization
|
|
58
|
-
* - Programmatic scroll-to-row with smooth animation
|
|
59
|
-
*
|
|
60
|
-
* @example
|
|
61
|
-
* ```tsx
|
|
62
|
-
* const infiniteBodyRef = useRef<DataGridInfiniteBodyRef>(null);
|
|
63
|
-
*
|
|
64
|
-
* // Jump to a specific row
|
|
65
|
-
* const handleJumpToRow = () => {
|
|
66
|
-
* infiniteBodyRef.current?.scrollToIndex(134);
|
|
67
|
-
* };
|
|
68
|
-
*
|
|
69
|
-
* <DataGrid maxHeight="400px" stickyHeader>
|
|
70
|
-
* <DataGridHeader caption={`Showing ${visibleCount} of ${data.length}`}>
|
|
71
|
-
* <DataGridRow>
|
|
72
|
-
* <DataGridCell>Name</DataGridCell>
|
|
73
|
-
* <DataGridCell>Role</DataGridCell>
|
|
74
|
-
* </DataGridRow>
|
|
75
|
-
* </DataGridHeader>
|
|
76
|
-
*
|
|
77
|
-
* <DataGridInfiniteBody
|
|
78
|
-
* ref={infiniteBodyRef}
|
|
79
|
-
* data={largeData}
|
|
80
|
-
* batchSize={25}
|
|
81
|
-
* onVisibleCountChange={(count) => setVisibleCount(count)}
|
|
82
|
-
* >
|
|
83
|
-
* {(row) => (
|
|
84
|
-
* <DataGridRow key={row.id}>
|
|
85
|
-
* <DataGridCell>{row.name}</DataGridCell>
|
|
86
|
-
* <DataGridCell>{row.role}</DataGridCell>
|
|
87
|
-
* </DataGridRow>
|
|
88
|
-
* )}
|
|
89
|
-
* </DataGridInfiniteBody>
|
|
90
|
-
* </DataGrid>
|
|
91
|
-
* ```
|
|
92
|
-
*
|
|
93
|
-
*/ function DataGridInfiniteBodyInner({ data, children: renderRow, batchSize = DEFAULT_BATCH_SIZE, threshold = DEFAULT_THRESHOLD, rootMargin = DEFAULT_ROOT_MARGIN, onVisibleCountChange, className }, ref) {
|
|
94
|
-
const ctx = useContext(DataGridContext);
|
|
95
|
-
const bodyRef = useRef(null);
|
|
96
|
-
const observerRef = useRef(null);
|
|
97
|
-
const pendingScrollRef = useRef(null);
|
|
98
|
-
const totalItems = data.length;
|
|
99
|
-
const initialCount = Math.min(batchSize + threshold, totalItems);
|
|
100
|
-
const [visibleCount, setVisibleCount] = useState(initialCount);
|
|
101
|
-
const hasMore = visibleCount < totalItems;
|
|
102
|
-
/**
|
|
103
|
-
* Scroll to a row by its index. Called after visibleCount updates.
|
|
104
|
-
* NOTE: We query for the wrapper with data-row-index, then scroll to the
|
|
105
|
-
* actual row element inside it (since the wrapper has display:contents and
|
|
106
|
-
* doesn't have a bounding box).
|
|
107
|
-
*/ const scrollToRowElement = useCallback((index)=>{
|
|
108
|
-
const body = bodyRef.current;
|
|
109
|
-
if (!body) {
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
112
|
-
const wrapper = body.querySelector(`[${ROW_INDEX_DATA_ATTR}="${index}"]`);
|
|
113
|
-
if (wrapper) {
|
|
114
|
-
// The wrapper has display:contents, so scroll to the actual row inside.
|
|
115
|
-
const row = wrapper.querySelector('[role="row"]');
|
|
116
|
-
if (row) {
|
|
117
|
-
row.scrollIntoView({
|
|
118
|
-
behavior: "smooth",
|
|
119
|
-
block: "center"
|
|
120
|
-
});
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
}, []);
|
|
124
|
-
/**
|
|
125
|
-
* Handle pending scroll after render (when visibleCount has been expanded).
|
|
126
|
-
*/ useEffect(()=>{
|
|
127
|
-
if (pendingScrollRef.current !== null) {
|
|
128
|
-
const targetIndex = pendingScrollRef.current;
|
|
129
|
-
// Only scroll if the target is now within visible range.
|
|
130
|
-
if (targetIndex < visibleCount) {
|
|
131
|
-
// Use requestAnimationFrame to ensure DOM has updated.
|
|
132
|
-
requestAnimationFrame(()=>{
|
|
133
|
-
scrollToRowElement(targetIndex);
|
|
134
|
-
});
|
|
135
|
-
pendingScrollRef.current = null;
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
}, [
|
|
139
|
-
visibleCount,
|
|
140
|
-
scrollToRowElement
|
|
141
|
-
]);
|
|
142
|
-
/**
|
|
143
|
-
* Expose imperative methods via ref.
|
|
144
|
-
*/ useImperativeHandle(ref, ()=>({
|
|
145
|
-
scrollToIndex: (index)=>{
|
|
146
|
-
if (index < 0 || index >= totalItems) {
|
|
147
|
-
console.warn(`scrollToIndex: index ${index} is out of bounds (0-${totalItems - 1})`);
|
|
148
|
-
return;
|
|
149
|
-
}
|
|
150
|
-
// If the row is already visible, just scroll to it.
|
|
151
|
-
if (index < visibleCount) {
|
|
152
|
-
scrollToRowElement(index);
|
|
153
|
-
return;
|
|
154
|
-
}
|
|
155
|
-
/**
|
|
156
|
-
* Otherwise, expand visible count to include the target row. Add some
|
|
157
|
-
* buffer so the row isn't at the very edge.
|
|
158
|
-
*/ const targetCount = Math.min(index + threshold + 1, totalItems);
|
|
159
|
-
pendingScrollRef.current = index;
|
|
160
|
-
setVisibleCount(targetCount);
|
|
161
|
-
}
|
|
162
|
-
}), [
|
|
163
|
-
totalItems,
|
|
164
|
-
visibleCount,
|
|
165
|
-
threshold,
|
|
166
|
-
scrollToRowElement
|
|
167
|
-
]);
|
|
168
|
-
// Reset visible count when data changes significantly.
|
|
169
|
-
useEffect(()=>{
|
|
170
|
-
setVisibleCount(Math.min(batchSize + threshold, totalItems));
|
|
171
|
-
}, [
|
|
172
|
-
totalItems,
|
|
173
|
-
batchSize,
|
|
174
|
-
threshold
|
|
175
|
-
]);
|
|
176
|
-
// Notify parent of visible count changes.
|
|
177
|
-
useEffect(()=>{
|
|
178
|
-
onVisibleCountChange?.(visibleCount, totalItems);
|
|
179
|
-
}, [
|
|
180
|
-
visibleCount,
|
|
181
|
-
totalItems,
|
|
182
|
-
onVisibleCountChange
|
|
183
|
-
]);
|
|
184
|
-
/**
|
|
185
|
-
* IntersectionObserver callback - triggered when marker becomes visible. Loads
|
|
186
|
-
* the next batch of items.
|
|
187
|
-
*/ const handleIntersection = useCallback((entries)=>{
|
|
188
|
-
const target = entries[0];
|
|
189
|
-
if (target?.isIntersecting) {
|
|
190
|
-
setVisibleCount((prev)=>Math.min(prev + batchSize, totalItems));
|
|
191
|
-
}
|
|
192
|
-
}, [
|
|
193
|
-
batchSize,
|
|
194
|
-
totalItems
|
|
195
|
-
]);
|
|
196
|
-
/**
|
|
197
|
-
* Callback ref for the marker element. Sets up IntersectionObserver when
|
|
198
|
-
* marker mounts, cleans up when it unmounts. This pattern ensures the observer
|
|
199
|
-
* always watches the current marker element, even when visibleCount changes
|
|
200
|
-
* and a new marker is created at a different position.
|
|
201
|
-
*/ const markerRefCallback = useCallback((node)=>{
|
|
202
|
-
// Clean up previous observer.
|
|
203
|
-
if (observerRef.current) {
|
|
204
|
-
observerRef.current.disconnect();
|
|
205
|
-
observerRef.current = null;
|
|
206
|
-
}
|
|
207
|
-
// Set up new observer if we have a marker and more items to load.
|
|
208
|
-
if (node && hasMore) {
|
|
209
|
-
const root = findScrollableAncestor(node);
|
|
210
|
-
observerRef.current = new IntersectionObserver(handleIntersection, {
|
|
211
|
-
root,
|
|
212
|
-
rootMargin
|
|
213
|
-
});
|
|
214
|
-
observerRef.current.observe(node);
|
|
215
|
-
}
|
|
216
|
-
}, [
|
|
217
|
-
hasMore,
|
|
218
|
-
handleIntersection,
|
|
219
|
-
rootMargin
|
|
220
|
-
]);
|
|
221
|
-
// Clean up observer on unmount.
|
|
222
|
-
useEffect(()=>{
|
|
223
|
-
return ()=>{
|
|
224
|
-
observerRef.current?.disconnect();
|
|
225
|
-
};
|
|
226
|
-
}, []);
|
|
227
|
-
/**
|
|
228
|
-
* Calculate marker position. The marker should be placed `threshold` items
|
|
229
|
-
* from the end of visible items. This allows seamless scrolling - new items
|
|
230
|
-
* load while user scrolls through the remaining `threshold` items.
|
|
231
|
-
*/ const markerPosition = useMemo(()=>{
|
|
232
|
-
if (!hasMore) {
|
|
233
|
-
return -1; // No marker needed when all items are loaded.
|
|
234
|
-
}
|
|
235
|
-
// Place marker at visibleCount - threshold, but ensure it's at least 0.
|
|
236
|
-
return Math.max(0, visibleCount - threshold);
|
|
237
|
-
}, [
|
|
238
|
-
hasMore,
|
|
239
|
-
visibleCount,
|
|
240
|
-
threshold
|
|
241
|
-
]);
|
|
242
|
-
/**
|
|
243
|
-
* Context value for body rows (shared base, rowIndex added per-row).
|
|
244
|
-
*/ const bodyContextBase = useMemo(()=>({
|
|
245
|
-
...ctx,
|
|
246
|
-
cellWrapper: CellWrapper.BODY
|
|
247
|
-
}), [
|
|
248
|
-
ctx
|
|
249
|
-
]);
|
|
250
|
-
/**
|
|
251
|
-
* Render items with marker at the correct position. Each row gets a
|
|
252
|
-
* data-row-index attribute for scrollToIndex functionality. Each row is
|
|
253
|
-
* wrapped with a context provider that includes the isLastRow flag for proper
|
|
254
|
-
* border removal (CSS :last-child doesn't work with wrappers).
|
|
255
|
-
*/ const renderedContent = useMemo(()=>{
|
|
256
|
-
const result = [];
|
|
257
|
-
// Determine the actual last visible index (for border styling).
|
|
258
|
-
const lastVisibleIndex = Math.min(visibleCount, totalItems) - 1;
|
|
259
|
-
for(let i = 0; i < visibleCount && i < totalItems; i++){
|
|
260
|
-
// Insert marker at the calculated position.
|
|
261
|
-
if (i === markerPosition) {
|
|
262
|
-
result.push(/*#__PURE__*/ jsx("div", {
|
|
263
|
-
ref: markerRefCallback,
|
|
264
|
-
"aria-hidden": "true",
|
|
265
|
-
style: {
|
|
266
|
-
height: "1px",
|
|
267
|
-
background: "transparent"
|
|
268
|
-
}
|
|
269
|
-
}, "__infinite-scroll-marker-inline__"));
|
|
270
|
-
}
|
|
271
|
-
/**
|
|
272
|
-
* Determine if this is the last row (only when all data is loaded). If
|
|
273
|
-
* hasMore is true, no row is "last" since more will be loaded.
|
|
274
|
-
*/ const isLastRow = !hasMore && i === lastVisibleIndex;
|
|
275
|
-
/**
|
|
276
|
-
* Wrap row with context provider that includes rowIndex for proper odd/even
|
|
277
|
-
* styling. Using display:contents so the wrapper doesn't affect grid layout.
|
|
278
|
-
*/ result.push(/*#__PURE__*/ jsx(DataGridContext.Provider, {
|
|
279
|
-
value: {
|
|
280
|
-
...bodyContextBase,
|
|
281
|
-
isLastRow
|
|
282
|
-
},
|
|
283
|
-
children: /*#__PURE__*/ jsx("div", {
|
|
284
|
-
[ROW_INDEX_DATA_ATTR]: i,
|
|
285
|
-
style: {
|
|
286
|
-
display: "contents"
|
|
287
|
-
},
|
|
288
|
-
children: renderRow(data[i], i)
|
|
289
|
-
})
|
|
290
|
-
}, i));
|
|
291
|
-
}
|
|
292
|
-
// If marker position is at the end (edge case with small data).
|
|
293
|
-
if (markerPosition === visibleCount && hasMore) {
|
|
294
|
-
result.push(/*#__PURE__*/ jsx("div", {
|
|
295
|
-
ref: markerRefCallback,
|
|
296
|
-
"aria-hidden": "true",
|
|
297
|
-
style: {
|
|
298
|
-
height: "1px",
|
|
299
|
-
background: "transparent"
|
|
300
|
-
}
|
|
301
|
-
}, "__infinite-scroll-marker-end__"));
|
|
302
|
-
}
|
|
303
|
-
return result;
|
|
304
|
-
}, [
|
|
305
|
-
data,
|
|
306
|
-
visibleCount,
|
|
307
|
-
totalItems,
|
|
308
|
-
markerPosition,
|
|
309
|
-
hasMore,
|
|
310
|
-
renderRow,
|
|
311
|
-
markerRefCallback,
|
|
312
|
-
bodyContextBase
|
|
313
|
-
]);
|
|
314
|
-
// Measure column widths for sticky header/footer sync.
|
|
315
|
-
useColumnMeasurement(bodyRef, renderedContent);
|
|
316
|
-
const bodyClass = getBodyClass(className);
|
|
317
|
-
return /*#__PURE__*/ jsx(DataGridContext.Provider, {
|
|
318
|
-
value: {
|
|
319
|
-
...ctx,
|
|
320
|
-
cellWrapper: CellWrapper.BODY
|
|
321
|
-
},
|
|
322
|
-
children: /*#__PURE__*/ jsx("div", {
|
|
323
|
-
ref: bodyRef,
|
|
324
|
-
role: "rowgroup",
|
|
325
|
-
className: bodyClass,
|
|
326
|
-
children: renderedContent
|
|
327
|
-
})
|
|
328
|
-
});
|
|
329
|
-
}
|
|
330
|
-
/**
|
|
331
|
-
* DataGridInfiniteBody with forwardRef support for imperative methods.
|
|
332
|
-
*/ const DataGridInfiniteBody = /*#__PURE__*/ forwardRef(DataGridInfiniteBodyInner);
|
|
333
|
-
|
|
334
|
-
export { DataGridInfiniteBody };
|