Skip to content

Commit 818c85a

Browse files
authored
fix: correct scroll position for zero cursor (#589)
* fix: correct scroll position for zero cursor * test: correct scroll position for zero cursor * chore: fix infinitegrid lint * chore: fix lint * test: fix InfiniteGrid error * fix: fix direction for corrected scroll position * test: test 환경 오차 범위 수정 및 centerStartIndex의 수정 * chore: fix lint * test: threshold를 0으로 수정하여 오차 변경 * test: fix correct pos for browser env
1 parent 6a604bc commit 818c85a

3 files changed

Lines changed: 162 additions & 26 deletions

File tree

packages/infinitegrid/src/Infinite.ts

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import Component from "@egjs/component";
22
import { diff } from "@egjs/list-differ";
3-
import { DIRECTION } from "./consts";
3+
import { DIRECTION, INVISIBLE_POS } from "./consts";
44
import { findIndex, findLastIndex, getNextCursors, isFlatOutline } from "./utils";
55

66

@@ -466,12 +466,18 @@ export class Infinite extends Component<InfiniteEvents> {
466466
return this.itemKeys[key];
467467
}
468468
public getItemPartByKey(partKey: string | number) {
469-
let itemPart!: InfiniteItemPart;
469+
let itemPart!: {
470+
itemIndex: number;
471+
part: InfiniteItemPart;
472+
};
470473

471-
this.items.forEach((item) => {
474+
this.items.forEach((item, itemIndex) => {
472475
item.parts?.forEach((part) => {
473476
if (part.key === partKey) {
474-
itemPart = part;
477+
itemPart = {
478+
itemIndex: itemIndex,
479+
part,
480+
};
475481
}
476482
});
477483
});
@@ -491,36 +497,55 @@ export class Infinite extends Component<InfiniteEvents> {
491497
* 보이는 영역의 가운데를 기준으로 스크롤을 한다.
492498
*/
493499
public getVisibleAreaByParts(parts: InfiniteItemPart[]) {
494-
const nextParts = parts.map((part) => this.getItemPartByKey(part.key)).filter(Boolean);
500+
const nextPartInfos = parts.map((part) => this.getItemPartByKey(part.key))
501+
.filter(Boolean);
495502

496-
if (!nextParts.length) {
503+
if (!nextPartInfos.length) {
497504
return null;
498505
}
506+
507+
let startCursor = Infinity;
508+
let endCursor = -1;
509+
510+
nextPartInfos.forEach((part) => {
511+
startCursor = Math.min(part.itemIndex, startCursor);
512+
endCursor = Math.min(part.itemIndex, endCursor);
513+
});
514+
const nextParts = nextPartInfos.map(({ part }) => part);
499515
const centerPos = getCenterPosByParts(nextParts);
500516

501517
return {
502518
parts: nextParts,
503519
centerPos,
520+
startCursor,
521+
endCursor,
504522
};
505523
}
506524
/**
507525
* 스크롤 가운데 위치에 가장 가까운 요소들
508526
*/
509527
public getVisibleArea(scrollPos: number) {
528+
const items = this.items;
510529
const centerScrollPos = scrollPos + this.size / 2;
511530
const visibleItems = this.getRenderedVisibleItems();
512531

513532
if (!visibleItems.length) {
514533
return null;
515534
}
516-
const minParts: Array<[number, InfiniteItemPart]> = [];
535+
const minParts: Array<[number, {
536+
itemIndex: number;
537+
part: InfiniteItemPart;
538+
}]> = [];
517539

518540
visibleItems.forEach((item) => {
519-
item.parts?.forEach((part) => {
541+
item.parts?.filter((p) => p.pos !== INVISIBLE_POS).forEach((part) => {
520542
const centerPos = part.pos + part.size / 2;
521543
const minDist = Math.abs(centerScrollPos - centerPos);
522544

523-
minParts.push([minDist, part]);
545+
minParts.push([minDist, {
546+
part,
547+
itemIndex: items.findIndex((allItem) => allItem.key === item.key),
548+
}]);
524549
});
525550
});
526551

@@ -535,16 +560,21 @@ export class Infinite extends Component<InfiniteEvents> {
535560
return null;
536561
}
537562

538-
const visibleParts = minParts.sort(([minPos1], [minPos2]) => {
563+
const visiblePartInfos = minParts.sort(([minPos1], [minPos2]) => {
539564
return minPos1 - minPos2;
540565
}).slice(0, maxOutlineLength).map(([, part]) => part);
541566

542-
if (!visibleParts.length) {
567+
if (!visiblePartInfos.length) {
543568
return null;
544569
}
570+
571+
const visibleParts = visiblePartInfos.map(({ part }) => part);
545572
const centerPos = getCenterPosByParts(visibleParts);
573+
const centerIndexes = visiblePartInfos.map((part) => part.itemIndex);
546574

547575
return {
576+
centerStartIndex: Math.min(...centerIndexes),
577+
centerEndIndex: Math.max(...centerIndexes),
548578
parts: visibleParts,
549579
centerPos,
550580
};

packages/infinitegrid/src/InfiniteGrid.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -923,10 +923,17 @@ class InfiniteGrid<Options extends InfiniteGridOptions = InfiniteGridOptions> ex
923923
this._syncInfinite();
924924

925925
if (prevVisibleArea) {
926-
const prevParts = prevVisibleArea.parts;
926+
// 화면의 가운데가 어디에 위치해있는지 확인
927+
const prevParts = prevVisibleArea.parts.filter((p) => p.pos !== INVISIBLE_POS);
927928
const nextVisibleArea = infinite.getVisibleAreaByParts(prevParts);
928929

929-
if (nextVisibleArea) {
930+
if (
931+
nextVisibleArea
932+
// 커서가 시작이 아니어야 그룹들의 위치 보정이 가능하다.
933+
// end direction만 해당
934+
// startCursor가 0이면 위의 아이템들의 위치가 심각하게 흔들릴 가능성이 매우 높다.
935+
&& (direction !== "end" || prevVisibleArea.centerStartIndex !== 0)
936+
) {
930937
let offset = nextVisibleArea.centerPos - prevVisibleArea.centerPos;
931938

932939
// If reversed, scroll size (case where container size is reduced)

packages/infinitegrid/test/unit/InfiniteGrid.spec.ts

Lines changed: 112 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -172,9 +172,6 @@ describe("test InfiniteGrid", () => {
172172

173173
const children = toArray(igContainer.children);
174174

175-
ig?.getItems().forEach(itm => {
176-
console.log(itm.cssRect, itm.mountState, itm.updateState);
177-
})
178175
// Then
179176
expect(e.startCursor).to.be.equals(0);
180177
expect(e.endCursor).to.be.equals(1);
@@ -422,19 +419,18 @@ describe("test InfiniteGrid", () => {
422419
ig!.append([0, 1, 2, 3, 4].map((child) => {
423420
return {
424421
groupKey: Math.floor(child / 5),
425-
key: child,
426-
html: `<div style="height: 200px;">${child}</div>`,
422+
key: `${child}`,
423+
html: `<div style="height: 200px;width: 2px;">${child}</div>`,
427424
};
428425
}));
429426

430427
await waitEvent(ig!, "renderComplete");
431428

432-
433429
ig!.prepend([5, 6, 7, 8, 9].map((child) => {
434430
return {
435431
groupKey: Math.floor(child / 5),
436432
key: child,
437-
html: `<div style="height: 200px;">${child}</div>`,
433+
html: `<div style="height: 200px;width: 2px;">${child}</div>`,
438434
};
439435
}));
440436

@@ -448,9 +444,8 @@ describe("test InfiniteGrid", () => {
448444
expect(ig!.getVisibleGroups().map((group) => group.groupKey)).to.be.deep.equals([1, 0]);
449445
expect(ig!.getScrollContainerElement().scrollTop).to.be.equals(1000);
450446
expect(children.length).to.be.equals(10);
451-
452447
children.forEach((child, i) => {
453-
expect(child.style.top).to.be.equals(`${i * 200}px`);
448+
expect(child.style.top).to.be.equals(`${i * 200}px`, `Index ${i} Error`);
454449
});
455450
});
456451
it("should check if item can be inserted in the center", async () => {
@@ -1280,7 +1275,7 @@ describe("test InfiniteGrid", () => {
12801275
await waitEvent(ig!, "renderComplete");
12811276
// 500 ~ 1000
12821277
// 300 / 100 100 [100 / 300 / 300 / 300] / 300 / 300
1283-
1278+
12841279

12851280
// Then
12861281
expect(ig!.getScrollContainerElement().scrollTop).to.be.equals(500);
@@ -1589,6 +1584,111 @@ describe("test InfiniteGrid", () => {
15891584
});
15901585
});
15911586
});
1587+
describe("test scroll offset", () => {
1588+
beforeEach(() => {
1589+
container!.innerHTML = `
1590+
<div class="wrapper" style="width: 100%; height: 500px;">
1591+
</div>
1592+
`;
1593+
const wrapper = container!.querySelector<HTMLElement>(".wrapper")!;
1594+
ig = new InfiniteGrid<InfiniteGridOptions>(wrapper, {
1595+
gridConstructor: SampleGrid,
1596+
container: true,
1597+
threshold: 0,
1598+
});
1599+
});
1600+
it(`should check if scroll position is corrected when size, pos, window size is changed (startCursor = 0)`, async () => {
1601+
// Given
1602+
ig!.syncItems([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17].map((child) => {
1603+
return {
1604+
groupKey: Math.floor(child / 3),
1605+
key: `key${child}`,
1606+
html: `<div style="height: 100px;width: 100%; ">${child}</div>`,
1607+
};
1608+
}));
1609+
1610+
// 커서를 전체로 지정하여 모든 아이템의 사이즈를 계산
1611+
ig!.setCursors(0, 5);
1612+
await waitEvent(ig!, "renderComplete");
1613+
1614+
// 현재 스크롤 위치에 따른 보이는 아이템 자동 변경: change cursor (0, 2)
1615+
await waitEvent(ig!, "renderComplete");
1616+
1617+
1618+
// top 100, center 350
1619+
// 브라우저간의 오차로 인한 테스트가 필요
1620+
ig!.getScrollContainerElement().scrollTop = 100;
1621+
1622+
// 스크롤 이동에 따른 보이는 아이템 자동 변경: change cursor (0, 3)
1623+
await waitEvent(ig!, "renderComplete");
1624+
// 300 + 50 => 700 (offset: 350)
1625+
1626+
1627+
// When
1628+
ig!.getItems().forEach((item) => {
1629+
item.element!.style.height = "200px";
1630+
});
1631+
1632+
// 스크롤 위치는 변경 되지 않는다.
1633+
ig!.renderItems({ useResize: true });
1634+
await waitEvent(ig!, "renderComplete");
1635+
1636+
1637+
// Then
1638+
const correctedPos = ig!.getScrollContainerElement().scrollTop;
1639+
expect(correctedPos).to.be.equals(450);
1640+
expect(ig!.getStartCursor()).to.be.equals(0);
1641+
expect(ig!.getEndCursor()).to.be.equals(1);
1642+
});
1643+
it(`should check if scroll position is corrected when size, pos, window size is changed (startCursor > 0)`, async () => {
1644+
// Given
1645+
ig!.syncItems([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17].map((child) => {
1646+
return {
1647+
groupKey: Math.floor(child / 3),
1648+
key: `key${child}`,
1649+
html: `<div style="height: 100px;width: 100%;">${child}</div>`,
1650+
};
1651+
}));
1652+
1653+
// 커서를 전체로 지정하여 모든 아이템의 사이즈를 계산
1654+
ig!.setCursors(0, 5);
1655+
await waitEvent(ig!, "renderComplete");
1656+
1657+
// 현재 스크롤 위치에 따른 보이는 아이템 자동 변경: change cursor (0, 2)
1658+
await waitEvent(ig!, "renderComplete");
1659+
1660+
1661+
// top: 750, center 1000
1662+
ig!.getScrollContainerElement().scrollTop = 750;
1663+
1664+
// 스크롤 이동에 따른 보이는 아이템 자동 변경: change cursor (2, 4)
1665+
await waitEvent(ig!, "renderComplete");
1666+
const prevStartCursor = ig!.getStartCursor();
1667+
const prevEndCursor = ig!.getEndCursor();
1668+
1669+
1670+
1671+
// When
1672+
ig!.getItems().forEach((item) => {
1673+
item.element!.style.height = "200px";
1674+
});
1675+
// 600 + 300 + 50 => 600 + 700 (offset: 350)
1676+
1677+
1678+
// 스크롤 위치는 변경 되지 않는다.
1679+
// 450만큼 스크롤 차이가 발생
1680+
ig!.renderItems({ useResize: true });
1681+
await waitEvent(ig!, "renderComplete");
1682+
1683+
// Then
1684+
expect(prevStartCursor).to.be.equals(2);
1685+
expect(prevEndCursor).to.be.equals(4);
1686+
const correctedPos = ig!.getScrollContainerElement().scrollTop;
1687+
expect(correctedPos).to.be.equals(1100);
1688+
expect(ig!.getStartCursor()).to.be.equals(2);
1689+
expect(ig!.getEndCursor()).to.be.equals(3);
1690+
});
1691+
});
15921692
describe("test ResizeObserver", () => {
15931693
it(`should check if renderComplete does trigger when useResizeObserver is enabled and container's size is changed`, async () => {
15941694
// Given
@@ -1770,9 +1870,8 @@ describe("test InfiniteGrid", () => {
17701870
return {
17711871
groupKey: Math.floor(child / 3),
17721872
key: `key${child}`,
1773-
html: `<div style="height: ${100 + child * 10}px;width: 100%;" ${
1774-
child === 7 ? `data-grid-not-equal-size="true"` : ""
1775-
}>${child}</div>`,
1873+
html: `<div style="height: ${100 + child * 10}px;width: 100%;" ${child === 7 ? `data-grid-not-equal-size="true"` : ""
1874+
}>${child}</div>`,
17761875
};
17771876
}));
17781877

0 commit comments

Comments
 (0)