Skip to content

Commit 7ca6b50

Browse files
committed
work
1 parent fc17ec7 commit 7ca6b50

1 file changed

Lines changed: 86 additions & 43 deletions

File tree

  • packages/frontend/navi/src/control/list

packages/frontend/navi/src/control/list/list.jsx

Lines changed: 86 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -724,6 +724,7 @@ const ListUI = (props) => {
724724
renderWindow,
725725
scrollToItem,
726726
pendingScrollRef,
727+
captureAnchor,
727728
} = useListScrollSync({
728729
ref,
729730
tracker,
@@ -738,6 +739,7 @@ const ListUI = (props) => {
738739
horizontal,
739740
});
740741

742+
virtual.captureAnchor = captureAnchor;
741743
virtual.virtualItemSizeSignal = virtualItemSizeSignal;
742744
virtual.horizontal = Boolean(horizontal);
743745
virtual.renderSkeleton = renderSkeleton;
@@ -1103,6 +1105,34 @@ const useListScrollSync = ({
11031105
const getScroller = () => getScrollerEl(ref.current, scroller, horizontal);
11041106
const getListEl = () => ref.current.querySelector(".navi_list");
11051107

1108+
// The row the scroll holds onto across a change of geometry, and where it
1109+
// sat when that change was decided. Captured at the two moments the list
1110+
// knows its own geometry is about to change — the window moving, a page
1111+
// arriving — because at those moments the DOM still shows the state to
1112+
// preserve. Capturing on every render instead would mean capturing at
1113+
// moments where there is nothing to preserve, and missing the ones where
1114+
// there is.
1115+
const anchorRef = useRef(null);
1116+
const captureAnchor = () => {
1117+
if (anchorRef.current || !ref.current || pendingScrollRef.current) {
1118+
return;
1119+
}
1120+
if (
1121+
!startPlaceRef.current.userTookOver &&
1122+
scrolledWanted !== "start" &&
1123+
scrolledWanted !== undefined
1124+
) {
1125+
// The list is holding itself somewhere; that is what owns the scroll.
1126+
return;
1127+
}
1128+
anchorRef.current = captureScrollAnchor({
1129+
scrollerEl: getScroller(),
1130+
listEl: getListEl(),
1131+
items: tracker.visibleItemsSignal.peek(),
1132+
horizontal,
1133+
});
1134+
};
1135+
11061136
const [renderWindow, setRenderWindow] = useState(() => {
11071137
// Opening somewhere else than the beginning starts by framing there: the
11081138
// rows the list will draw are the rows it will ask for.
@@ -1119,6 +1149,7 @@ const useListScrollSync = ({
11191149
if (newStart === start && newEnd === end) {
11201150
return;
11211151
}
1152+
captureAnchor();
11221153
debugScroll(`updateRenderWindow(${newStart}, ${newEnd}, "${reason}")`);
11231154
const renderWindow = { start: newStart, end: newEnd };
11241155
renderWindowRef.current = renderWindow;
@@ -1275,11 +1306,6 @@ const useListScrollSync = ({
12751306
// eslint-disable-next-line no-unused-expressions
12761307
virtualItemSizeSignal.value;
12771308
}
1278-
// The row the scroll is held onto across a commit (see the anchoring below).
1279-
// A deliberate move — opening the list somewhere, coming back to a row — is
1280-
// the list saying where it wants to be: whatever it was holding onto before
1281-
// no longer applies, or the correction would undo the move.
1282-
const anchorRef = useRef(null);
12831309
// Set around a scroll the list performs itself. What it protects against is
12841310
// not the scroll event as such, but what the listener would conclude from it:
12851311
// the position it is about to read was chosen to keep the rows where they
@@ -1537,17 +1563,11 @@ const useListScrollSync = ({
15371563
const scrollerEl = getScroller();
15381564
if (openAt === "end") {
15391565
anchorRef.current = null;
1540-
console.info(
1541-
`[place] before write: scrollHeight=${Math.round(scrollerEl.scrollHeight)} client=${Math.round(scrollerEl.clientHeight)} top=${Math.round(scrollerEl.scrollTop)}`,
1542-
);
15431566
if (horizontal) {
15441567
scrollerEl.scrollLeft = scrollerEl.scrollWidth;
15451568
} else {
15461569
scrollerEl.scrollTop = scrollerEl.scrollHeight;
15471570
}
1548-
console.info(
1549-
`[place] after write: top=${Math.round(scrollerEl.scrollTop)} max=${Math.round(scrollerEl.scrollHeight - scrollerEl.clientHeight)}`,
1550-
);
15511571
return;
15521572
}
15531573
if (typeof openAt === "object" && openAt.id !== undefined) {
@@ -1658,12 +1678,20 @@ const useListScrollSync = ({
16581678
return undefined;
16591679
}
16601680
const scrollerEl = getScroller();
1661-
const observer = new ResizeObserver(() => {
1662-
// Held somewhere: its own height changing is the end of the list moving,
1663-
// so it aims at it again. Nothing else can tell it — the size of a row is
1664-
// measured here but read by the runs, so a size that settles re-renders
1665-
// THEM, not this.
1666-
if (onGeometryChangeRef.current()) {
1681+
const listEl = getListEl();
1682+
const observer = new ResizeObserver((entries) => {
1683+
// Two things resize here, and they call for opposite answers. The LIST
1684+
// growing is its own content settling: only a list holding itself
1685+
// somewhere cares (the end it aims at has moved), and a list the user is
1686+
// reading must not be touched — its rows are held still by the anchoring,
1687+
// which this would undo. The SCROLLER resizing is the window around it
1688+
// changing shape, and then the row that was at the top goes back where it
1689+
// was.
1690+
const listResized = entries.some((entry) => entry.target === listEl);
1691+
if (listResized && onGeometryChangeRef.current()) {
1692+
return;
1693+
}
1694+
if (!entries.some((entry) => entry.target === scrollerEl)) {
16671695
return;
16681696
}
16691697
const position = positionRef.current;
@@ -1697,7 +1725,7 @@ const useListScrollSync = ({
16971725
}
16981726
});
16991727
observer.observe(scrollerEl);
1700-
observer.observe(getListEl());
1728+
observer.observe(listEl);
17011729
return () => {
17021730
observer.disconnect();
17031731
};
@@ -1708,20 +1736,6 @@ const useListScrollSync = ({
17081736
// on changes it attributes to a scroll, and the fillers resize in the very
17091737
// same commit — so the row at the top of the viewport is measured before the
17101738
// commit and put back at the same offset after it.
1711-
if (
1712-
!heldSomewhere &&
1713-
!searchText &&
1714-
!anchorRef.current &&
1715-
!pendingScrollRef.current &&
1716-
ref.current
1717-
) {
1718-
anchorRef.current = captureScrollAnchor({
1719-
scrollerEl: getScroller(),
1720-
listEl: getListEl(),
1721-
items: tracker.visibleItemsSignal.peek(),
1722-
horizontal,
1723-
});
1724-
}
17251739
useLayoutEffect(() => {
17261740
const anchor = anchorRef.current;
17271741
if (!anchor || !ref.current || heldSomewhere) {
@@ -1767,9 +1781,9 @@ const useListScrollSync = ({
17671781
return;
17681782
}
17691783
}
1770-
anchorRef.current = null;
17711784
const anchorEl = findRowElement(getListEl(), anchor.id);
17721785
if (!anchorEl) {
1786+
anchorRef.current = null;
17731787
return;
17741788
}
17751789
const scrollerEl = getScroller();
@@ -1780,11 +1794,14 @@ const useListScrollSync = ({
17801794
: anchorRect.top - viewportRect.top;
17811795
const drift = offsetNow - anchor.offset;
17821796
if (drift === 0) {
1797+
// Nothing moved in this commit — which does not mean nothing will: what
1798+
// was captured is a change that has not landed yet (a page merged into a
1799+
// run re-renders the run, and this list a commit later). The anchor is
1800+
// kept until it has something to correct, and dropped the moment the
1801+
// user scrolls, which is the one thing that makes it stale.
17831802
return;
17841803
}
1785-
debugScroll(
1786-
`anchored row ${anchor.id} drifted by ${Math.round(drift)}px, compensating scroll`,
1787-
);
1804+
anchorRef.current = null;
17881805
scrolledByListRef.current = true;
17891806
if (horizontal) {
17901807
scrollerEl.scrollLeft += drift;
@@ -1803,6 +1820,8 @@ const useListScrollSync = ({
18031820
const listEl = getListEl();
18041821
const onScroll = () => {
18051822
updateCurrentScroll();
1823+
// Where the user is now is where things must be held from now on.
1824+
anchorRef.current = null;
18061825
if (scrolledByListRef.current) {
18071826
// The window stays where it is — the position it would be re-derived
18081827
// from was chosen to keep the rows still — but where the list is has
@@ -1872,6 +1891,7 @@ const useListScrollSync = ({
18721891
renderWindow: renderWindowRef.current,
18731892
pendingScrollRef,
18741893
scrollToItem,
1894+
captureAnchor,
18751895
};
18761896
};
18771897
// The band of the scroller the user actually sees. A page-level scroller is
@@ -1979,12 +1999,26 @@ const captureScrollAnchor = ({ scrollerEl, listEl, items, horizontal }) => {
19791999
// matter: the scroller may be larger than the list (scroller="parent") as well
19802000
// as smaller (the list scrolls inside its own box).
19812001
const getListVisibleScanRange = (viewportRect, listRect, horizontal) => {
2002+
// The screen has a say too: what is asked here is answered by
2003+
// elementFromPoint, which only knows about points that are actually on it. A
2004+
// list whose scroll box hangs below the fold of the page would otherwise be
2005+
// probed where nothing can be hit — and would silently stop keeping its rows
2006+
// still, which is exactly when it matters.
2007+
const screenTo = horizontal
2008+
? document.documentElement.clientWidth
2009+
: document.documentElement.clientHeight;
19822010
const viewportFrom = horizontal ? viewportRect.left : viewportRect.top;
19832011
const viewportTo = horizontal ? viewportRect.right : viewportRect.bottom;
19842012
const listFrom = horizontal ? listRect.left : listRect.top;
19852013
const listTo = horizontal ? listRect.right : listRect.bottom;
1986-
const from = listFrom > viewportFrom ? listFrom : viewportFrom;
1987-
const to = listTo < viewportTo ? listTo : viewportTo;
2014+
let from = listFrom > viewportFrom ? listFrom : viewportFrom;
2015+
let to = listTo < viewportTo ? listTo : viewportTo;
2016+
if (from < 0) {
2017+
from = 0;
2018+
}
2019+
if (to > screenTo) {
2020+
to = screenTo;
2021+
}
19882022
if (to - from < 2) {
19892023
return null;
19902024
}
@@ -2167,8 +2201,14 @@ const useVirtualItemSizeSignal = (ref, virtualItemSizeProp = 0, horizontal) => {
21672201
samples.sum = 0;
21682202
samples.count = 0;
21692203
samples.fromSkeletons = false;
2170-
} else if (!samples.fromSkeletons && measure.fromSkeletons) {
2171-
return;
2204+
} else if (measure.fromSkeletons) {
2205+
if (!samples.fromSkeletons || samples.count > 0) {
2206+
// Rows on their way are given the size this very estimate holds, so
2207+
// measuring them again says nothing — and would say it in a loop: the
2208+
// size sets their height, their height sets the size. They seed it
2209+
// once, when there is nothing else to go on, and never again.
2210+
return;
2211+
}
21722212
}
21732213
samples.sum += measure.size * measure.rowCount;
21742214
samples.count += measure.rowCount;
@@ -2935,6 +2975,9 @@ const createListVirtual = () => {
29352975
// The list is on its way somewhere: what the window frames is not what it
29362976
// is about to frame, so a run must not fetch for it (see holdWindow).
29372977
holdPending: false,
2978+
// Called by a run just before rows land in it: what is on screen must not
2979+
// move because something arrived above it. Set by the list itself.
2980+
captureAnchor: () => {},
29382981
horizontal: false,
29392982
virtualItemSizeSignal: null,
29402983
renderSkeleton: undefined,
@@ -3515,9 +3558,6 @@ const useItemStore = ({ count, itemsAction, memoryBudget }) => {
35153558
if (stillWanted) {
35163559
return;
35173560
}
3518-
console.info(
3519-
`[abort] request ${request.start}..${request.end} window ${windowFrom}..${windowTo}`,
3520-
);
35213561
request.controller?.abort();
35223562
request.busy = false;
35233563
}
@@ -3563,6 +3603,9 @@ const useItemStore = ({ count, itemsAction, memoryBudget }) => {
35633603
const pageCount = Array.isArray(page)
35643604
? pageItems.length
35653605
: (page.count ?? pageStart + pageItems.length);
3606+
// Before the rows land: what is on screen has to stay where it is,
3607+
// and the DOM still shows the state to hold onto.
3608+
virtual.captureAnchor();
35663609
let i = 0;
35673610
while (i < pageItems.length) {
35683611
pages.byIndex.set(pageStart + i, pageItems[i]);

0 commit comments

Comments
 (0)