@jsenv/navi 0.29.441 → 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.
@@ -72814,15 +72814,16 @@ document.addEventListener(
72814
72814
  );
72815
72815
 
72816
72816
  /*
72817
- * Whether writing a scroll position now would cost the user the scroll in
72818
- * progress. A write is an instant scroll, and the browser abandons for it any
72819
- * scroll it is playing on its own, short of where that one was going: a key
72820
- * (Home, PageDown, Space), a smooth `scrollTo` or `scrollIntoView`, the status
72821
- * bar tap on iOS. A scroll following a hand is not abandoned: the wheel's next
72822
- * turn carries on, and on most engines a finger keeps dragging from wherever
72823
- * the write left it, and the fling it leaves behind once it lifts carries on
72824
- * from there too (Chrome under DevTools' touch emulation: flings written to at
72825
- * every move of a list's window went as far as flings never written to).
72817
+ * Moves a scroll by what the content moved under the user, unless the write
72818
+ * would cost them the scroll in progress. A write is an instant scroll, and
72819
+ * the browser abandons for it any scroll it is playing on its own, short of
72820
+ * where that one was going: a key (Home, PageDown, Space), a smooth `scrollTo`
72821
+ * or `scrollIntoView`, the status bar tap on iOS. A scroll following a hand is
72822
+ * not abandoned: the wheel's next turn carries on, and on most engines a
72823
+ * finger keeps dragging from wherever the write left it, and the fling it
72824
+ * leaves behind once it lifts carries on from there too (Chrome under
72825
+ * DevTools' touch emulation: flings written to at every move of a list's
72826
+ * window went as far as flings never written to).
72826
72827
  *
72827
72828
  * A fling must not be taken for a scroll the browser plays on its own. A list
72828
72829
  * then leaves uncorrected, for the whole fling, the room its fillers lose above
@@ -72849,6 +72850,18 @@ document.addEventListener(
72849
72850
  * while the finger is still on one of them, and the touchend of a removed
72850
72851
  * element never reaches the document.
72851
72852
  *
72853
+ * Chrome ends a scroll at every write, a finger's fling included: "scrollend"
72854
+ * a frame after the write, and the fling goes on a frame later as a scroll of
72855
+ * its own, as far as it was going (a bare page written to every 120ms of a
72856
+ * fling: 9 "scrollend" for 8 writes, the fling as long). Taken for a new
72857
+ * scroll, it is nobody's once it starts far enough from the finger's last
72858
+ * move, and the list's corrections are refused for the rest of the fling: in
72859
+ * wematch, cards drawn above the screen pushing it down at every window slide.
72860
+ * So a scroll starting right after the "scrollend" of a scroll written to here
72861
+ * goes on as that scroll: same start, same hand. Only a written one: a scroll
72862
+ * starting right after one that ended on its own is a new one, a key or a
72863
+ * smooth `scrollTo` must not pass for the fling before it.
72864
+ *
72852
72865
  * In a browser without "scrollend", no write is said to interrupt anything: a
72853
72866
  * scroll would never be seen ending.
72854
72867
  */
@@ -72858,8 +72871,8 @@ document.addEventListener(
72858
72871
  // hand on a scroll that came from elsewhere.
72859
72872
  const WHEEL_REACH = 100;
72860
72873
  // The fling Chrome plays as a scroll of its own starts a frame or two after the
72861
- // drag's last move (DevTools' touch emulation, 4× CPU: 12ms after the drag's
72862
- // "scrollend").
72874
+ // "scrollend" of the drag, or of a write (DevTools' touch emulation, 4× CPU:
72875
+ // 12ms after the drag's, 24 to 40ms after a write's in wematch).
72863
72876
  const FLING_REACH = 150;
72864
72877
  // iOS's touch handling, the one engine whose finger drops writes (see above).
72865
72878
  const FINGER_KEEPS_WRITES = !window.CSS.supports(
@@ -72868,14 +72881,41 @@ const FINGER_KEEPS_WRITES = !window.CSS.supports(
72868
72881
  );
72869
72882
 
72870
72883
  const flightMap = new WeakMap();
72884
+ // Scrollers written to since their last "scrollend": that "scrollend" may be
72885
+ // the write's.
72886
+ const writtenScrollerSet = new WeakSet();
72887
+ // The flight of a written scroller, from its "scrollend" until a scroll goes on
72888
+ // with it.
72889
+ const writtenFlightEndMap = new WeakMap();
72871
72890
  let fingersDown = 0;
72872
72891
  let wheelTimeStamp = -Infinity;
72873
72892
  let fingerScrollTimeStamp = -Infinity;
72874
72893
 
72875
72894
  /**
72895
+ * Answers whether it wrote.
72896
+ *
72876
72897
  * @param {Element} scrollerEl
72898
+ * @param {number} delta
72899
+ * @param {{ horizontal?: boolean }} [options]
72877
72900
  * @returns {boolean}
72878
72901
  */
72902
+ const scrollByUnlessInterrupting = (
72903
+ scrollerEl,
72904
+ delta,
72905
+ { horizontal } = {},
72906
+ ) => {
72907
+ if (wouldInterruptScroll(scrollerEl)) {
72908
+ return false;
72909
+ }
72910
+ writtenScrollerSet.add(scrollerEl);
72911
+ if (horizontal) {
72912
+ scrollerEl.scrollLeft += delta;
72913
+ } else {
72914
+ scrollerEl.scrollTop += delta;
72915
+ }
72916
+ return true;
72917
+ };
72918
+
72879
72919
  const wouldInterruptScroll = (scrollerEl) => {
72880
72920
  const flight = flightMap.get(scrollerEl);
72881
72921
  if (!flight) {
@@ -72918,6 +72958,15 @@ if ("onscrollend" in window) {
72918
72958
  if (flightMap.has(scroller)) {
72919
72959
  return;
72920
72960
  }
72961
+ const writtenFlightEnd = writtenFlightEndMap.get(scroller);
72962
+ writtenFlightEndMap.delete(scroller);
72963
+ if (
72964
+ writtenFlightEnd &&
72965
+ e.timeStamp - writtenFlightEnd.timeStamp < FLING_REACH
72966
+ ) {
72967
+ flightMap.set(scroller, writtenFlightEnd.flight);
72968
+ return;
72969
+ }
72921
72970
  flightMap.set(scroller, { timeStamp: e.timeStamp });
72922
72971
  },
72923
72972
  { capture: true, passive: true },
@@ -72925,7 +72974,13 @@ if ("onscrollend" in window) {
72925
72974
  document.addEventListener(
72926
72975
  "scrollend",
72927
72976
  (e) => {
72928
- flightMap.delete(asScroller(e.target));
72977
+ const scroller = asScroller(e.target);
72978
+ const flight = flightMap.get(scroller);
72979
+ flightMap.delete(scroller);
72980
+ if (flight && writtenScrollerSet.has(scroller)) {
72981
+ writtenFlightEndMap.set(scroller, { flight, timeStamp: e.timeStamp });
72982
+ }
72983
+ writtenScrollerSet.delete(scroller);
72929
72984
  },
72930
72985
  { capture: true, passive: true },
72931
72986
  );
@@ -73647,13 +73702,24 @@ const createListItems = () => {
73647
73702
  }
73648
73703
  return null;
73649
73704
  },
73650
- // The room the item at that index took when it was last drawn, or
73651
- // undefined: never drawn, not held, or not an item of a run.
73705
+ // The room the item at that index takes: the room it took when it was
73706
+ // last drawn, else the room its run is given for it (see List.Items'
73707
+ // itemSize), else undefined.
73652
73708
  roomAt: (index) => {
73653
73709
  for (const locator of locatorByOwner.values()) {
73654
- const id = locator.idAt(index);
73655
- if (id !== undefined) {
73656
- return roomById.get(id);
73710
+ const room = locator.roomAt(index);
73711
+ if (room !== undefined) {
73712
+ return room;
73713
+ }
73714
+ }
73715
+ return undefined;
73716
+ },
73717
+ // The room the run of that index is given for it, whatever was measured.
73718
+ givenRoomAt: (index) => {
73719
+ for (const locator of locatorByOwner.values()) {
73720
+ const room = locator.givenRoomAt(index);
73721
+ if (room !== undefined) {
73722
+ return room;
73657
73723
  }
73658
73724
  }
73659
73725
  return undefined;
@@ -75142,6 +75208,23 @@ const css$x = /* css */`@property --list-group-label-height {
75142
75208
  &[data-hidden-while-empty]:not(:has([navi-list-item-real])) {
75143
75209
  display: none;
75144
75210
  }
75211
+
75212
+ &[data-continued] {
75213
+ margin-top: calc(-1 * var(--x-list-gap, 0px));
75214
+ display: grid;
75215
+
75216
+ & > .navi_list_item_group_label, & > .navi_list_item_group_list {
75217
+ grid-area: 1 / 1;
75218
+ }
75219
+
75220
+ & > .navi_list_item_group_label {
75221
+ align-self: start;
75222
+ }
75223
+ }
75224
+
75225
+ &[data-continued="hole"] > .navi_list_item_group_label {
75226
+ visibility: hidden;
75227
+ }
75145
75228
  }
75146
75229
 
75147
75230
  .navi_list_container[data-horizontal] > .navi_list_scroll_container > .navi_list > .navi_list_item_group {
@@ -75962,6 +76045,7 @@ const useListScrollSync = ({
75962
76045
  // that changed since is measured again when drawn again, and the anchoring
75963
76046
  // holds the view across the difference. Items laid side by side in a grid
75964
76047
  // share a line, which the fillers hold at the one size.
76048
+ const givenRoomWarnedRef = useRef(false);
75965
76049
  const rememberItemRooms = () => {
75966
76050
  if (!ref.current || itemsPerLineRef.current !== 1) {
75967
76051
  return;
@@ -75970,7 +76054,22 @@ const useListScrollSync = ({
75970
76054
  if (!listEl) {
75971
76055
  return;
75972
76056
  }
75973
- readItemRooms(listEl, horizontal, listItems.roomById);
76057
+ readItemRooms(listEl, horizontal, (item, room, complete) => {
76058
+ listItems.roomById.set(item.id, room);
76059
+ if (!complete || givenRoomWarnedRef.current) {
76060
+ return;
76061
+ }
76062
+ const givenRoom = listItems.givenRoomAt(item.index);
76063
+ if (givenRoom === undefined) {
76064
+ return;
76065
+ }
76066
+ const difference = room - givenRoom;
76067
+ if (difference > -0.5 && difference < 0.5) {
76068
+ return;
76069
+ }
76070
+ givenRoomWarnedRef.current = true;
76071
+ console.warn(`List.Items itemSize gives ${givenRoom}px to the item at index ${item.index}, which took ${Math.round(room * 10) / 10}px from the end of the item before it to its own end. The items not drawn are held at the room itemSize gives, so the screen moves by the difference when they are drawn above it — and nothing corrects it during a fling on iOS.`);
76072
+ });
75974
76073
  };
75975
76074
  const updateRenderWindow = (newStart, newEnd, reason) => {
75976
76075
  const {
@@ -76732,18 +76831,14 @@ const useListScrollSync = ({
76732
76831
  if (delta > -0.5 && delta < 0.5) {
76733
76832
  return;
76734
76833
  }
76735
- if (wouldInterruptScroll(scrollerEl)) {
76736
- // As the anchoring's write would (see holdAnchorStill). The document's
76737
- // box is the scroller of a list scrolling the page, and it resizes with
76738
- // every item drawn taller than its filler: while the page scrolls, that
76739
- // is at every window move.
76740
- return;
76741
- }
76742
- anchorRef.current = null;
76743
- if (horizontal) {
76744
- scrollerEl.scrollLeft += delta;
76745
- } else {
76746
- scrollerEl.scrollTop += delta;
76834
+ // Not into a scroll it would cut short, as the anchoring's write (see
76835
+ // holdAnchorStill). The document's box is the scroller of a list
76836
+ // scrolling the page, and it resizes with every item drawn taller than
76837
+ // its filler: while the page scrolls, that is at every window move.
76838
+ if (scrollByUnlessInterrupting(scrollerEl, delta, {
76839
+ horizontal
76840
+ })) {
76841
+ anchorRef.current = null;
76747
76842
  }
76748
76843
  };
76749
76844
  const observer = new ResizeObserver(entries => {
@@ -76836,19 +76931,15 @@ const useListScrollSync = ({
76836
76931
  return;
76837
76932
  }
76838
76933
  anchorRef.current = null;
76839
- if (wouldInterruptScroll(scrollerEl)) {
76840
- // Nobody is looking at items standing still: they are passing by, on
76841
- // their way somewhere the browser takes them (a key, a smooth scrollTo,
76842
- // the status bar, a fling on iOS). The write would stop that scroll short
76843
- // of where it was going. Left alone, the items drawn above push the rest
76844
- // by `drift` while all of it moves, and the scroll lands.
76845
- return;
76846
- }
76847
- scrolledByListRef.current = true;
76848
- if (horizontal) {
76849
- scrollerEl.scrollLeft += drift;
76850
- } else {
76851
- scrollerEl.scrollTop += drift;
76934
+ // Not written into a scroll nobody is looking at: its items are passing
76935
+ // by, on their way somewhere the browser takes them (a key, a smooth
76936
+ // scrollTo, the status bar, a fling on iOS). The write would stop that
76937
+ // scroll short of where it was going. Left alone, the items drawn above
76938
+ // push the rest by `drift` while all of it moves, and the scroll lands.
76939
+ if (scrollByUnlessInterrupting(scrollerEl, drift, {
76940
+ horizontal
76941
+ })) {
76942
+ scrolledByListRef.current = true;
76852
76943
  }
76853
76944
  };
76854
76945
  useLayoutEffect(holdAnchorStill);
@@ -77983,18 +78074,17 @@ const readWindowGeometry = (scrollerEl, listEl, horizontal) => {
77983
78074
  };
77984
78075
  };
77985
78076
  // The room each item drawn takes along the scroll axis (see
77986
- // rememberItemRooms): up to the next item when that one is drawn too, so that
77987
- // what stands between them — a separator, the label of a group the next one
77988
- // opens — is counted; its own box otherwise. A stand-in is not measured: it
77989
- // takes the room it is given, not the room of its item.
77990
- const readItemRooms = (listEl, horizontal, roomById) => {
78077
+ // rememberItemRooms): from where the item before it ends to where it ends, so
78078
+ // that what stands between the two — a separator, the label of the group it
78079
+ // opens — is counted with the item it comes with. A group the window cuts
78080
+ // draws its label without room (see the data-continued rule above): that
78081
+ // label's room is held by the item that opens the group, in the filler. The
78082
+ // first item drawn is measured from the end of what stands before it in the
78083
+ // list (a filler, a header); a stand-in is not measured, it takes the room it
78084
+ // is given and not the room of its item, but an item after it ends where it
78085
+ // ends. `complete` says whether the room was read up to an end before it.
78086
+ const readItemRooms = (listEl, horizontal, onRoom) => {
77991
78087
  let previous = null;
77992
- const keepRoom = (item, next) => {
77993
- if (item.id === null) {
77994
- return;
77995
- }
77996
- roomById.set(item.id, next && next.index === item.index + 1 ? next.from - item.from : item.to - item.from);
77997
- };
77998
78088
  for (const itemEl of listEl.querySelectorAll(`[${LIST_ITEM_INDEX_ATTRIBUTE}]`)) {
77999
78089
  if (!isOwnItem(itemEl, listEl)) {
78000
78090
  continue;
@@ -78009,14 +78099,41 @@ const readItemRooms = (listEl, horizontal, roomById) => {
78009
78099
  from: horizontal ? rect.left : rect.top,
78010
78100
  to: horizontal ? rect.right : rect.bottom
78011
78101
  };
78012
- if (previous) {
78013
- keepRoom(previous, item);
78102
+ if (item.id !== null) {
78103
+ const endBefore = previous && previous.index === item.index - 1 ? previous.to : readEndBefore(itemEl, listEl, horizontal);
78104
+ if (endBefore === null) {
78105
+ onRoom(item, item.to - item.from, false);
78106
+ } else {
78107
+ onRoom(item, item.to - endBefore, true);
78108
+ }
78014
78109
  }
78015
78110
  previous = item;
78016
78111
  }
78017
- if (previous) {
78018
- keepRoom(previous, null);
78112
+ };
78113
+ // Where what stands before an item in the list ends: the element before the
78114
+ // item, or before its group when it is the first of it — or the start of the
78115
+ // list's content when nothing does. Null when the element before is not laid
78116
+ // out.
78117
+ const readEndBefore = (itemEl, listEl, horizontal) => {
78118
+ let slotEl = itemEl;
78119
+ const groupListEl = itemEl.parentElement;
78120
+ if (groupListEl && groupListEl.classList.contains("navi_list_item_group_list") && groupListEl.firstElementChild === itemEl) {
78121
+ slotEl = groupListEl.closest(".navi_list_item_group") || itemEl;
78122
+ }
78123
+ const beforeEl = slotEl.previousElementSibling;
78124
+ if (beforeEl) {
78125
+ const rect = beforeEl.getBoundingClientRect();
78126
+ if (rect.width === 0 && rect.height === 0) {
78127
+ return null;
78128
+ }
78129
+ return horizontal ? rect.right : rect.bottom;
78019
78130
  }
78131
+ if (slotEl.parentElement !== listEl) {
78132
+ return null;
78133
+ }
78134
+ const listRect = listEl.getBoundingClientRect();
78135
+ const listStyle = window.getComputedStyle(listEl);
78136
+ return horizontal ? listRect.left + parseFloat(listStyle.borderLeftWidth) + parseFloat(listStyle.paddingLeft) : listRect.top + parseFloat(listStyle.borderTopWidth) + parseFloat(listStyle.paddingTop);
78020
78137
  };
78021
78138
  // The first item the screen shows from `position` on, and the item after the
78022
78139
  // last one it shows up to `position` (exclusive, like the window's end). An
@@ -78566,6 +78683,8 @@ const UnorderedList = ({
78566
78683
  grid: trackColumns ? true : undefined,
78567
78684
  gridTemplateColumns: trackColumns,
78568
78685
  ...rest,
78686
+ // The gap a group the window cuts takes back (see data-continued).
78687
+ style: withListGap(rest.style, spacing),
78569
78688
  spacing: spacing,
78570
78689
  baseClassName: "navi_list",
78571
78690
  children: [!suppressFallback && searchFallbackShown && jsx(SearchFallback, {
@@ -78598,6 +78717,21 @@ const UnorderedList = ({
78598
78717
  })]
78599
78718
  });
78600
78719
  };
78720
+ // The list's spacing as a variable, next to the style it is given (an object
78721
+ // or a css string, as Box takes either).
78722
+ const withListGap = (style, spacing) => {
78723
+ if (spacing === undefined) {
78724
+ return style;
78725
+ }
78726
+ const gap = stringifySpacingStyle(spacing, "gap");
78727
+ if (typeof style === "string") {
78728
+ return `${style};--x-list-gap:${gap}`;
78729
+ }
78730
+ return {
78731
+ ...style,
78732
+ "--x-list-gap": gap
78733
+ };
78734
+ };
78601
78735
 
78602
78736
  // The "no match" message. Whether it shows is decided by ListUI (see its
78603
78737
  // searchFallbackShown): a search left nothing to display, and the caller did
@@ -78635,19 +78769,20 @@ const Fallback = ({
78635
78769
  // every item it draws must not be redrawn — every item of it — because the size
78636
78770
  // settled after the first commit.
78637
78771
  // It holds lines: the items side by side on one take its room together, and a
78638
- // last line left short takes it all the same. An item the list drew before
78639
- // takes the room it took then instead (see rememberItemRooms).
78772
+ // last line left short takes it all the same. An item whose room is known takes
78773
+ // that room instead: the one it took when the list drew it before (see
78774
+ // rememberItemRooms), or the one its run is given (see List.Items' itemSize).
78640
78775
  const VirtualFiller = ({
78641
78776
  edge,
78642
78777
  itemCount,
78643
78778
  itemsPerLine,
78644
- itemsRemembered,
78645
- roomRemembered,
78779
+ itemsKnown,
78780
+ roomKnown,
78646
78781
  findChunks
78647
78782
  }) => {
78648
78783
  const listItems = useContext(ListItemsContext);
78649
78784
  const lineSize = listItems.virtualItemSizeSignal.value;
78650
- const sizeToFill = Math.ceil(itemCount / itemsPerLine) * lineSize - itemsRemembered * lineSize / itemsPerLine + roomRemembered;
78785
+ const sizeToFill = Math.ceil(itemCount / itemsPerLine) * lineSize - itemsKnown * lineSize / itemsPerLine + roomKnown;
78651
78786
  // A filler resizing moves what stands below it — the items on screen, when it
78652
78787
  // holds the room of items above them — and it resizes in a commit of its own
78653
78788
  // when the item size settles after the list has rendered: the list puts its
@@ -79364,6 +79499,7 @@ const VISIBILITY_HIDDEN_STYLE = {
79364
79499
  * pageSize?: number,
79365
79500
  * memoryBudget?: number,
79366
79501
  * renderSkeleton?: false | ((index: number) => import("ignore:preact").ComponentChildren),
79502
+ * itemSize?: (index: number) => number,
79367
79503
  * renderError?: (failure: {error: any, retry: () => void, start: number, end: number}) => import("ignore:preact").ComponentChildren,
79368
79504
  * onRequestStateChange?: (state: {busy: boolean, refreshing: boolean, range: {start: number, end: number}|null}) => void,
79369
79505
  * }>}
@@ -79396,6 +79532,10 @@ const VISIBILITY_HIDDEN_STYLE = {
79396
79532
  * `<List.Group>` whose label (`renderGroupLabel`) stays on screen for as long
79397
79533
  * as one of them is. The groups are found in the data as it arrives, which is
79398
79534
  * the only way a list that discovers its items page by page can have any.
79535
+ * Style the label, not the group element: a group the render window cuts is
79536
+ * laid out by the list, its label over its first items, so that the label
79537
+ * and the gap before it — held in the room of the item opening the group —
79538
+ * take no room twice.
79399
79539
  * @param {(item: any, index: number) => any} [props.renderGroupLabel]
79400
79540
  * The label of the group an item opens, given that item.
79401
79541
  * @param {(item: any, index: number) => object} [props.groupLabelProps]
@@ -79414,9 +79554,26 @@ const VISIBILITY_HIDDEN_STYLE = {
79414
79554
  * further out. `Infinity` keeps every item the run ever received; `0` keeps
79415
79555
  * only the ones around the window.
79416
79556
  * @param {false|(index: number) => any} [props.renderSkeleton]
79417
- * What to draw for an item the run does not hold. Defaults to List's own
79557
+ * What to draw for an item the run does not hold, at the room that item will
79558
+ * take: a screen of skeletons alone has no item for the list to hold still,
79559
+ * and when the page lands, what is below a skeleton of the wrong size moves
79560
+ * by the difference (see docs/scroll.md). Defaults to List's own
79418
79561
  * `renderSkeleton`, then to a bare `<List.Item skeleton>`; `false` leaves the
79419
79562
  * item empty (its room is still held, or the list would jump as it loads).
79563
+ * @param {(index: number) => number} [props.itemSize]
79564
+ * The room the item at an index takes along the scroll axis, in px, known
79565
+ * before the item arrives: from where the item before it ends to where it
79566
+ * ends — what stands between the two is its own (the gap, a separator, the
79567
+ * label of the group it opens; the first item of the list has no gap before
79568
+ * it). The fillers then hold every item the list has not drawn at its own
79569
+ * room instead of List's `virtualItemSize`, and an item drawn above the
79570
+ * screen takes exactly the room held for it: nothing on screen moves. It is
79571
+ * the only way the screen holds still during a fling on iOS, where writing
79572
+ * the scroll to correct it would stop the fling. The same index
79573
+ * `renderSkeleton` is given — the two draw on the same knowledge. A room
79574
+ * measured on an item drawn wins over it, and in development one that
79575
+ * differs from it is reported. Items side by side in a grid (`columns`)
79576
+ * share a line, which keeps `virtualItemSize`.
79420
79577
  * @param {(failure: {error: any, retry: () => void, start: number, end: number}) => any} [props.renderError]
79421
79578
  * What to draw where items were asked for and never came: given the `error`,
79422
79579
  * a `retry` to call, and the `start`/`end` of the range that failed — the
@@ -79489,6 +79646,7 @@ const useRunItems = (store, {
79489
79646
  renderItem,
79490
79647
  findText,
79491
79648
  pageSize,
79649
+ itemSize,
79492
79650
  groupBy,
79493
79651
  renderGroupLabel,
79494
79652
  groupLabelProps,
@@ -79572,6 +79730,22 @@ const useRunItems = (store, {
79572
79730
  const item = getItemAt(itemIndex);
79573
79731
  return item === undefined ? undefined : idOf(item, itemIndex);
79574
79732
  };
79733
+ // The room the caller says an item takes (`itemSize`), drawn or not.
79734
+ const givenRoomOf = itemIndex => {
79735
+ if (!itemSize || itemsPerLine !== 1 || itemIndex < runStart || itemIndex >= runEnd) {
79736
+ return undefined;
79737
+ }
79738
+ const room = itemSize(itemIndex);
79739
+ return room > 0 ? room : undefined;
79740
+ };
79741
+ // The room an item took when the list last drew it (see rememberItemRooms),
79742
+ // else the room the caller gives it, else undefined: a filler holds an item
79743
+ // it knows nothing about at the item size.
79744
+ const roomOf = itemIndex => {
79745
+ const id = idAt(itemIndex);
79746
+ const remembered = id === undefined ? undefined : listItems.roomById.get(id);
79747
+ return remembered === undefined ? givenRoomOf(itemIndex) : remembered;
79748
+ };
79575
79749
  listItems.setItemLocator(ownerId, {
79576
79750
  indexOf: id => {
79577
79751
  let found = null;
@@ -79583,32 +79757,27 @@ const useRunItems = (store, {
79583
79757
  });
79584
79758
  return found;
79585
79759
  },
79586
- idAt
79760
+ roomAt: roomOf,
79761
+ givenRoomAt: givenRoomOf
79587
79762
  });
79588
- // The room an item took when the list last drew it (see rememberItemRooms),
79589
- // or undefined: a filler holds an item it never drew at the item size.
79590
- const roomOf = itemIndex => {
79591
- const id = idAt(itemIndex);
79592
- return id === undefined ? undefined : listItems.roomById.get(id);
79593
- };
79594
- const readRemembered = (from, to) => {
79595
- const remembered = {
79763
+ const readKnownRooms = (from, to) => {
79764
+ const known = {
79596
79765
  room: 0,
79597
79766
  count: 0
79598
79767
  };
79599
- if (listItems.roomById.size === 0) {
79600
- return remembered;
79768
+ if (listItems.roomById.size === 0 && !itemSize) {
79769
+ return known;
79601
79770
  }
79602
79771
  let itemIndex = from;
79603
79772
  while (itemIndex < to) {
79604
79773
  const room = roomOf(itemIndex);
79605
79774
  if (room !== undefined) {
79606
- remembered.room += room;
79607
- remembered.count++;
79775
+ known.room += room;
79776
+ known.count++;
79608
79777
  }
79609
79778
  itemIndex++;
79610
79779
  }
79611
- return remembered;
79780
+ return known;
79612
79781
  };
79613
79782
  useLayoutEffect(() => {
79614
79783
  return () => {
@@ -79729,6 +79898,7 @@ const useRunItems = (store, {
79729
79898
  nodes.push(jsx(ListItemGroup, {
79730
79899
  label: group.label,
79731
79900
  labelProps: group.labelProps,
79901
+ continued: group.continued,
79732
79902
  children: group.children
79733
79903
  }, groupCount === 1 ? `${ownerId}_group_${group.key}` : `${ownerId}_group_${group.key}_${groupCount}`));
79734
79904
  group = null;
@@ -79766,10 +79936,38 @@ const useRunItems = (store, {
79766
79936
  }
79767
79937
  return groupBy(item, itemIndex);
79768
79938
  };
79939
+ // Where a group opened at that item began, when not there (see
79940
+ // data-continued): "above" the window, or in the "hole" of items not held
79941
+ // drawn right before it. Items not held belong to the group they lead to as
79942
+ // often as not — a page lands every hundred items, a group starts a few
79943
+ // times in a thread — so a group the window starts in is taken to go on
79944
+ // above it unless the item before, held, says otherwise.
79945
+ // Along y only, never built along x: there a group stands its label over
79946
+ // its items, so the label takes no room on the axis (unless it is wider than
79947
+ // the items drawn) and only the gap would be taken back — `margin-left`,
79948
+ // without the overlay of the data-continued rule. Nothing measured it yet.
79949
+ let afterHole = false;
79950
+ const whereGroupContinues = (groupKey, itemIndex) => {
79951
+ if (listItems.horizontal) {
79952
+ return undefined;
79953
+ }
79954
+ if (afterHole) {
79955
+ return "hole";
79956
+ }
79957
+ if (itemIndex !== windowFrom || windowFrom <= runStart) {
79958
+ return undefined;
79959
+ }
79960
+ const itemBefore = getItemAt(windowFrom - 1);
79961
+ if (itemBefore === undefined || groupBy(itemBefore, windowFrom - 1) === groupKey) {
79962
+ return "above";
79963
+ }
79964
+ return undefined;
79965
+ };
79769
79966
  const pushItem = (itemNode, item, itemIndex, groupKey) => {
79770
79967
  if (groupKey === undefined) {
79771
79968
  closeGroup();
79772
79969
  nodes.push(itemNode);
79970
+ afterHole = item === undefined;
79773
79971
  return;
79774
79972
  }
79775
79973
  if (!group || group.key !== groupKey) {
@@ -79778,9 +79976,11 @@ const useRunItems = (store, {
79778
79976
  key: groupKey,
79779
79977
  label: renderGroupLabel ? renderGroupLabel(item, itemIndex) : groupKey,
79780
79978
  labelProps: groupLabelProps ? groupLabelProps(item, itemIndex) : undefined,
79979
+ continued: whereGroupContinues(groupKey, itemIndex),
79781
79980
  children: []
79782
79981
  };
79783
79982
  }
79983
+ afterHole = false;
79784
79984
  group.children.push(itemNode);
79785
79985
  };
79786
79986
  // The text of the items a filler stands for, cut in chunks aligned on the
@@ -79823,13 +80023,13 @@ const useRunItems = (store, {
79823
80023
  // one run, and what sits before or after it (a header, items given one by
79824
80024
  // one) is not virtualized at all.
79825
80025
  if (windowFrom > runStart) {
79826
- const remembered = readRemembered(runStart, windowFrom);
80026
+ const known = readKnownRooms(runStart, windowFrom);
79827
80027
  nodes.push(jsx(VirtualFiller, {
79828
80028
  edge: "before",
79829
80029
  itemCount: windowFrom - runStart,
79830
80030
  itemsPerLine: itemsPerLine,
79831
- itemsRemembered: remembered.count,
79832
- roomRemembered: remembered.room,
80031
+ itemsKnown: known.count,
80032
+ roomKnown: known.room,
79833
80033
  findChunks: getFindChunks(runStart, windowFrom)
79834
80034
  }, "navi-list-filler-before"));
79835
80035
  }
@@ -79856,6 +80056,7 @@ const useRunItems = (store, {
79856
80056
  retry: store.retry
79857
80057
  })
79858
80058
  }, `${ownerId}_failure_${failureFrom}`));
80059
+ afterHole = false;
79859
80060
  itemIndex = failureTo + 1;
79860
80061
  continue;
79861
80062
  }
@@ -79931,13 +80132,13 @@ const useRunItems = (store, {
79931
80132
  }
79932
80133
  closeGroup();
79933
80134
  if (runEnd > windowTo) {
79934
- const remembered = readRemembered(windowTo, runEnd);
80135
+ const known = readKnownRooms(windowTo, runEnd);
79935
80136
  nodes.push(jsx(VirtualFiller, {
79936
80137
  edge: "after",
79937
80138
  itemCount: runEnd - windowTo,
79938
80139
  itemsPerLine: itemsPerLine,
79939
- itemsRemembered: remembered.count,
79940
- roomRemembered: remembered.room,
80140
+ itemsKnown: known.count,
80141
+ roomKnown: known.room,
79941
80142
  findChunks: getFindChunks(windowTo, runEnd)
79942
80143
  }, "navi-list-filler-after"));
79943
80144
  }
@@ -80591,12 +80792,17 @@ const useItemStore = ({
80591
80792
  * hiddenWhileEmpty — the group leaves the flow (`display: none`) while it
80592
80793
  * holds no real item — a search that emptied it, items not
80593
80794
  * arrived yet
80795
+ * continued — set by a run of items, not by callers: the group began
80796
+ * before the first item drawn ("above" the render window,
80797
+ * or in a "hole" of items not held), and what stands
80798
+ * before that item takes no room (see data-continued)
80594
80799
  * ...rest — forwarded to the outer <li role="presentation">
80595
80800
  */
80596
80801
  const ListItemGroup = ({
80597
80802
  label,
80598
80803
  labelProps,
80599
80804
  hiddenWhileEmpty,
80805
+ continued,
80600
80806
  children,
80601
80807
  ...rest
80602
80808
  }) => {
@@ -80639,6 +80845,7 @@ const ListItemGroup = ({
80639
80845
  baseClassName: "navi_list_item_group",
80640
80846
  role: "presentation",
80641
80847
  "data-hidden-while-empty": hiddenWhileEmpty ? "" : undefined,
80848
+ "data-continued": continued,
80642
80849
  children: [jsx("span", {
80643
80850
  ...labelRest,
80644
80851
  ref: labelRef,
@@ -80885,7 +81092,8 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
80885
81092
  * the fillers hold for it outside the render window, what a scroll position
80886
81093
  * inside them is read with, and the least an item on its way takes (see
80887
81094
  * `renderSkeleton`). An item the list has drawn is held at the room it took
80888
- * then. Left out, it is the average of the items measured so far — once when
81095
+ * then, and an item whose run says its room (`<List.Items itemSize>`) at
81096
+ * that room. Left out, it is the average of the items measured so far — once when
80889
81097
  * the list mounts, again when a popup around it opens, and after each commit
80890
81098
  * while items are held off screen. Given, it is the worst case: the size
80891
81099
  * every item has, or, in a list whose items differ, the smallest an item can