@jsenv/navi 0.29.447 → 0.29.449

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.
@@ -4988,9 +4988,11 @@ const actionRunEffect = (
4988
4988
  return params;
4989
4989
  });
4990
4990
  // Whether the getter asks for a run is read off what it returned, never off
4991
- // the params of the instance it leads to: an action handed here holding
4992
- // params of its own (an instance, a binding) keeps them when the getter
4993
- // returns nothing — and nothing must still run nothing.
4991
+ // the instances it moves between: an action handed here holding params of
4992
+ // its own (an instance, a binding) keeps them when the getter returns
4993
+ // nothing — and nothing must still run nothing; an instance reset under the
4994
+ // effect (a same-resource DELETE) reads as never run — and the next params
4995
+ // the getter asks for must still run.
4994
4996
  let asking = Boolean(actionParamsSignal.peek());
4995
4997
  const actionRunnedByThisEffect = action.bindParams(actionParamsSignal, {
4996
4998
  debounce,
@@ -5017,11 +5019,7 @@ const actionRunEffect = (
5017
5019
  return;
5018
5020
  }
5019
5021
 
5020
- if (
5021
- actionTargetPrevious &&
5022
- !actionTargetPrevious.isPrerun &&
5023
- actionTarget
5024
- ) {
5022
+ if (actionTargetPrevious && actionTarget) {
5025
5023
  // params changed
5026
5024
  if (!asking) {
5027
5025
  // falsy params, don't run
@@ -75001,6 +74999,7 @@ const css$x = /* css */`@property --list-group-label-height {
75001
74999
  }
75002
75000
 
75003
75001
  .navi_list {
75002
+ --x-list-item-between: 0px;
75004
75003
  box-sizing: border-box;
75005
75004
  outline: none;
75006
75005
  margin: 0;
@@ -75937,7 +75936,7 @@ const useListScrollSync = ({
75937
75936
  setScrollerElResolved(current => current === scrollerElNow ? current : scrollerElNow);
75938
75937
  };
75939
75938
  useLayoutEffect(resolveScroller);
75940
- useStickyScrollportWarning(ref, scroller);
75939
+ useStickyScrollportWarning(ref, scroller, getScroller, horizontal);
75941
75940
  useDuplicateHeaderWarning(ref);
75942
75941
  useStuckWindowWarning({
75943
75942
  ref,
@@ -76013,6 +76012,32 @@ const useListScrollSync = ({
76013
76012
  }
76014
76013
  });
76015
76014
 
76015
+ // What stands between two items along the axis — the gap, a separator and
76016
+ // the gaps around it — which an item on its way takes out of the room it is
76017
+ // given (see getSkeletonItem): virtualItemSize is an item's room, from the
76018
+ // end of the item before to its own end, and a skeleton whose box took all
76019
+ // of it would be taller than the item replacing it, moving everything below
76020
+ // by the difference. Read before the anchoring of this commit runs, and
76021
+ // written to the list's own box so the skeletons follow without rendering.
76022
+ // Items side by side in a grid share a line: none stands between them.
76023
+ useLayoutEffect(() => {
76024
+ if (columns || !ref.current || !listItems.hasRuns()) {
76025
+ return;
76026
+ }
76027
+ const listEl = getListEl();
76028
+ if (!listEl) {
76029
+ return;
76030
+ }
76031
+ const spaceBetween = readSpaceBetweenItems(listEl, horizontal);
76032
+ if (spaceBetween === null) {
76033
+ return;
76034
+ }
76035
+ const value = `${spaceBetween}px`;
76036
+ if (listEl.style.getPropertyValue("--x-list-item-between") !== value) {
76037
+ listEl.style.setProperty("--x-list-item-between", value);
76038
+ }
76039
+ });
76040
+
76016
76041
  // What the window holds goes through three stages (see settleWindow):
76017
76042
  // - "first": the first commit, a count of items since nothing is laid out
76018
76043
  // yet to measure a size with — `initial` or the budget when they are one,
@@ -77502,25 +77527,41 @@ const canScrollerScroll = (scrollerEl, axis) => {
77502
77527
  return scrollSize - clientSize > 1;
77503
77528
  };
77504
77529
  // What a sticky part of the list sticks to is the nearest scroll container in
77505
- // the DOM; `scroller` has no say in it. A list told the page scrolls it can
77506
- // therefore have its group labels and its header stuck to a wrapper that never
77507
- // scrolls — and pushed down by that wrapper's scroll-padding on top of it. The
77508
- // usual culprit is an app wrapper carrying `overflow-x: auto` to keep the
77509
- // document from overflowing horizontally on mobile; `overflow-x: clip` keeps
77510
- // that guarantee without making a scroll container.
77530
+ // the DOM; `scroller` has no say in it. A list scrolled by a box around it can
77531
+ // therefore have its group labels and its header stuck to a wrapper between
77532
+ // the two that never scrolls along that axis — and pushed down by that
77533
+ // wrapper's scroll-padding on top of it. The usual culprit is a wrapper
77534
+ // carrying `overflow-x: auto`: to keep the document from overflowing
77535
+ // horizontally on mobile, where `overflow-x: clip` keeps that guarantee without
77536
+ // making a scroll container, or to let a wide table scroll sideways, where the
77537
+ // header cannot stick to both boxes.
77511
77538
  const STICKY_LIST_PART_SELECTOR = `.navi_list_item_header, .navi_list_item_footer, .navi_list_item_group_label`;
77512
- const useStickyScrollportWarning = (ref, scroller) => {
77539
+ const useStickyScrollportWarning = (ref, scroller, getScroller, horizontal) => {
77513
77540
  const doneRef = useRef(false);
77514
77541
  useLayoutEffect(() => {
77515
- if (doneRef.current || scroller !== "document") {
77542
+ if (doneRef.current) {
77543
+ return;
77544
+ }
77545
+ const scrollerNamed = scroller && typeof scroller === "object";
77546
+ if (scroller !== "document" && scroller !== "parent" && !scrollerNamed) {
77547
+ // The list scrolls itself: its sticky parts stick to its own box.
77548
+ return;
77549
+ }
77550
+ if (scrollerNamed && !(scroller.nodeType === 1 || scroller.current)) {
77551
+ // The ref is not set yet: the box that scrolls is not known.
77516
77552
  return;
77517
77553
  }
77518
77554
  const listContainerEl = ref.current;
77519
77555
  if (!listContainerEl) {
77520
77556
  return;
77521
77557
  }
77558
+ const scrollerEl = getScroller();
77559
+ if (scroller === "parent" && scrollerEl !== document.scrollingElement && !canScroll(scrollerEl, horizontal ? "x" : "y")) {
77560
+ // Measured: until the box around scrolls, which one it is is not known.
77561
+ return;
77562
+ }
77522
77563
  const scrollportEl = findScrollportEl(listContainerEl);
77523
- if (!scrollportEl) {
77564
+ if ((scrollportEl || document.scrollingElement) === scrollerEl) {
77524
77565
  doneRef.current = true;
77525
77566
  return;
77526
77567
  }
@@ -77530,8 +77571,10 @@ const useStickyScrollportWarning = (ref, scroller) => {
77530
77571
  return;
77531
77572
  }
77532
77573
  doneRef.current = true;
77533
- console.warn(`<List scroller="document"> is inside ${getElementSignature(scrollportEl)}, a scroll container: its sticky group labels and header stick to that box instead of to the page. Give that box "overflow: clip" (it clips without creating a scroll container), or tell the list about it with scroller={element}.`, {
77574
+ const scrolledBy = scrollerEl === document.scrollingElement ? "the page" : getElementSignature(scrollerEl);
77575
+ console.warn(`<List> is scrolled by ${scrolledBy}, but it is inside ${getElementSignature(scrollportEl)}, a scroll container closer to it: its sticky header and group labels stick to that box instead. A box there to clip: give it "overflow: clip", which clips without creating a scroll container. A box that scrolls the list: name it, scroller={element}. A box there to scroll a wide list sideways: the sticky parts cannot stick to both boxes — let ${scrolledBy} scroll sideways too, or keep the header at the top of the list.`, {
77534
77576
  list: listContainerEl,
77577
+ scroller: scrollerEl,
77535
77578
  scrollport: scrollportEl,
77536
77579
  sticky: stickyEl
77537
77580
  });
@@ -78045,6 +78088,32 @@ const findItemsFrom = (listEl, from, horizontal) => {
78045
78088
  // items (a card holding a list of its own): both carry the same attributes.
78046
78089
  // Groups nest the items in a list of their own class, not in another list.
78047
78090
  const isOwnItem = (itemEl, listEl) => itemEl.closest(".navi_list") === listEl;
78091
+ // From where an item ends to where the item after it starts, read on the first
78092
+ // two that follow each other in the same container (a group's label stands
78093
+ // between two items of different groups). Null when no two items do.
78094
+ const readSpaceBetweenItems = (listEl, horizontal) => {
78095
+ let previous = null;
78096
+ for (const itemEl of listEl.querySelectorAll(`[${LIST_ITEM_INDEX_ATTRIBUTE}]`)) {
78097
+ if (!isOwnItem(itemEl, listEl)) {
78098
+ continue;
78099
+ }
78100
+ const rect = itemEl.getBoundingClientRect();
78101
+ if (rect.width === 0 && rect.height === 0) {
78102
+ previous = null;
78103
+ continue;
78104
+ }
78105
+ const index = Number(itemEl.getAttribute(LIST_ITEM_INDEX_ATTRIBUTE));
78106
+ if (previous && previous.index === index - 1 && previous.containerEl === itemEl.parentElement) {
78107
+ return (horizontal ? rect.left : rect.top) - previous.to;
78108
+ }
78109
+ previous = {
78110
+ index,
78111
+ containerEl: itemEl.parentElement,
78112
+ to: horizontal ? rect.right : rect.bottom
78113
+ };
78114
+ }
78115
+ return null;
78116
+ };
78048
78117
  // The item drawn at an index of the list, a stand-in or a real one.
78049
78118
  const findItemElementAt = (listEl, index) => {
78050
78119
  for (const itemEl of listEl.querySelectorAll(`[${LIST_ITEM_INDEX_ATTRIBUTE}="${index}"]`)) {
@@ -78160,7 +78229,10 @@ const readItemRooms = (listEl, horizontal, onRoom) => {
78160
78229
  // Where what stands before an item in the list ends: the element before the
78161
78230
  // item, or before its group when it is the first of it — or the start of the
78162
78231
  // list's content when nothing does. Null when the element before is not laid
78163
- // out.
78232
+ // out. It is where that element ends in the flow: where the item (or its
78233
+ // group) starts, less the gap and the margins between the two. Its own box is
78234
+ // where it is painted, which a sticky header stuck to the top of the screen
78235
+ // puts thousands of pixels away from its place in the list.
78164
78236
  const readEndBefore = (itemEl, listEl, horizontal) => {
78165
78237
  let slotEl = itemEl;
78166
78238
  const groupListEl = itemEl.parentElement;
@@ -78169,11 +78241,20 @@ const readEndBefore = (itemEl, listEl, horizontal) => {
78169
78241
  }
78170
78242
  const beforeEl = slotEl.previousElementSibling;
78171
78243
  if (beforeEl) {
78172
- const rect = beforeEl.getBoundingClientRect();
78173
- if (rect.width === 0 && rect.height === 0) {
78244
+ const beforeRect = beforeEl.getBoundingClientRect();
78245
+ if (beforeRect.width === 0 && beforeRect.height === 0) {
78174
78246
  return null;
78175
78247
  }
78176
- return horizontal ? rect.right : rect.bottom;
78248
+ const slotRect = slotEl.getBoundingClientRect();
78249
+ const containerStyle = window.getComputedStyle(slotEl.parentElement);
78250
+ const slotStyle = window.getComputedStyle(slotEl);
78251
+ const beforeStyle = window.getComputedStyle(beforeEl);
78252
+ // A gap the browser computes as "normal" is none in a flex or grid list.
78253
+ const lengthOf = value => parseFloat(value) || 0;
78254
+ if (horizontal) {
78255
+ return slotRect.left - lengthOf(containerStyle.columnGap) - lengthOf(slotStyle.marginLeft) - lengthOf(beforeStyle.marginRight);
78256
+ }
78257
+ return slotRect.top - lengthOf(containerStyle.rowGap) - lengthOf(slotStyle.marginTop) - lengthOf(beforeStyle.marginBottom);
78177
78258
  }
78178
78259
  if (slotEl.parentElement !== listEl) {
78179
78260
  return null;
@@ -79503,10 +79584,12 @@ const VISIBILITY_HIDDEN_STYLE = {
79503
79584
  * A collection read a slice at a time comes from `itemsAction(range)`: the run
79504
79585
  * asks for what it is about to draw and keeps what it gets. The range says the
79505
79586
  * same thing three ways, so a source can read it however it paginates —
79506
- * `{ start, end }` (places in the collection, a negative `start` counting back
79507
- * from the end like `Range: items=-25`, which is what a list opening on its
79508
- * last items asks for before it knows how many there are), `limit` (how many
79509
- * items), and
79587
+ * `{ start, end }` (places in the collection, `end` included as in
79588
+ * `Range: items=0-99`; a negative `start` counting back from the end like
79589
+ * `Range: items=-25`, which is what a list opening on its last items asks for
79590
+ * before it knows how many there are), `limit` (how many items:
79591
+ * `end - start + 1` — an API whose end is excluded is called with
79592
+ * `start + limit`), and
79510
79593
  * `before`/`after`/`around` (the id of an item to count from, for a source
79511
79594
  * paginating by cursor). Answer with the items (an array — that is all of
79512
79595
  * them), or with a range the way a Content-Range does: `{ items, start, count }`
@@ -79521,10 +79604,11 @@ const VISIBILITY_HIDDEN_STYLE = {
79521
79604
  * (see docs/resource.md).
79522
79605
  *
79523
79606
  * A collection that changes as a whole (a search reordering it) is a different
79524
- * collection: with `items`, another array is another collection and the run
79525
- * draws it from its first item; with `itemsAction`, give the run a `key` that
79526
- * changes with it, the way one does for anything else that is not the same
79527
- * thing anymore.
79607
+ * collection, and the run draws it from its first item: with `items`, another
79608
+ * array is another collection; with `itemsAction`, another reader is — a
79609
+ * reader bound to other params (`bindParams({ q })` with another `q`). A param
79610
+ * held in a signal changes what the reader reads without changing the reader:
79611
+ * give the run a `key` that follows it.
79528
79612
  *
79529
79613
  * An item says what it is where it is drawn: `renderItem` returns a
79530
79614
  * `<List.Item>` carrying its own props (`selectable`, `value`, `selected`…),
@@ -79539,6 +79623,7 @@ const VISIBILITY_HIDDEN_STYLE = {
79539
79623
  * findText?: (item: any, index: number) => string,
79540
79624
  * items?: any[],
79541
79625
  * itemsAction?: (range: {start: number, end: number, limit: number, before?: string, after?: string, around?: string, count?: number, signal: AbortSignal}) => any,
79626
+ * debounce?: number,
79542
79627
  * count?: number,
79543
79628
  * groupBy?: (item: any, index: number) => any,
79544
79629
  * renderGroupLabel?: (item: any, index: number) => import("ignore:preact").ComponentChildren,
@@ -79549,6 +79634,7 @@ const VISIBILITY_HIDDEN_STYLE = {
79549
79634
  * itemSize?: (index: number) => number,
79550
79635
  * renderError?: (failure: {error: any, retry: () => void, start: number, end: number}) => import("ignore:preact").ComponentChildren,
79551
79636
  * onRequestStateChange?: (state: {busy: boolean, refreshing: boolean, range: {start: number, end: number}|null}) => void,
79637
+ * onCountChange?: (count: number|undefined) => void,
79552
79638
  * }>}
79553
79639
  * @param {(item: any, index: number, state: {refreshing: boolean}) => any} props.renderItem
79554
79640
  * What is drawn for one item, given its data and where it sits — its place in the list,
@@ -79573,6 +79659,12 @@ const VISIBILITY_HIDDEN_STYLE = {
79573
79659
  * @param {(range: object) => any} [props.itemsAction]
79574
79660
  * Where the items come from when the collection is read a slice at a time:
79575
79661
  * a resource's range reader (`RESOURCE.GET_RANGE.bindParams(...)`).
79662
+ * @param {number} [props.debounce]
79663
+ * Milliseconds the reader must hold still before the run draws the
79664
+ * collection it reads: params following a field as the user types
79665
+ * (`bindParams({ q })`) ask once typing stops, not once per key. Meanwhile
79666
+ * the run keeps drawing the collection from before, `refreshing`. The
79667
+ * first collection is drawn at once.
79576
79668
  * @param {(item: any, index: number) => any} [props.groupBy]
79577
79669
  * What tells items that belong together apart from the others — the day of a
79578
79670
  * message, the month of a game. Consecutive items sharing it are wrapped in a
@@ -79636,24 +79728,101 @@ const VISIBILITY_HIDDEN_STYLE = {
79636
79728
  * collection's own ranks as `itemsAction` sees them, `null` once nothing is.
79637
79729
  * A range called off and asked again right away stays one `busy`, and a list
79638
79730
  * unmounted while asking says `busy: false` on its way out.
79731
+ * @param {(count: number|undefined) => void} [props.onCountChange]
79732
+ * Called with how many items the collection holds when that changes — for
79733
+ * the screen around the list to say it ("5 of 83 books"): the `count` the
79734
+ * source answers with each slice, or the one given to the run.
79735
+ * `undefined` until it is known; a run drawing another collection says it
79736
+ * again, so the number always belongs to the collection on screen.
79639
79737
  */
79640
79738
  const ListItems = ({
79739
+ itemsAction,
79740
+ debounce,
79741
+ ...props
79742
+ }) => {
79743
+ // Read so that this component renders in the pass of the List moving its
79744
+ // window, and the run with it: the List frames its window on the items its
79745
+ // runs drew in that same commit (see settleWindow). Skipped as an unchanged
79746
+ // child, it would leave the run to a render of its own, after that commit.
79747
+ useContext(RenderWindowContext);
79748
+ const {
79749
+ reader,
79750
+ settling
79751
+ } = useSettledReader(itemsAction, debounce);
79752
+ return jsx(ListItemsRun
79753
+ // Another reader is another collection — bindParams gives back the
79754
+ // reader it made for equal params — and the run holds the ranks, the
79755
+ // count and the asks of the one it draws.
79756
+ , {
79757
+ itemsAction: reader,
79758
+ settling: settling,
79759
+ ...props
79760
+ }, reader && reader.isRangeReader ? readerKeyOf(reader) : undefined);
79761
+ };
79762
+ const ListItemsRun = ({
79641
79763
  items,
79642
79764
  itemsAction,
79765
+ settling,
79643
79766
  count,
79644
79767
  memoryBudget,
79645
79768
  onRequestStateChange,
79769
+ onCountChange,
79646
79770
  ...runProps
79647
79771
  }) => {
79648
79772
  const store = useItemStore({
79649
79773
  items,
79650
79774
  count,
79651
79775
  itemsAction,
79776
+ settling,
79652
79777
  memoryBudget,
79653
- onRequestStateChange
79778
+ onRequestStateChange,
79779
+ onCountChange
79654
79780
  });
79655
79781
  return useRunItems(store, runProps);
79656
79782
  };
79783
+ const readerKeyByReader = new WeakMap();
79784
+ let readerKeyPrevious = 0;
79785
+ const readerKeyOf = reader => {
79786
+ let key = readerKeyByReader.get(reader);
79787
+ if (key === undefined) {
79788
+ readerKeyPrevious++;
79789
+ key = readerKeyPrevious;
79790
+ readerKeyByReader.set(reader, key);
79791
+ }
79792
+ return key;
79793
+ };
79794
+ // The reader the run draws: the one it is given, or under `debounce` the one
79795
+ // it was drawing until the one given has held still that long. Only between
79796
+ // two range readers — the first collection is drawn at once, and a collection
79797
+ // held in memory costs no request to follow.
79798
+ const useSettledReader = (itemsAction, debounce) => {
79799
+ const settledRef = useRef(itemsAction);
79800
+ const [, setSettleCount] = useState(0);
79801
+ const settled = settledRef.current;
79802
+ const waits = Boolean(debounce) && itemsAction !== settled && Boolean(itemsAction && itemsAction.isRangeReader) && Boolean(settled && settled.isRangeReader);
79803
+ if (!waits) {
79804
+ settledRef.current = itemsAction;
79805
+ }
79806
+ useLayoutEffect(() => {
79807
+ if (!waits) {
79808
+ return undefined;
79809
+ }
79810
+ const timeout = setTimeout(() => {
79811
+ settledRef.current = itemsAction;
79812
+ setSettleCount(settleCount => settleCount + 1);
79813
+ }, debounce);
79814
+ return () => {
79815
+ clearTimeout(timeout);
79816
+ };
79817
+ }, [waits, itemsAction, debounce]);
79818
+ return waits ? {
79819
+ reader: settled,
79820
+ settling: true
79821
+ } : {
79822
+ reader: itemsAction,
79823
+ settling: false
79824
+ };
79825
+ };
79657
79826
 
79658
79827
  // The items a `loading` list is told to expect (loadingSkeletonCount): a run
79659
79828
  // holding none of them, and asking for none — they arrive through the app.
@@ -79734,10 +79903,13 @@ const useRunItems = (store, {
79734
79903
  skeletonItem = {};
79735
79904
  const virtualItemSize = listItems.virtualItemSizeSignal.value;
79736
79905
  if (virtualItemSize) {
79906
+ // The room is the item's and what stands before it (see
79907
+ // readSpaceBetweenItems): the box takes what is left of it.
79908
+ const minSize = `calc(${virtualItemSize}px - var(--x-list-item-between, 0px))`;
79737
79909
  if (listItems.horizontal) {
79738
- skeletonItem.itemMinWidth = `${virtualItemSize}px`;
79910
+ skeletonItem.itemMinWidth = minSize;
79739
79911
  } else {
79740
- skeletonItem.itemMinHeight = `${virtualItemSize}px`;
79912
+ skeletonItem.itemMinHeight = minSize;
79741
79913
  }
79742
79914
  }
79743
79915
  return skeletonItem;
@@ -80271,6 +80443,7 @@ const ListItemsFailure = ({
80271
80443
  // hits the network again.
80272
80444
  const ITEM_STORE_MAX_DEFAULT = 1000;
80273
80445
  const ITEM_STORE_KEEP_AROUND = 250;
80446
+ const COUNT_NOT_SAID = {};
80274
80447
  const rangeIsSame = (a, b) => {
80275
80448
  if (!a || !b) {
80276
80449
  return a === b;
@@ -80287,8 +80460,10 @@ const useItemStore = ({
80287
80460
  items,
80288
80461
  count,
80289
80462
  itemsAction,
80463
+ settling,
80290
80464
  memoryBudget,
80291
- onRequestStateChange
80465
+ onRequestStateChange,
80466
+ onCountChange
80292
80467
  }) => {
80293
80468
  // A collection given whole (`items`) is held from the first render: nothing
80294
80469
  // to ask for, nothing to keep across mounts, nothing to invalidate. The rest
@@ -80380,6 +80555,68 @@ const useItemStore = ({
80380
80555
  if (staleRanksRef.current === null) {
80381
80556
  staleRanksRef.current = restored ? new Set(pages.byIndex.keys()) : new Set();
80382
80557
  }
80558
+ const takeInPage = ({
80559
+ pageItems,
80560
+ pageStart,
80561
+ pageCount
80562
+ }) => {
80563
+ // An item from before is gone from where it stood when the page puts
80564
+ // it at another rank (it would be drawn twice), or when its rank is
80565
+ // past the end of the collection.
80566
+ const staleRanks = staleRanksRef.current;
80567
+ const pageEnd = pageStart + pageItems.length;
80568
+ let dropped = false;
80569
+ if (staleRanks.size > 0) {
80570
+ const pageIds = new Set();
80571
+ for (const pageItem of pageItems) {
80572
+ if (pageItem && pageItem.id !== undefined) {
80573
+ pageIds.add(pageItem.id);
80574
+ }
80575
+ }
80576
+ for (const rank of staleRanks) {
80577
+ if (rank >= pageStart && rank < pageEnd) {
80578
+ continue;
80579
+ }
80580
+ const staleItem = pages.byIndex.get(rank);
80581
+ if (rank >= pageCount || staleItem && pageIds.has(staleItem.id)) {
80582
+ staleRanks.delete(rank);
80583
+ pages.byIndex.delete(rank);
80584
+ dropped = true;
80585
+ }
80586
+ }
80587
+ }
80588
+ let i = 0;
80589
+ while (i < pageItems.length) {
80590
+ pages.byIndex.set(pageStart + i, pageItems[i]);
80591
+ staleRanks.delete(pageStart + i);
80592
+ i++;
80593
+ }
80594
+ pages.count = pageCount;
80595
+ // Which rank holds which id, kept by the source so a list drawing
80596
+ // this collection again finds it drawn (see readComposition above).
80597
+ if (itemsAction.writeComposition) {
80598
+ itemsAction.writeComposition({
80599
+ byIndex: pages.byIndex,
80600
+ count: pageCount,
80601
+ replace: dropped
80602
+ });
80603
+ }
80604
+ };
80605
+ // The pages answered since the last render, taken in by the render and not
80606
+ // as they land: what the list reads of the run — how many items it stands for
80607
+ // (see listItems.take), where an item sits (its locator) — then moves in one
80608
+ // step. Taken in on landing, an item could be located past the count the list
80609
+ // still holds, and a list held on it would aim its window there at every
80610
+ // render, framed back each time.
80611
+ const pagesLandedRef = useRef([]);
80612
+ const pagesLanded = pagesLandedRef.current;
80613
+ const pagesTakenIn = pagesLanded.length > 0;
80614
+ if (pagesTakenIn) {
80615
+ pagesLandedRef.current = [];
80616
+ for (const page of pagesLanded) {
80617
+ takeInPage(page);
80618
+ }
80619
+ }
80383
80620
  const [refreshing, setRefreshing] = useState(false);
80384
80621
  // A source that says when what it reads has moved (a resource range reader
80385
80622
  // does: see rerunOn.GET_RANGE) is heard here — a write deciding who belongs
@@ -80410,28 +80647,55 @@ const useItemStore = ({
80410
80647
  const [failure, setFailure] = useState(null);
80411
80648
  const listItems = useContext(ListItemsContext);
80412
80649
  // The items are there, which is what the list waits for to place itself on the
80413
- // item it is held at (see placeWhereHeld). Said from an effect: a signal read
80414
- // during this very render must not be written during it.
80650
+ // item it is held at (see placeWhereHeld). Said from an effect, once the run
80651
+ // has drawn them: a signal read during this very render must not be written
80652
+ // during it.
80415
80653
  useLayoutEffect(() => {
80416
- if (!inMemory || itemsHeldRef.current) {
80654
+ if (inMemory) {
80655
+ if (itemsHeldRef.current) {
80656
+ return;
80657
+ }
80658
+ itemsHeldRef.current = true;
80659
+ } else if (!pagesTakenIn) {
80417
80660
  return;
80418
80661
  }
80419
- itemsHeldRef.current = true;
80420
80662
  listItems.pagesSignal.value = listItems.pagesSignal.peek() + 1;
80421
80663
  });
80422
80664
  // Before the first answer a run does not know how many items it stands for.
80423
80665
  // It stands for a page of them: a list that is about to be filled looks
80424
80666
  // like items on their way, not like an empty list.
80425
80667
  const itemCount = pages.count ?? count ?? listItems.pageSize;
80668
+ // The items drawn are from before while they are read again, and while the
80669
+ // collection that replaces them waits for its params to hold still (see
80670
+ // useSettledReader).
80671
+ const refreshingShown = refreshing || Boolean(settling);
80426
80672
  useLayoutEffect(() => {
80427
- if (!refreshing) {
80673
+ if (!refreshingShown) {
80428
80674
  return null;
80429
80675
  }
80430
80676
  listItems.refreshingSignal.value = listItems.refreshingSignal.peek() + 1;
80431
80677
  return () => {
80432
80678
  listItems.refreshingSignal.value = listItems.refreshingSignal.peek() - 1;
80433
80679
  };
80434
- }, [refreshing]);
80680
+ }, [refreshingShown]);
80681
+
80682
+ // How many items the collection holds, for the screen around the list to
80683
+ // say: the count the source answered or the caller gave, never the page the
80684
+ // run stands for before it knows. Said by each run on its first commit, so a
80685
+ // run drawing another collection takes back what the one before said.
80686
+ const countKnown = pages.count ?? count;
80687
+ const onCountChangeRef = useRef(onCountChange);
80688
+ onCountChangeRef.current = onCountChange;
80689
+ const countSaidRef = useRef(COUNT_NOT_SAID);
80690
+ useLayoutEffect(() => {
80691
+ if (countSaidRef.current === countKnown) {
80692
+ return;
80693
+ }
80694
+ countSaidRef.current = countKnown;
80695
+ if (onCountChangeRef.current) {
80696
+ onCountChangeRef.current(countKnown);
80697
+ }
80698
+ });
80435
80699
 
80436
80700
  // What the run is doing, for the screen around the list to draw: the list has
80437
80701
  // its own skeletons, what is around it has to be told. Read from the request
@@ -80466,8 +80730,11 @@ const useItemStore = ({
80466
80730
  };
80467
80731
  // A list taken off the screen while it was asking leaves nothing ringing
80468
80732
  // behind it: whoever is drawing "looking for items" has to stop.
80733
+ const mountedRef = useRef(false);
80469
80734
  useLayoutEffect(() => {
80735
+ mountedRef.current = true;
80470
80736
  return () => {
80737
+ mountedRef.current = false;
80471
80738
  if (requestStateRef.current.busy && onRequestStateChangeRef.current) {
80472
80739
  onRequestStateChangeRef.current({
80473
80740
  busy: false,
@@ -80480,7 +80747,7 @@ const useItemStore = ({
80480
80747
  const store = {
80481
80748
  itemCount,
80482
80749
  failure,
80483
- refreshing,
80750
+ refreshing: refreshingShown,
80484
80751
  // JS memory is cheap next to the DOM, but a long enough scroll accumulates
80485
80752
  // everything it ever went through. Items far from what is on screen are
80486
80753
  // dropped and simply asked for again if the user goes back — the same
@@ -80729,51 +80996,21 @@ const useItemStore = ({
80729
80996
  const pageItems = Array.isArray(page) ? page : page.items;
80730
80997
  const pageStart = Array.isArray(page) ? 0 : page.start ?? 0;
80731
80998
  const pageCount = Array.isArray(page) ? pageItems.length : page.count ?? pageStart + pageItems.length;
80999
+ const pageLanded = {
81000
+ pageItems,
81001
+ pageStart,
81002
+ pageCount
81003
+ };
81004
+ if (!mountedRef.current) {
81005
+ // No render is coming to take it in, and the source still keeps
81006
+ // what came back for the next list drawing this collection.
81007
+ takeInPage(pageLanded);
81008
+ return;
81009
+ }
80732
81010
  // Before the items land: what is on screen has to stay where it is,
80733
81011
  // and the DOM still shows the state to hold onto.
80734
81012
  listItems.captureAnchor();
80735
- // An item from before is gone from where it stood when the page puts
80736
- // it at another rank (it would be drawn twice), or when its rank is
80737
- // past the end of the collection.
80738
- const staleRanksNow = staleRanksRef.current;
80739
- const pageEnd = pageStart + pageItems.length;
80740
- let dropped = false;
80741
- if (staleRanksNow.size > 0) {
80742
- const pageIds = new Set();
80743
- for (const pageItem of pageItems) {
80744
- if (pageItem && pageItem.id !== undefined) {
80745
- pageIds.add(pageItem.id);
80746
- }
80747
- }
80748
- for (const rank of staleRanksNow) {
80749
- if (rank >= pageStart && rank < pageEnd) {
80750
- continue;
80751
- }
80752
- const staleItem = pages.byIndex.get(rank);
80753
- if (rank >= pageCount || staleItem && pageIds.has(staleItem.id)) {
80754
- staleRanksNow.delete(rank);
80755
- pages.byIndex.delete(rank);
80756
- dropped = true;
80757
- }
80758
- }
80759
- }
80760
- let i = 0;
80761
- while (i < pageItems.length) {
80762
- pages.byIndex.set(pageStart + i, pageItems[i]);
80763
- staleRanksNow.delete(pageStart + i);
80764
- i++;
80765
- }
80766
- pages.count = pageCount;
80767
- // Which rank holds which id, kept by the source so a list drawing
80768
- // this collection again finds it drawn (see readComposition above).
80769
- if (itemsAction.writeComposition) {
80770
- itemsAction.writeComposition({
80771
- byIndex: pages.byIndex,
80772
- count: pageCount,
80773
- replace: dropped
80774
- });
80775
- }
80776
- listItems.pagesSignal.value = listItems.pagesSignal.peek() + 1;
81013
+ pagesLandedRef.current.push(pageLanded);
80777
81014
  setPageVersion(version => version + 1);
80778
81015
  };
80779
81016
  const failed = error => {
@@ -81137,8 +81374,9 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
81137
81374
  * @param {number} [props.virtualItemSize]
81138
81375
  * The room an item never drawn is held at, in px along the scroll axis: what
81139
81376
  * the fillers hold for it outside the render window, what a scroll position
81140
- * inside them is read with, and the least an item on its way takes (see
81141
- * `renderSkeleton`). An item the list has drawn is held at the room it took
81377
+ * inside them is read with, and the least room an item on its way takes
81378
+ * (see `renderSkeleton`) — a room counts what stands before the item, a
81379
+ * gap or a separator, so the item's own box takes the rest of it. An item the list has drawn is held at the room it took
81142
81380
  * then, and an item whose run says its room (`<List.Items itemSize>`) at
81143
81381
  * that room. Left out, it is the average of the items measured so far — once when
81144
81382
  * the list mounts, again when a popup around it opens, and after each commit