Skip to content

Commit 9aa8bcf

Browse files
authored
expose firstIndex in virtual scroller (#823)
2 parents b77acfa + df1312f commit 9aa8bcf

3 files changed

Lines changed: 102 additions & 13 deletions

File tree

.changeset/shaky-chicken-marry.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@solid-primitives/virtual": patch
3+
---
4+
5+
expose `firstIndex` and `lastIndex` in virtual lists, to support counting

packages/virtual/src/index.tsx

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ type VirtualListReturn<T extends readonly any[]> = [
1515
containerHeight: number;
1616
viewerTop: number;
1717
visibleItems: T;
18+
firstIndex: number;
19+
lastIndex: number | undefined;
1820
}>,
1921
onScroll: (e: Event) => void,
2022
];
@@ -41,20 +43,23 @@ export function createVirtualList<T extends readonly any[]>({
4143

4244
const [offset, setOffset] = createSignal(0);
4345

44-
const getFirstIdx = () => Math.max(0, Math.floor(offset() / rowHeight) - overscanCount);
45-
46-
const getLastIdx = () =>
47-
Math.min(
48-
items.length,
49-
Math.floor(offset() / rowHeight) + Math.ceil(rootHeight / rowHeight) + overscanCount,
50-
);
51-
5246
return [
53-
() => ({
54-
containerHeight: items.length * rowHeight,
55-
viewerTop: getFirstIdx() * rowHeight,
56-
visibleItems: items.slice(getFirstIdx(), getLastIdx()) as unknown as T,
57-
}),
47+
() => {
48+
const firstIndex = Math.max(0, Math.floor(offset() / rowHeight) - overscanCount);
49+
const lastIndex = Math.min(
50+
items.length,
51+
Math.floor(offset() / rowHeight) + Math.ceil(rootHeight / rowHeight) + overscanCount,
52+
);
53+
54+
return {
55+
containerHeight: items.length * rowHeight,
56+
viewerTop: firstIndex * rowHeight,
57+
visibleItems: items.slice(firstIndex, lastIndex) as unknown as T,
58+
firstIndex,
59+
lastIndex: lastIndex > 0 ? lastIndex - 1 : undefined,
60+
// -1 because slice is an exclusive range
61+
};
62+
},
5863
e => {
5964
// @ts-expect-error
6065
if (e.target?.scrollTop !== undefined) setOffset(e.target.scrollTop);

packages/virtual/test/index.test.tsx

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,26 @@ describe("createVirtualList", () => {
5151
expect(virtual().visibleItems).toEqual([0, 1, 2]);
5252
});
5353

54+
test("returns firstIndex representing the first index of the visibleList", () => {
55+
const [virtual] = createVirtualList({
56+
items: TEST_LIST,
57+
rootHeight: 20,
58+
rowHeight: 10,
59+
});
60+
61+
expect(virtual().firstIndex).toEqual(0);
62+
});
63+
64+
test("returns lastIndex representing the last item in the visibleList's index", () => {
65+
const [virtual] = createVirtualList({
66+
items: TEST_LIST,
67+
rootHeight: 20,
68+
rowHeight: 10,
69+
});
70+
71+
expect(virtual().lastIndex).toEqual(2);
72+
});
73+
5474
test("returns onScroll which sets viewerTop and visibleItems based on rootElement's scrolltop", () => {
5575
const el = document.createElement("div");
5676

@@ -62,35 +82,55 @@ describe("createVirtualList", () => {
6282

6383
expect(virtual().visibleItems).toEqual([0, 1, 2]);
6484
expect(virtual().viewerTop).toEqual(0);
85+
expect(virtual().firstIndex).toEqual(0);
86+
expect(virtual().lastIndex).toEqual(2);
6587

6688
el.scrollTop += 10;
6789

6890
// no change until onScroll is called
6991
expect(virtual().visibleItems).toEqual([0, 1, 2]);
7092
expect(virtual().viewerTop).toEqual(0);
93+
expect(virtual().firstIndex).toEqual(0);
94+
expect(virtual().lastIndex).toEqual(2);
7195

7296
onScroll(TARGETED_SCROLL_EVENT(el));
7397

7498
expect(virtual().visibleItems).toEqual([0, 1, 2, 3]);
7599
expect(virtual().viewerTop).toEqual(0);
100+
expect(virtual().firstIndex).toEqual(0);
101+
expect(virtual().lastIndex).toEqual(3);
76102

77103
el.scrollTop += 10;
78104
onScroll(TARGETED_SCROLL_EVENT(el));
79105

80106
expect(virtual().visibleItems).toEqual([1, 2, 3, 4]);
81107
expect(virtual().viewerTop).toEqual(10);
108+
expect(virtual().firstIndex).toEqual(1);
109+
expect(virtual().lastIndex).toEqual(4);
82110

83111
el.scrollTop -= 10;
84112
onScroll(TARGETED_SCROLL_EVENT(el));
85113

86114
expect(virtual().visibleItems).toEqual([0, 1, 2, 3]);
87115
expect(virtual().viewerTop).toEqual(0);
116+
expect(virtual().firstIndex).toEqual(0);
117+
expect(virtual().lastIndex).toEqual(3);
88118

89119
el.scrollTop -= 10;
90120
onScroll(TARGETED_SCROLL_EVENT(el));
91121

92122
expect(virtual().visibleItems).toEqual([0, 1, 2]);
93123
expect(virtual().viewerTop).toEqual(0);
124+
expect(virtual().firstIndex).toEqual(0);
125+
expect(virtual().lastIndex).toEqual(2);
126+
127+
el.scrollTop += 7_000;
128+
onScroll(TARGETED_SCROLL_EVENT(el));
129+
130+
expect(virtual().visibleItems).toEqual([699, 700, 701, 702]);
131+
expect(virtual().viewerTop).toEqual(6990);
132+
expect(virtual().firstIndex).toEqual(699);
133+
expect(virtual().lastIndex).toEqual(702);
94134
});
95135

96136
test("onScroll handles reaching the bottom of the list", () => {
@@ -126,6 +166,8 @@ describe("createVirtualList", () => {
126166
onScroll(TARGETED_SCROLL_EVENT(el));
127167

128168
expect(virtual().visibleItems).toEqual([8, 9, 10, 11, 12, 13]);
169+
expect(virtual().firstIndex).toEqual(8);
170+
expect(virtual().lastIndex).toEqual(13);
129171
});
130172

131173
test("overscanCount defaults to 1 if undefined or zero", () => {
@@ -147,6 +189,41 @@ describe("createVirtualList", () => {
147189
expect(virtualZero().visibleItems).toEqual([0, 1, 2]);
148190
});
149191

192+
test("lastIndex is undefined in an empty list", () => {
193+
const [virtual] = createVirtualList({
194+
items: [],
195+
rootHeight: 20,
196+
rowHeight: 10,
197+
});
198+
199+
expect(virtual().lastIndex).toEqual(undefined);
200+
});
201+
202+
test("lastIndex is 0 in a singleton list", () => {
203+
const [virtual] = createVirtualList({
204+
items: [10],
205+
rootHeight: 20,
206+
rowHeight: 10,
207+
});
208+
209+
expect(virtual().lastIndex).toEqual(0);
210+
});
211+
212+
test("handles singleton list", () => {
213+
const [virtual] = createVirtualList({
214+
items: [10],
215+
rootHeight: 20,
216+
rowHeight: 10,
217+
overscanCount: 0,
218+
});
219+
220+
expect(virtual().containerHeight).toEqual(10);
221+
expect(virtual().viewerTop).toEqual(0);
222+
expect(virtual().visibleItems).toEqual([10]);
223+
expect(virtual().firstIndex).toEqual(0);
224+
expect(virtual().lastIndex).toEqual(0);
225+
});
226+
150227
test("handles empty list", () => {
151228
const [virtual] = createVirtualList({
152229
items: [],
@@ -158,6 +235,8 @@ describe("createVirtualList", () => {
158235
expect(virtual().containerHeight).toEqual(0);
159236
expect(virtual().viewerTop).toEqual(0);
160237
expect(virtual().visibleItems).toEqual([]);
238+
expect(virtual().firstIndex).toEqual(0);
239+
expect(virtual().lastIndex).toEqual(undefined);
161240
});
162241
});
163242

0 commit comments

Comments
 (0)