@versini/ui-datagrid 0.3.8 → 0.4.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/README.md +106 -29
- package/dist/DataGrid/DataGrid.js +1 -1
- package/dist/DataGrid/DataGridContext.js +1 -1
- package/dist/DataGrid/DataGridTypes.d.ts +280 -0
- package/dist/DataGrid/DataGridTypes.js +9 -0
- package/dist/DataGrid/index.js +1 -1
- package/dist/DataGridAnimated/AnimatedWrapper.d.ts +11 -7
- package/dist/DataGridAnimated/AnimatedWrapper.js +12 -8
- package/dist/DataGridAnimated/index.js +1 -1
- package/dist/DataGridAnimated/useAnimatedHeight.js +1 -1
- package/dist/DataGridBody/DataGridBody.js +12 -56
- package/dist/DataGridBody/getBodyClass.d.ts +10 -0
- package/dist/DataGridBody/getBodyClass.js +24 -0
- package/dist/DataGridBody/index.js +1 -1
- package/dist/DataGridBody/useColumnMeasurement.d.ts +11 -0
- package/dist/DataGridBody/useColumnMeasurement.js +68 -0
- package/dist/DataGridCell/DataGridCell.js +1 -1
- package/dist/DataGridCell/index.js +1 -1
- package/dist/DataGridCellSort/DataGridCellSort.js +1 -1
- package/dist/DataGridCellSort/index.js +1 -1
- package/dist/DataGridConstants/DataGridConstants.js +1 -1
- package/dist/DataGridConstants/index.js +1 -1
- package/dist/DataGridFooter/DataGridFooter.js +1 -1
- package/dist/DataGridFooter/index.js +1 -1
- package/dist/DataGridHeader/DataGridHeader.js +1 -1
- package/dist/DataGridHeader/index.js +1 -1
- package/dist/DataGridInfinite/DataGridInfiniteBody.d.ts +52 -0
- package/dist/DataGridInfinite/DataGridInfiniteBody.js +335 -0
- package/dist/DataGridInfinite/index.d.ts +2 -4
- package/dist/DataGridInfinite/index.js +4 -8
- package/dist/DataGridRow/DataGridRow.js +5 -3
- package/dist/DataGridRow/index.js +1 -1
- package/dist/DataGridSorting/index.js +1 -1
- package/dist/DataGridSorting/sortingUtils.js +1 -1
- package/dist/utilities/classes.d.ts +6 -2
- package/dist/utilities/classes.js +35 -8
- package/package.json +2 -2
- package/dist/DataGridInfinite/InfiniteScrollMarker.d.ts +0 -31
- package/dist/DataGridInfinite/InfiniteScrollMarker.js +0 -54
- package/dist/DataGridInfinite/useInfiniteScroll.d.ts +0 -92
- package/dist/DataGridInfinite/useInfiniteScroll.js +0 -136
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Get the CSS class for a DataGrid body element. When columns are provided, use
|
|
3
|
+
* display:contents so the body doesn't interfere with the grid flow. Rows will
|
|
4
|
+
* use subgrid.
|
|
5
|
+
*
|
|
6
|
+
* @param hasColumns - Whether the DataGrid has columns defined
|
|
7
|
+
* @param className - Additional class name to merge
|
|
8
|
+
*
|
|
9
|
+
*/
|
|
10
|
+
export declare function getBodyClass(hasColumns: boolean | undefined, className?: string): string;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
@versini/ui-datagrid v0.4.1
|
|
3
|
+
© 2026 gizmette.com
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import clsx from "clsx";
|
|
7
|
+
|
|
8
|
+
;// CONCATENATED MODULE: external "clsx"
|
|
9
|
+
|
|
10
|
+
;// CONCATENATED MODULE: ./src/DataGridBody/getBodyClass.ts
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Get the CSS class for a DataGrid body element. When columns are provided, use
|
|
14
|
+
* display:contents so the body doesn't interfere with the grid flow. Rows will
|
|
15
|
+
* use subgrid.
|
|
16
|
+
*
|
|
17
|
+
* @param hasColumns - Whether the DataGrid has columns defined
|
|
18
|
+
* @param className - Additional class name to merge
|
|
19
|
+
*
|
|
20
|
+
*/ function getBodyClass(hasColumns, className) {
|
|
21
|
+
return hasColumns ? clsx("contents", className) : clsx("flex flex-col", className);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export { getBodyClass };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook to measure column widths from the first body row's cells. This is needed
|
|
3
|
+
* because sticky header/footer are absolutely positioned and can't use CSS
|
|
4
|
+
* subgrid. We measure the body cells (which ARE in the grid flow) and report
|
|
5
|
+
* the widths so header/footer can use the same pixel values.
|
|
6
|
+
*
|
|
7
|
+
* @param bodyRef - Ref to the body element containing the rows
|
|
8
|
+
* @param contentDependency - Dependency that changes when content changes (children or renderedContent)
|
|
9
|
+
*
|
|
10
|
+
*/
|
|
11
|
+
export declare function useColumnMeasurement(bodyRef: React.RefObject<HTMLDivElement | null>, contentDependency: unknown): void;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
@versini/ui-datagrid v0.4.1
|
|
3
|
+
© 2026 gizmette.com
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useContext, useLayoutEffect } from "react";
|
|
7
|
+
import { DataGridContext } from "../DataGrid/DataGridContext.js";
|
|
8
|
+
|
|
9
|
+
;// CONCATENATED MODULE: external "react"
|
|
10
|
+
|
|
11
|
+
;// CONCATENATED MODULE: external "../DataGrid/DataGridContext.js"
|
|
12
|
+
|
|
13
|
+
;// CONCATENATED MODULE: ./src/DataGridBody/useColumnMeasurement.ts
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Hook to measure column widths from the first body row's cells. This is needed
|
|
18
|
+
* because sticky header/footer are absolutely positioned and can't use CSS
|
|
19
|
+
* subgrid. We measure the body cells (which ARE in the grid flow) and report
|
|
20
|
+
* the widths so header/footer can use the same pixel values.
|
|
21
|
+
*
|
|
22
|
+
* @param bodyRef - Ref to the body element containing the rows
|
|
23
|
+
* @param contentDependency - Dependency that changes when content changes (children or renderedContent)
|
|
24
|
+
*
|
|
25
|
+
*/ function useColumnMeasurement(bodyRef, contentDependency) {
|
|
26
|
+
const ctx = useContext(DataGridContext);
|
|
27
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: contentDependency triggers remeasurement when rows change
|
|
28
|
+
useLayoutEffect(()=>{
|
|
29
|
+
const element = bodyRef.current;
|
|
30
|
+
const needsMeasurement = ctx.columns && (ctx.stickyHeader || ctx.stickyFooter);
|
|
31
|
+
if (!element || !needsMeasurement || !ctx.setMeasuredColumnWidths) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const firstRow = element.querySelector('[role="row"]');
|
|
35
|
+
if (!firstRow) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const cells = firstRow.querySelectorAll('[role="cell"], [role="columnheader"], [role="gridcell"]');
|
|
39
|
+
if (cells.length === 0) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const measureColumns = ()=>{
|
|
43
|
+
const widths = Array.from(cells).map((cell)=>cell.getBoundingClientRect().width);
|
|
44
|
+
ctx.setMeasuredColumnWidths?.(widths);
|
|
45
|
+
};
|
|
46
|
+
// Initial measurement.
|
|
47
|
+
measureColumns();
|
|
48
|
+
// Set up ResizeObserver to re-measure when cells resize.
|
|
49
|
+
const observer = new ResizeObserver(()=>{
|
|
50
|
+
measureColumns();
|
|
51
|
+
});
|
|
52
|
+
// Observe the body element for any size changes.
|
|
53
|
+
observer.observe(element);
|
|
54
|
+
// Also observe the first row's cells directly for more accurate updates.
|
|
55
|
+
for (const cell of cells){
|
|
56
|
+
observer.observe(cell);
|
|
57
|
+
}
|
|
58
|
+
return ()=>observer.disconnect();
|
|
59
|
+
}, [
|
|
60
|
+
ctx.columns,
|
|
61
|
+
ctx.stickyHeader,
|
|
62
|
+
ctx.stickyFooter,
|
|
63
|
+
ctx.setMeasuredColumnWidths,
|
|
64
|
+
contentDependency
|
|
65
|
+
]);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export { useColumnMeasurement };
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export type DataGridInfiniteBodyProps<T> = {
|
|
2
|
+
/**
|
|
3
|
+
* The full dataset to render progressively.
|
|
4
|
+
*/
|
|
5
|
+
data: T[];
|
|
6
|
+
/**
|
|
7
|
+
* Render function for each row. Should return a DataGridRow element. The
|
|
8
|
+
* consumer is responsible for providing the key prop on the returned element.
|
|
9
|
+
*/
|
|
10
|
+
children: (item: T, index: number) => React.ReactNode;
|
|
11
|
+
/**
|
|
12
|
+
* Number of items to show initially and to add on each scroll.
|
|
13
|
+
* @default 20
|
|
14
|
+
*/
|
|
15
|
+
batchSize?: number;
|
|
16
|
+
/**
|
|
17
|
+
* How many items to keep below the marker for seamless scrolling. The marker
|
|
18
|
+
* is placed `threshold` items before the end of visible items.
|
|
19
|
+
* @default 5
|
|
20
|
+
*/
|
|
21
|
+
threshold?: number;
|
|
22
|
+
/**
|
|
23
|
+
* IntersectionObserver root margin.
|
|
24
|
+
* @default "20px"
|
|
25
|
+
*/
|
|
26
|
+
rootMargin?: string;
|
|
27
|
+
/**
|
|
28
|
+
* Callback when visible count changes. Useful for displaying count in header.
|
|
29
|
+
*/
|
|
30
|
+
onVisibleCountChange?: (visibleCount: number, totalItems: number) => void;
|
|
31
|
+
/**
|
|
32
|
+
* CSS class to apply to the body.
|
|
33
|
+
*/
|
|
34
|
+
className?: string;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Ref handle for DataGridInfiniteBody, exposing imperative methods.
|
|
38
|
+
*/
|
|
39
|
+
export type DataGridInfiniteBodyRef = {
|
|
40
|
+
/**
|
|
41
|
+
* Scroll to a specific row index with smooth animation. If the row is not yet
|
|
42
|
+
* visible, it will expand the visible count first.
|
|
43
|
+
* @param index - The index of the row to scroll to
|
|
44
|
+
*/
|
|
45
|
+
scrollToIndex: (index: number) => void;
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* DataGridInfiniteBody with forwardRef support for imperative methods.
|
|
49
|
+
*/
|
|
50
|
+
export declare const DataGridInfiniteBody: <T>(props: DataGridInfiniteBodyProps<T> & {
|
|
51
|
+
ref?: React.ForwardedRef<DataGridInfiniteBodyRef>;
|
|
52
|
+
}) => React.ReactElement;
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
@versini/ui-datagrid v0.4.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 row index for proper
|
|
254
|
+
* odd/even styling (CSS :nth-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
|
+
rowIndex: i,
|
|
282
|
+
isLastRow
|
|
283
|
+
},
|
|
284
|
+
children: /*#__PURE__*/ jsx("div", {
|
|
285
|
+
[ROW_INDEX_DATA_ATTR]: i,
|
|
286
|
+
style: {
|
|
287
|
+
display: "contents"
|
|
288
|
+
},
|
|
289
|
+
children: renderRow(data[i], i)
|
|
290
|
+
})
|
|
291
|
+
}, i));
|
|
292
|
+
}
|
|
293
|
+
// If marker position is at the end (edge case with small data).
|
|
294
|
+
if (markerPosition === visibleCount && hasMore) {
|
|
295
|
+
result.push(/*#__PURE__*/ jsx("div", {
|
|
296
|
+
ref: markerRefCallback,
|
|
297
|
+
"aria-hidden": "true",
|
|
298
|
+
style: {
|
|
299
|
+
height: "1px",
|
|
300
|
+
background: "transparent"
|
|
301
|
+
}
|
|
302
|
+
}, "__infinite-scroll-marker-end__"));
|
|
303
|
+
}
|
|
304
|
+
return result;
|
|
305
|
+
}, [
|
|
306
|
+
data,
|
|
307
|
+
visibleCount,
|
|
308
|
+
totalItems,
|
|
309
|
+
markerPosition,
|
|
310
|
+
hasMore,
|
|
311
|
+
renderRow,
|
|
312
|
+
markerRefCallback,
|
|
313
|
+
bodyContextBase
|
|
314
|
+
]);
|
|
315
|
+
// Measure column widths for sticky header/footer sync.
|
|
316
|
+
useColumnMeasurement(bodyRef, renderedContent);
|
|
317
|
+
const bodyClass = getBodyClass(Boolean(ctx.columns), className);
|
|
318
|
+
return /*#__PURE__*/ jsx(DataGridContext.Provider, {
|
|
319
|
+
value: {
|
|
320
|
+
...ctx,
|
|
321
|
+
cellWrapper: CellWrapper.BODY
|
|
322
|
+
},
|
|
323
|
+
children: /*#__PURE__*/ jsx("div", {
|
|
324
|
+
ref: bodyRef,
|
|
325
|
+
role: "rowgroup",
|
|
326
|
+
className: bodyClass,
|
|
327
|
+
children: renderedContent
|
|
328
|
+
})
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* DataGridInfiniteBody with forwardRef support for imperative methods.
|
|
333
|
+
*/ const DataGridInfiniteBody = /*#__PURE__*/ forwardRef(DataGridInfiniteBodyInner);
|
|
334
|
+
|
|
335
|
+
export { DataGridInfiniteBody };
|
|
@@ -1,4 +1,2 @@
|
|
|
1
|
-
export type {
|
|
2
|
-
export {
|
|
3
|
-
export type { UseInfiniteScrollOptions, UseInfiniteScrollReturn, } from "./useInfiniteScroll";
|
|
4
|
-
export { useInfiniteScroll } from "./useInfiniteScroll";
|
|
1
|
+
export type { DataGridInfiniteBodyProps, DataGridInfiniteBodyRef, } from "./DataGridInfiniteBody";
|
|
2
|
+
export { DataGridInfiniteBody } from "./DataGridInfiniteBody";
|
|
@@ -1,17 +1,13 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
@versini/ui-datagrid v0.
|
|
2
|
+
@versini/ui-datagrid v0.4.1
|
|
3
3
|
© 2026 gizmette.com
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
7
|
-
import { useInfiniteScroll } from "./useInfiniteScroll.js";
|
|
6
|
+
import { DataGridInfiniteBody } from "./DataGridInfiniteBody.js";
|
|
8
7
|
|
|
9
|
-
;// CONCATENATED MODULE: external "./
|
|
10
|
-
|
|
11
|
-
;// CONCATENATED MODULE: external "./useInfiniteScroll.js"
|
|
8
|
+
;// CONCATENATED MODULE: external "./DataGridInfiniteBody.js"
|
|
12
9
|
|
|
13
10
|
;// CONCATENATED MODULE: ./src/DataGridInfinite/index.ts
|
|
14
11
|
|
|
15
12
|
|
|
16
|
-
|
|
17
|
-
export { InfiniteScrollMarker, useInfiniteScroll };
|
|
13
|
+
export { DataGridInfiniteBody };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
@versini/ui-datagrid v0.
|
|
2
|
+
@versini/ui-datagrid v0.4.1
|
|
3
3
|
© 2026 gizmette.com
|
|
4
4
|
*/
|
|
5
5
|
|
|
@@ -31,7 +31,7 @@ import { getRowClasses } from "../utilities/classes.js";
|
|
|
31
31
|
// Count the number of direct children to determine column count.
|
|
32
32
|
const columnCount = react.Children.count(children);
|
|
33
33
|
return /*#__PURE__*/ jsx(DataGridContext.Consumer, {
|
|
34
|
-
children: ({ mode, cellWrapper, stickyHeader, stickyFooter, columns, measuredColumnWidths })=>{
|
|
34
|
+
children: ({ mode, cellWrapper, stickyHeader, stickyFooter, columns, measuredColumnWidths, rowIndex, isLastRow })=>{
|
|
35
35
|
/**
|
|
36
36
|
* Determine if this row is inside a sticky header/footer. Sticky elements
|
|
37
37
|
* are absolutely positioned and outside the main grid flow, so they can't
|
|
@@ -88,7 +88,9 @@ import { getRowClasses } from "../utilities/classes.js";
|
|
|
88
88
|
className: getRowClasses({
|
|
89
89
|
mode,
|
|
90
90
|
className,
|
|
91
|
-
cellWrapper
|
|
91
|
+
cellWrapper,
|
|
92
|
+
rowIndex,
|
|
93
|
+
isLastRow
|
|
92
94
|
}),
|
|
93
95
|
style: {
|
|
94
96
|
...rowStyle,
|