@versini/ui-datagrid 4.0.0 → 4.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/197.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- @versini/ui-datagrid v4.0.0
2
+ @versini/ui-datagrid v4.0.1
3
3
  © 2026 gizmette.com
4
4
  */
5
5
 
package/dist/430.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- @versini/ui-datagrid v4.0.0
2
+ @versini/ui-datagrid v4.0.1
3
3
  © 2026 gizmette.com
4
4
  */
5
5
 
package/dist/799.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- @versini/ui-datagrid v4.0.0
2
+ @versini/ui-datagrid v4.0.1
3
3
  © 2026 gizmette.com
4
4
  */
5
5
 
package/dist/91.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- @versini/ui-datagrid v4.0.0
2
+ @versini/ui-datagrid v4.0.1
3
3
  © 2026 gizmette.com
4
4
  */
5
5
 
@@ -1,5 +1,5 @@
1
1
  /*!
2
- @versini/ui-datagrid v4.0.0
2
+ @versini/ui-datagrid v4.0.1
3
3
  © 2026 gizmette.com
4
4
  */
5
5
 
@@ -1,5 +1,5 @@
1
1
  /*!
2
- @versini/ui-datagrid v4.0.0
2
+ @versini/ui-datagrid v4.0.1
3
3
  © 2026 gizmette.com
4
4
  */
5
5
 
@@ -1,5 +1,5 @@
1
1
  /*!
2
- @versini/ui-datagrid v4.0.0
2
+ @versini/ui-datagrid v4.0.1
3
3
  © 2026 gizmette.com
4
4
  */
5
5
 
@@ -1,5 +1,5 @@
1
1
  /*!
2
- @versini/ui-datagrid v4.0.0
2
+ @versini/ui-datagrid v4.0.1
3
3
  © 2026 gizmette.com
4
4
  */
5
5
 
@@ -1,5 +1,5 @@
1
1
  /*!
2
- @versini/ui-datagrid v4.0.0
2
+ @versini/ui-datagrid v4.0.1
3
3
  © 2026 gizmette.com
4
4
  */
5
5
 
@@ -1,5 +1,5 @@
1
1
  /*!
2
- @versini/ui-datagrid v4.0.0
2
+ @versini/ui-datagrid v4.0.1
3
3
  © 2026 gizmette.com
4
4
  */
5
5
 
@@ -1,5 +1,5 @@
1
1
  /*!
2
- @versini/ui-datagrid v4.0.0
2
+ @versini/ui-datagrid v4.0.1
3
3
  © 2026 gizmette.com
4
4
  */
5
5
 
@@ -1,5 +1,5 @@
1
1
  /*!
2
- @versini/ui-datagrid v4.0.0
2
+ @versini/ui-datagrid v4.0.1
3
3
  © 2026 gizmette.com
4
4
  */
5
5
 
@@ -1,5 +1,5 @@
1
1
  /*!
2
- @versini/ui-datagrid v4.0.0
2
+ @versini/ui-datagrid v4.0.1
3
3
  © 2026 gizmette.com
4
4
  */
5
5
 
@@ -23,6 +23,9 @@ import { getBodyClass, useColumnMeasurement } from "../197.js";
23
23
  const DEFAULT_OVERSCAN = 8;
24
24
  const DEFAULT_ESTIMATED_ROW_HEIGHT = 44;
25
25
  const DEFAULT_COMPACT_ESTIMATED_ROW_HEIGHT = 29;
26
+ // Fallback used to restore the normal window after a jump when the browser does
27
+ // not fire `scrollend` (e.g. Safari < 17.4) or the target was already centered.
28
+ const JUMP_SETTLE_FALLBACK_MS = 1000;
26
29
  /**
27
30
  * A virtualized DataGrid body for large datasets.
28
31
  *
@@ -73,6 +76,22 @@ const DEFAULT_COMPACT_ESTIMATED_ROW_HEIGHT = 29;
73
76
  * estimate far from the real height hurts scrollbar accuracy and scrollToIndex.
74
77
  */ const resolvedEstimatedRowHeight = estimatedRowHeight ?? (ctx.compact ? DEFAULT_COMPACT_ESTIMATED_ROW_HEIGHT : DEFAULT_ESTIMATED_ROW_HEIGHT);
75
78
  const [scrollMargin, setScrollMargin] = useState(0);
79
+ /**
80
+ * Jump-to-row state. A programmatic `scrollToIndex` first widens the
81
+ * virtualized window (`jump.overscan`) so every row between the current view
82
+ * and the target is mounted, then lets the browser natively smooth-scroll to
83
+ * the real target element. Scrolling over real, laid-out rows lands exactly and
84
+ * glides smoothly — the virtualizer never re-estimates the target offset
85
+ * mid-flight (which is what made the jump stutter near the target). The window
86
+ * is restored once the scroll settles, so the path rows recycle back out.
87
+ *
88
+ * `seq` makes every request a fresh object: two jumps that happen to compute
89
+ * the same `overscan` still re-run the jump effect (a plain number would be a
90
+ * no-op setState and the second jump would be dropped). `handledSeqRef` records
91
+ * the last seq the effect acted on, so it scrolls once per request.
92
+ */ const [jump, setJump] = useState(null);
93
+ const jumpSeqRef = useRef(0);
94
+ const handledSeqRef = useRef(-1);
76
95
  /**
77
96
  * The scroll target is provided by the parent DataGrid: its internal bounded
78
97
  * scroller (sticky / maxHeight), or the page (window scroll) when it owns
@@ -117,7 +136,9 @@ const DEFAULT_COMPACT_ESTIMATED_ROW_HEIGHT = 29;
117
136
  // useVirtualizer's element constraint excludes Window.
118
137
  getScrollElement: ()=>useWindow ? typeof window !== "undefined" ? window : null : scrollEl,
119
138
  estimateSize: ()=>resolvedEstimatedRowHeight,
120
- overscan,
139
+ // While a jump is in flight, `jump.overscan` widens the window to mount the
140
+ // rows along the path; otherwise the consumer's (or default) overscan.
141
+ overscan: jump?.overscan ?? overscan,
121
142
  scrollMargin,
122
143
  // React 19: let React batch the scroll-driven updates natively.
123
144
  useFlushSync: false,
@@ -168,28 +189,76 @@ const DEFAULT_COMPACT_ESTIMATED_ROW_HEIGHT = 29;
168
189
  totalItems
169
190
  ]);
170
191
  /**
171
- * Expose scrollToIndex via ref; delegate to the virtualizer (centers the row,
172
- * mounting it first if needed).
192
+ * Expose scrollToIndex via ref. Rather than letting the virtualizer scroll to
193
+ * an estimated offset (which re-corrects as rows measure, stuttering near the
194
+ * target), we widen the window to mount every row from the current view to the
195
+ * target, then a layout effect natively smooth-scrolls to the real element.
196
+ * Works for any index, mounted or not.
173
197
  */ useImperativeHandle(ref, ()=>({
174
198
  scrollToIndex: (index)=>{
175
199
  if (index < 0 || index >= totalItems) {
176
200
  console.warn(`scrollToIndex: index ${index} is out of bounds${totalItems === 0 ? " (the grid is empty)" : ` (0-${totalItems - 1})`}`);
177
201
  return;
178
202
  }
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"
203
+ const items = virtualizer.getVirtualItems();
204
+ const first = items.length > 0 ? items[0].index : 0;
205
+ const last = items.length > 0 ? items[items.length - 1].index : 0;
206
+ // Overscan is symmetric, so widen by the larger gap to either edge of
207
+ // the current window; that guarantees the target (and the rows between)
208
+ // mount. The jump effect scrolls once they render, then restores the
209
+ // normal window.
210
+ const reach = Math.max(index - first, last - index, 0);
211
+ jumpSeqRef.current += 1;
212
+ setJump({
213
+ index,
214
+ overscan: reach + overscan,
215
+ seq: jumpSeqRef.current
188
216
  });
189
217
  }
190
218
  }), [
191
219
  totalItems,
192
- virtualizer
220
+ virtualizer,
221
+ overscan
222
+ ]);
223
+ /**
224
+ * Perform a pending jump once the widened window has mounted the target row
225
+ * (react-virtual recomputes the window synchronously, so it is mounted in the
226
+ * commit that follows the `jump` change), then restore the normal window after
227
+ * the scroll settles. `scrollend` fires when the native smooth scroll finishes;
228
+ * the timeout is a fallback for browsers without it and for the no-op case
229
+ * (target already centered, so no scroll — and thus no `scrollend` — fires).
230
+ */ useLayoutEffect(()=>{
231
+ if (jump === null || handledSeqRef.current === jump.seq) {
232
+ return;
233
+ }
234
+ handledSeqRef.current = jump.seq;
235
+ const rowEl = bodyRef.current?.querySelector(`[data-index="${jump.index}"]`);
236
+ if (!rowEl) {
237
+ // The widened window should always mount the target; if it somehow did
238
+ // not, don't stay stuck in widened mode — restore the normal window so
239
+ // later jumps aren't dropped.
240
+ setJump(null);
241
+ return;
242
+ }
243
+ const reduceMotion = typeof window !== "undefined" && !!window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
244
+ rowEl.scrollIntoView({
245
+ behavior: reduceMotion ? "auto" : "smooth",
246
+ block: "center"
247
+ });
248
+ const scroller = useWindow ? typeof window !== "undefined" ? window : null : scrollEl;
249
+ const settle = ()=>setJump(null);
250
+ scroller?.addEventListener("scrollend", settle, {
251
+ once: true
252
+ });
253
+ const timer = setTimeout(settle, JUMP_SETTLE_FALLBACK_MS);
254
+ return ()=>{
255
+ clearTimeout(timer);
256
+ scroller?.removeEventListener("scrollend", settle);
257
+ };
258
+ }, [
259
+ jump,
260
+ useWindow,
261
+ scrollEl
193
262
  ]);
194
263
  /**
195
264
  * Report a monotonic high-water mark via onVisibleCountChange: the furthest
@@ -1,5 +1,5 @@
1
1
  /*!
2
- @versini/ui-datagrid v4.0.0
2
+ @versini/ui-datagrid v4.0.1
3
3
  © 2026 gizmette.com
4
4
  */
5
5
 
@@ -1,5 +1,5 @@
1
1
  /*!
2
- @versini/ui-datagrid v4.0.0
2
+ @versini/ui-datagrid v4.0.1
3
3
  © 2026 gizmette.com
4
4
  */
5
5
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@versini/ui-datagrid",
3
- "version": "4.0.0",
3
+ "version": "4.0.1",
4
4
  "license": "MIT",
5
5
  "author": "Arno Versini",
6
6
  "publishConfig": {
@@ -99,5 +99,5 @@
99
99
  "sideEffects": [
100
100
  "**/*.css"
101
101
  ],
102
- "gitHead": "4f7d84ec0f33c46dd0757121a5f04da7071b2d4c"
102
+ "gitHead": "15ac9e9cbe112e9ed49be5c857a6d4ac4444472e"
103
103
  }