@versini/ui-datagrid 3.1.1 → 4.0.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/README.md +18 -18
- package/dist/197.js +56 -34
- package/dist/430.js +1 -1
- package/dist/799.js +1 -1
- package/dist/91.js +1 -1
- package/dist/DataGrid/DataGrid.d.ts +1 -1
- package/dist/DataGrid/DataGridTypes.d.ts +52 -0
- package/dist/DataGrid/index.js +53 -9
- package/dist/DataGridAnimated/AnimatedWrapper.d.ts +1 -2
- package/dist/DataGridAnimated/index.js +16 -3
- package/dist/DataGridBody/DataGridBody.d.ts +1 -1
- package/dist/DataGridBody/index.js +1 -1
- package/dist/DataGridCell/DataGridCell.d.ts +1 -1
- package/dist/DataGridCell/index.js +1 -1
- package/dist/DataGridCellSort/ButtonSort.d.ts +1 -1
- package/dist/DataGridCellSort/DataGridCellSort.d.ts +1 -1
- package/dist/DataGridCellSort/index.js +1 -1
- package/dist/DataGridConstants/index.js +1 -1
- package/dist/DataGridFooter/DataGridFooter.d.ts +1 -1
- package/dist/DataGridFooter/index.js +4 -2
- package/dist/DataGridHeader/DataGridHeader.d.ts +1 -1
- package/dist/DataGridHeader/index.js +4 -2
- package/dist/DataGridInfinite/DataGridInfiniteBody.d.ts +33 -44
- package/dist/DataGridInfinite/index.js +220 -225
- package/dist/DataGridRow/DataGridRow.d.ts +2 -1
- package/dist/DataGridRow/index.js +5 -2
- package/dist/DataGridSorting/index.js +1 -1
- package/package.json +6 -5
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
@versini/ui-datagrid
|
|
2
|
+
@versini/ui-datagrid v4.0.0
|
|
3
3
|
© 2026 gizmette.com
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { jsx } from "react/jsx-runtime";
|
|
7
|
-
import {
|
|
6
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
7
|
+
import { elementScroll, observeElementOffset, observeElementRect, observeWindowOffset, observeWindowRect, useVirtualizer, windowScroll } from "@tanstack/react-virtual";
|
|
8
|
+
import { useContext, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
8
9
|
import { DataGridContext } from "../430.js";
|
|
9
10
|
import { CellWrapper } from "../799.js";
|
|
10
11
|
import { getBodyClass, useColumnMeasurement } from "../197.js";
|
|
@@ -17,291 +18,250 @@ import { getBodyClass, useColumnMeasurement } from "../197.js";
|
|
|
17
18
|
|
|
18
19
|
|
|
19
20
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
* Finds the nearest scrollable ancestor of an element. Returns null if no
|
|
26
|
-
* scrollable ancestor is found (uses viewport).
|
|
27
|
-
*/ function findScrollableAncestor(element) {
|
|
28
|
-
let parent = element.parentElement;
|
|
29
|
-
while(parent){
|
|
30
|
-
const style = getComputedStyle(parent);
|
|
31
|
-
const overflowY = style.overflowY;
|
|
32
|
-
if (overflowY === "auto" || overflowY === "scroll") {
|
|
33
|
-
return parent;
|
|
34
|
-
}
|
|
35
|
-
parent = parent.parentElement;
|
|
36
|
-
}
|
|
37
|
-
return null;
|
|
38
|
-
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
const DEFAULT_OVERSCAN = 8;
|
|
24
|
+
const DEFAULT_ESTIMATED_ROW_HEIGHT = 44;
|
|
25
|
+
const DEFAULT_COMPACT_ESTIMATED_ROW_HEIGHT = 29;
|
|
39
26
|
/**
|
|
40
|
-
* A
|
|
27
|
+
* A virtualized DataGrid body for large datasets.
|
|
28
|
+
*
|
|
29
|
+
* Only the rows near the viewport stay mounted (windowing via
|
|
30
|
+
* `@tanstack/react-virtual`), so the DOM stays small and scrolling stays smooth
|
|
31
|
+
* at any row count. Rows are rendered in normal grid flow between two full-span
|
|
32
|
+
* spacer elements, so `columns`/subgrid (including intrinsic `auto` tracks) keep
|
|
33
|
+
* working without any consumer change. The virtualizer windows against the
|
|
34
|
+
* DataGrid's own scroll container (created by `maxHeight` or a sticky
|
|
35
|
+
* header/footer), or the page (window scroll) when the grid has neither.
|
|
41
36
|
*
|
|
42
|
-
*
|
|
43
|
-
* -
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
* - Programmatic scroll-to-row with smooth animation
|
|
37
|
+
* Because off-screen rows are removed from the DOM, the grid exposes
|
|
38
|
+
* `aria-rowcount`/`aria-rowindex` and keeps keyboard focus on the grid if a
|
|
39
|
+
* focused row recycles out. Browser find-in-page and uncontrolled per-row state
|
|
40
|
+
* do not persist across recycling (inherent to DOM virtualization).
|
|
47
41
|
*
|
|
48
42
|
* @example
|
|
49
43
|
* ```tsx
|
|
50
|
-
* const infiniteBodyRef = useRef<DataGridInfiniteBodyRef>(null);
|
|
51
|
-
*
|
|
52
|
-
* // Jump to a specific row
|
|
53
|
-
* const handleJumpToRow = () => {
|
|
54
|
-
* infiniteBodyRef.current?.scrollToIndex(134);
|
|
55
|
-
* };
|
|
56
|
-
*
|
|
57
44
|
* <DataGrid maxHeight="400px" stickyHeader>
|
|
58
|
-
* <DataGridHeader caption={`Showing ${
|
|
45
|
+
* <DataGridHeader caption={`Showing ${reached} of ${data.length}`}>
|
|
59
46
|
* <DataGridRow>
|
|
60
47
|
* <DataGridCell>Name</DataGridCell>
|
|
61
|
-
* <DataGridCell>Role</DataGridCell>
|
|
62
48
|
* </DataGridRow>
|
|
63
49
|
* </DataGridHeader>
|
|
64
50
|
*
|
|
65
|
-
* <DataGridInfiniteBody
|
|
66
|
-
* ref={infiniteBodyRef}
|
|
67
|
-
* data={largeData}
|
|
68
|
-
* batchSize={25}
|
|
69
|
-
* onVisibleCountChange={(count) => setVisibleCount(count)}
|
|
70
|
-
* >
|
|
51
|
+
* <DataGridInfiniteBody data={largeData} estimatedRowHeight={56}>
|
|
71
52
|
* {(row) => (
|
|
72
53
|
* <DataGridRow key={row.id}>
|
|
73
54
|
* <DataGridCell>{row.name}</DataGridCell>
|
|
74
|
-
* <DataGridCell>{row.role}</DataGridCell>
|
|
75
55
|
* </DataGridRow>
|
|
76
56
|
* )}
|
|
77
57
|
* </DataGridInfiniteBody>
|
|
78
58
|
* </DataGrid>
|
|
79
59
|
* ```
|
|
80
60
|
*
|
|
81
|
-
*/ function DataGridInfiniteBody({ data = [], children: renderRow,
|
|
61
|
+
*/ function DataGridInfiniteBody({ data = [], children: renderRow, overscan = DEFAULT_OVERSCAN, estimatedRowHeight, onVisibleCountChange, className, noData = false, noDataText, ref }) {
|
|
82
62
|
const ctx = useContext(DataGridContext);
|
|
83
63
|
const bodyRef = useRef(null);
|
|
84
|
-
|
|
85
|
-
|
|
64
|
+
// A zero-height grid item at the list start. The display:contents body has no
|
|
65
|
+
// box, so we measure this real element to find the list's scroll-margin.
|
|
66
|
+
const sentinelRef = useRef(null);
|
|
86
67
|
const totalItems = data.length;
|
|
87
|
-
const
|
|
88
|
-
const
|
|
89
|
-
const hasMore = visibleCount < totalItems;
|
|
68
|
+
const headerRows = ctx.headerRows ?? 0;
|
|
69
|
+
const footerRows = ctx.footerRows ?? 0;
|
|
90
70
|
/**
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
71
|
+
* Default the row-height estimate from the grid's density when the consumer
|
|
72
|
+
* doesn't set it: `compact` rows are much shorter (~29px vs ~44px), and an
|
|
73
|
+
* estimate far from the real height hurts scrollbar accuracy and scrollToIndex.
|
|
74
|
+
*/ const resolvedEstimatedRowHeight = estimatedRowHeight ?? (ctx.compact ? DEFAULT_COMPACT_ESTIMATED_ROW_HEIGHT : DEFAULT_ESTIMATED_ROW_HEIGHT);
|
|
75
|
+
const [scrollMargin, setScrollMargin] = useState(0);
|
|
76
|
+
/**
|
|
77
|
+
* The scroll target is provided by the parent DataGrid: its internal bounded
|
|
78
|
+
* scroller (sticky / maxHeight), or the page (window scroll) when it owns
|
|
79
|
+
* none. This avoids fragile DOM-walking that can mistake an unbounded
|
|
80
|
+
* `overflow-x:auto` wrapper for a real vertical scroller.
|
|
81
|
+
*/ const useWindow = ctx.pageScroll === true;
|
|
82
|
+
const scrollEl = useWindow ? null : ctx.scrollContainer ?? null;
|
|
83
|
+
const resolved = useWindow || scrollEl !== null;
|
|
84
|
+
// Measure the list's offset within the scroller (= sticky-header padding /
|
|
85
|
+
// inline-header height / page offset). Keeps the window origin correct.
|
|
86
|
+
useLayoutEffect(()=>{
|
|
87
|
+
if (!resolved || noData) {
|
|
98
88
|
return;
|
|
99
89
|
}
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
if (row) {
|
|
105
|
-
row.scrollIntoView({
|
|
106
|
-
behavior: "smooth",
|
|
107
|
-
block: "center"
|
|
108
|
-
});
|
|
90
|
+
const compute = ()=>{
|
|
91
|
+
const node = sentinelRef.current;
|
|
92
|
+
if (!node) {
|
|
93
|
+
return;
|
|
109
94
|
}
|
|
95
|
+
const top = node.getBoundingClientRect().top;
|
|
96
|
+
const next = useWindow ? Math.max(0, Math.round(top + window.scrollY)) : Math.max(0, Math.round(top - scrollEl.getBoundingClientRect().top + scrollEl.scrollTop));
|
|
97
|
+
setScrollMargin((prev)=>prev === next ? prev : next);
|
|
98
|
+
};
|
|
99
|
+
compute();
|
|
100
|
+
const target = useWindow ? document.documentElement : scrollEl;
|
|
101
|
+
if (!target) {
|
|
102
|
+
return;
|
|
110
103
|
}
|
|
111
|
-
|
|
104
|
+
const observer = new ResizeObserver(compute);
|
|
105
|
+
observer.observe(target);
|
|
106
|
+
return ()=>observer.disconnect();
|
|
107
|
+
}, [
|
|
108
|
+
resolved,
|
|
109
|
+
useWindow,
|
|
110
|
+
scrollEl,
|
|
111
|
+
noData
|
|
112
|
+
]);
|
|
113
|
+
const virtualizer = useVirtualizer({
|
|
114
|
+
count: totalItems,
|
|
115
|
+
// In window mode the scroll element IS `window` (the window observers read
|
|
116
|
+
// window.innerHeight / window.scrollY off it); cast bridges the type since
|
|
117
|
+
// useVirtualizer's element constraint excludes Window.
|
|
118
|
+
getScrollElement: ()=>useWindow ? typeof window !== "undefined" ? window : null : scrollEl,
|
|
119
|
+
estimateSize: ()=>resolvedEstimatedRowHeight,
|
|
120
|
+
overscan,
|
|
121
|
+
scrollMargin,
|
|
122
|
+
// React 19: let React batch the scroll-driven updates natively.
|
|
123
|
+
useFlushSync: false,
|
|
124
|
+
// Window vs element observers are selected to match getScrollElement; the
|
|
125
|
+
// casts bridge the Element|Window union react-virtual can't narrow here.
|
|
126
|
+
observeElementRect: useWindow ? observeWindowRect : observeElementRect,
|
|
127
|
+
observeElementOffset: useWindow ? observeWindowOffset : observeElementOffset,
|
|
128
|
+
scrollToFn: useWindow ? windowScroll : elementScroll,
|
|
129
|
+
// Window mode: seed the offset from the live scroll position (mirrors
|
|
130
|
+
// useWindowVirtualizer). Without this, on mount the virtualizer's offset is
|
|
131
|
+
// 0, so it scrolls an already-scrolled page to the top and computes the
|
|
132
|
+
// wrong initial window.
|
|
133
|
+
initialOffset: useWindow ? ()=>typeof window !== "undefined" ? window.scrollY : 0 : undefined
|
|
134
|
+
});
|
|
135
|
+
const virtualItems = virtualizer.getVirtualItems();
|
|
136
|
+
const totalSize = virtualizer.getTotalSize();
|
|
137
|
+
const lastVisibleIndex = virtualItems.length > 0 ? virtualItems[virtualItems.length - 1].index : -1;
|
|
112
138
|
/**
|
|
113
|
-
*
|
|
139
|
+
* Dev-only: warn about scroll-container setups that silently break
|
|
140
|
+
* virtualization — windowing against the page while a bounded ancestor is
|
|
141
|
+
* actually the scroller (only the first screen renders), or an internal
|
|
142
|
+
* scroller that isn't height-bounded (every row mounts). Runs after paint so
|
|
143
|
+
* the measurements are real; stripped from production builds.
|
|
114
144
|
*/ useEffect(()=>{
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
145
|
+
// Treat a missing `process` (a non-Node runtime whose bundler didn't replace
|
|
146
|
+
// this expression) as production, so reading NODE_ENV can never throw.
|
|
147
|
+
const isProduction = typeof process === "undefined" || process.env.NODE_ENV === "production";
|
|
148
|
+
if (isProduction || noData || totalItems === 0 || typeof window === "undefined") {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (useWindow) {
|
|
152
|
+
let el = bodyRef.current?.parentElement ?? null;
|
|
153
|
+
while(el && el !== document.body && el !== document.documentElement){
|
|
154
|
+
const { overflowY } = getComputedStyle(el);
|
|
155
|
+
if ((overflowY === "auto" || overflowY === "scroll") && el.scrollHeight > el.clientHeight) {
|
|
156
|
+
console.warn("[DataGridInfiniteBody] Virtualizing against the page (window scroll), but " + "the grid is inside a scrollable container, so only the first screen of " + "rows will render. Set `maxHeight` on the DataGrid to scroll inside it.");
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
el = el.parentElement;
|
|
124
160
|
}
|
|
161
|
+
} else if (scrollEl && scrollEl.clientHeight > window.innerHeight && scrollEl.scrollHeight <= scrollEl.clientHeight + 1) {
|
|
162
|
+
console.warn("[DataGridInfiniteBody] The scroll container is not height-bounded, so every " + "row mounts and virtualization has no effect. Set `maxHeight` on the DataGrid.");
|
|
125
163
|
}
|
|
126
164
|
}, [
|
|
127
|
-
|
|
128
|
-
|
|
165
|
+
useWindow,
|
|
166
|
+
scrollEl,
|
|
167
|
+
noData,
|
|
168
|
+
totalItems
|
|
129
169
|
]);
|
|
130
170
|
/**
|
|
131
|
-
* Expose
|
|
171
|
+
* Expose scrollToIndex via ref; delegate to the virtualizer (centers the row,
|
|
172
|
+
* mounting it first if needed).
|
|
132
173
|
*/ useImperativeHandle(ref, ()=>({
|
|
133
174
|
scrollToIndex: (index)=>{
|
|
134
175
|
if (index < 0 || index >= totalItems) {
|
|
135
|
-
console.warn(`scrollToIndex: index ${index} is out of bounds (0-${totalItems - 1})`);
|
|
136
|
-
return;
|
|
137
|
-
}
|
|
138
|
-
// If the row is already visible, just scroll to it.
|
|
139
|
-
if (index < visibleCount) {
|
|
140
|
-
scrollToRowElement(index);
|
|
176
|
+
console.warn(`scrollToIndex: index ${index} is out of bounds${totalItems === 0 ? " (the grid is empty)" : ` (0-${totalItems - 1})`}`);
|
|
141
177
|
return;
|
|
142
178
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
179
|
+
// Smooth scroll, unless the user prefers reduced motion. Smoothness is
|
|
180
|
+
// best when `estimatedRowHeight` is close to the real row height; a far-
|
|
181
|
+
// off estimate makes the virtualizer re-correct the offset as rows are
|
|
182
|
+
// measured, which reads as a stutter near the target (a react-virtual
|
|
183
|
+
// limitation with dynamically measured rows).
|
|
184
|
+
const reduceMotion = typeof window !== "undefined" && !!window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
|
|
185
|
+
virtualizer.scrollToIndex(index, {
|
|
186
|
+
align: "center",
|
|
187
|
+
behavior: reduceMotion ? "auto" : "smooth"
|
|
188
|
+
});
|
|
149
189
|
}
|
|
150
190
|
}), [
|
|
151
191
|
totalItems,
|
|
152
|
-
|
|
153
|
-
threshold,
|
|
154
|
-
scrollToRowElement
|
|
192
|
+
virtualizer
|
|
155
193
|
]);
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
batchSize,
|
|
162
|
-
threshold
|
|
163
|
-
]);
|
|
164
|
-
// Notify parent of visible count changes.
|
|
194
|
+
/**
|
|
195
|
+
* Report a monotonic high-water mark via onVisibleCountChange: the furthest
|
|
196
|
+
* row index reached + 1 (resets when the dataset length changes).
|
|
197
|
+
*/ const highWaterRef = useRef(0);
|
|
198
|
+
const prevTotalRef = useRef(totalItems);
|
|
165
199
|
useEffect(()=>{
|
|
166
|
-
|
|
200
|
+
if (prevTotalRef.current !== totalItems) {
|
|
201
|
+
prevTotalRef.current = totalItems;
|
|
202
|
+
highWaterRef.current = 0;
|
|
203
|
+
}
|
|
204
|
+
const hw = Math.min(lastVisibleIndex + 1, totalItems);
|
|
205
|
+
if (hw > highWaterRef.current) {
|
|
206
|
+
highWaterRef.current = hw;
|
|
207
|
+
onVisibleCountChange?.(hw, totalItems);
|
|
208
|
+
}
|
|
167
209
|
}, [
|
|
168
|
-
|
|
210
|
+
lastVisibleIndex,
|
|
169
211
|
totalItems,
|
|
170
212
|
onVisibleCountChange
|
|
171
213
|
]);
|
|
172
214
|
/**
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
if (
|
|
178
|
-
|
|
215
|
+
* Report the total row count to the grid for aria-rowcount (header + body +
|
|
216
|
+
* footer rows), and clear it when this body unmounts. Empty/noData grids
|
|
217
|
+
* report nothing, so they render no aria-rowcount and no focus sentinel.
|
|
218
|
+
*/ useEffect(()=>{
|
|
219
|
+
if (noData || totalItems === 0) {
|
|
220
|
+
ctx.setRowCount?.(undefined);
|
|
221
|
+
return;
|
|
179
222
|
}
|
|
223
|
+
ctx.setRowCount?.(totalItems + headerRows + footerRows);
|
|
224
|
+
return ()=>ctx.setRowCount?.(undefined);
|
|
180
225
|
}, [
|
|
181
|
-
|
|
182
|
-
totalItems
|
|
226
|
+
noData,
|
|
227
|
+
totalItems,
|
|
228
|
+
headerRows,
|
|
229
|
+
footerRows,
|
|
230
|
+
ctx.setRowCount
|
|
183
231
|
]);
|
|
184
232
|
/**
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
233
|
+
* Focus preservation: when a focused per-row control recycles out of the DOM,
|
|
234
|
+
* focus would fall to document.body. Track the focused row index; if it
|
|
235
|
+
* unmounts and focus drops to the body, move focus to the grid sentinel.
|
|
236
|
+
*/ const focusedIndexRef = useRef(null);
|
|
237
|
+
useLayoutEffect(()=>{
|
|
238
|
+
const body = bodyRef.current;
|
|
239
|
+
const active = document.activeElement;
|
|
240
|
+
if (active && body?.contains(active)) {
|
|
241
|
+
const rowEl = active.closest("[data-index]");
|
|
242
|
+
focusedIndexRef.current = rowEl ? Number(rowEl.getAttribute("data-index")) : null;
|
|
243
|
+
return;
|
|
194
244
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
const root = findScrollableAncestor(node);
|
|
198
|
-
observerRef.current = new IntersectionObserver(handleIntersection, {
|
|
199
|
-
root,
|
|
200
|
-
rootMargin
|
|
201
|
-
});
|
|
202
|
-
observerRef.current.observe(node);
|
|
245
|
+
if (focusedIndexRef.current !== null && (active === document.body || active === null) && body && !body.querySelector(`[data-index="${focusedIndexRef.current}"]`)) {
|
|
246
|
+
ctx.focusGrid?.();
|
|
203
247
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
handleIntersection,
|
|
207
|
-
rootMargin
|
|
208
|
-
]);
|
|
209
|
-
// Clean up observer on unmount.
|
|
210
|
-
useEffect(()=>{
|
|
211
|
-
return ()=>{
|
|
212
|
-
observerRef.current?.disconnect();
|
|
213
|
-
};
|
|
214
|
-
}, []);
|
|
215
|
-
/**
|
|
216
|
-
* Calculate marker position. The marker should be placed `threshold` items
|
|
217
|
-
* from the end of visible items. This allows seamless scrolling - new items
|
|
218
|
-
* load while user scrolls through the remaining `threshold` items.
|
|
219
|
-
*/ const markerPosition = useMemo(()=>{
|
|
220
|
-
if (!hasMore) {
|
|
221
|
-
return -1; // No marker needed when all items are loaded.
|
|
222
|
-
}
|
|
223
|
-
// Place marker at visibleCount - threshold, but ensure it's at least 0.
|
|
224
|
-
return Math.max(0, visibleCount - threshold);
|
|
225
|
-
}, [
|
|
226
|
-
hasMore,
|
|
227
|
-
visibleCount,
|
|
228
|
-
threshold
|
|
229
|
-
]);
|
|
248
|
+
focusedIndexRef.current = null;
|
|
249
|
+
});
|
|
230
250
|
/**
|
|
231
|
-
* Context value for body rows (shared base
|
|
251
|
+
* Context value for body rows (shared base; per-row fields added below).
|
|
232
252
|
*/ const bodyContextBase = useMemo(()=>({
|
|
233
253
|
...ctx,
|
|
234
254
|
cellWrapper: CellWrapper.BODY
|
|
235
255
|
}), [
|
|
236
256
|
ctx
|
|
237
257
|
]);
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
* border removal (CSS :last-child doesn't work with wrappers).
|
|
243
|
-
*/ const renderedContent = useMemo(()=>{
|
|
244
|
-
const result = [];
|
|
245
|
-
// Determine the actual last visible index (for border styling).
|
|
246
|
-
const lastVisibleIndex = Math.min(visibleCount, totalItems) - 1;
|
|
247
|
-
for(let i = 0; i < visibleCount && i < totalItems; i++){
|
|
248
|
-
// Insert marker at the calculated position.
|
|
249
|
-
if (i === markerPosition) {
|
|
250
|
-
result.push(/*#__PURE__*/ jsx("div", {
|
|
251
|
-
ref: markerRefCallback,
|
|
252
|
-
"aria-hidden": "true",
|
|
253
|
-
style: {
|
|
254
|
-
height: "1px",
|
|
255
|
-
background: "transparent"
|
|
256
|
-
}
|
|
257
|
-
}, "__infinite-scroll-marker-inline__"));
|
|
258
|
-
}
|
|
259
|
-
/**
|
|
260
|
-
* Determine if this is the last row (only when all data is loaded). If
|
|
261
|
-
* hasMore is true, no row is "last" since more will be loaded.
|
|
262
|
-
*/ const isLastRow = !hasMore && i === lastVisibleIndex;
|
|
263
|
-
/**
|
|
264
|
-
* Wrap row with context provider that includes rowIndex for proper odd/even
|
|
265
|
-
* styling. Using display:contents so the wrapper doesn't affect grid layout.
|
|
266
|
-
*/ result.push(/*#__PURE__*/ jsx(DataGridContext.Provider, {
|
|
267
|
-
value: {
|
|
268
|
-
...bodyContextBase,
|
|
269
|
-
isLastRow
|
|
270
|
-
},
|
|
271
|
-
children: /*#__PURE__*/ jsx("div", {
|
|
272
|
-
[ROW_INDEX_DATA_ATTR]: i,
|
|
273
|
-
style: {
|
|
274
|
-
display: "contents"
|
|
275
|
-
},
|
|
276
|
-
children: renderRow(data[i], i)
|
|
277
|
-
})
|
|
278
|
-
}, i));
|
|
279
|
-
}
|
|
280
|
-
// If marker position is at the end (edge case with small data).
|
|
281
|
-
if (markerPosition === visibleCount && hasMore) {
|
|
282
|
-
result.push(/*#__PURE__*/ jsx("div", {
|
|
283
|
-
ref: markerRefCallback,
|
|
284
|
-
"aria-hidden": "true",
|
|
285
|
-
style: {
|
|
286
|
-
height: "1px",
|
|
287
|
-
background: "transparent"
|
|
288
|
-
}
|
|
289
|
-
}, "__infinite-scroll-marker-end__"));
|
|
290
|
-
}
|
|
291
|
-
return result;
|
|
292
|
-
}, [
|
|
293
|
-
data,
|
|
294
|
-
visibleCount,
|
|
295
|
-
totalItems,
|
|
296
|
-
markerPosition,
|
|
297
|
-
hasMore,
|
|
298
|
-
renderRow,
|
|
299
|
-
markerRefCallback,
|
|
300
|
-
bodyContextBase
|
|
301
|
-
]);
|
|
302
|
-
// Measure column widths for sticky header/footer sync.
|
|
303
|
-
useColumnMeasurement(bodyRef, renderedContent, noData);
|
|
258
|
+
// Measure column widths for sticky header/footer sync. Re-key on the first
|
|
259
|
+
// mounted row so the sticky header re-syncs as the window (and the auto-column
|
|
260
|
+
// width) slides.
|
|
261
|
+
useColumnMeasurement(bodyRef, virtualItems[0]?.index, noData);
|
|
304
262
|
const bodyClass = getBodyClass(className);
|
|
263
|
+
const paddingTop = virtualItems.length > 0 ? Math.max(0, virtualItems[0].start - scrollMargin) : 0;
|
|
264
|
+
const paddingBottom = virtualItems.length > 0 ? Math.max(0, totalSize - virtualItems[virtualItems.length - 1].end) : 0;
|
|
305
265
|
return /*#__PURE__*/ jsx(DataGridContext.Provider, {
|
|
306
266
|
value: {
|
|
307
267
|
...ctx,
|
|
@@ -324,7 +284,42 @@ const ROW_INDEX_DATA_ATTR = "data-row-index";
|
|
|
324
284
|
},
|
|
325
285
|
children: noDataText ?? "No Data"
|
|
326
286
|
})
|
|
327
|
-
}) :
|
|
287
|
+
}) : /*#__PURE__*/ jsxs(Fragment, {
|
|
288
|
+
children: [
|
|
289
|
+
/*#__PURE__*/ jsx("div", {
|
|
290
|
+
ref: sentinelRef,
|
|
291
|
+
"aria-hidden": "true",
|
|
292
|
+
style: {
|
|
293
|
+
gridColumn: "1 / -1",
|
|
294
|
+
height: 0
|
|
295
|
+
}
|
|
296
|
+
}),
|
|
297
|
+
paddingTop > 0 && /*#__PURE__*/ jsx("div", {
|
|
298
|
+
"aria-hidden": "true",
|
|
299
|
+
style: {
|
|
300
|
+
gridColumn: "1 / -1",
|
|
301
|
+
height: paddingTop
|
|
302
|
+
}
|
|
303
|
+
}),
|
|
304
|
+
virtualItems.map((vItem)=>/*#__PURE__*/ jsx(DataGridContext.Provider, {
|
|
305
|
+
value: {
|
|
306
|
+
...bodyContextBase,
|
|
307
|
+
isLastRow: vItem.index === totalItems - 1,
|
|
308
|
+
ariaRowIndex: vItem.index + headerRows + 1,
|
|
309
|
+
dataIndex: vItem.index,
|
|
310
|
+
measureRowElement: virtualizer.measureElement
|
|
311
|
+
},
|
|
312
|
+
children: renderRow(data[vItem.index], vItem.index)
|
|
313
|
+
}, vItem.key)),
|
|
314
|
+
paddingBottom > 0 && /*#__PURE__*/ jsx("div", {
|
|
315
|
+
"aria-hidden": "true",
|
|
316
|
+
style: {
|
|
317
|
+
gridColumn: "1 / -1",
|
|
318
|
+
height: paddingBottom
|
|
319
|
+
}
|
|
320
|
+
})
|
|
321
|
+
]
|
|
322
|
+
})
|
|
328
323
|
})
|
|
329
324
|
});
|
|
330
325
|
}
|
|
@@ -1,2 +1,3 @@
|
|
|
1
|
+
import React from "react";
|
|
1
2
|
import type { DataGridRowProps } from "../DataGrid/DataGridTypes";
|
|
2
|
-
export declare const DataGridRow: ({ className, children, active, style: userStyle, ...rest }: DataGridRowProps) =>
|
|
3
|
+
export declare const DataGridRow: ({ className, children, active, style: userStyle, ...rest }: DataGridRowProps) => React.JSX.Element;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
@versini/ui-datagrid
|
|
2
|
+
@versini/ui-datagrid v4.0.0
|
|
3
3
|
© 2026 gizmette.com
|
|
4
4
|
*/
|
|
5
5
|
|
|
@@ -22,7 +22,7 @@ import { getRowClasses } from "../91.js";
|
|
|
22
22
|
// Count the number of direct children to determine column count.
|
|
23
23
|
const columnCount = react.Children.count(children);
|
|
24
24
|
return /*#__PURE__*/ jsx(DataGridContext.Consumer, {
|
|
25
|
-
children: ({ mode, cellWrapper, stickyHeader, stickyFooter, columns, measuredColumnWidths, isLastRow })=>{
|
|
25
|
+
children: ({ mode, cellWrapper, stickyHeader, stickyFooter, columns, measuredColumnWidths, isLastRow, ariaRowIndex, dataIndex, measureRowElement })=>{
|
|
26
26
|
/**
|
|
27
27
|
* Determine if this row is inside a sticky header/footer. Sticky elements
|
|
28
28
|
* are absolutely positioned and outside the main grid flow, so they can't
|
|
@@ -75,6 +75,7 @@ import { getRowClasses } from "../91.js";
|
|
|
75
75
|
};
|
|
76
76
|
}
|
|
77
77
|
return /*#__PURE__*/ jsx("div", {
|
|
78
|
+
ref: measureRowElement,
|
|
78
79
|
role: "row",
|
|
79
80
|
className: getRowClasses({
|
|
80
81
|
mode,
|
|
@@ -89,6 +90,8 @@ import { getRowClasses } from "../91.js";
|
|
|
89
90
|
...userStyle
|
|
90
91
|
},
|
|
91
92
|
"data-active": active || undefined,
|
|
93
|
+
"data-index": dataIndex,
|
|
94
|
+
"aria-rowindex": ariaRowIndex,
|
|
92
95
|
...rest,
|
|
93
96
|
children: children
|
|
94
97
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@versini/ui-datagrid",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "Arno Versini",
|
|
6
6
|
"publishConfig": {
|
|
@@ -87,16 +87,17 @@
|
|
|
87
87
|
},
|
|
88
88
|
"devDependencies": {
|
|
89
89
|
"@testing-library/jest-dom": "6.9.1",
|
|
90
|
-
"@versini/ui-button": "15.0.
|
|
90
|
+
"@versini/ui-button": "15.0.5",
|
|
91
91
|
"@versini/ui-types": "10.0.0"
|
|
92
92
|
},
|
|
93
93
|
"dependencies": {
|
|
94
|
-
"@
|
|
94
|
+
"@tanstack/react-virtual": "3.14.4",
|
|
95
|
+
"@versini/ui-icons": "4.29.0",
|
|
95
96
|
"clsx": "2.1.1",
|
|
96
|
-
"tailwindcss": "4.3.
|
|
97
|
+
"tailwindcss": "4.3.1"
|
|
97
98
|
},
|
|
98
99
|
"sideEffects": [
|
|
99
100
|
"**/*.css"
|
|
100
101
|
],
|
|
101
|
-
"gitHead": "
|
|
102
|
+
"gitHead": "4f7d84ec0f33c46dd0757121a5f04da7071b2d4c"
|
|
102
103
|
}
|