@jsenv/navi 0.29.439 → 0.29.440

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -72732,9 +72732,16 @@ document.addEventListener(
72732
72732
  * progress. A write is an instant scroll, and the browser abandons for it any
72733
72733
  * scroll it is playing on its own, short of where that one was going: a key
72734
72734
  * (Home, PageDown, Space), a smooth `scrollTo` or `scrollIntoView`, the status
72735
- * bar tap on iOS, the fling a finger leaves behind once it lifts. A scroll
72736
- * following a hand is not abandoned: the wheel's next turn carries on, and on
72737
- * most engines a finger keeps dragging from wherever the write left it.
72735
+ * bar tap on iOS. A scroll following a hand is not abandoned: the wheel's next
72736
+ * turn carries on, and on most engines a finger keeps dragging from wherever
72737
+ * the write left it, and the fling it leaves behind once it lifts carries on
72738
+ * from there too (Chrome under DevTools' touch emulation: flings written to at
72739
+ * every move of a list's window went as far as flings never written to).
72740
+ *
72741
+ * A fling must not be taken for a scroll the browser plays on its own. A list
72742
+ * then leaves uncorrected, for the whole fling, the room its fillers lose above
72743
+ * the screen as its window slides: in wematch, cards jumping half a screen
72744
+ * several times per fling.
72738
72745
  *
72739
72746
  * Not on iOS. Its finger places the content from where the drag began, so a
72740
72747
  * write under it is undone at the finger's next move — or, when the finger
@@ -72742,11 +72749,15 @@ document.addEventListener(
72742
72749
  * stops on any write during its momentum, as the status bar's scroll to the
72743
72750
  * top does (Safari 26.5 on the simulator: flings of 1700px stopped 130 to 340px
72744
72751
  * after the lift, 3 in 3; a write 50ms before the lift stopped 2 flings in 3).
72745
- * No finger there is a hand a write could hold to. Chrome flings on through a
72746
- * write, and loses nothing by not getting one.
72747
- *
72748
- * A scroll is followed from its first "scroll" event to its "scrollend", and
72749
- * is a hand's while a finger is down, or when a wheel turned around the time it
72752
+ * No finger there is a hand a write could hold to, nor the fling it leaves.
72753
+ *
72754
+ * A scroll is followed from its first "scroll" event to its "scrollend". It is
72755
+ * a finger's while a finger is down, and still once the finger lifts if a
72756
+ * finger moved it around the time it started or since: the fling goes on in
72757
+ * the drag's own scroll, or in a scroll of its own that Chrome starts right
72758
+ * after the drag's "scrollend". What counts is a scroll the finger moved, not
72759
+ * a lift: a tap on a button calling a smooth `scrollTo` lifts a finger that
72760
+ * moved nothing. A scroll is a wheel's when a wheel turned around the time it
72750
72761
  * started or since. A finger is down from its touchstart to its touchend,
72751
72762
  * heard on what it touched: a list removes the items scrolled out of its window
72752
72763
  * while the finger is still on one of them, and the touchend of a removed
@@ -72760,6 +72771,10 @@ document.addEventListener(
72760
72771
  // next one. A turn further from it moved nothing (a wheel at an edge) and is no
72761
72772
  // hand on a scroll that came from elsewhere.
72762
72773
  const WHEEL_REACH = 100;
72774
+ // The fling Chrome plays as a scroll of its own starts a frame or two after the
72775
+ // drag's last move (DevTools' touch emulation, 4× CPU: 12ms after the drag's
72776
+ // "scrollend").
72777
+ const FLING_REACH = 150;
72763
72778
  // iOS's touch handling, the one engine whose finger drops writes (see above).
72764
72779
  const FINGER_KEEPS_WRITES = !window.CSS.supports(
72765
72780
  "-webkit-touch-callout",
@@ -72769,6 +72784,7 @@ const FINGER_KEEPS_WRITES = !window.CSS.supports(
72769
72784
  const flightMap = new WeakMap();
72770
72785
  let fingersDown = 0;
72771
72786
  let wheelTimeStamp = -Infinity;
72787
+ let fingerScrollTimeStamp = -Infinity;
72772
72788
 
72773
72789
  /**
72774
72790
  * @param {Element} scrollerEl
@@ -72779,7 +72795,10 @@ const wouldInterruptScroll = (scrollerEl) => {
72779
72795
  if (!flight) {
72780
72796
  return false;
72781
72797
  }
72782
- if (fingersDown > 0) {
72798
+ if (
72799
+ fingersDown > 0 ||
72800
+ fingerScrollTimeStamp > flight.timeStamp - FLING_REACH
72801
+ ) {
72783
72802
  return !FINGER_KEEPS_WRITES;
72784
72803
  }
72785
72804
  if (wheelTimeStamp > flight.timeStamp - WHEEL_REACH) {
@@ -72806,6 +72825,9 @@ if ("onscrollend" in window) {
72806
72825
  document.addEventListener(
72807
72826
  "scroll",
72808
72827
  (e) => {
72828
+ if (fingersDown > 0) {
72829
+ fingerScrollTimeStamp = e.timeStamp;
72830
+ }
72809
72831
  const scroller = asScroller(e.target);
72810
72832
  if (flightMap.has(scroller)) {
72811
72833
  return;
@@ -73136,7 +73158,13 @@ const createListItems = () => {
73136
73158
  // One, as a rule; a child that renders several items keeps them in the order
73137
73159
  // they first rendered, which is all it can be told.
73138
73160
  const ownerIdsBySlot = new Map();
73161
+ // What each run answers about the items it holds, drawn or not: where the
73162
+ // one of an id sits, which one sits at an index.
73139
73163
  const locatorByOwner = new Map();
73164
+ // The room each item took along the scroll axis the last time it was drawn,
73165
+ // by id: what a filler holds for it once it is not (see rememberItemRooms in
73166
+ // list.jsx). A number per item drawn while the list is mounted.
73167
+ const roomById = new Map();
73140
73168
  // The slots as the tree reads, first to last, and where each stands in it.
73141
73169
  // Rebuilt once a walk has changed the tree, read to place the owners.
73142
73170
  const slotWalk = [];
@@ -73485,6 +73513,7 @@ const createListItems = () => {
73485
73513
  holdViewStill: () => {},
73486
73514
  horizontal: false,
73487
73515
  virtualItemSizeSignal: null,
73516
+ roomById,
73488
73517
  renderSkeleton: undefined,
73489
73518
  // The children a walk stands over, in order — said in one call, before any
73490
73519
  // of them renders, so that what a child asks next is answered against the
@@ -73516,22 +73545,33 @@ const createListItems = () => {
73516
73545
  // Whether any run of items lives in this list: what makes a render window
73517
73546
  // mean anything (see List's renderBudget).
73518
73547
  hasRuns: () => locatorByOwner.size > 0,
73519
- setItemLocator: (ownerId, locate) => {
73520
- locatorByOwner.set(ownerId, locate);
73548
+ setItemLocator: (ownerId, locator) => {
73549
+ locatorByOwner.set(ownerId, locator);
73521
73550
  },
73522
73551
  dropItemLocator: (ownerId) => {
73523
73552
  locatorByOwner.delete(ownerId);
73524
73553
  },
73525
73554
  // Where the item named by that id sits, asked of whoever holds it.
73526
73555
  locateItem: (id) => {
73527
- for (const locate of locatorByOwner.values()) {
73528
- const index = locate(id);
73556
+ for (const locator of locatorByOwner.values()) {
73557
+ const index = locator.indexOf(id);
73529
73558
  if (index !== null) {
73530
73559
  return index;
73531
73560
  }
73532
73561
  }
73533
73562
  return null;
73534
73563
  },
73564
+ // The room the item at that index took when it was last drawn, or
73565
+ // undefined: never drawn, not held, or not an item of a run.
73566
+ roomAt: (index) => {
73567
+ for (const locator of locatorByOwner.values()) {
73568
+ const id = locator.idAt(index);
73569
+ if (id !== undefined) {
73570
+ return roomById.get(id);
73571
+ }
73572
+ }
73573
+ return undefined;
73574
+ },
73535
73575
  // The place the owner's items start at — read from a signal, so that the
73536
73576
  // owner is rendered again when it moves (see the top of this file). Asked
73537
73577
  // on every render, and answered without a second look for as long as the
@@ -75648,6 +75688,8 @@ const useListScrollSync = ({
75648
75688
  // state: the render moves it where the list is held (holdWindow) without a
75649
75689
  // commit of its own.
75650
75690
  const renderWindowRef = useRef(null);
75691
+ // A window was asked for that no render has drawn yet (see settleWindow).
75692
+ const windowAskedRef = useRef(false);
75651
75693
  const windowLeavesItemsOut = () => {
75652
75694
  const renderWindow = renderWindowRef.current;
75653
75695
  if (!renderWindow) {
@@ -75742,6 +75784,8 @@ const useListScrollSync = ({
75742
75784
  return;
75743
75785
  }
75744
75786
  captureAnchor();
75787
+ // What was measured is the room of an item alone on its line.
75788
+ listItems.roomById.clear();
75745
75789
  itemsPerLineRef.current = itemsPerLineNow;
75746
75790
  setItemsPerLine(itemsPerLineNow);
75747
75791
  };
@@ -75823,6 +75867,25 @@ const useListScrollSync = ({
75823
75867
  }
75824
75868
  }
75825
75869
  }
75870
+ // The items about to leave the window are still drawn: the room each takes
75871
+ // is kept, and the filler holding its place from then on holds exactly that
75872
+ // (see VirtualFiller). Nothing above the screen then changes as the window
75873
+ // slides, which matters most where the anchoring cannot write: a scroll in
75874
+ // flight (see holdAnchorStill). Every item drawn is read, since any may be
75875
+ // the next to leave. A room kept is the room of the last drawing: an item
75876
+ // that changed since is measured again when drawn again, and the anchoring
75877
+ // holds the view across the difference. Items laid side by side in a grid
75878
+ // share a line, which the fillers hold at the one size.
75879
+ const rememberItemRooms = () => {
75880
+ if (!ref.current || itemsPerLineRef.current !== 1) {
75881
+ return;
75882
+ }
75883
+ const listEl = getListEl();
75884
+ if (!listEl) {
75885
+ return;
75886
+ }
75887
+ readItemRooms(listEl, horizontal, listItems.roomById);
75888
+ };
75826
75889
  const updateRenderWindow = (newStart, newEnd, reason) => {
75827
75890
  const {
75828
75891
  start,
@@ -75831,6 +75894,7 @@ const useListScrollSync = ({
75831
75894
  if (newStart === start && newEnd === end) {
75832
75895
  return null;
75833
75896
  }
75897
+ rememberItemRooms();
75834
75898
  captureAnchor();
75835
75899
  debugScroll(`updateRenderWindow(${newStart}, ${newEnd}, "${reason}")`);
75836
75900
  const renderWindow = {
@@ -75838,6 +75902,7 @@ const useListScrollSync = ({
75838
75902
  end: newEnd
75839
75903
  };
75840
75904
  renderWindowRef.current = renderWindow;
75905
+ windowAskedRef.current = true;
75841
75906
  setRenderWindow(renderWindow);
75842
75907
  return renderWindow;
75843
75908
  };
@@ -76249,6 +76314,20 @@ const useListScrollSync = ({
76249
76314
  }
76250
76315
  const scrollerEl = getScroller();
76251
76316
  if (openAt === "end") {
76317
+ const total = listItems.totalSignal.peek();
76318
+ const {
76319
+ start,
76320
+ end
76321
+ } = renderWindowRef.current;
76322
+ if (end < total) {
76323
+ // The window was framed before the runs said how many items there are
76324
+ // (see holdWindow): the end of the document is the end of a filler.
76325
+ // Aimed at the last items first, like a named item.
76326
+ const windowSize = end - start;
76327
+ const wantedStart = total - windowSize < 0 ? 0 : total - windowSize;
76328
+ updateRenderWindow(wantedStart, total, "opening at the end");
76329
+ return;
76330
+ }
76252
76331
  sendHeldScroll(horizontal ? scrollerEl.scrollWidth : scrollerEl.scrollHeight);
76253
76332
  return;
76254
76333
  }
@@ -76326,6 +76405,13 @@ const useListScrollSync = ({
76326
76405
  if (stage === "steady") {
76327
76406
  return;
76328
76407
  }
76408
+ if (windowAskedRef.current) {
76409
+ // The hold has just aimed the window where the list opens — its end, an
76410
+ // item it found — and the next commit draws it. Sized now, the window
76411
+ // would weigh the items there by the ones drawn here, nowhere near them:
76412
+ // the one-line items at the start of a thread opening on its cards.
76413
+ return;
76414
+ }
76329
76415
  const initialBudget = initialBudgetRef.current;
76330
76416
  if (initialBudget && !paintedRef.current) {
76331
76417
  if (stage === "picture") {
@@ -76667,9 +76753,9 @@ const useListScrollSync = ({
76667
76753
  if (wouldInterruptScroll(scrollerEl)) {
76668
76754
  // Nobody is looking at items standing still: they are passing by, on
76669
76755
  // their way somewhere the browser takes them (a key, a smooth scrollTo,
76670
- // the status bar, a fling). The write would stop that scroll short of
76671
- // where it was going. Left alone, the items drawn above push the rest by
76672
- // `drift` while all of it moves, and the scroll lands.
76756
+ // the status bar, a fling on iOS). The write would stop that scroll short
76757
+ // of where it was going. Left alone, the items drawn above push the rest
76758
+ // by `drift` while all of it moves, and the scroll lands.
76673
76759
  return;
76674
76760
  }
76675
76761
  scrolledByListRef.current = true;
@@ -76798,8 +76884,14 @@ const useListScrollSync = ({
76798
76884
  bandEnd = walkAfter(hold.index, screenSize - hold.above, pixelsOf).index;
76799
76885
  forward = true;
76800
76886
  } else {
76801
- bandStart = findBandStart(items, bandFrom, itemSize);
76802
- bandEnd = bandTo > bandFrom ? findBandEnd(items, bandTo, itemSize) : bandStart;
76887
+ // In the room a filler holds, an item is counted at what the filler
76888
+ // gives it.
76889
+ const fillerSizeOf = itemSize > 0 ? index => {
76890
+ const room = listItems.roomAt(index);
76891
+ return room === undefined ? itemSize : room;
76892
+ } : null;
76893
+ bandStart = findBandStart(items, bandFrom, fillerSizeOf, total);
76894
+ bandEnd = bandTo > bandFrom ? findBandEnd(items, bandTo, fillerSizeOf, total) : bandStart;
76803
76895
  forward = scrollDirectionRef.current > 0;
76804
76896
  }
76805
76897
  if (bandStart > total) {
@@ -77057,6 +77149,8 @@ const useListScrollSync = ({
77057
77149
  };
77058
77150
  }, [scrollerElResolved]);
77059
77151
  holdWindow();
77152
+ // Whatever was asked for until now is what this render draws.
77153
+ windowAskedRef.current = false;
77060
77154
  const {
77061
77155
  start: windowStart,
77062
77156
  end: windowEnd
@@ -77802,12 +77896,49 @@ const readWindowGeometry = (scrollerEl, listEl, horizontal) => {
77802
77896
  items
77803
77897
  };
77804
77898
  };
77899
+ // The room each item drawn takes along the scroll axis (see
77900
+ // rememberItemRooms): up to the next item when that one is drawn too, so that
77901
+ // what stands between them — a separator, the label of a group the next one
77902
+ // opens — is counted; its own box otherwise. A stand-in is not measured: it
77903
+ // takes the room it is given, not the room of its item.
77904
+ const readItemRooms = (listEl, horizontal, roomById) => {
77905
+ let previous = null;
77906
+ const keepRoom = (item, next) => {
77907
+ if (item.id === null) {
77908
+ return;
77909
+ }
77910
+ roomById.set(item.id, next && next.index === item.index + 1 ? next.from - item.from : item.to - item.from);
77911
+ };
77912
+ for (const itemEl of listEl.querySelectorAll(`[${LIST_ITEM_INDEX_ATTRIBUTE}]`)) {
77913
+ if (!isOwnItem(itemEl, listEl)) {
77914
+ continue;
77915
+ }
77916
+ const rect = itemEl.getBoundingClientRect();
77917
+ if (rect.width === 0 && rect.height === 0) {
77918
+ continue;
77919
+ }
77920
+ const item = {
77921
+ id: getItemName(itemEl),
77922
+ index: Number(itemEl.getAttribute(LIST_ITEM_INDEX_ATTRIBUTE)),
77923
+ from: horizontal ? rect.left : rect.top,
77924
+ to: horizontal ? rect.right : rect.bottom
77925
+ };
77926
+ if (previous) {
77927
+ keepRoom(previous, item);
77928
+ }
77929
+ previous = item;
77930
+ }
77931
+ if (previous) {
77932
+ keepRoom(previous, null);
77933
+ }
77934
+ };
77805
77935
  // The first item the screen shows from `position` on, and the item after the
77806
77936
  // last one it shows up to `position` (exclusive, like the window's end). An
77807
77937
  // item drawn is read off its box; in the room a filler holds for items not
77808
- // drawn, the item is counted at the size the filler gives each of them — the
77809
- // only way a scroll position there says which item it is.
77810
- const findBandStart = (items, position, itemSize) => {
77938
+ // drawn, each item is counted at the size the filler gives it (`sizeOf`) —
77939
+ // the only way a scroll position there says which item it is. Without a size
77940
+ // (`null`), the room a filler holds says nothing.
77941
+ const findBandStart = (items, position, sizeOf, total) => {
77811
77942
  let low = 0;
77812
77943
  let high = items.length;
77813
77944
  while (low < high) {
@@ -77821,29 +77952,28 @@ const findBandStart = (items, position, itemSize) => {
77821
77952
  const next = items[low];
77822
77953
  const previous = items[low - 1];
77823
77954
  if (!next) {
77824
- if (!itemSize) {
77955
+ if (!sizeOf) {
77825
77956
  return previous.index + 1;
77826
77957
  }
77827
- return previous.index + 1 + Math.floor((position - previous.to) / itemSize);
77958
+ return findIndexAfter(previous.index + 1, previous.to, position, sizeOf, total);
77828
77959
  }
77829
77960
  if (next.from <= position) {
77830
77961
  return next.index;
77831
77962
  }
77832
77963
  if (!previous) {
77833
- if (!itemSize) {
77964
+ if (!sizeOf) {
77834
77965
  return next.index;
77835
77966
  }
77836
- const index = next.index - Math.ceil((next.from - position) / itemSize);
77967
+ const index = findIndexBefore(next.index, next.from, position, sizeOf);
77837
77968
  return index < 0 ? 0 : index;
77838
77969
  }
77839
- if (!itemSize || next.index === previous.index + 1) {
77970
+ if (!sizeOf || next.index === previous.index + 1) {
77840
77971
  // Between two items that follow each other: a separator, a group label.
77841
77972
  return next.index;
77842
77973
  }
77843
- const index = previous.index + 1 + Math.floor((position - previous.to) / itemSize);
77844
- return index < next.index ? index : next.index;
77974
+ return findIndexAfter(previous.index + 1, previous.to, position, sizeOf, next.index);
77845
77975
  };
77846
- const findBandEnd = (items, position, itemSize) => {
77976
+ const findBandEnd = (items, position, sizeOf, total) => {
77847
77977
  let low = 0;
77848
77978
  let high = items.length;
77849
77979
  while (low < high) {
@@ -77857,24 +77987,53 @@ const findBandEnd = (items, position, itemSize) => {
77857
77987
  const previous = items[low - 1];
77858
77988
  const next = items[low];
77859
77989
  if (!previous) {
77860
- if (!itemSize) {
77990
+ if (!sizeOf) {
77861
77991
  return next.index;
77862
77992
  }
77863
- const index = next.index - Math.ceil((next.from - position) / itemSize) + 1;
77864
- return index < 0 ? 0 : index;
77993
+ return findIndexBefore(next.index, next.from, position, sizeOf) + 1;
77865
77994
  }
77866
77995
  if (previous.to >= position) {
77867
77996
  return previous.index + 1;
77868
77997
  }
77869
- if (!itemSize || next && next.index === previous.index + 1) {
77998
+ if (!sizeOf || next && next.index === previous.index + 1) {
77870
77999
  return previous.index + 1;
77871
78000
  }
77872
- const index = previous.index + 2 + Math.floor((position - previous.to) / itemSize);
78001
+ const index = findIndexAfter(previous.index + 1, previous.to, position, sizeOf, next ? next.index : total) + 1;
77873
78002
  if (next && index > next.index) {
77874
78003
  return next.index;
77875
78004
  }
77876
78005
  return index;
77877
78006
  };
78007
+ // The item whose room holds `position`, the rooms laid one after the other
78008
+ // from `from` on, starting with the item at `index` — `limit` when `position`
78009
+ // is past them all.
78010
+ const findIndexAfter = (index, from, position, sizeOf, limit) => {
78011
+ let at = from;
78012
+ let itemIndex = index;
78013
+ while (itemIndex < limit) {
78014
+ const size = sizeOf(itemIndex);
78015
+ if (at + size > position) {
78016
+ return itemIndex;
78017
+ }
78018
+ at += size;
78019
+ itemIndex++;
78020
+ }
78021
+ return limit;
78022
+ };
78023
+ // The same, the rooms laid back from `to`, ending with the item before the
78024
+ // one at `index` — -1 when `position` is before them all.
78025
+ const findIndexBefore = (index, to, position, sizeOf) => {
78026
+ let at = to;
78027
+ let itemIndex = index;
78028
+ while (at > position) {
78029
+ if (itemIndex === 0) {
78030
+ return -1;
78031
+ }
78032
+ itemIndex--;
78033
+ at -= sizeOf(itemIndex);
78034
+ }
78035
+ return itemIndex;
78036
+ };
77878
78037
  // How much the window holds on each side of the screen, in the budget's unit:
77879
78038
  // read off the items' boxes where they stand, or — while the list holds itself
77880
78039
  // on an item, and the screen is where that item is going to stand (see
@@ -78390,16 +78549,19 @@ const Fallback = ({
78390
78549
  // every item it draws must not be redrawn — every item of it — because the size
78391
78550
  // settled after the first commit.
78392
78551
  // It holds lines: the items side by side on one take its room together, and a
78393
- // last line left short takes it all the same.
78552
+ // last line left short takes it all the same. An item the list drew before
78553
+ // takes the room it took then instead (see rememberItemRooms).
78394
78554
  const VirtualFiller = ({
78395
78555
  edge,
78396
78556
  itemCount,
78397
78557
  itemsPerLine,
78558
+ itemsRemembered,
78559
+ roomRemembered,
78398
78560
  findChunks
78399
78561
  }) => {
78400
78562
  const listItems = useContext(ListItemsContext);
78401
78563
  const lineSize = listItems.virtualItemSizeSignal.value;
78402
- const sizeToFill = Math.ceil(itemCount / itemsPerLine) * lineSize;
78564
+ const sizeToFill = Math.ceil(itemCount / itemsPerLine) * lineSize - itemsRemembered * lineSize / itemsPerLine + roomRemembered;
78403
78565
  // A filler resizing moves what stands below it — the items on screen, when it
78404
78566
  // holds the room of items above them — and it resizes in a commit of its own
78405
78567
  // when the item size settles after the list has rendered: the list puts its
@@ -78417,14 +78579,14 @@ const VirtualFiller = ({
78417
78579
  "navi-virtual-filler": edge,
78418
78580
  "aria-hidden": true,
78419
78581
  style: {
78420
- "--size-to-fill": `${sizeToFill}px`,
78421
- "--x-find-line-size": findChunks ? `${lineSize / itemsPerLine}px` : undefined
78582
+ "--size-to-fill": `${sizeToFill}px`
78422
78583
  },
78423
78584
  children: findChunks && findChunks.map(chunk => jsx("div", {
78424
78585
  className: "navi_list_find_stand_in",
78425
78586
  hidden: "until-found",
78426
78587
  style: {
78427
- "--x-find-line-count": chunk.to - chunk.from
78588
+ "--x-find-line-count": chunk.to - chunk.from,
78589
+ "--x-find-line-size": `${chunk.room === undefined ? lineSize / itemsPerLine : chunk.room}px`
78428
78590
  },
78429
78591
  children: chunk.text
78430
78592
  }, `${chunk.from}_${chunk.to}`))
@@ -79317,16 +79479,51 @@ const useRunItems = (store, {
79317
79479
  // Where an item named from outside actually sits. Only the run can answer:
79318
79480
  // items it holds but does not draw are nowhere else — a list only knows the
79319
79481
  // items it has drawn (they register themselves, see ListItemUI).
79320
- listItems.setItemLocator(ownerId, id => {
79321
- let found = null;
79322
- store.eachHeld((item, rank) => {
79323
- const itemIndex = indexOfRank(rank);
79324
- if (found === null && idOf(item, itemIndex) === id) {
79325
- found = itemIndex;
79482
+ const idAt = itemIndex => {
79483
+ if (itemIndex < runStart || itemIndex >= runEnd) {
79484
+ return undefined;
79485
+ }
79486
+ const item = getItemAt(itemIndex);
79487
+ return item === undefined ? undefined : idOf(item, itemIndex);
79488
+ };
79489
+ listItems.setItemLocator(ownerId, {
79490
+ indexOf: id => {
79491
+ let found = null;
79492
+ store.eachHeld((item, rank) => {
79493
+ const itemIndex = indexOfRank(rank);
79494
+ if (found === null && idOf(item, itemIndex) === id) {
79495
+ found = itemIndex;
79496
+ }
79497
+ });
79498
+ return found;
79499
+ },
79500
+ idAt
79501
+ });
79502
+ // The room an item took when the list last drew it (see rememberItemRooms),
79503
+ // or undefined: a filler holds an item it never drew at the item size.
79504
+ const roomOf = itemIndex => {
79505
+ const id = idAt(itemIndex);
79506
+ return id === undefined ? undefined : listItems.roomById.get(id);
79507
+ };
79508
+ const readRemembered = (from, to) => {
79509
+ const remembered = {
79510
+ room: 0,
79511
+ count: 0
79512
+ };
79513
+ if (listItems.roomById.size === 0) {
79514
+ return remembered;
79515
+ }
79516
+ let itemIndex = from;
79517
+ while (itemIndex < to) {
79518
+ const room = roomOf(itemIndex);
79519
+ if (room !== undefined) {
79520
+ remembered.room += room;
79521
+ remembered.count++;
79326
79522
  }
79327
- });
79328
- return found;
79329
- });
79523
+ itemIndex++;
79524
+ }
79525
+ return remembered;
79526
+ };
79330
79527
  useLayoutEffect(() => {
79331
79528
  return () => {
79332
79529
  listItems.dropItemLocator(ownerId);
@@ -79502,7 +79699,9 @@ const useRunItems = (store, {
79502
79699
  };
79503
79700
  // The text of the items a filler stands for, cut in chunks aligned on the
79504
79701
  // run's own ranks: the window sliding changes the chunk at its edge and
79505
- // leaves the others as they are.
79702
+ // leaves the others as they are. A chunk's lines are all as tall as the
79703
+ // room its items take in the filler, so it also ends where that room
79704
+ // changes: a match is then found where its item will be drawn.
79506
79705
  const getFindChunks = (from, to) => {
79507
79706
  if (!findText) {
79508
79707
  return null;
@@ -79512,10 +79711,11 @@ const useRunItems = (store, {
79512
79711
  while (chunkFrom < to) {
79513
79712
  const chunkIndex = Math.floor(rankOf(chunkFrom) / FIND_CHUNK_ITEM_COUNT);
79514
79713
  const alignedTo = indexOfRank((chunkIndex + 1) * FIND_CHUNK_ITEM_COUNT);
79515
- const chunkTo = alignedTo < to ? alignedTo : to;
79714
+ const chunkEnd = alignedTo < to ? alignedTo : to;
79715
+ const room = roomOf(chunkFrom);
79516
79716
  const lines = [];
79517
79717
  let lineIndex = chunkFrom;
79518
- while (lineIndex < chunkTo) {
79718
+ while (lineIndex < chunkEnd && roomOf(lineIndex) === room) {
79519
79719
  const item = getItemAt(lineIndex);
79520
79720
  // An item not held has nothing to find, but keeps its line: the items
79521
79721
  // after it stay at their place.
@@ -79524,10 +79724,11 @@ const useRunItems = (store, {
79524
79724
  }
79525
79725
  chunks.push({
79526
79726
  from: chunkFrom,
79527
- to: chunkTo,
79727
+ to: lineIndex,
79728
+ room,
79528
79729
  text: lines.join("\n")
79529
79730
  });
79530
- chunkFrom = chunkTo;
79731
+ chunkFrom = lineIndex;
79531
79732
  }
79532
79733
  return chunks;
79533
79734
  };
@@ -79536,10 +79737,13 @@ const useRunItems = (store, {
79536
79737
  // one run, and what sits before or after it (a header, items given one by
79537
79738
  // one) is not virtualized at all.
79538
79739
  if (windowFrom > runStart) {
79740
+ const remembered = readRemembered(runStart, windowFrom);
79539
79741
  nodes.push(jsx(VirtualFiller, {
79540
79742
  edge: "before",
79541
79743
  itemCount: windowFrom - runStart,
79542
79744
  itemsPerLine: itemsPerLine,
79745
+ itemsRemembered: remembered.count,
79746
+ roomRemembered: remembered.room,
79543
79747
  findChunks: getFindChunks(runStart, windowFrom)
79544
79748
  }, "navi-list-filler-before"));
79545
79749
  }
@@ -79641,10 +79845,13 @@ const useRunItems = (store, {
79641
79845
  }
79642
79846
  closeGroup();
79643
79847
  if (runEnd > windowTo) {
79848
+ const remembered = readRemembered(windowTo, runEnd);
79644
79849
  nodes.push(jsx(VirtualFiller, {
79645
79850
  edge: "after",
79646
79851
  itemCount: runEnd - windowTo,
79647
79852
  itemsPerLine: itemsPerLine,
79853
+ itemsRemembered: remembered.count,
79854
+ roomRemembered: remembered.room,
79648
79855
  findChunks: getFindChunks(windowTo, runEnd)
79649
79856
  }, "navi-list-filler-after"));
79650
79857
  }
@@ -80588,19 +80795,20 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
80588
80795
  * source for is a page (see `<List.Items pageSize>`), whatever either says,
80589
80796
  * so the first picture costs no second request.
80590
80797
  * @param {number} [props.virtualItemSize]
80591
- * The room an item not drawn is held at, in px along the scroll axis: what
80592
- * the fillers hold for each item outside the render window, what a scroll
80593
- * position inside them is read with, and the least an item on its way takes
80594
- * (see `renderSkeleton`). Left out, it is the average of the items measured
80595
- * so far — once when the list mounts, again when a popup around it opens,
80596
- * and after each commit while items are held off screen. Given, it is the
80597
- * worst case: the size every item has, or, in a list whose items differ, the
80598
- * smallest an item can be (a thread of one-line items and cards: the
80599
- * one-line size) — an item guessed too small builds one more, an item
80600
- * guessed too big leaves a blank (see docs/scroll.md, "What the list knows,
80601
- * and what it guesses"). The items drawn are measured either way: the render
80602
- * window is sized on them. In a grid (`columns`), the size is a line's: a
80603
- * card's, which the items side by side on it share.
80798
+ * The room an item never drawn is held at, in px along the scroll axis: what
80799
+ * the fillers hold for it outside the render window, what a scroll position
80800
+ * inside them is read with, and the least an item on its way takes (see
80801
+ * `renderSkeleton`). An item the list has drawn is held at the room it took
80802
+ * then. Left out, it is the average of the items measured so far — once when
80803
+ * the list mounts, again when a popup around it opens, and after each commit
80804
+ * while items are held off screen. Given, it is the worst case: the size
80805
+ * every item has, or, in a list whose items differ, the smallest an item can
80806
+ * be (a thread of one-line items and cards: the one-line size) — an item
80807
+ * guessed too small builds one more, an item guessed too big leaves a blank
80808
+ * (see docs/scroll.md, "What the list knows, and what it guesses"). The items
80809
+ * drawn are measured either way: the render window is sized on them. In a
80810
+ * grid (`columns`), the size is a line's: a card's, which the items side by
80811
+ * side on it share, and the lines drawn are held at it too.
80604
80812
  * @param {"self"|"parent"|"document"|Element|{current: Element}} [props.scroller="self"]
80605
80813
  * Which box scrolls — and with it, which box the render window follows and
80606
80814
  * which box a scroll position is read from (`onScrolledChange`). `"self"`