@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.
@@ -71337,9 +71337,16 @@ document.addEventListener(
71337
71337
  * progress. A write is an instant scroll, and the browser abandons for it any
71338
71338
  * scroll it is playing on its own, short of where that one was going: a key
71339
71339
  * (Home, PageDown, Space), a smooth `scrollTo` or `scrollIntoView`, the status
71340
- * bar tap on iOS, the fling a finger leaves behind once it lifts. A scroll
71341
- * following a hand is not abandoned: the wheel's next turn carries on, and on
71342
- * most engines a finger keeps dragging from wherever the write left it.
71340
+ * bar tap on iOS. A scroll following a hand is not abandoned: the wheel's next
71341
+ * turn carries on, and on most engines a finger keeps dragging from wherever
71342
+ * the write left it, and the fling it leaves behind once it lifts carries on
71343
+ * from there too (Chrome under DevTools' touch emulation: flings written to at
71344
+ * every move of a list's window went as far as flings never written to).
71345
+ *
71346
+ * A fling must not be taken for a scroll the browser plays on its own. A list
71347
+ * then leaves uncorrected, for the whole fling, the room its fillers lose above
71348
+ * the screen as its window slides: in wematch, cards jumping half a screen
71349
+ * several times per fling.
71343
71350
  *
71344
71351
  * Not on iOS. Its finger places the content from where the drag began, so a
71345
71352
  * write under it is undone at the finger's next move — or, when the finger
@@ -71347,11 +71354,15 @@ document.addEventListener(
71347
71354
  * stops on any write during its momentum, as the status bar's scroll to the
71348
71355
  * top does (Safari 26.5 on the simulator: flings of 1700px stopped 130 to 340px
71349
71356
  * after the lift, 3 in 3; a write 50ms before the lift stopped 2 flings in 3).
71350
- * No finger there is a hand a write could hold to. Chrome flings on through a
71351
- * write, and loses nothing by not getting one.
71352
- *
71353
- * A scroll is followed from its first "scroll" event to its "scrollend", and
71354
- * is a hand's while a finger is down, or when a wheel turned around the time it
71357
+ * No finger there is a hand a write could hold to, nor the fling it leaves.
71358
+ *
71359
+ * A scroll is followed from its first "scroll" event to its "scrollend". It is
71360
+ * a finger's while a finger is down, and still once the finger lifts if a
71361
+ * finger moved it around the time it started or since: the fling goes on in
71362
+ * the drag's own scroll, or in a scroll of its own that Chrome starts right
71363
+ * after the drag's "scrollend". What counts is a scroll the finger moved, not
71364
+ * a lift: a tap on a button calling a smooth `scrollTo` lifts a finger that
71365
+ * moved nothing. A scroll is a wheel's when a wheel turned around the time it
71355
71366
  * started or since. A finger is down from its touchstart to its touchend,
71356
71367
  * heard on what it touched: a list removes the items scrolled out of its window
71357
71368
  * while the finger is still on one of them, and the touchend of a removed
@@ -71365,6 +71376,10 @@ document.addEventListener(
71365
71376
  // next one. A turn further from it moved nothing (a wheel at an edge) and is no
71366
71377
  // hand on a scroll that came from elsewhere.
71367
71378
  const WHEEL_REACH = 100;
71379
+ // The fling Chrome plays as a scroll of its own starts a frame or two after the
71380
+ // drag's last move (DevTools' touch emulation, 4× CPU: 12ms after the drag's
71381
+ // "scrollend").
71382
+ const FLING_REACH = 150;
71368
71383
  // iOS's touch handling, the one engine whose finger drops writes (see above).
71369
71384
  const FINGER_KEEPS_WRITES = !window.CSS.supports(
71370
71385
  "-webkit-touch-callout",
@@ -71374,6 +71389,7 @@ const FINGER_KEEPS_WRITES = !window.CSS.supports(
71374
71389
  const flightMap = new WeakMap();
71375
71390
  let fingersDown = 0;
71376
71391
  let wheelTimeStamp = -Infinity;
71392
+ let fingerScrollTimeStamp = -Infinity;
71377
71393
 
71378
71394
  /**
71379
71395
  * @param {Element} scrollerEl
@@ -71384,7 +71400,10 @@ const wouldInterruptScroll = (scrollerEl) => {
71384
71400
  if (!flight) {
71385
71401
  return false;
71386
71402
  }
71387
- if (fingersDown > 0) {
71403
+ if (
71404
+ fingersDown > 0 ||
71405
+ fingerScrollTimeStamp > flight.timeStamp - FLING_REACH
71406
+ ) {
71388
71407
  return !FINGER_KEEPS_WRITES;
71389
71408
  }
71390
71409
  if (wheelTimeStamp > flight.timeStamp - WHEEL_REACH) {
@@ -71411,6 +71430,9 @@ if ("onscrollend" in window) {
71411
71430
  document.addEventListener(
71412
71431
  "scroll",
71413
71432
  (e) => {
71433
+ if (fingersDown > 0) {
71434
+ fingerScrollTimeStamp = e.timeStamp;
71435
+ }
71414
71436
  const scroller = asScroller(e.target);
71415
71437
  if (flightMap.has(scroller)) {
71416
71438
  return;
@@ -71741,7 +71763,13 @@ const createListItems = () => {
71741
71763
  // One, as a rule; a child that renders several items keeps them in the order
71742
71764
  // they first rendered, which is all it can be told.
71743
71765
  const ownerIdsBySlot = new Map();
71766
+ // What each run answers about the items it holds, drawn or not: where the
71767
+ // one of an id sits, which one sits at an index.
71744
71768
  const locatorByOwner = new Map();
71769
+ // The room each item took along the scroll axis the last time it was drawn,
71770
+ // by id: what a filler holds for it once it is not (see rememberItemRooms in
71771
+ // list.jsx). A number per item drawn while the list is mounted.
71772
+ const roomById = new Map();
71745
71773
  // The slots as the tree reads, first to last, and where each stands in it.
71746
71774
  // Rebuilt once a walk has changed the tree, read to place the owners.
71747
71775
  const slotWalk = [];
@@ -72090,6 +72118,7 @@ const createListItems = () => {
72090
72118
  holdViewStill: () => {},
72091
72119
  horizontal: false,
72092
72120
  virtualItemSizeSignal: null,
72121
+ roomById,
72093
72122
  renderSkeleton: undefined,
72094
72123
  // The children a walk stands over, in order — said in one call, before any
72095
72124
  // of them renders, so that what a child asks next is answered against the
@@ -72121,22 +72150,33 @@ const createListItems = () => {
72121
72150
  // Whether any run of items lives in this list: what makes a render window
72122
72151
  // mean anything (see List's renderBudget).
72123
72152
  hasRuns: () => locatorByOwner.size > 0,
72124
- setItemLocator: (ownerId, locate) => {
72125
- locatorByOwner.set(ownerId, locate);
72153
+ setItemLocator: (ownerId, locator) => {
72154
+ locatorByOwner.set(ownerId, locator);
72126
72155
  },
72127
72156
  dropItemLocator: (ownerId) => {
72128
72157
  locatorByOwner.delete(ownerId);
72129
72158
  },
72130
72159
  // Where the item named by that id sits, asked of whoever holds it.
72131
72160
  locateItem: (id) => {
72132
- for (const locate of locatorByOwner.values()) {
72133
- const index = locate(id);
72161
+ for (const locator of locatorByOwner.values()) {
72162
+ const index = locator.indexOf(id);
72134
72163
  if (index !== null) {
72135
72164
  return index;
72136
72165
  }
72137
72166
  }
72138
72167
  return null;
72139
72168
  },
72169
+ // The room the item at that index took when it was last drawn, or
72170
+ // undefined: never drawn, not held, or not an item of a run.
72171
+ roomAt: (index) => {
72172
+ for (const locator of locatorByOwner.values()) {
72173
+ const id = locator.idAt(index);
72174
+ if (id !== undefined) {
72175
+ return roomById.get(id);
72176
+ }
72177
+ }
72178
+ return undefined;
72179
+ },
72140
72180
  // The place the owner's items start at — read from a signal, so that the
72141
72181
  // owner is rendered again when it moves (see the top of this file). Asked
72142
72182
  // on every render, and answered without a second look for as long as the
@@ -74214,6 +74254,8 @@ const useListScrollSync = ({
74214
74254
  // state: the render moves it where the list is held (holdWindow) without a
74215
74255
  // commit of its own.
74216
74256
  const renderWindowRef = useRef(null);
74257
+ // A window was asked for that no render has drawn yet (see settleWindow).
74258
+ const windowAskedRef = useRef(false);
74217
74259
  const windowLeavesItemsOut = () => {
74218
74260
  const renderWindow = renderWindowRef.current;
74219
74261
  if (!renderWindow) {
@@ -74308,6 +74350,8 @@ const useListScrollSync = ({
74308
74350
  return;
74309
74351
  }
74310
74352
  captureAnchor();
74353
+ // What was measured is the room of an item alone on its line.
74354
+ listItems.roomById.clear();
74311
74355
  itemsPerLineRef.current = itemsPerLineNow;
74312
74356
  setItemsPerLine(itemsPerLineNow);
74313
74357
  };
@@ -74389,6 +74433,25 @@ const useListScrollSync = ({
74389
74433
  }
74390
74434
  }
74391
74435
  }
74436
+ // The items about to leave the window are still drawn: the room each takes
74437
+ // is kept, and the filler holding its place from then on holds exactly that
74438
+ // (see VirtualFiller). Nothing above the screen then changes as the window
74439
+ // slides, which matters most where the anchoring cannot write: a scroll in
74440
+ // flight (see holdAnchorStill). Every item drawn is read, since any may be
74441
+ // the next to leave. A room kept is the room of the last drawing: an item
74442
+ // that changed since is measured again when drawn again, and the anchoring
74443
+ // holds the view across the difference. Items laid side by side in a grid
74444
+ // share a line, which the fillers hold at the one size.
74445
+ const rememberItemRooms = () => {
74446
+ if (!ref.current || itemsPerLineRef.current !== 1) {
74447
+ return;
74448
+ }
74449
+ const listEl = getListEl();
74450
+ if (!listEl) {
74451
+ return;
74452
+ }
74453
+ readItemRooms(listEl, horizontal, listItems.roomById);
74454
+ };
74392
74455
  const updateRenderWindow = (newStart, newEnd, reason) => {
74393
74456
  const {
74394
74457
  start,
@@ -74397,12 +74460,14 @@ const useListScrollSync = ({
74397
74460
  if (newStart === start && newEnd === end) {
74398
74461
  return null;
74399
74462
  }
74463
+ rememberItemRooms();
74400
74464
  captureAnchor();
74401
74465
  const renderWindow = {
74402
74466
  start: newStart,
74403
74467
  end: newEnd
74404
74468
  };
74405
74469
  renderWindowRef.current = renderWindow;
74470
+ windowAskedRef.current = true;
74406
74471
  setRenderWindow(renderWindow);
74407
74472
  return renderWindow;
74408
74473
  };
@@ -74812,6 +74877,20 @@ const useListScrollSync = ({
74812
74877
  }
74813
74878
  const scrollerEl = getScroller();
74814
74879
  if (openAt === "end") {
74880
+ const total = listItems.totalSignal.peek();
74881
+ const {
74882
+ start,
74883
+ end
74884
+ } = renderWindowRef.current;
74885
+ if (end < total) {
74886
+ // The window was framed before the runs said how many items there are
74887
+ // (see holdWindow): the end of the document is the end of a filler.
74888
+ // Aimed at the last items first, like a named item.
74889
+ const windowSize = end - start;
74890
+ const wantedStart = total - windowSize < 0 ? 0 : total - windowSize;
74891
+ updateRenderWindow(wantedStart, total);
74892
+ return;
74893
+ }
74815
74894
  sendHeldScroll(horizontal ? scrollerEl.scrollWidth : scrollerEl.scrollHeight);
74816
74895
  return;
74817
74896
  }
@@ -74889,6 +74968,13 @@ const useListScrollSync = ({
74889
74968
  if (stage === "steady") {
74890
74969
  return;
74891
74970
  }
74971
+ if (windowAskedRef.current) {
74972
+ // The hold has just aimed the window where the list opens — its end, an
74973
+ // item it found — and the next commit draws it. Sized now, the window
74974
+ // would weigh the items there by the ones drawn here, nowhere near them:
74975
+ // the one-line items at the start of a thread opening on its cards.
74976
+ return;
74977
+ }
74892
74978
  const initialBudget = initialBudgetRef.current;
74893
74979
  if (initialBudget && !paintedRef.current) {
74894
74980
  if (stage === "picture") {
@@ -75229,9 +75315,9 @@ const useListScrollSync = ({
75229
75315
  if (wouldInterruptScroll(scrollerEl)) {
75230
75316
  // Nobody is looking at items standing still: they are passing by, on
75231
75317
  // their way somewhere the browser takes them (a key, a smooth scrollTo,
75232
- // the status bar, a fling). The write would stop that scroll short of
75233
- // where it was going. Left alone, the items drawn above push the rest by
75234
- // `drift` while all of it moves, and the scroll lands.
75318
+ // the status bar, a fling on iOS). The write would stop that scroll short
75319
+ // of where it was going. Left alone, the items drawn above push the rest
75320
+ // by `drift` while all of it moves, and the scroll lands.
75235
75321
  return;
75236
75322
  }
75237
75323
  scrolledByListRef.current = true;
@@ -75360,8 +75446,14 @@ const useListScrollSync = ({
75360
75446
  bandEnd = walkAfter(hold.index, screenSize - hold.above, pixelsOf).index;
75361
75447
  forward = true;
75362
75448
  } else {
75363
- bandStart = findBandStart(items, bandFrom, itemSize);
75364
- bandEnd = bandTo > bandFrom ? findBandEnd(items, bandTo, itemSize) : bandStart;
75449
+ // In the room a filler holds, an item is counted at what the filler
75450
+ // gives it.
75451
+ const fillerSizeOf = itemSize > 0 ? index => {
75452
+ const room = listItems.roomAt(index);
75453
+ return room === undefined ? itemSize : room;
75454
+ } : null;
75455
+ bandStart = findBandStart(items, bandFrom, fillerSizeOf, total);
75456
+ bandEnd = bandTo > bandFrom ? findBandEnd(items, bandTo, fillerSizeOf, total) : bandStart;
75365
75457
  forward = scrollDirectionRef.current > 0;
75366
75458
  }
75367
75459
  if (bandStart > total) {
@@ -75610,6 +75702,8 @@ const useListScrollSync = ({
75610
75702
  };
75611
75703
  }, [scrollerElResolved]);
75612
75704
  holdWindow();
75705
+ // Whatever was asked for until now is what this render draws.
75706
+ windowAskedRef.current = false;
75613
75707
  const {
75614
75708
  start: windowStart,
75615
75709
  end: windowEnd
@@ -76270,12 +76364,49 @@ const readWindowGeometry = (scrollerEl, listEl, horizontal) => {
76270
76364
  items
76271
76365
  };
76272
76366
  };
76367
+ // The room each item drawn takes along the scroll axis (see
76368
+ // rememberItemRooms): up to the next item when that one is drawn too, so that
76369
+ // what stands between them — a separator, the label of a group the next one
76370
+ // opens — is counted; its own box otherwise. A stand-in is not measured: it
76371
+ // takes the room it is given, not the room of its item.
76372
+ const readItemRooms = (listEl, horizontal, roomById) => {
76373
+ let previous = null;
76374
+ const keepRoom = (item, next) => {
76375
+ if (item.id === null) {
76376
+ return;
76377
+ }
76378
+ roomById.set(item.id, next && next.index === item.index + 1 ? next.from - item.from : item.to - item.from);
76379
+ };
76380
+ for (const itemEl of listEl.querySelectorAll(`[${LIST_ITEM_INDEX_ATTRIBUTE}]`)) {
76381
+ if (!isOwnItem(itemEl, listEl)) {
76382
+ continue;
76383
+ }
76384
+ const rect = itemEl.getBoundingClientRect();
76385
+ if (rect.width === 0 && rect.height === 0) {
76386
+ continue;
76387
+ }
76388
+ const item = {
76389
+ id: getItemName(itemEl),
76390
+ index: Number(itemEl.getAttribute(LIST_ITEM_INDEX_ATTRIBUTE)),
76391
+ from: horizontal ? rect.left : rect.top,
76392
+ to: horizontal ? rect.right : rect.bottom
76393
+ };
76394
+ if (previous) {
76395
+ keepRoom(previous, item);
76396
+ }
76397
+ previous = item;
76398
+ }
76399
+ if (previous) {
76400
+ keepRoom(previous, null);
76401
+ }
76402
+ };
76273
76403
  // The first item the screen shows from `position` on, and the item after the
76274
76404
  // last one it shows up to `position` (exclusive, like the window's end). An
76275
76405
  // item drawn is read off its box; in the room a filler holds for items not
76276
- // drawn, the item is counted at the size the filler gives each of them — the
76277
- // only way a scroll position there says which item it is.
76278
- const findBandStart = (items, position, itemSize) => {
76406
+ // drawn, each item is counted at the size the filler gives it (`sizeOf`) —
76407
+ // the only way a scroll position there says which item it is. Without a size
76408
+ // (`null`), the room a filler holds says nothing.
76409
+ const findBandStart = (items, position, sizeOf, total) => {
76279
76410
  let low = 0;
76280
76411
  let high = items.length;
76281
76412
  while (low < high) {
@@ -76289,29 +76420,28 @@ const findBandStart = (items, position, itemSize) => {
76289
76420
  const next = items[low];
76290
76421
  const previous = items[low - 1];
76291
76422
  if (!next) {
76292
- if (!itemSize) {
76423
+ if (!sizeOf) {
76293
76424
  return previous.index + 1;
76294
76425
  }
76295
- return previous.index + 1 + Math.floor((position - previous.to) / itemSize);
76426
+ return findIndexAfter(previous.index + 1, previous.to, position, sizeOf, total);
76296
76427
  }
76297
76428
  if (next.from <= position) {
76298
76429
  return next.index;
76299
76430
  }
76300
76431
  if (!previous) {
76301
- if (!itemSize) {
76432
+ if (!sizeOf) {
76302
76433
  return next.index;
76303
76434
  }
76304
- const index = next.index - Math.ceil((next.from - position) / itemSize);
76435
+ const index = findIndexBefore(next.index, next.from, position, sizeOf);
76305
76436
  return index < 0 ? 0 : index;
76306
76437
  }
76307
- if (!itemSize || next.index === previous.index + 1) {
76438
+ if (!sizeOf || next.index === previous.index + 1) {
76308
76439
  // Between two items that follow each other: a separator, a group label.
76309
76440
  return next.index;
76310
76441
  }
76311
- const index = previous.index + 1 + Math.floor((position - previous.to) / itemSize);
76312
- return index < next.index ? index : next.index;
76442
+ return findIndexAfter(previous.index + 1, previous.to, position, sizeOf, next.index);
76313
76443
  };
76314
- const findBandEnd = (items, position, itemSize) => {
76444
+ const findBandEnd = (items, position, sizeOf, total) => {
76315
76445
  let low = 0;
76316
76446
  let high = items.length;
76317
76447
  while (low < high) {
@@ -76325,24 +76455,53 @@ const findBandEnd = (items, position, itemSize) => {
76325
76455
  const previous = items[low - 1];
76326
76456
  const next = items[low];
76327
76457
  if (!previous) {
76328
- if (!itemSize) {
76458
+ if (!sizeOf) {
76329
76459
  return next.index;
76330
76460
  }
76331
- const index = next.index - Math.ceil((next.from - position) / itemSize) + 1;
76332
- return index < 0 ? 0 : index;
76461
+ return findIndexBefore(next.index, next.from, position, sizeOf) + 1;
76333
76462
  }
76334
76463
  if (previous.to >= position) {
76335
76464
  return previous.index + 1;
76336
76465
  }
76337
- if (!itemSize || next && next.index === previous.index + 1) {
76466
+ if (!sizeOf || next && next.index === previous.index + 1) {
76338
76467
  return previous.index + 1;
76339
76468
  }
76340
- const index = previous.index + 2 + Math.floor((position - previous.to) / itemSize);
76469
+ const index = findIndexAfter(previous.index + 1, previous.to, position, sizeOf, next ? next.index : total) + 1;
76341
76470
  if (next && index > next.index) {
76342
76471
  return next.index;
76343
76472
  }
76344
76473
  return index;
76345
76474
  };
76475
+ // The item whose room holds `position`, the rooms laid one after the other
76476
+ // from `from` on, starting with the item at `index` — `limit` when `position`
76477
+ // is past them all.
76478
+ const findIndexAfter = (index, from, position, sizeOf, limit) => {
76479
+ let at = from;
76480
+ let itemIndex = index;
76481
+ while (itemIndex < limit) {
76482
+ const size = sizeOf(itemIndex);
76483
+ if (at + size > position) {
76484
+ return itemIndex;
76485
+ }
76486
+ at += size;
76487
+ itemIndex++;
76488
+ }
76489
+ return limit;
76490
+ };
76491
+ // The same, the rooms laid back from `to`, ending with the item before the
76492
+ // one at `index` — -1 when `position` is before them all.
76493
+ const findIndexBefore = (index, to, position, sizeOf) => {
76494
+ let at = to;
76495
+ let itemIndex = index;
76496
+ while (at > position) {
76497
+ if (itemIndex === 0) {
76498
+ return -1;
76499
+ }
76500
+ itemIndex--;
76501
+ at -= sizeOf(itemIndex);
76502
+ }
76503
+ return itemIndex;
76504
+ };
76346
76505
  // How much the window holds on each side of the screen, in the budget's unit:
76347
76506
  // read off the items' boxes where they stand, or — while the list holds itself
76348
76507
  // on an item, and the screen is where that item is going to stand (see
@@ -76858,16 +77017,19 @@ const Fallback = ({
76858
77017
  // every item it draws must not be redrawn — every item of it — because the size
76859
77018
  // settled after the first commit.
76860
77019
  // It holds lines: the items side by side on one take its room together, and a
76861
- // last line left short takes it all the same.
77020
+ // last line left short takes it all the same. An item the list drew before
77021
+ // takes the room it took then instead (see rememberItemRooms).
76862
77022
  const VirtualFiller = ({
76863
77023
  edge,
76864
77024
  itemCount,
76865
77025
  itemsPerLine,
77026
+ itemsRemembered,
77027
+ roomRemembered,
76866
77028
  findChunks
76867
77029
  }) => {
76868
77030
  const listItems = useContext(ListItemsContext);
76869
77031
  const lineSize = listItems.virtualItemSizeSignal.value;
76870
- const sizeToFill = Math.ceil(itemCount / itemsPerLine) * lineSize;
77032
+ const sizeToFill = Math.ceil(itemCount / itemsPerLine) * lineSize - itemsRemembered * lineSize / itemsPerLine + roomRemembered;
76871
77033
  // A filler resizing moves what stands below it — the items on screen, when it
76872
77034
  // holds the room of items above them — and it resizes in a commit of its own
76873
77035
  // when the item size settles after the list has rendered: the list puts its
@@ -76885,14 +77047,14 @@ const VirtualFiller = ({
76885
77047
  "navi-virtual-filler": edge,
76886
77048
  "aria-hidden": true,
76887
77049
  style: {
76888
- "--size-to-fill": `${sizeToFill}px`,
76889
- "--x-find-line-size": findChunks ? `${lineSize / itemsPerLine}px` : undefined
77050
+ "--size-to-fill": `${sizeToFill}px`
76890
77051
  },
76891
77052
  children: findChunks && findChunks.map(chunk => jsx("div", {
76892
77053
  className: "navi_list_find_stand_in",
76893
77054
  hidden: "until-found",
76894
77055
  style: {
76895
- "--x-find-line-count": chunk.to - chunk.from
77056
+ "--x-find-line-count": chunk.to - chunk.from,
77057
+ "--x-find-line-size": `${chunk.room === undefined ? lineSize / itemsPerLine : chunk.room}px`
76896
77058
  },
76897
77059
  children: chunk.text
76898
77060
  }, `${chunk.from}_${chunk.to}`))
@@ -77782,16 +77944,51 @@ const useRunItems = (store, {
77782
77944
  // Where an item named from outside actually sits. Only the run can answer:
77783
77945
  // items it holds but does not draw are nowhere else — a list only knows the
77784
77946
  // items it has drawn (they register themselves, see ListItemUI).
77785
- listItems.setItemLocator(ownerId, id => {
77786
- let found = null;
77787
- store.eachHeld((item, rank) => {
77788
- const itemIndex = indexOfRank(rank);
77789
- if (found === null && idOf(item, itemIndex) === id) {
77790
- found = itemIndex;
77947
+ const idAt = itemIndex => {
77948
+ if (itemIndex < runStart || itemIndex >= runEnd) {
77949
+ return undefined;
77950
+ }
77951
+ const item = getItemAt(itemIndex);
77952
+ return item === undefined ? undefined : idOf(item, itemIndex);
77953
+ };
77954
+ listItems.setItemLocator(ownerId, {
77955
+ indexOf: id => {
77956
+ let found = null;
77957
+ store.eachHeld((item, rank) => {
77958
+ const itemIndex = indexOfRank(rank);
77959
+ if (found === null && idOf(item, itemIndex) === id) {
77960
+ found = itemIndex;
77961
+ }
77962
+ });
77963
+ return found;
77964
+ },
77965
+ idAt
77966
+ });
77967
+ // The room an item took when the list last drew it (see rememberItemRooms),
77968
+ // or undefined: a filler holds an item it never drew at the item size.
77969
+ const roomOf = itemIndex => {
77970
+ const id = idAt(itemIndex);
77971
+ return id === undefined ? undefined : listItems.roomById.get(id);
77972
+ };
77973
+ const readRemembered = (from, to) => {
77974
+ const remembered = {
77975
+ room: 0,
77976
+ count: 0
77977
+ };
77978
+ if (listItems.roomById.size === 0) {
77979
+ return remembered;
77980
+ }
77981
+ let itemIndex = from;
77982
+ while (itemIndex < to) {
77983
+ const room = roomOf(itemIndex);
77984
+ if (room !== undefined) {
77985
+ remembered.room += room;
77986
+ remembered.count++;
77791
77987
  }
77792
- });
77793
- return found;
77794
- });
77988
+ itemIndex++;
77989
+ }
77990
+ return remembered;
77991
+ };
77795
77992
  useLayoutEffect(() => {
77796
77993
  return () => {
77797
77994
  listItems.dropItemLocator(ownerId);
@@ -77967,7 +78164,9 @@ const useRunItems = (store, {
77967
78164
  };
77968
78165
  // The text of the items a filler stands for, cut in chunks aligned on the
77969
78166
  // run's own ranks: the window sliding changes the chunk at its edge and
77970
- // leaves the others as they are.
78167
+ // leaves the others as they are. A chunk's lines are all as tall as the
78168
+ // room its items take in the filler, so it also ends where that room
78169
+ // changes: a match is then found where its item will be drawn.
77971
78170
  const getFindChunks = (from, to) => {
77972
78171
  if (!findText) {
77973
78172
  return null;
@@ -77977,10 +78176,11 @@ const useRunItems = (store, {
77977
78176
  while (chunkFrom < to) {
77978
78177
  const chunkIndex = Math.floor(rankOf(chunkFrom) / FIND_CHUNK_ITEM_COUNT);
77979
78178
  const alignedTo = indexOfRank((chunkIndex + 1) * FIND_CHUNK_ITEM_COUNT);
77980
- const chunkTo = alignedTo < to ? alignedTo : to;
78179
+ const chunkEnd = alignedTo < to ? alignedTo : to;
78180
+ const room = roomOf(chunkFrom);
77981
78181
  const lines = [];
77982
78182
  let lineIndex = chunkFrom;
77983
- while (lineIndex < chunkTo) {
78183
+ while (lineIndex < chunkEnd && roomOf(lineIndex) === room) {
77984
78184
  const item = getItemAt(lineIndex);
77985
78185
  // An item not held has nothing to find, but keeps its line: the items
77986
78186
  // after it stay at their place.
@@ -77989,10 +78189,11 @@ const useRunItems = (store, {
77989
78189
  }
77990
78190
  chunks.push({
77991
78191
  from: chunkFrom,
77992
- to: chunkTo,
78192
+ to: lineIndex,
78193
+ room,
77993
78194
  text: lines.join("\n")
77994
78195
  });
77995
- chunkFrom = chunkTo;
78196
+ chunkFrom = lineIndex;
77996
78197
  }
77997
78198
  return chunks;
77998
78199
  };
@@ -78001,10 +78202,13 @@ const useRunItems = (store, {
78001
78202
  // one run, and what sits before or after it (a header, items given one by
78002
78203
  // one) is not virtualized at all.
78003
78204
  if (windowFrom > runStart) {
78205
+ const remembered = readRemembered(runStart, windowFrom);
78004
78206
  nodes.push(jsx(VirtualFiller, {
78005
78207
  edge: "before",
78006
78208
  itemCount: windowFrom - runStart,
78007
78209
  itemsPerLine: itemsPerLine,
78210
+ itemsRemembered: remembered.count,
78211
+ roomRemembered: remembered.room,
78008
78212
  findChunks: getFindChunks(runStart, windowFrom)
78009
78213
  }, "navi-list-filler-before"));
78010
78214
  }
@@ -78106,10 +78310,13 @@ const useRunItems = (store, {
78106
78310
  }
78107
78311
  closeGroup();
78108
78312
  if (runEnd > windowTo) {
78313
+ const remembered = readRemembered(windowTo, runEnd);
78109
78314
  nodes.push(jsx(VirtualFiller, {
78110
78315
  edge: "after",
78111
78316
  itemCount: runEnd - windowTo,
78112
78317
  itemsPerLine: itemsPerLine,
78318
+ itemsRemembered: remembered.count,
78319
+ roomRemembered: remembered.room,
78113
78320
  findChunks: getFindChunks(windowTo, runEnd)
78114
78321
  }, "navi-list-filler-after"));
78115
78322
  }
@@ -79044,19 +79251,20 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
79044
79251
  * source for is a page (see `<List.Items pageSize>`), whatever either says,
79045
79252
  * so the first picture costs no second request.
79046
79253
  * @param {number} [props.virtualItemSize]
79047
- * The room an item not drawn is held at, in px along the scroll axis: what
79048
- * the fillers hold for each item outside the render window, what a scroll
79049
- * position inside them is read with, and the least an item on its way takes
79050
- * (see `renderSkeleton`). Left out, it is the average of the items measured
79051
- * so far — once when the list mounts, again when a popup around it opens,
79052
- * and after each commit while items are held off screen. Given, it is the
79053
- * worst case: the size every item has, or, in a list whose items differ, the
79054
- * smallest an item can be (a thread of one-line items and cards: the
79055
- * one-line size) — an item guessed too small builds one more, an item
79056
- * guessed too big leaves a blank (see docs/scroll.md, "What the list knows,
79057
- * and what it guesses"). The items drawn are measured either way: the render
79058
- * window is sized on them. In a grid (`columns`), the size is a line's: a
79059
- * card's, which the items side by side on it share.
79254
+ * The room an item never drawn is held at, in px along the scroll axis: what
79255
+ * the fillers hold for it outside the render window, what a scroll position
79256
+ * inside them is read with, and the least an item on its way takes (see
79257
+ * `renderSkeleton`). An item the list has drawn is held at the room it took
79258
+ * then. Left out, it is the average of the items measured so far — once when
79259
+ * the list mounts, again when a popup around it opens, and after each commit
79260
+ * while items are held off screen. Given, it is the worst case: the size
79261
+ * every item has, or, in a list whose items differ, the smallest an item can
79262
+ * be (a thread of one-line items and cards: the one-line size) — an item
79263
+ * guessed too small builds one more, an item guessed too big leaves a blank
79264
+ * (see docs/scroll.md, "What the list knows, and what it guesses"). The items
79265
+ * drawn are measured either way: the render window is sized on them. In a
79266
+ * grid (`columns`), the size is a line's: a card's, which the items side by
79267
+ * side on it share, and the lines drawn are held at it too.
79060
79268
  * @param {"self"|"parent"|"document"|Element|{current: Element}} [props.scroller="self"]
79061
79269
  * Which box scrolls — and with it, which box the render window follows and
79062
79270
  * which box a scroll position is read from (`onScrolledChange`). `"self"`