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
16 changes: 11 additions & 5 deletions src/primitives/Lazy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -144,11 +144,17 @@ function createLazy<T>(
const updateOffset = (_event: KeyboardEvent, container: lng.ElementNode) => {
const maxOffset = props.each ? props.each.length : 0;
const selected = container.selected || 0;
const rendered = offset(); // == container.children.length

// Already mounted everything, or still far enough from the rendered edge
// that the buffer covers the next selection — no work to do.
if (rendered >= maxOffset || selected < rendered - buffer()) return;
// The rendered edge is the actual child count, not offset(): the two
// diverge when an item renders empty (no node inserted) or renders
// multiple top-level nodes. `selected` indexes into children, so the
// proximity check must use child units.
const rendered = container.children.length;

// Already mounted everything (offset is in data units — a diverged child
// count must neither stop mounting early nor keep it running forever),
// or still far enough from the rendered edge that the buffer covers the
// next selection — no work to do.
if (offset() >= maxOffset || selected < rendered - buffer()) return;

const bump = () => setOffset((prev) => Math.min(prev + 1, maxOffset));

Expand Down
7 changes: 6 additions & 1 deletion src/primitives/VirtualGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,13 @@ import * as lng from '@solidtv/solid';
import * as lngp from '@solidtv/solid/primitives';
import { List } from '@solid-primitives/list';
import * as utils from '../utils.js';
// Imported directly, not via lngp: this call runs at module init, and the
// barrel's withScrolling binding is not yet initialized when the barrel is
// the import entry (circular init — index.ts re-exports withScrolling after
// VirtualGrid).
import { withScrolling } from './utils/withScrolling.js';

const columnScroll = lngp.withScrolling(false);
const columnScroll = withScrolling(false);

const rowStyles: lng.NodeStyles = {
display: 'flex',
Expand Down
89 changes: 89 additions & 0 deletions tests/lazy.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import * as v from 'vitest';
import * as lng from '@solidtv/solid';
import { LazyColumn } from '../src/primitives/Lazy.jsx';
import { renderer } from './setup.js';

const wait = (ms = 10) => new Promise((r) => setTimeout(r, ms));

// updateOffset runs before moveSelection in the chained onDown handler, the
// same order propagateKeyPress invokes it with (handler.call(elm, e, elm)).
const pressDown = (col: lng.ElementNode) =>
(col as any).onDown.call(col, { key: 'ArrowDown' } as KeyboardEvent, col);

v.describe('Lazy components with empty content', () => {
v.test('keeps mounting past items that render empty (#26)', async () => {
let col!: lng.ElementNode;
// Odd items render nothing: 10 data items, 5 rendered children total.
const each = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];

const dispose = renderer.render(() => (
<view width={1920} height={1080}>
<LazyColumn ref={col} each={each} upCount={3} sync y={0}>
{(item) => (
<lng.Show when={item() % 2 === 0}>
<view width={300} height={80} />
</lng.Show>
)}
</LazyColumn>
</view>
));
await wait();

col.setFocus();
await wait();

// Walk to the end of the list. 10 data items and one mount per press —
// a dozen presses is more than enough when the guard terminates
// correctly, and few enough to finish fast if it doesn't.
for (let i = 0; i < 12; i++) {
pressDown(col);
await wait(1);
}

v.assert.equal(
col.children.length,
5,
'all non-empty items mounted despite empty siblings',
);
v.assert.equal(col.selected, 4, 'selection reached the last child');

dispose();
});

v.test('multi-node item templates do not stall mounting early', async () => {
let col!: lng.ElementNode;
// Each item renders two top-level nodes, so children.length runs at
// twice the item count and reaches each.length before all items mount.
const each = [0, 1, 2, 3, 4, 5];

const dispose = renderer.render(() => (
<view width={1920} height={1080}>
<LazyColumn ref={col} each={each} upCount={2} sync y={0}>
{() => (
<>
<view width={300} height={80} />
<view width={300} height={20} skipFocus />
</>
)}
</LazyColumn>
</view>
));
await wait();

col.setFocus();
await wait();

for (let i = 0; i < 16; i++) {
pressDown(col);
await wait(1);
}

v.assert.equal(
col.children.length,
12,
'every item mounted even though children outnumber data items',
);

dispose();
});
});
Loading