@jsenv/navi 0.29.442 → 0.29.443

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.
@@ -71419,15 +71419,16 @@ document.addEventListener(
71419
71419
  );
71420
71420
 
71421
71421
  /*
71422
- * Whether writing a scroll position now would cost the user the scroll in
71423
- * progress. A write is an instant scroll, and the browser abandons for it any
71424
- * scroll it is playing on its own, short of where that one was going: a key
71425
- * (Home, PageDown, Space), a smooth `scrollTo` or `scrollIntoView`, the status
71426
- * bar tap on iOS. A scroll following a hand is not abandoned: the wheel's next
71427
- * turn carries on, and on most engines a finger keeps dragging from wherever
71428
- * the write left it, and the fling it leaves behind once it lifts carries on
71429
- * from there too (Chrome under DevTools' touch emulation: flings written to at
71430
- * every move of a list's window went as far as flings never written to).
71422
+ * Moves a scroll by what the content moved under the user, unless the write
71423
+ * would cost them the scroll in progress. A write is an instant scroll, and
71424
+ * the browser abandons for it any scroll it is playing on its own, short of
71425
+ * where that one was going: a key (Home, PageDown, Space), a smooth `scrollTo`
71426
+ * or `scrollIntoView`, the status bar tap on iOS. A scroll following a hand is
71427
+ * not abandoned: the wheel's next turn carries on, and on most engines a
71428
+ * finger keeps dragging from wherever the write left it, and the fling it
71429
+ * leaves behind once it lifts carries on from there too (Chrome under
71430
+ * DevTools' touch emulation: flings written to at every move of a list's
71431
+ * window went as far as flings never written to).
71431
71432
  *
71432
71433
  * A fling must not be taken for a scroll the browser plays on its own. A list
71433
71434
  * then leaves uncorrected, for the whole fling, the room its fillers lose above
@@ -71454,6 +71455,18 @@ document.addEventListener(
71454
71455
  * while the finger is still on one of them, and the touchend of a removed
71455
71456
  * element never reaches the document.
71456
71457
  *
71458
+ * Chrome ends a scroll at every write, a finger's fling included: "scrollend"
71459
+ * a frame after the write, and the fling goes on a frame later as a scroll of
71460
+ * its own, as far as it was going (a bare page written to every 120ms of a
71461
+ * fling: 9 "scrollend" for 8 writes, the fling as long). Taken for a new
71462
+ * scroll, it is nobody's once it starts far enough from the finger's last
71463
+ * move, and the list's corrections are refused for the rest of the fling: in
71464
+ * wematch, cards drawn above the screen pushing it down at every window slide.
71465
+ * So a scroll starting right after the "scrollend" of a scroll written to here
71466
+ * goes on as that scroll: same start, same hand. Only a written one: a scroll
71467
+ * starting right after one that ended on its own is a new one, a key or a
71468
+ * smooth `scrollTo` must not pass for the fling before it.
71469
+ *
71457
71470
  * In a browser without "scrollend", no write is said to interrupt anything: a
71458
71471
  * scroll would never be seen ending.
71459
71472
  */
@@ -71463,8 +71476,8 @@ document.addEventListener(
71463
71476
  // hand on a scroll that came from elsewhere.
71464
71477
  const WHEEL_REACH = 100;
71465
71478
  // The fling Chrome plays as a scroll of its own starts a frame or two after the
71466
- // drag's last move (DevTools' touch emulation, 4× CPU: 12ms after the drag's
71467
- // "scrollend").
71479
+ // "scrollend" of the drag, or of a write (DevTools' touch emulation, 4× CPU:
71480
+ // 12ms after the drag's, 24 to 40ms after a write's in wematch).
71468
71481
  const FLING_REACH = 150;
71469
71482
  // iOS's touch handling, the one engine whose finger drops writes (see above).
71470
71483
  const FINGER_KEEPS_WRITES = !window.CSS.supports(
@@ -71473,14 +71486,41 @@ const FINGER_KEEPS_WRITES = !window.CSS.supports(
71473
71486
  );
71474
71487
 
71475
71488
  const flightMap = new WeakMap();
71489
+ // Scrollers written to since their last "scrollend": that "scrollend" may be
71490
+ // the write's.
71491
+ const writtenScrollerSet = new WeakSet();
71492
+ // The flight of a written scroller, from its "scrollend" until a scroll goes on
71493
+ // with it.
71494
+ const writtenFlightEndMap = new WeakMap();
71476
71495
  let fingersDown = 0;
71477
71496
  let wheelTimeStamp = -Infinity;
71478
71497
  let fingerScrollTimeStamp = -Infinity;
71479
71498
 
71480
71499
  /**
71500
+ * Answers whether it wrote.
71501
+ *
71481
71502
  * @param {Element} scrollerEl
71503
+ * @param {number} delta
71504
+ * @param {{ horizontal?: boolean }} [options]
71482
71505
  * @returns {boolean}
71483
71506
  */
71507
+ const scrollByUnlessInterrupting = (
71508
+ scrollerEl,
71509
+ delta,
71510
+ { horizontal } = {},
71511
+ ) => {
71512
+ if (wouldInterruptScroll(scrollerEl)) {
71513
+ return false;
71514
+ }
71515
+ writtenScrollerSet.add(scrollerEl);
71516
+ if (horizontal) {
71517
+ scrollerEl.scrollLeft += delta;
71518
+ } else {
71519
+ scrollerEl.scrollTop += delta;
71520
+ }
71521
+ return true;
71522
+ };
71523
+
71484
71524
  const wouldInterruptScroll = (scrollerEl) => {
71485
71525
  const flight = flightMap.get(scrollerEl);
71486
71526
  if (!flight) {
@@ -71523,6 +71563,15 @@ if ("onscrollend" in window) {
71523
71563
  if (flightMap.has(scroller)) {
71524
71564
  return;
71525
71565
  }
71566
+ const writtenFlightEnd = writtenFlightEndMap.get(scroller);
71567
+ writtenFlightEndMap.delete(scroller);
71568
+ if (
71569
+ writtenFlightEnd &&
71570
+ e.timeStamp - writtenFlightEnd.timeStamp < FLING_REACH
71571
+ ) {
71572
+ flightMap.set(scroller, writtenFlightEnd.flight);
71573
+ return;
71574
+ }
71526
71575
  flightMap.set(scroller, { timeStamp: e.timeStamp });
71527
71576
  },
71528
71577
  { capture: true, passive: true },
@@ -71530,7 +71579,13 @@ if ("onscrollend" in window) {
71530
71579
  document.addEventListener(
71531
71580
  "scrollend",
71532
71581
  (e) => {
71533
- flightMap.delete(asScroller(e.target));
71582
+ const scroller = asScroller(e.target);
71583
+ const flight = flightMap.get(scroller);
71584
+ flightMap.delete(scroller);
71585
+ if (flight && writtenScrollerSet.has(scroller)) {
71586
+ writtenFlightEndMap.set(scroller, { flight, timeStamp: e.timeStamp });
71587
+ }
71588
+ writtenScrollerSet.delete(scroller);
71534
71589
  },
71535
71590
  { capture: true, passive: true },
71536
71591
  );
@@ -72252,13 +72307,24 @@ const createListItems = () => {
72252
72307
  }
72253
72308
  return null;
72254
72309
  },
72255
- // The room the item at that index took when it was last drawn, or
72256
- // undefined: never drawn, not held, or not an item of a run.
72310
+ // The room the item at that index takes: the room it took when it was
72311
+ // last drawn, else the room its run is given for it (see List.Items'
72312
+ // itemSize), else undefined.
72257
72313
  roomAt: (index) => {
72258
72314
  for (const locator of locatorByOwner.values()) {
72259
- const id = locator.idAt(index);
72260
- if (id !== undefined) {
72261
- return roomById.get(id);
72315
+ const room = locator.roomAt(index);
72316
+ if (room !== undefined) {
72317
+ return room;
72318
+ }
72319
+ }
72320
+ return undefined;
72321
+ },
72322
+ // The room the run of that index is given for it, whatever was measured.
72323
+ givenRoomAt: (index) => {
72324
+ for (const locator of locatorByOwner.values()) {
72325
+ const room = locator.givenRoomAt(index);
72326
+ if (room !== undefined) {
72327
+ return room;
72262
72328
  }
72263
72329
  }
72264
72330
  return undefined;
@@ -73747,6 +73813,23 @@ const css$x = /* css */`@property --list-group-label-height {
73747
73813
  &[data-hidden-while-empty]:not(:has([navi-list-item-real])) {
73748
73814
  display: none;
73749
73815
  }
73816
+
73817
+ &[data-continued] {
73818
+ margin-top: calc(-1 * var(--x-list-gap, 0px));
73819
+ display: grid;
73820
+
73821
+ & > .navi_list_item_group_label, & > .navi_list_item_group_list {
73822
+ grid-area: 1 / 1;
73823
+ }
73824
+
73825
+ & > .navi_list_item_group_label {
73826
+ align-self: start;
73827
+ }
73828
+ }
73829
+
73830
+ &[data-continued="hole"] > .navi_list_item_group_label {
73831
+ visibility: hidden;
73832
+ }
73750
73833
  }
73751
73834
 
73752
73835
  .navi_list_container[data-horizontal] > .navi_list_scroll_container > .navi_list > .navi_list_item_group {
@@ -74528,6 +74611,7 @@ const useListScrollSync = ({
74528
74611
  // that changed since is measured again when drawn again, and the anchoring
74529
74612
  // holds the view across the difference. Items laid side by side in a grid
74530
74613
  // share a line, which the fillers hold at the one size.
74614
+ useRef(false);
74531
74615
  const rememberItemRooms = () => {
74532
74616
  if (!ref.current || itemsPerLineRef.current !== 1) {
74533
74617
  return;
@@ -74536,7 +74620,12 @@ const useListScrollSync = ({
74536
74620
  if (!listEl) {
74537
74621
  return;
74538
74622
  }
74539
- readItemRooms(listEl, horizontal, listItems.roomById);
74623
+ readItemRooms(listEl, horizontal, (item, room, complete) => {
74624
+ listItems.roomById.set(item.id, room);
74625
+ {
74626
+ return;
74627
+ }
74628
+ });
74540
74629
  };
74541
74630
  const updateRenderWindow = (newStart, newEnd, reason) => {
74542
74631
  const {
@@ -75294,18 +75383,14 @@ const useListScrollSync = ({
75294
75383
  if (delta > -0.5 && delta < 0.5) {
75295
75384
  return;
75296
75385
  }
75297
- if (wouldInterruptScroll(scrollerEl)) {
75298
- // As the anchoring's write would (see holdAnchorStill). The document's
75299
- // box is the scroller of a list scrolling the page, and it resizes with
75300
- // every item drawn taller than its filler: while the page scrolls, that
75301
- // is at every window move.
75302
- return;
75303
- }
75304
- anchorRef.current = null;
75305
- if (horizontal) {
75306
- scrollerEl.scrollLeft += delta;
75307
- } else {
75308
- scrollerEl.scrollTop += delta;
75386
+ // Not into a scroll it would cut short, as the anchoring's write (see
75387
+ // holdAnchorStill). The document's box is the scroller of a list
75388
+ // scrolling the page, and it resizes with every item drawn taller than
75389
+ // its filler: while the page scrolls, that is at every window move.
75390
+ if (scrollByUnlessInterrupting(scrollerEl, delta, {
75391
+ horizontal
75392
+ })) {
75393
+ anchorRef.current = null;
75309
75394
  }
75310
75395
  };
75311
75396
  const observer = new ResizeObserver(entries => {
@@ -75398,19 +75483,15 @@ const useListScrollSync = ({
75398
75483
  return;
75399
75484
  }
75400
75485
  anchorRef.current = null;
75401
- if (wouldInterruptScroll(scrollerEl)) {
75402
- // Nobody is looking at items standing still: they are passing by, on
75403
- // their way somewhere the browser takes them (a key, a smooth scrollTo,
75404
- // the status bar, a fling on iOS). The write would stop that scroll short
75405
- // of where it was going. Left alone, the items drawn above push the rest
75406
- // by `drift` while all of it moves, and the scroll lands.
75407
- return;
75408
- }
75409
- scrolledByListRef.current = true;
75410
- if (horizontal) {
75411
- scrollerEl.scrollLeft += drift;
75412
- } else {
75413
- scrollerEl.scrollTop += drift;
75486
+ // Not written into a scroll nobody is looking at: its items are passing
75487
+ // by, on their way somewhere the browser takes them (a key, a smooth
75488
+ // scrollTo, the status bar, a fling on iOS). The write would stop that
75489
+ // scroll short of where it was going. Left alone, the items drawn above
75490
+ // push the rest by `drift` while all of it moves, and the scroll lands.
75491
+ if (scrollByUnlessInterrupting(scrollerEl, drift, {
75492
+ horizontal
75493
+ })) {
75494
+ scrolledByListRef.current = true;
75414
75495
  }
75415
75496
  };
75416
75497
  useLayoutEffect(holdAnchorStill);
@@ -76451,18 +76532,17 @@ const readWindowGeometry = (scrollerEl, listEl, horizontal) => {
76451
76532
  };
76452
76533
  };
76453
76534
  // The room each item drawn takes along the scroll axis (see
76454
- // rememberItemRooms): up to the next item when that one is drawn too, so that
76455
- // what stands between them — a separator, the label of a group the next one
76456
- // opens — is counted; its own box otherwise. A stand-in is not measured: it
76457
- // takes the room it is given, not the room of its item.
76458
- const readItemRooms = (listEl, horizontal, roomById) => {
76535
+ // rememberItemRooms): from where the item before it ends to where it ends, so
76536
+ // that what stands between the two — a separator, the label of the group it
76537
+ // opens — is counted with the item it comes with. A group the window cuts
76538
+ // draws its label without room (see the data-continued rule above): that
76539
+ // label's room is held by the item that opens the group, in the filler. The
76540
+ // first item drawn is measured from the end of what stands before it in the
76541
+ // list (a filler, a header); a stand-in is not measured, it takes the room it
76542
+ // is given and not the room of its item, but an item after it ends where it
76543
+ // ends. `complete` says whether the room was read up to an end before it.
76544
+ const readItemRooms = (listEl, horizontal, onRoom) => {
76459
76545
  let previous = null;
76460
- const keepRoom = (item, next) => {
76461
- if (item.id === null) {
76462
- return;
76463
- }
76464
- roomById.set(item.id, next && next.index === item.index + 1 ? next.from - item.from : item.to - item.from);
76465
- };
76466
76546
  for (const itemEl of listEl.querySelectorAll(`[${LIST_ITEM_INDEX_ATTRIBUTE}]`)) {
76467
76547
  if (!isOwnItem(itemEl, listEl)) {
76468
76548
  continue;
@@ -76477,14 +76557,41 @@ const readItemRooms = (listEl, horizontal, roomById) => {
76477
76557
  from: horizontal ? rect.left : rect.top,
76478
76558
  to: horizontal ? rect.right : rect.bottom
76479
76559
  };
76480
- if (previous) {
76481
- keepRoom(previous, item);
76560
+ if (item.id !== null) {
76561
+ const endBefore = previous && previous.index === item.index - 1 ? previous.to : readEndBefore(itemEl, listEl, horizontal);
76562
+ if (endBefore === null) {
76563
+ onRoom(item, item.to - item.from, false);
76564
+ } else {
76565
+ onRoom(item, item.to - endBefore, true);
76566
+ }
76482
76567
  }
76483
76568
  previous = item;
76484
76569
  }
76485
- if (previous) {
76486
- keepRoom(previous, null);
76570
+ };
76571
+ // Where what stands before an item in the list ends: the element before the
76572
+ // item, or before its group when it is the first of it — or the start of the
76573
+ // list's content when nothing does. Null when the element before is not laid
76574
+ // out.
76575
+ const readEndBefore = (itemEl, listEl, horizontal) => {
76576
+ let slotEl = itemEl;
76577
+ const groupListEl = itemEl.parentElement;
76578
+ if (groupListEl && groupListEl.classList.contains("navi_list_item_group_list") && groupListEl.firstElementChild === itemEl) {
76579
+ slotEl = groupListEl.closest(".navi_list_item_group") || itemEl;
76580
+ }
76581
+ const beforeEl = slotEl.previousElementSibling;
76582
+ if (beforeEl) {
76583
+ const rect = beforeEl.getBoundingClientRect();
76584
+ if (rect.width === 0 && rect.height === 0) {
76585
+ return null;
76586
+ }
76587
+ return horizontal ? rect.right : rect.bottom;
76487
76588
  }
76589
+ if (slotEl.parentElement !== listEl) {
76590
+ return null;
76591
+ }
76592
+ const listRect = listEl.getBoundingClientRect();
76593
+ const listStyle = window.getComputedStyle(listEl);
76594
+ return horizontal ? listRect.left + parseFloat(listStyle.borderLeftWidth) + parseFloat(listStyle.paddingLeft) : listRect.top + parseFloat(listStyle.borderTopWidth) + parseFloat(listStyle.paddingTop);
76488
76595
  };
76489
76596
  // The first item the screen shows from `position` on, and the item after the
76490
76597
  // last one it shows up to `position` (exclusive, like the window's end). An
@@ -77034,6 +77141,8 @@ const UnorderedList = ({
77034
77141
  grid: trackColumns ? true : undefined,
77035
77142
  gridTemplateColumns: trackColumns,
77036
77143
  ...rest,
77144
+ // The gap a group the window cuts takes back (see data-continued).
77145
+ style: withListGap(rest.style, spacing),
77037
77146
  spacing: spacing,
77038
77147
  baseClassName: "navi_list",
77039
77148
  children: [!suppressFallback && searchFallbackShown && jsx(SearchFallback, {
@@ -77066,6 +77175,21 @@ const UnorderedList = ({
77066
77175
  })]
77067
77176
  });
77068
77177
  };
77178
+ // The list's spacing as a variable, next to the style it is given (an object
77179
+ // or a css string, as Box takes either).
77180
+ const withListGap = (style, spacing) => {
77181
+ if (spacing === undefined) {
77182
+ return style;
77183
+ }
77184
+ const gap = stringifySpacingStyle(spacing, "gap");
77185
+ if (typeof style === "string") {
77186
+ return `${style};--x-list-gap:${gap}`;
77187
+ }
77188
+ return {
77189
+ ...style,
77190
+ "--x-list-gap": gap
77191
+ };
77192
+ };
77069
77193
 
77070
77194
  // The "no match" message. Whether it shows is decided by ListUI (see its
77071
77195
  // searchFallbackShown): a search left nothing to display, and the caller did
@@ -77103,19 +77227,20 @@ const Fallback = ({
77103
77227
  // every item it draws must not be redrawn — every item of it — because the size
77104
77228
  // settled after the first commit.
77105
77229
  // It holds lines: the items side by side on one take its room together, and a
77106
- // last line left short takes it all the same. An item the list drew before
77107
- // takes the room it took then instead (see rememberItemRooms).
77230
+ // last line left short takes it all the same. An item whose room is known takes
77231
+ // that room instead: the one it took when the list drew it before (see
77232
+ // rememberItemRooms), or the one its run is given (see List.Items' itemSize).
77108
77233
  const VirtualFiller = ({
77109
77234
  edge,
77110
77235
  itemCount,
77111
77236
  itemsPerLine,
77112
- itemsRemembered,
77113
- roomRemembered,
77237
+ itemsKnown,
77238
+ roomKnown,
77114
77239
  findChunks
77115
77240
  }) => {
77116
77241
  const listItems = useContext(ListItemsContext);
77117
77242
  const lineSize = listItems.virtualItemSizeSignal.value;
77118
- const sizeToFill = Math.ceil(itemCount / itemsPerLine) * lineSize - itemsRemembered * lineSize / itemsPerLine + roomRemembered;
77243
+ const sizeToFill = Math.ceil(itemCount / itemsPerLine) * lineSize - itemsKnown * lineSize / itemsPerLine + roomKnown;
77119
77244
  // A filler resizing moves what stands below it — the items on screen, when it
77120
77245
  // holds the room of items above them — and it resizes in a commit of its own
77121
77246
  // when the item size settles after the list has rendered: the list puts its
@@ -77829,6 +77954,7 @@ const VISIBILITY_HIDDEN_STYLE = {
77829
77954
  * pageSize?: number,
77830
77955
  * memoryBudget?: number,
77831
77956
  * renderSkeleton?: false | ((index: number) => import("ignore:preact").ComponentChildren),
77957
+ * itemSize?: (index: number) => number,
77832
77958
  * renderError?: (failure: {error: any, retry: () => void, start: number, end: number}) => import("ignore:preact").ComponentChildren,
77833
77959
  * onRequestStateChange?: (state: {busy: boolean, refreshing: boolean, range: {start: number, end: number}|null}) => void,
77834
77960
  * }>}
@@ -77861,6 +77987,10 @@ const VISIBILITY_HIDDEN_STYLE = {
77861
77987
  * `<List.Group>` whose label (`renderGroupLabel`) stays on screen for as long
77862
77988
  * as one of them is. The groups are found in the data as it arrives, which is
77863
77989
  * the only way a list that discovers its items page by page can have any.
77990
+ * Style the label, not the group element: a group the render window cuts is
77991
+ * laid out by the list, its label over its first items, so that the label
77992
+ * and the gap before it — held in the room of the item opening the group —
77993
+ * take no room twice.
77864
77994
  * @param {(item: any, index: number) => any} [props.renderGroupLabel]
77865
77995
  * The label of the group an item opens, given that item.
77866
77996
  * @param {(item: any, index: number) => object} [props.groupLabelProps]
@@ -77879,9 +78009,26 @@ const VISIBILITY_HIDDEN_STYLE = {
77879
78009
  * further out. `Infinity` keeps every item the run ever received; `0` keeps
77880
78010
  * only the ones around the window.
77881
78011
  * @param {false|(index: number) => any} [props.renderSkeleton]
77882
- * What to draw for an item the run does not hold. Defaults to List's own
78012
+ * What to draw for an item the run does not hold, at the room that item will
78013
+ * take: a screen of skeletons alone has no item for the list to hold still,
78014
+ * and when the page lands, what is below a skeleton of the wrong size moves
78015
+ * by the difference (see docs/scroll.md). Defaults to List's own
77883
78016
  * `renderSkeleton`, then to a bare `<List.Item skeleton>`; `false` leaves the
77884
78017
  * item empty (its room is still held, or the list would jump as it loads).
78018
+ * @param {(index: number) => number} [props.itemSize]
78019
+ * The room the item at an index takes along the scroll axis, in px, known
78020
+ * before the item arrives: from where the item before it ends to where it
78021
+ * ends — what stands between the two is its own (the gap, a separator, the
78022
+ * label of the group it opens; the first item of the list has no gap before
78023
+ * it). The fillers then hold every item the list has not drawn at its own
78024
+ * room instead of List's `virtualItemSize`, and an item drawn above the
78025
+ * screen takes exactly the room held for it: nothing on screen moves. It is
78026
+ * the only way the screen holds still during a fling on iOS, where writing
78027
+ * the scroll to correct it would stop the fling. The same index
78028
+ * `renderSkeleton` is given — the two draw on the same knowledge. A room
78029
+ * measured on an item drawn wins over it, and in development one that
78030
+ * differs from it is reported. Items side by side in a grid (`columns`)
78031
+ * share a line, which keeps `virtualItemSize`.
77885
78032
  * @param {(failure: {error: any, retry: () => void, start: number, end: number}) => any} [props.renderError]
77886
78033
  * What to draw where items were asked for and never came: given the `error`,
77887
78034
  * a `retry` to call, and the `start`/`end` of the range that failed — the
@@ -77954,6 +78101,7 @@ const useRunItems = (store, {
77954
78101
  renderItem,
77955
78102
  findText,
77956
78103
  pageSize,
78104
+ itemSize,
77957
78105
  groupBy,
77958
78106
  renderGroupLabel,
77959
78107
  groupLabelProps,
@@ -78037,6 +78185,22 @@ const useRunItems = (store, {
78037
78185
  const item = getItemAt(itemIndex);
78038
78186
  return item === undefined ? undefined : idOf(item, itemIndex);
78039
78187
  };
78188
+ // The room the caller says an item takes (`itemSize`), drawn or not.
78189
+ const givenRoomOf = itemIndex => {
78190
+ if (!itemSize || itemsPerLine !== 1 || itemIndex < runStart || itemIndex >= runEnd) {
78191
+ return undefined;
78192
+ }
78193
+ const room = itemSize(itemIndex);
78194
+ return room > 0 ? room : undefined;
78195
+ };
78196
+ // The room an item took when the list last drew it (see rememberItemRooms),
78197
+ // else the room the caller gives it, else undefined: a filler holds an item
78198
+ // it knows nothing about at the item size.
78199
+ const roomOf = itemIndex => {
78200
+ const id = idAt(itemIndex);
78201
+ const remembered = id === undefined ? undefined : listItems.roomById.get(id);
78202
+ return remembered === undefined ? givenRoomOf(itemIndex) : remembered;
78203
+ };
78040
78204
  listItems.setItemLocator(ownerId, {
78041
78205
  indexOf: id => {
78042
78206
  let found = null;
@@ -78048,32 +78212,27 @@ const useRunItems = (store, {
78048
78212
  });
78049
78213
  return found;
78050
78214
  },
78051
- idAt
78215
+ roomAt: roomOf,
78216
+ givenRoomAt: givenRoomOf
78052
78217
  });
78053
- // The room an item took when the list last drew it (see rememberItemRooms),
78054
- // or undefined: a filler holds an item it never drew at the item size.
78055
- const roomOf = itemIndex => {
78056
- const id = idAt(itemIndex);
78057
- return id === undefined ? undefined : listItems.roomById.get(id);
78058
- };
78059
- const readRemembered = (from, to) => {
78060
- const remembered = {
78218
+ const readKnownRooms = (from, to) => {
78219
+ const known = {
78061
78220
  room: 0,
78062
78221
  count: 0
78063
78222
  };
78064
- if (listItems.roomById.size === 0) {
78065
- return remembered;
78223
+ if (listItems.roomById.size === 0 && !itemSize) {
78224
+ return known;
78066
78225
  }
78067
78226
  let itemIndex = from;
78068
78227
  while (itemIndex < to) {
78069
78228
  const room = roomOf(itemIndex);
78070
78229
  if (room !== undefined) {
78071
- remembered.room += room;
78072
- remembered.count++;
78230
+ known.room += room;
78231
+ known.count++;
78073
78232
  }
78074
78233
  itemIndex++;
78075
78234
  }
78076
- return remembered;
78235
+ return known;
78077
78236
  };
78078
78237
  useLayoutEffect(() => {
78079
78238
  return () => {
@@ -78194,6 +78353,7 @@ const useRunItems = (store, {
78194
78353
  nodes.push(jsx(ListItemGroup, {
78195
78354
  label: group.label,
78196
78355
  labelProps: group.labelProps,
78356
+ continued: group.continued,
78197
78357
  children: group.children
78198
78358
  }, groupCount === 1 ? `${ownerId}_group_${group.key}` : `${ownerId}_group_${group.key}_${groupCount}`));
78199
78359
  group = null;
@@ -78231,10 +78391,38 @@ const useRunItems = (store, {
78231
78391
  }
78232
78392
  return groupBy(item, itemIndex);
78233
78393
  };
78394
+ // Where a group opened at that item began, when not there (see
78395
+ // data-continued): "above" the window, or in the "hole" of items not held
78396
+ // drawn right before it. Items not held belong to the group they lead to as
78397
+ // often as not — a page lands every hundred items, a group starts a few
78398
+ // times in a thread — so a group the window starts in is taken to go on
78399
+ // above it unless the item before, held, says otherwise.
78400
+ // Along y only, never built along x: there a group stands its label over
78401
+ // its items, so the label takes no room on the axis (unless it is wider than
78402
+ // the items drawn) and only the gap would be taken back — `margin-left`,
78403
+ // without the overlay of the data-continued rule. Nothing measured it yet.
78404
+ let afterHole = false;
78405
+ const whereGroupContinues = (groupKey, itemIndex) => {
78406
+ if (listItems.horizontal) {
78407
+ return undefined;
78408
+ }
78409
+ if (afterHole) {
78410
+ return "hole";
78411
+ }
78412
+ if (itemIndex !== windowFrom || windowFrom <= runStart) {
78413
+ return undefined;
78414
+ }
78415
+ const itemBefore = getItemAt(windowFrom - 1);
78416
+ if (itemBefore === undefined || groupBy(itemBefore, windowFrom - 1) === groupKey) {
78417
+ return "above";
78418
+ }
78419
+ return undefined;
78420
+ };
78234
78421
  const pushItem = (itemNode, item, itemIndex, groupKey) => {
78235
78422
  if (groupKey === undefined) {
78236
78423
  closeGroup();
78237
78424
  nodes.push(itemNode);
78425
+ afterHole = item === undefined;
78238
78426
  return;
78239
78427
  }
78240
78428
  if (!group || group.key !== groupKey) {
@@ -78243,9 +78431,11 @@ const useRunItems = (store, {
78243
78431
  key: groupKey,
78244
78432
  label: renderGroupLabel ? renderGroupLabel(item, itemIndex) : groupKey,
78245
78433
  labelProps: groupLabelProps ? groupLabelProps(item, itemIndex) : undefined,
78434
+ continued: whereGroupContinues(groupKey, itemIndex),
78246
78435
  children: []
78247
78436
  };
78248
78437
  }
78438
+ afterHole = false;
78249
78439
  group.children.push(itemNode);
78250
78440
  };
78251
78441
  // The text of the items a filler stands for, cut in chunks aligned on the
@@ -78288,13 +78478,13 @@ const useRunItems = (store, {
78288
78478
  // one run, and what sits before or after it (a header, items given one by
78289
78479
  // one) is not virtualized at all.
78290
78480
  if (windowFrom > runStart) {
78291
- const remembered = readRemembered(runStart, windowFrom);
78481
+ const known = readKnownRooms(runStart, windowFrom);
78292
78482
  nodes.push(jsx(VirtualFiller, {
78293
78483
  edge: "before",
78294
78484
  itemCount: windowFrom - runStart,
78295
78485
  itemsPerLine: itemsPerLine,
78296
- itemsRemembered: remembered.count,
78297
- roomRemembered: remembered.room,
78486
+ itemsKnown: known.count,
78487
+ roomKnown: known.room,
78298
78488
  findChunks: getFindChunks(runStart, windowFrom)
78299
78489
  }, "navi-list-filler-before"));
78300
78490
  }
@@ -78321,6 +78511,7 @@ const useRunItems = (store, {
78321
78511
  retry: store.retry
78322
78512
  })
78323
78513
  }, `${ownerId}_failure_${failureFrom}`));
78514
+ afterHole = false;
78324
78515
  itemIndex = failureTo + 1;
78325
78516
  continue;
78326
78517
  }
@@ -78396,13 +78587,13 @@ const useRunItems = (store, {
78396
78587
  }
78397
78588
  closeGroup();
78398
78589
  if (runEnd > windowTo) {
78399
- const remembered = readRemembered(windowTo, runEnd);
78590
+ const known = readKnownRooms(windowTo, runEnd);
78400
78591
  nodes.push(jsx(VirtualFiller, {
78401
78592
  edge: "after",
78402
78593
  itemCount: runEnd - windowTo,
78403
78594
  itemsPerLine: itemsPerLine,
78404
- itemsRemembered: remembered.count,
78405
- roomRemembered: remembered.room,
78595
+ itemsKnown: known.count,
78596
+ roomKnown: known.room,
78406
78597
  findChunks: getFindChunks(windowTo, runEnd)
78407
78598
  }, "navi-list-filler-after"));
78408
78599
  }
@@ -79047,12 +79238,17 @@ const useItemStore = ({
79047
79238
  * hiddenWhileEmpty — the group leaves the flow (`display: none`) while it
79048
79239
  * holds no real item — a search that emptied it, items not
79049
79240
  * arrived yet
79241
+ * continued — set by a run of items, not by callers: the group began
79242
+ * before the first item drawn ("above" the render window,
79243
+ * or in a "hole" of items not held), and what stands
79244
+ * before that item takes no room (see data-continued)
79050
79245
  * ...rest — forwarded to the outer <li role="presentation">
79051
79246
  */
79052
79247
  const ListItemGroup = ({
79053
79248
  label,
79054
79249
  labelProps,
79055
79250
  hiddenWhileEmpty,
79251
+ continued,
79056
79252
  children,
79057
79253
  ...rest
79058
79254
  }) => {
@@ -79095,6 +79291,7 @@ const ListItemGroup = ({
79095
79291
  baseClassName: "navi_list_item_group",
79096
79292
  role: "presentation",
79097
79293
  "data-hidden-while-empty": hiddenWhileEmpty ? "" : undefined,
79294
+ "data-continued": continued,
79098
79295
  children: [jsx("span", {
79099
79296
  ...labelRest,
79100
79297
  ref: labelRef,
@@ -79341,7 +79538,8 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
79341
79538
  * the fillers hold for it outside the render window, what a scroll position
79342
79539
  * inside them is read with, and the least an item on its way takes (see
79343
79540
  * `renderSkeleton`). An item the list has drawn is held at the room it took
79344
- * then. Left out, it is the average of the items measured so far — once when
79541
+ * then, and an item whose run says its room (`<List.Items itemSize>`) at
79542
+ * that room. Left out, it is the average of the items measured so far — once when
79345
79543
  * the list mounts, again when a popup around it opens, and after each commit
79346
79544
  * while items are held off screen. Given, it is the worst case: the size
79347
79545
  * every item has, or, in a list whose items differ, the smallest an item can