Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion src/primitives/Virtual.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,13 @@ function createVirtual<T>(
const total = itemCount();
if (total === 0) return applySlice(0, noItems, 0, delta, 0, true, 0);

if (total <= props.displaySize) {
// Everything is mounted and the row does not scroll. Not for auto without
// wrap at exactly displaySize items: there, as for a longer list, the
// last item ends one slot short of displaySize, so the row scrolls.
if (
total < props.displaySize ||
(total === props.displaySize && (props.wrap || scrollType() !== 'auto'))
) {
const clamped = utils.clamp(c, 0, total - 1);
return applySlice(
0,
Expand Down
8 changes: 6 additions & 2 deletions src/primitives/VirtualGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,10 @@
setCursor(prevStart + idx);
if (newRowIndex === prevRowIndex) return;

// Read before the window changes: the post-mutation flex pass can run
// before applyShift, and active.y read there is already the new layout,
// so the rows jumped a row instead of animating once the window slid.
shiftPrevRowY = this.y + active.y;
setSlice(items().slice(start(), end()));

// this.selected is relative to the slice
Expand Down Expand Up @@ -149,14 +153,14 @@
let shiftActive!: lng.ElementNode;
let shiftIdx = 0;
let shiftLastIdx: number | undefined;
let shiftPrevRowY = 0;
function applyShift() {
const view = shiftView;
const active = shiftActive;
const prevRowY = view.y + active.y;
// One flex pass per window shift: lay out here only if the post-mutation
// layout has not already run since the window changed.
if (layoutPending) view.updateLayout();
view.lng.y = prevRowY - active.y;
view.lng.y = shiftPrevRowY - active.y;
columnScroll(shiftIdx, view, active, shiftLastIdx);
}

Expand All @@ -180,7 +184,7 @@
}

let cachedSelected: number | undefined;
const updateSelected = ([selected, _items]: [number?, any?]) => {

Check warning on line 187 in src/primitives/VirtualGrid.tsx

View workflow job for this annotation

GitHub Actions / build-test

typescript(no-explicit-any)

src/primitives/VirtualGrid.tsx:187:57: Unexpected `any`. Specify a different type.
if (!viewRef || selected == null) return;

if (cachedSelected !== undefined) {
Expand Down
82 changes: 82 additions & 0 deletions tests/contract-virtual-grid.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -710,4 +710,86 @@ v.describe('VirtualGrid', () => {
v.expect(focusedId()).toBe('g14');
},
);

// Animations are on, as in an app. The DOM renderer's animations run on
// requestAnimationFrame, and jsdom's frame time is on another clock than
// the performance.now() the animations start from, so frames are driven
// here with a time far ahead: each y transition ends on its first frame.
v.it(
'with animations on, every row change animates y from where the rows were: they do not jump when the window slides (row 3 on, buffer 2)',
async () => {
const settle = async () => {
for (let i = 0; i < 4; i++) await flush();
};
v.vi.stubGlobal(
'requestAnimationFrame',
(cb: (time: number) => void) =>
setTimeout(() => cb(performance.now() + 1e6), 0),
);
lng.Config.animationsEnabled = true;
try {
let grid!: lng.ElementNode;
// Visual y of each mounted cell, by data item.
const cellYs = () =>
new Map(
grid.children.map((c) => [
(c as lng.ElementNode).item as number,
grid.lng.y + c.lng.y,
]),
);
let atStart: Map<number, number> | undefined;
const onAnimation = {
animating(this: lng.ElementNode, name: string) {
if (this === grid && name === 'y') atStart = cellYs();
},
};
dispose = await mount(() => (
<view width={1920} height={1080}>
<VirtualGrid
ref={grid}
autofocus
y={40}
width={700}
columns={3}
each={range(60)}
onAnimation={onAnimation}
>
{(item) => <Cell item={item()} />}
</VirtualGrid>
</view>
));
// Down 3 onward slides the window down (a row above is unmounted),
// and Up from there slides it back.
const steps = [
...[1, 2, 3, 4, 5, 6].map((row) => ['ArrowDown', row] as const),
...[5, 4, 3, 2, 1, 0].map((row) => ['ArrowUp', row] as const),
];
for (const [key, row] of steps) {
const before = cellYs();
atStart = undefined;
await press(key);
await settle();
v.expect(focusedId()).toBe(`g${row * 3}`);
v.expect(
atStart,
`${key} to row ${row}: no y transition`,
).toBeDefined();
const jumped = [...atStart!].filter(
([item, y]) => before.has(item) && before.get(item) !== y,
);
v.expect(
jumped,
`${key} to row ${row}: cells moved before the transition`,
).toEqual([]);
// The transition ran to the row's place.
v.expect(grid.lng.y, `${key} to row ${row}: y`).toBe(
Math.max(-160, 40 - row * 100),
);
}
} finally {
lng.Config.animationsEnabled = false;
v.vi.unstubAllGlobals();
}
},
);
});
47 changes: 47 additions & 0 deletions tests/contract-virtual.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -596,6 +596,53 @@ v.describe('VirtualRow: window and scroll modes', () => {
]);
},
);

v.it(
'scroll="auto", item count === displaySize: the row scrolls one slot, the last item ends where it does for a longer list',
async () => {
let row!: lng.ElementNode;
dispose = await mount(() => (
<view width={1920} height={1080}>
<VirtualRow
ref={row}
autofocus
x={50}
each={[0, 1, 2, 3]}
displaySize={4}
>
{(item) => <Item item={item()} />}
</VirtualRow>
</view>
));
const focusedX = () => row.x + (lng.activeElement() as lng.ElementNode).x;
const seen = [[focusedId(), focusedX()]];
for (const key of [
'ArrowRight',
'ArrowRight',
'ArrowRight',
'ArrowRight',
'ArrowLeft',
'ArrowLeft',
'ArrowLeft',
]) {
await press(key);
seen.push([focusedId(), focusedX()]);
}
// As with 5 or 12 items, the last item ends at 510, one slot short of
// 740: with no scroll it ended at 740, out of view. Left retraces the
// longer lists' path back to 50.
v.expect(seen).toEqual([
['v0', 50],
['v1', 50],
['v2', 280],
['v3', 510],
['v3', 510],
['v2', 510],
['v1', 280],
['v0', 50],
]);
},
);
});

v.describe('VirtualRow: bufferSize and onEndReached', () => {
Expand Down
Loading