Skip to content

Commit

Permalink
Add align option to scrollToIndex method
Browse files Browse the repository at this point in the history
  • Loading branch information
inokawa committed Aug 13, 2023
1 parent 9992367 commit ea2c768
Show file tree
Hide file tree
Showing 8 changed files with 238 additions and 92 deletions.
240 changes: 171 additions & 69 deletions e2e/VList.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,98 +295,200 @@ test.describe("check if scrollToIndex works", () => {
await page.goto(storyUrl("basics-vlist--scroll-to"));
});

test("mid", async ({ page }) => {
const scrollable = await page.waitForSelector(scrollableSelector);
test.describe("align start", () => {
test("mid", async ({ page }) => {
const scrollable = await page.waitForSelector(scrollableSelector);

// check if start is displayed
await expect((await getFirstItem(scrollable)).text).toEqual("0");

const button = (await page
.getByRole("button", { name: "scroll to index" })
.elementHandle())!;
const input = await page.evaluateHandle(
(el) => el!.previousSibling as HTMLInputElement,
button
);

// check if start is displayed
await expect((await getFirstItem(scrollable)).text).toEqual("0");
await clearInput(input);
await input.type("700");
await button.click();

const button = (await page
.getByRole("button", { name: "scroll to index" })
.elementHandle())!;
const input = await page.evaluateHandle(
(el) => el!.previousSibling as HTMLInputElement,
button
);
await scrollable.waitForElementState("stable");

await clearInput(input);
await input.type("700");
await button.click();
// Check if scrolled precisely
const firstItem = await getFirstItem(scrollable);
await expect(firstItem.text).toEqual("700");
await expect(firstItem.top).toEqual(0);

await scrollable.waitForElementState("stable");
// Check if unnecessary items are not rendered
await expect(await scrollable.innerText()).not.toContain("650");
await expect(await scrollable.innerText()).not.toContain("750");
});

// Check if scrolled precisely
const firstItem = await getFirstItem(scrollable);
await expect(firstItem.text).toEqual("700");
await expect(firstItem.top).toEqual(0);
test("start", async ({ page }) => {
const scrollable = await page.waitForSelector(scrollableSelector);

// Check if unnecessary items are not rendered
await expect(await scrollable.innerText()).not.toContain("650");
await expect(await scrollable.innerText()).not.toContain("750");
});
// check if start is displayed
await expect((await getFirstItem(scrollable)).text).toEqual("0");

test("start", async ({ page }) => {
const scrollable = await page.waitForSelector(scrollableSelector);
const button = (await page
.getByRole("button", { name: "scroll to index" })
.elementHandle())!;
const input = await page.evaluateHandle(
(el) => el!.previousSibling as HTMLInputElement,
button
);

// check if start is displayed
await expect((await getFirstItem(scrollable)).text).toEqual("0");
await clearInput(input);
await input.type("500");
await button.click();

const button = (await page
.getByRole("button", { name: "scroll to index" })
.elementHandle())!;
const input = await page.evaluateHandle(
(el) => el!.previousSibling as HTMLInputElement,
button
);
await scrollable.waitForElementState("stable");

await clearInput(input);
await input.type("500");
await button.click();
await expect(await scrollable.innerText()).toContain("500");

await scrollable.waitForElementState("stable");
await clearInput(input);
await input.type("0");
await button.click();

await expect(await scrollable.innerText()).toContain("500");
// Check if scrolled precisely
const firstItem = await getFirstItem(scrollable);
await expect(firstItem.text).toEqual("0");
await expect(firstItem.top).toEqual(0);

await clearInput(input);
await input.type("0");
await button.click();
// Check if unnecessary items are not rendered
await expect(await scrollable.innerText()).not.toContain("50\n");
});

test("end", async ({ page }) => {
const scrollable = await page.waitForSelector(scrollableSelector);

// check if start is displayed
await expect((await getFirstItem(scrollable)).text).toEqual("0");

const button = (await page
.getByRole("button", { name: "scroll to index" })
.elementHandle())!;
const input = await page.evaluateHandle(
(el) => el!.previousSibling as HTMLInputElement,
button
);

await clearInput(input);
await input.type("999");
await button.click();

await scrollable.waitForElementState("stable");

// Check if scrolled precisely
const firstItem = await getFirstItem(scrollable);
await expect(firstItem.text).toEqual("0");
await expect(firstItem.top).toEqual(0);
// Check if scrolled precisely
const lastItem = await getLastItem(scrollable);
await expect(lastItem.text).toEqual("999");
await expect(lastItem.bottom).toEqual(0);

// Check if unnecessary items are not rendered
await expect(await scrollable.innerText()).not.toContain("50\n");
// Check if unnecessary items are not rendered
await expect(await scrollable.innerText()).not.toContain("949");
});
});

test("end", async ({ page }) => {
const scrollable = await page.waitForSelector(scrollableSelector);
test.describe("align end", () => {
test.beforeEach(async ({ page }) => {
await page.getByRole("radio", { name: "end" }).click();
});

// check if start is displayed
await expect((await getFirstItem(scrollable)).text).toEqual("0");
test("mid", async ({ page }) => {
const scrollable = await page.waitForSelector(scrollableSelector);

const button = (await page
.getByRole("button", { name: "scroll to index" })
.elementHandle())!;
const input = await page.evaluateHandle(
(el) => el!.previousSibling as HTMLInputElement,
button
);
// check if start is displayed
await expect((await getFirstItem(scrollable)).text).toEqual("0");

await clearInput(input);
await input.type("999");
await button.click();
const button = (await page
.getByRole("button", { name: "scroll to index" })
.elementHandle())!;
const input = await page.evaluateHandle(
(el) => el!.previousSibling as HTMLInputElement,
button
);

await scrollable.waitForElementState("stable");
await clearInput(input);
await input.type("700");
await button.click();

await scrollable.waitForElementState("stable");

// Check if scrolled precisely
const lastItem = await getLastItem(scrollable);
await expect(lastItem.text).toEqual("700");
await expect(lastItem.bottom).toBeLessThanOrEqual(1); // FIXME: may not be 0 in Safari

// Check if unnecessary items are not rendered
await expect(await scrollable.innerText()).not.toContain("650");
await expect(await scrollable.innerText()).not.toContain("750");
});

// Check if scrolled precisely
const lastItem = await getLastItem(scrollable);
await expect(lastItem.text).toEqual("999");
await expect(lastItem.bottom).toEqual(0);
test("start", async ({ page }) => {
const scrollable = await page.waitForSelector(scrollableSelector);

// Check if unnecessary items are not rendered
await expect(await scrollable.innerText()).not.toContain("949");
// check if start is displayed
await expect((await getFirstItem(scrollable)).text).toEqual("0");

const button = (await page
.getByRole("button", { name: "scroll to index" })
.elementHandle())!;
const input = await page.evaluateHandle(
(el) => el!.previousSibling as HTMLInputElement,
button
);

await clearInput(input);
await input.type("500");
await button.click();

await scrollable.waitForElementState("stable");

await expect(await scrollable.innerText()).toContain("500");

await clearInput(input);
await input.type("0");
await button.click();

// Check if scrolled precisely
const firstItem = await getFirstItem(scrollable);
await expect(firstItem.text).toEqual("0");
await expect(firstItem.top).toEqual(0);

// Check if unnecessary items are not rendered
await expect(await scrollable.innerText()).not.toContain("50\n");
});

test("end", async ({ page }) => {
const scrollable = await page.waitForSelector(scrollableSelector);

// check if start is displayed
await expect((await getFirstItem(scrollable)).text).toEqual("0");

const button = (await page
.getByRole("button", { name: "scroll to index" })
.elementHandle())!;
const input = await page.evaluateHandle(
(el) => el!.previousSibling as HTMLInputElement,
button
);

await clearInput(input);
await input.type("999");
await button.click();

await scrollable.waitForElementState("stable");

// Check if scrolled precisely
const lastItem = await getLastItem(scrollable);
await expect(lastItem.text).toEqual("999");
await expect(lastItem.bottom).toBeLessThanOrEqual(1); // FIXME: may not be 0 in Safari

// Check if unnecessary items are not rendered
await expect(await scrollable.innerText()).not.toContain("949");
});
});
});

Expand Down
26 changes: 16 additions & 10 deletions src/core/scroller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
UPDATE_SIZE,
ACTION_MANUAL_SCROLL,
} from "./store";
import { ScrollToIndexAlign } from "./types";
import { debounce, throttle, max, min, timeout } from "./utils";

// Infer scroll state also from wheel events
Expand Down Expand Up @@ -43,7 +44,7 @@ export type Scroller = {
_getActualScrollSize: () => number;
_scrollTo: (offset: number) => void;
_scrollBy: (offset: number) => void;
_scrollToIndex: (index: number) => void;
_scrollToIndex: (index: number, align?: ScrollToIndexAlign) => void;
_fixScrollJump: (jump: ScrollJump) => void;
};

Expand Down Expand Up @@ -73,10 +74,7 @@ export const createScroller = (
return offset;
};

const scrollManually = async (
index: number,
getCurrentOffset: () => number
) => {
const scrollManually = async (getCurrentOffset: () => number) => {
if (!rootElement) return;

const getOffset = (): number => {
Expand All @@ -90,9 +88,10 @@ export const createScroller = (
while (true) {
// Sync viewport to scroll destination
// In order to scroll to the correct position, mount the items and measure their sizes before scrolling.
store._update(ACTION_BEFORE_MANUAL_SCROLL, getOffset());
const targetOffset = getOffset();
store._update(ACTION_BEFORE_MANUAL_SCROLL, targetOffset);

if (!store._hasUnmeasuredItemsInRange(index)) {
if (!store._hasUnmeasuredItemsInTargetViewport(targetOffset)) {
break;
}

Expand Down Expand Up @@ -138,7 +137,7 @@ export const createScroller = (
const scrollTo = (offset: number) => {
offset = max(offset, 0);

scrollManually(store._getItemIndexForScrollTo(offset), () => offset);
scrollManually(() => offset);
};

return {
Expand Down Expand Up @@ -176,10 +175,17 @@ export const createScroller = (
_scrollBy(offset) {
scrollTo(store._getScrollOffset() + offset);
},
_scrollToIndex(index) {
_scrollToIndex(index, align) {
index = min(store._getItemLength() - 1, max(0, index));

scrollManually(index, () => store._getItemOffset(index));
scrollManually(
align === "end"
? () =>
store._getItemOffset(index) +
store._getItemSize(index) -
store._getViewportSize()
: () => store._getItemOffset(index)
);
},
_fixScrollJump: (jump) => {
if (!rootElement) return;
Expand Down
20 changes: 12 additions & 8 deletions src/core/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export type VirtualStore = {
_getCache(): CacheSnapshot;
_getRange(): ItemsRange;
_isUnmeasuredItem(index: number): boolean;
_hasUnmeasuredItemsInRange(startIndex: number): boolean;
_hasUnmeasuredItemsInTargetViewport(offset: number): boolean;
_getItemOffset(index: number): number;
_getItemSize(index: number): number;
_getItemLength(): number;
Expand All @@ -74,7 +74,6 @@ export type VirtualStore = {
_getCorrectedScrollSize(): number;
_getJumpCount(): number;
_flushJump(): ScrollJump;
_getItemIndexForScrollTo(offset: number): number;
_subscribe(target: number, cb: Subscriber): () => void;
_update(...action: Actions): void;
_updateCacheLength(length: number): void;
Expand Down Expand Up @@ -134,11 +133,19 @@ export const createVirtualStore = (
_isUnmeasuredItem(index) {
return cache._sizes[index] === UNCACHED;
},
_hasUnmeasuredItemsInRange(startIndex) {
_hasUnmeasuredItemsInTargetViewport(offset) {
const startIndex = findStartIndexWithOffset(
cache as Writeable<Cache>,
offset,
_prevRange[0] // TODO binary search may be better here
);
return hasUnmeasuredItemsInRange(
cache,
max(0, startIndex - 2),
min(cache._length - 1, startIndex + 2)
max(0, startIndex - 1),
min(
cache._length - 1,
findEndIndex(cache, startIndex, viewportSize) + 1
)
);
},
_getItemOffset(index) {
Expand Down Expand Up @@ -175,9 +182,6 @@ export const createVirtualStore = (
jump = 0;
return prevJump;
},
_getItemIndexForScrollTo(offset) {
return findStartIndexWithOffset(cache as Writeable<Cache>, offset, 0);
},
_subscribe(target, cb) {
const sub: [number, Subscriber] = [target, cb];
subscribers.add(sub);
Expand Down
Loading

0 comments on commit ea2c768

Please sign in to comment.