Package: @solidtv/solid 1.3.9 (renderer @solidtv/renderer 1.6.1)
File: src/primitives/VirtualGrid.tsx, onSelectedChanged
Symptom
With scroll="always" (the default) and the default transition: { y: true }, moving Down through a
VirtualGrid animates only the first buffer + 1 rows. From then on, every row change jumps the
grid by one row in a single frame, with no tween. Up behaves the same once the slice shifts.
Reproduction
A VirtualGrid with columns={5}, rows={4}, buffer={2}, and about 40 items. Press Down
repeatedly. Rows 0→1 and 1→2 tween. From row 3 on, start() is non-zero and every step re-slices,
and none of those steps tweens.
Measured in Chrome with a per-frame sampler on the grid's lng.y and wrappers on lng.y's setter
and lng.animateProp, for one Down past row 3:
lng.y stayed at -400 on every frame;
- there was exactly one write to
lng.y (-400, from the compensation line);
animateProp was never called.
Root cause
setSlice(items().slice(start(), end()));
// ...
queueMicrotask(() => {
const prevRowY = this.y + active.y; // <-- measured here
this.updateLayout();
this.lng.y = prevRowY - active.y;
columnScroll(idx, elm, active, lastIdx);
});
setSlice synchronously inserts and removes children. Each insert calls addToLayoutQueue, which
calls schedulePostMutation() → queueMicrotask(runPostMutation) (core/elementNode.ts).
That microtask is queued before VirtualGrid's own, so runPostMutation runs first. Its layout
phase calls updateLayout() on the grid, and the flex layout moves active to its new slice
position.
When VirtualGrid's microtask runs, active.y is already the new value, so:
prevRowY - active.y === this.y, and the compensation is a no-op;
columnScroll computes -active.y + offset, which is the same target as the previous step,
because in steady state the focused card always sits buffer rows into the slice;
componentRef.y !== nextPosition is false, so it never assigns y and no transition starts.
The content moves up a row via layout alone, which is the one-frame jump.
(Wrapping updateLayout confirms the order. Two calls per Down: the first, from
runPostMutation, moves active.y 600 → 400. The second, VirtualGrid's own, sees 400 → 400.)
Why Virtual.tsx doesn't have this bug
Virtual.tsx reads its equivalent value synchronously, before the microtask:
const prevChildPos = (targetPosition ?? this[axis]) + active[axis];
queueMicrotask(() => {
elm.updateLayout();
...
this.lng[axis] = prevChildPos - active[axis];
Suggested fix
Take the measurement synchronously, right after setSlice, while the old layout still stands:
setSlice(items().slice(start(), end()));
+ const prevRowY = this.y + active.y;
// this.selected is relative to the slice
...
queueMicrotask(() => {
- const prevRowY = this.y + active.y;
this.updateLayout();
this.lng.y = prevRowY - active.y;
columnScroll(idx, elm, active, lastIdx);
});
With this change, every row change tweens in the browser, for example -200 → -400 over ~23
frames on a 200px row pitch. Held navigation also chains smoothly: each step picks up the running
tween instead of jumping.
A regression test that fails before and passes after: mount a grid, and for each of 6 Downs, flush
microtasks and assert that a y tween is queued on the grid with a target different from its
current y. Before the fix this yields [true, true, false, false, false, false].
Package:
@solidtv/solid1.3.9 (renderer@solidtv/renderer1.6.1)File:
src/primitives/VirtualGrid.tsx,onSelectedChangedSymptom
With
scroll="always"(the default) and the defaulttransition: { y: true }, moving Down through aVirtualGridanimates only the firstbuffer + 1rows. From then on, every row change jumps thegrid by one row in a single frame, with no tween. Up behaves the same once the slice shifts.
Reproduction
A
VirtualGridwithcolumns={5},rows={4},buffer={2}, and about 40 items. Press Downrepeatedly. Rows 0→1 and 1→2 tween. From row 3 on,
start()is non-zero and every step re-slices,and none of those steps tweens.
Measured in Chrome with a per-frame sampler on the grid's
lng.yand wrappers onlng.y's setterand
lng.animateProp, for one Down past row 3:lng.ystayed at -400 on every frame;lng.y(-400, from the compensation line);animatePropwas never called.Root cause
setSlicesynchronously inserts and removes children. Each insert callsaddToLayoutQueue, whichcalls
schedulePostMutation()→queueMicrotask(runPostMutation)(core/elementNode.ts).That microtask is queued before VirtualGrid's own, so
runPostMutationruns first. Its layoutphase calls
updateLayout()on the grid, and the flex layout movesactiveto its new sliceposition.
When VirtualGrid's microtask runs,
active.yis already the new value, so:prevRowY - active.y === this.y, and the compensation is a no-op;columnScrollcomputes-active.y + offset, which is the same target as the previous step,because in steady state the focused card always sits
bufferrows into the slice;componentRef.y !== nextPositionis false, so it never assignsyand no transition starts.The content moves up a row via layout alone, which is the one-frame jump.
(Wrapping
updateLayoutconfirms the order. Two calls per Down: the first, fromrunPostMutation, movesactive.y600 → 400. The second, VirtualGrid's own, sees 400 → 400.)Why
Virtual.tsxdoesn't have this bugVirtual.tsxreads its equivalent value synchronously, before the microtask:Suggested fix
Take the measurement synchronously, right after
setSlice, while the old layout still stands:With this change, every row change tweens in the browser, for example -200 → -400 over ~23
frames on a 200px row pitch. Held navigation also chains smoothly: each step picks up the running
tween instead of jumping.
A regression test that fails before and passes after: mount a grid, and for each of 6 Downs, flush
microtasks and assert that a
ytween is queued on the grid with a target different from itscurrent
y. Before the fix this yields[true, true, false, false, false, false].