@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.
@@ -4827,9 +4827,11 @@ const actionRunEffect = (
4827
4827
  return params;
4828
4828
  });
4829
4829
  // Whether the getter asks for a run is read off what it returned, never off
4830
- // the params of the instance it leads to: an action handed here holding
4831
- // params of its own (an instance, a binding) keeps them when the getter
4832
- // returns nothing — and nothing must still run nothing.
4830
+ // the instances it moves between: an action handed here holding params of
4831
+ // its own (an instance, a binding) keeps them when the getter returns
4832
+ // nothing — and nothing must still run nothing; an instance reset under the
4833
+ // effect (a same-resource DELETE) reads as never run — and the next params
4834
+ // the getter asks for must still run.
4833
4835
  let asking = Boolean(actionParamsSignal.peek());
4834
4836
  const actionRunnedByThisEffect = action.bindParams(actionParamsSignal, {
4835
4837
  debounce,
@@ -4856,11 +4858,7 @@ const actionRunEffect = (
4856
4858
  return;
4857
4859
  }
4858
4860
 
4859
- if (
4860
- actionTargetPrevious &&
4861
- !actionTargetPrevious.isPrerun &&
4862
- actionTarget
4863
- ) {
4861
+ if (actionTargetPrevious && actionTarget) {
4864
4862
  // params changed
4865
4863
  if (!asking) {
4866
4864
  // falsy params, don't run
@@ -73595,6 +73593,7 @@ const css$x = /* css */`@property --list-group-label-height {
73595
73593
  }
73596
73594
 
73597
73595
  .navi_list {
73596
+ --x-list-item-between: 0px;
73598
73597
  box-sizing: border-box;
73599
73598
  outline: none;
73600
73599
  margin: 0;
@@ -74568,6 +74567,32 @@ const useListScrollSync = ({
74568
74567
  }
74569
74568
  });
74570
74569
 
74570
+ // What stands between two items along the axis — the gap, a separator and
74571
+ // the gaps around it — which an item on its way takes out of the room it is
74572
+ // given (see getSkeletonItem): virtualItemSize is an item's room, from the
74573
+ // end of the item before to its own end, and a skeleton whose box took all
74574
+ // of it would be taller than the item replacing it, moving everything below
74575
+ // by the difference. Read before the anchoring of this commit runs, and
74576
+ // written to the list's own box so the skeletons follow without rendering.
74577
+ // Items side by side in a grid share a line: none stands between them.
74578
+ useLayoutEffect(() => {
74579
+ if (columns || !ref.current || !listItems.hasRuns()) {
74580
+ return;
74581
+ }
74582
+ const listEl = getListEl();
74583
+ if (!listEl) {
74584
+ return;
74585
+ }
74586
+ const spaceBetween = readSpaceBetweenItems(listEl, horizontal);
74587
+ if (spaceBetween === null) {
74588
+ return;
74589
+ }
74590
+ const value = `${spaceBetween}px`;
74591
+ if (listEl.style.getPropertyValue("--x-list-item-between") !== value) {
74592
+ listEl.style.setProperty("--x-list-item-between", value);
74593
+ }
74594
+ });
74595
+
74571
74596
  // What the window holds goes through three stages (see settleWindow):
74572
74597
  // - "first": the first commit, a count of items since nothing is laid out
74573
74598
  // yet to measure a size with — `initial` or the budget when they are one,
@@ -76011,14 +76036,16 @@ const canScrollerScroll = (scrollerEl, axis) => {
76011
76036
  return scrollSize - clientSize > 1;
76012
76037
  };
76013
76038
  // What a sticky part of the list sticks to is the nearest scroll container in
76014
- // the DOM; `scroller` has no say in it. A list told the page scrolls it can
76015
- // therefore have its group labels and its header stuck to a wrapper that never
76016
- // scrolls — and pushed down by that wrapper's scroll-padding on top of it. The
76017
- // usual culprit is an app wrapper carrying `overflow-x: auto` to keep the
76018
- // document from overflowing horizontally on mobile; `overflow-x: clip` keeps
76019
- // that guarantee without making a scroll container.
76039
+ // the DOM; `scroller` has no say in it. A list scrolled by a box around it can
76040
+ // therefore have its group labels and its header stuck to a wrapper between
76041
+ // the two that never scrolls along that axis — and pushed down by that
76042
+ // wrapper's scroll-padding on top of it. The usual culprit is a wrapper
76043
+ // carrying `overflow-x: auto`: to keep the document from overflowing
76044
+ // horizontally on mobile, where `overflow-x: clip` keeps that guarantee without
76045
+ // making a scroll container, or to let a wide table scroll sideways, where the
76046
+ // header cannot stick to both boxes.
76020
76047
  const STICKY_LIST_PART_SELECTOR = `.navi_list_item_header, .navi_list_item_footer, .navi_list_item_group_label`;
76021
- const useStickyScrollportWarning = (ref, scroller) => {
76048
+ const useStickyScrollportWarning = (ref, scroller, getScroller, horizontal) => {
76022
76049
  useRef(false);
76023
76050
  useLayoutEffect(() => {
76024
76051
  {
@@ -76492,6 +76519,32 @@ const findItemsFrom = (listEl, from, horizontal) => {
76492
76519
  // items (a card holding a list of its own): both carry the same attributes.
76493
76520
  // Groups nest the items in a list of their own class, not in another list.
76494
76521
  const isOwnItem = (itemEl, listEl) => itemEl.closest(".navi_list") === listEl;
76522
+ // From where an item ends to where the item after it starts, read on the first
76523
+ // two that follow each other in the same container (a group's label stands
76524
+ // between two items of different groups). Null when no two items do.
76525
+ const readSpaceBetweenItems = (listEl, horizontal) => {
76526
+ let previous = null;
76527
+ for (const itemEl of listEl.querySelectorAll(`[${LIST_ITEM_INDEX_ATTRIBUTE}]`)) {
76528
+ if (!isOwnItem(itemEl, listEl)) {
76529
+ continue;
76530
+ }
76531
+ const rect = itemEl.getBoundingClientRect();
76532
+ if (rect.width === 0 && rect.height === 0) {
76533
+ previous = null;
76534
+ continue;
76535
+ }
76536
+ const index = Number(itemEl.getAttribute(LIST_ITEM_INDEX_ATTRIBUTE));
76537
+ if (previous && previous.index === index - 1 && previous.containerEl === itemEl.parentElement) {
76538
+ return (horizontal ? rect.left : rect.top) - previous.to;
76539
+ }
76540
+ previous = {
76541
+ index,
76542
+ containerEl: itemEl.parentElement,
76543
+ to: horizontal ? rect.right : rect.bottom
76544
+ };
76545
+ }
76546
+ return null;
76547
+ };
76495
76548
  // The item drawn at an index of the list, a stand-in or a real one.
76496
76549
  const findItemElementAt = (listEl, index) => {
76497
76550
  for (const itemEl of listEl.querySelectorAll(`[${LIST_ITEM_INDEX_ATTRIBUTE}="${index}"]`)) {
@@ -76607,7 +76660,10 @@ const readItemRooms = (listEl, horizontal, onRoom) => {
76607
76660
  // Where what stands before an item in the list ends: the element before the
76608
76661
  // item, or before its group when it is the first of it — or the start of the
76609
76662
  // list's content when nothing does. Null when the element before is not laid
76610
- // out.
76663
+ // out. It is where that element ends in the flow: where the item (or its
76664
+ // group) starts, less the gap and the margins between the two. Its own box is
76665
+ // where it is painted, which a sticky header stuck to the top of the screen
76666
+ // puts thousands of pixels away from its place in the list.
76611
76667
  const readEndBefore = (itemEl, listEl, horizontal) => {
76612
76668
  let slotEl = itemEl;
76613
76669
  const groupListEl = itemEl.parentElement;
@@ -76616,11 +76672,20 @@ const readEndBefore = (itemEl, listEl, horizontal) => {
76616
76672
  }
76617
76673
  const beforeEl = slotEl.previousElementSibling;
76618
76674
  if (beforeEl) {
76619
- const rect = beforeEl.getBoundingClientRect();
76620
- if (rect.width === 0 && rect.height === 0) {
76675
+ const beforeRect = beforeEl.getBoundingClientRect();
76676
+ if (beforeRect.width === 0 && beforeRect.height === 0) {
76621
76677
  return null;
76622
76678
  }
76623
- return horizontal ? rect.right : rect.bottom;
76679
+ const slotRect = slotEl.getBoundingClientRect();
76680
+ const containerStyle = window.getComputedStyle(slotEl.parentElement);
76681
+ const slotStyle = window.getComputedStyle(slotEl);
76682
+ const beforeStyle = window.getComputedStyle(beforeEl);
76683
+ // A gap the browser computes as "normal" is none in a flex or grid list.
76684
+ const lengthOf = value => parseFloat(value) || 0;
76685
+ if (horizontal) {
76686
+ return slotRect.left - lengthOf(containerStyle.columnGap) - lengthOf(slotStyle.marginLeft) - lengthOf(beforeStyle.marginRight);
76687
+ }
76688
+ return slotRect.top - lengthOf(containerStyle.rowGap) - lengthOf(slotStyle.marginTop) - lengthOf(beforeStyle.marginBottom);
76624
76689
  }
76625
76690
  if (slotEl.parentElement !== listEl) {
76626
76691
  return null;
@@ -77947,10 +78012,12 @@ const VISIBILITY_HIDDEN_STYLE = {
77947
78012
  * A collection read a slice at a time comes from `itemsAction(range)`: the run
77948
78013
  * asks for what it is about to draw and keeps what it gets. The range says the
77949
78014
  * same thing three ways, so a source can read it however it paginates —
77950
- * `{ start, end }` (places in the collection, a negative `start` counting back
77951
- * from the end like `Range: items=-25`, which is what a list opening on its
77952
- * last items asks for before it knows how many there are), `limit` (how many
77953
- * items), and
78015
+ * `{ start, end }` (places in the collection, `end` included as in
78016
+ * `Range: items=0-99`; a negative `start` counting back from the end like
78017
+ * `Range: items=-25`, which is what a list opening on its last items asks for
78018
+ * before it knows how many there are), `limit` (how many items:
78019
+ * `end - start + 1` — an API whose end is excluded is called with
78020
+ * `start + limit`), and
77954
78021
  * `before`/`after`/`around` (the id of an item to count from, for a source
77955
78022
  * paginating by cursor). Answer with the items (an array — that is all of
77956
78023
  * them), or with a range the way a Content-Range does: `{ items, start, count }`
@@ -77965,10 +78032,11 @@ const VISIBILITY_HIDDEN_STYLE = {
77965
78032
  * (see docs/resource.md).
77966
78033
  *
77967
78034
  * A collection that changes as a whole (a search reordering it) is a different
77968
- * collection: with `items`, another array is another collection and the run
77969
- * draws it from its first item; with `itemsAction`, give the run a `key` that
77970
- * changes with it, the way one does for anything else that is not the same
77971
- * thing anymore.
78035
+ * collection, and the run draws it from its first item: with `items`, another
78036
+ * array is another collection; with `itemsAction`, another reader is — a
78037
+ * reader bound to other params (`bindParams({ q })` with another `q`). A param
78038
+ * held in a signal changes what the reader reads without changing the reader:
78039
+ * give the run a `key` that follows it.
77972
78040
  *
77973
78041
  * An item says what it is where it is drawn: `renderItem` returns a
77974
78042
  * `<List.Item>` carrying its own props (`selectable`, `value`, `selected`…),
@@ -77983,6 +78051,7 @@ const VISIBILITY_HIDDEN_STYLE = {
77983
78051
  * findText?: (item: any, index: number) => string,
77984
78052
  * items?: any[],
77985
78053
  * itemsAction?: (range: {start: number, end: number, limit: number, before?: string, after?: string, around?: string, count?: number, signal: AbortSignal}) => any,
78054
+ * debounce?: number,
77986
78055
  * count?: number,
77987
78056
  * groupBy?: (item: any, index: number) => any,
77988
78057
  * renderGroupLabel?: (item: any, index: number) => import("ignore:preact").ComponentChildren,
@@ -77993,6 +78062,7 @@ const VISIBILITY_HIDDEN_STYLE = {
77993
78062
  * itemSize?: (index: number) => number,
77994
78063
  * renderError?: (failure: {error: any, retry: () => void, start: number, end: number}) => import("ignore:preact").ComponentChildren,
77995
78064
  * onRequestStateChange?: (state: {busy: boolean, refreshing: boolean, range: {start: number, end: number}|null}) => void,
78065
+ * onCountChange?: (count: number|undefined) => void,
77996
78066
  * }>}
77997
78067
  * @param {(item: any, index: number, state: {refreshing: boolean}) => any} props.renderItem
77998
78068
  * What is drawn for one item, given its data and where it sits — its place in the list,
@@ -78017,6 +78087,12 @@ const VISIBILITY_HIDDEN_STYLE = {
78017
78087
  * @param {(range: object) => any} [props.itemsAction]
78018
78088
  * Where the items come from when the collection is read a slice at a time:
78019
78089
  * a resource's range reader (`RESOURCE.GET_RANGE.bindParams(...)`).
78090
+ * @param {number} [props.debounce]
78091
+ * Milliseconds the reader must hold still before the run draws the
78092
+ * collection it reads: params following a field as the user types
78093
+ * (`bindParams({ q })`) ask once typing stops, not once per key. Meanwhile
78094
+ * the run keeps drawing the collection from before, `refreshing`. The
78095
+ * first collection is drawn at once.
78020
78096
  * @param {(item: any, index: number) => any} [props.groupBy]
78021
78097
  * What tells items that belong together apart from the others — the day of a
78022
78098
  * message, the month of a game. Consecutive items sharing it are wrapped in a
@@ -78080,24 +78156,101 @@ const VISIBILITY_HIDDEN_STYLE = {
78080
78156
  * collection's own ranks as `itemsAction` sees them, `null` once nothing is.
78081
78157
  * A range called off and asked again right away stays one `busy`, and a list
78082
78158
  * unmounted while asking says `busy: false` on its way out.
78159
+ * @param {(count: number|undefined) => void} [props.onCountChange]
78160
+ * Called with how many items the collection holds when that changes — for
78161
+ * the screen around the list to say it ("5 of 83 books"): the `count` the
78162
+ * source answers with each slice, or the one given to the run.
78163
+ * `undefined` until it is known; a run drawing another collection says it
78164
+ * again, so the number always belongs to the collection on screen.
78083
78165
  */
78084
78166
  const ListItems = ({
78167
+ itemsAction,
78168
+ debounce,
78169
+ ...props
78170
+ }) => {
78171
+ // Read so that this component renders in the pass of the List moving its
78172
+ // window, and the run with it: the List frames its window on the items its
78173
+ // runs drew in that same commit (see settleWindow). Skipped as an unchanged
78174
+ // child, it would leave the run to a render of its own, after that commit.
78175
+ useContext(RenderWindowContext);
78176
+ const {
78177
+ reader,
78178
+ settling
78179
+ } = useSettledReader(itemsAction, debounce);
78180
+ return jsx(ListItemsRun
78181
+ // Another reader is another collection — bindParams gives back the
78182
+ // reader it made for equal params — and the run holds the ranks, the
78183
+ // count and the asks of the one it draws.
78184
+ , {
78185
+ itemsAction: reader,
78186
+ settling: settling,
78187
+ ...props
78188
+ }, reader && reader.isRangeReader ? readerKeyOf(reader) : undefined);
78189
+ };
78190
+ const ListItemsRun = ({
78085
78191
  items,
78086
78192
  itemsAction,
78193
+ settling,
78087
78194
  count,
78088
78195
  memoryBudget,
78089
78196
  onRequestStateChange,
78197
+ onCountChange,
78090
78198
  ...runProps
78091
78199
  }) => {
78092
78200
  const store = useItemStore({
78093
78201
  items,
78094
78202
  count,
78095
78203
  itemsAction,
78204
+ settling,
78096
78205
  memoryBudget,
78097
- onRequestStateChange
78206
+ onRequestStateChange,
78207
+ onCountChange
78098
78208
  });
78099
78209
  return useRunItems(store, runProps);
78100
78210
  };
78211
+ const readerKeyByReader = new WeakMap();
78212
+ let readerKeyPrevious = 0;
78213
+ const readerKeyOf = reader => {
78214
+ let key = readerKeyByReader.get(reader);
78215
+ if (key === undefined) {
78216
+ readerKeyPrevious++;
78217
+ key = readerKeyPrevious;
78218
+ readerKeyByReader.set(reader, key);
78219
+ }
78220
+ return key;
78221
+ };
78222
+ // The reader the run draws: the one it is given, or under `debounce` the one
78223
+ // it was drawing until the one given has held still that long. Only between
78224
+ // two range readers — the first collection is drawn at once, and a collection
78225
+ // held in memory costs no request to follow.
78226
+ const useSettledReader = (itemsAction, debounce) => {
78227
+ const settledRef = useRef(itemsAction);
78228
+ const [, setSettleCount] = useState(0);
78229
+ const settled = settledRef.current;
78230
+ const waits = Boolean(debounce) && itemsAction !== settled && Boolean(itemsAction && itemsAction.isRangeReader) && Boolean(settled && settled.isRangeReader);
78231
+ if (!waits) {
78232
+ settledRef.current = itemsAction;
78233
+ }
78234
+ useLayoutEffect(() => {
78235
+ if (!waits) {
78236
+ return undefined;
78237
+ }
78238
+ const timeout = setTimeout(() => {
78239
+ settledRef.current = itemsAction;
78240
+ setSettleCount(settleCount => settleCount + 1);
78241
+ }, debounce);
78242
+ return () => {
78243
+ clearTimeout(timeout);
78244
+ };
78245
+ }, [waits, itemsAction, debounce]);
78246
+ return waits ? {
78247
+ reader: settled,
78248
+ settling: true
78249
+ } : {
78250
+ reader: itemsAction,
78251
+ settling: false
78252
+ };
78253
+ };
78101
78254
 
78102
78255
  // The items a `loading` list is told to expect (loadingSkeletonCount): a run
78103
78256
  // holding none of them, and asking for none — they arrive through the app.
@@ -78178,10 +78331,13 @@ const useRunItems = (store, {
78178
78331
  skeletonItem = {};
78179
78332
  const virtualItemSize = listItems.virtualItemSizeSignal.value;
78180
78333
  if (virtualItemSize) {
78334
+ // The room is the item's and what stands before it (see
78335
+ // readSpaceBetweenItems): the box takes what is left of it.
78336
+ const minSize = `calc(${virtualItemSize}px - var(--x-list-item-between, 0px))`;
78181
78337
  if (listItems.horizontal) {
78182
- skeletonItem.itemMinWidth = `${virtualItemSize}px`;
78338
+ skeletonItem.itemMinWidth = minSize;
78183
78339
  } else {
78184
- skeletonItem.itemMinHeight = `${virtualItemSize}px`;
78340
+ skeletonItem.itemMinHeight = minSize;
78185
78341
  }
78186
78342
  }
78187
78343
  return skeletonItem;
@@ -78715,6 +78871,7 @@ const ListItemsFailure = ({
78715
78871
  // hits the network again.
78716
78872
  const ITEM_STORE_MAX_DEFAULT = 1000;
78717
78873
  const ITEM_STORE_KEEP_AROUND = 250;
78874
+ const COUNT_NOT_SAID = {};
78718
78875
  const rangeIsSame = (a, b) => {
78719
78876
  if (!a || !b) {
78720
78877
  return a === b;
@@ -78731,8 +78888,10 @@ const useItemStore = ({
78731
78888
  items,
78732
78889
  count,
78733
78890
  itemsAction,
78891
+ settling,
78734
78892
  memoryBudget,
78735
- onRequestStateChange
78893
+ onRequestStateChange,
78894
+ onCountChange
78736
78895
  }) => {
78737
78896
  // A collection given whole (`items`) is held from the first render: nothing
78738
78897
  // to ask for, nothing to keep across mounts, nothing to invalidate. The rest
@@ -78815,6 +78974,68 @@ const useItemStore = ({
78815
78974
  if (staleRanksRef.current === null) {
78816
78975
  staleRanksRef.current = restored ? new Set(pages.byIndex.keys()) : new Set();
78817
78976
  }
78977
+ const takeInPage = ({
78978
+ pageItems,
78979
+ pageStart,
78980
+ pageCount
78981
+ }) => {
78982
+ // An item from before is gone from where it stood when the page puts
78983
+ // it at another rank (it would be drawn twice), or when its rank is
78984
+ // past the end of the collection.
78985
+ const staleRanks = staleRanksRef.current;
78986
+ const pageEnd = pageStart + pageItems.length;
78987
+ let dropped = false;
78988
+ if (staleRanks.size > 0) {
78989
+ const pageIds = new Set();
78990
+ for (const pageItem of pageItems) {
78991
+ if (pageItem && pageItem.id !== undefined) {
78992
+ pageIds.add(pageItem.id);
78993
+ }
78994
+ }
78995
+ for (const rank of staleRanks) {
78996
+ if (rank >= pageStart && rank < pageEnd) {
78997
+ continue;
78998
+ }
78999
+ const staleItem = pages.byIndex.get(rank);
79000
+ if (rank >= pageCount || staleItem && pageIds.has(staleItem.id)) {
79001
+ staleRanks.delete(rank);
79002
+ pages.byIndex.delete(rank);
79003
+ dropped = true;
79004
+ }
79005
+ }
79006
+ }
79007
+ let i = 0;
79008
+ while (i < pageItems.length) {
79009
+ pages.byIndex.set(pageStart + i, pageItems[i]);
79010
+ staleRanks.delete(pageStart + i);
79011
+ i++;
79012
+ }
79013
+ pages.count = pageCount;
79014
+ // Which rank holds which id, kept by the source so a list drawing
79015
+ // this collection again finds it drawn (see readComposition above).
79016
+ if (itemsAction.writeComposition) {
79017
+ itemsAction.writeComposition({
79018
+ byIndex: pages.byIndex,
79019
+ count: pageCount,
79020
+ replace: dropped
79021
+ });
79022
+ }
79023
+ };
79024
+ // The pages answered since the last render, taken in by the render and not
79025
+ // as they land: what the list reads of the run — how many items it stands for
79026
+ // (see listItems.take), where an item sits (its locator) — then moves in one
79027
+ // step. Taken in on landing, an item could be located past the count the list
79028
+ // still holds, and a list held on it would aim its window there at every
79029
+ // render, framed back each time.
79030
+ const pagesLandedRef = useRef([]);
79031
+ const pagesLanded = pagesLandedRef.current;
79032
+ const pagesTakenIn = pagesLanded.length > 0;
79033
+ if (pagesTakenIn) {
79034
+ pagesLandedRef.current = [];
79035
+ for (const page of pagesLanded) {
79036
+ takeInPage(page);
79037
+ }
79038
+ }
78818
79039
  const [refreshing, setRefreshing] = useState(false);
78819
79040
  // A source that says when what it reads has moved (a resource range reader
78820
79041
  // does: see rerunOn.GET_RANGE) is heard here — a write deciding who belongs
@@ -78845,28 +79066,55 @@ const useItemStore = ({
78845
79066
  const [failure, setFailure] = useState(null);
78846
79067
  const listItems = useContext(ListItemsContext);
78847
79068
  // The items are there, which is what the list waits for to place itself on the
78848
- // item it is held at (see placeWhereHeld). Said from an effect: a signal read
78849
- // during this very render must not be written during it.
79069
+ // item it is held at (see placeWhereHeld). Said from an effect, once the run
79070
+ // has drawn them: a signal read during this very render must not be written
79071
+ // during it.
78850
79072
  useLayoutEffect(() => {
78851
- if (!inMemory || itemsHeldRef.current) {
79073
+ if (inMemory) {
79074
+ if (itemsHeldRef.current) {
79075
+ return;
79076
+ }
79077
+ itemsHeldRef.current = true;
79078
+ } else if (!pagesTakenIn) {
78852
79079
  return;
78853
79080
  }
78854
- itemsHeldRef.current = true;
78855
79081
  listItems.pagesSignal.value = listItems.pagesSignal.peek() + 1;
78856
79082
  });
78857
79083
  // Before the first answer a run does not know how many items it stands for.
78858
79084
  // It stands for a page of them: a list that is about to be filled looks
78859
79085
  // like items on their way, not like an empty list.
78860
79086
  const itemCount = pages.count ?? count ?? listItems.pageSize;
79087
+ // The items drawn are from before while they are read again, and while the
79088
+ // collection that replaces them waits for its params to hold still (see
79089
+ // useSettledReader).
79090
+ const refreshingShown = refreshing || Boolean(settling);
78861
79091
  useLayoutEffect(() => {
78862
- if (!refreshing) {
79092
+ if (!refreshingShown) {
78863
79093
  return null;
78864
79094
  }
78865
79095
  listItems.refreshingSignal.value = listItems.refreshingSignal.peek() + 1;
78866
79096
  return () => {
78867
79097
  listItems.refreshingSignal.value = listItems.refreshingSignal.peek() - 1;
78868
79098
  };
78869
- }, [refreshing]);
79099
+ }, [refreshingShown]);
79100
+
79101
+ // How many items the collection holds, for the screen around the list to
79102
+ // say: the count the source answered or the caller gave, never the page the
79103
+ // run stands for before it knows. Said by each run on its first commit, so a
79104
+ // run drawing another collection takes back what the one before said.
79105
+ const countKnown = pages.count ?? count;
79106
+ const onCountChangeRef = useRef(onCountChange);
79107
+ onCountChangeRef.current = onCountChange;
79108
+ const countSaidRef = useRef(COUNT_NOT_SAID);
79109
+ useLayoutEffect(() => {
79110
+ if (countSaidRef.current === countKnown) {
79111
+ return;
79112
+ }
79113
+ countSaidRef.current = countKnown;
79114
+ if (onCountChangeRef.current) {
79115
+ onCountChangeRef.current(countKnown);
79116
+ }
79117
+ });
78870
79118
 
78871
79119
  // What the run is doing, for the screen around the list to draw: the list has
78872
79120
  // its own skeletons, what is around it has to be told. Read from the request
@@ -78901,8 +79149,11 @@ const useItemStore = ({
78901
79149
  };
78902
79150
  // A list taken off the screen while it was asking leaves nothing ringing
78903
79151
  // behind it: whoever is drawing "looking for items" has to stop.
79152
+ const mountedRef = useRef(false);
78904
79153
  useLayoutEffect(() => {
79154
+ mountedRef.current = true;
78905
79155
  return () => {
79156
+ mountedRef.current = false;
78906
79157
  if (requestStateRef.current.busy && onRequestStateChangeRef.current) {
78907
79158
  onRequestStateChangeRef.current({
78908
79159
  busy: false,
@@ -78915,7 +79166,7 @@ const useItemStore = ({
78915
79166
  const store = {
78916
79167
  itemCount,
78917
79168
  failure,
78918
- refreshing,
79169
+ refreshing: refreshingShown,
78919
79170
  // JS memory is cheap next to the DOM, but a long enough scroll accumulates
78920
79171
  // everything it ever went through. Items far from what is on screen are
78921
79172
  // dropped and simply asked for again if the user goes back — the same
@@ -79164,51 +79415,21 @@ const useItemStore = ({
79164
79415
  const pageItems = Array.isArray(page) ? page : page.items;
79165
79416
  const pageStart = Array.isArray(page) ? 0 : page.start ?? 0;
79166
79417
  const pageCount = Array.isArray(page) ? pageItems.length : page.count ?? pageStart + pageItems.length;
79418
+ const pageLanded = {
79419
+ pageItems,
79420
+ pageStart,
79421
+ pageCount
79422
+ };
79423
+ if (!mountedRef.current) {
79424
+ // No render is coming to take it in, and the source still keeps
79425
+ // what came back for the next list drawing this collection.
79426
+ takeInPage(pageLanded);
79427
+ return;
79428
+ }
79167
79429
  // Before the items land: what is on screen has to stay where it is,
79168
79430
  // and the DOM still shows the state to hold onto.
79169
79431
  listItems.captureAnchor();
79170
- // An item from before is gone from where it stood when the page puts
79171
- // it at another rank (it would be drawn twice), or when its rank is
79172
- // past the end of the collection.
79173
- const staleRanksNow = staleRanksRef.current;
79174
- const pageEnd = pageStart + pageItems.length;
79175
- let dropped = false;
79176
- if (staleRanksNow.size > 0) {
79177
- const pageIds = new Set();
79178
- for (const pageItem of pageItems) {
79179
- if (pageItem && pageItem.id !== undefined) {
79180
- pageIds.add(pageItem.id);
79181
- }
79182
- }
79183
- for (const rank of staleRanksNow) {
79184
- if (rank >= pageStart && rank < pageEnd) {
79185
- continue;
79186
- }
79187
- const staleItem = pages.byIndex.get(rank);
79188
- if (rank >= pageCount || staleItem && pageIds.has(staleItem.id)) {
79189
- staleRanksNow.delete(rank);
79190
- pages.byIndex.delete(rank);
79191
- dropped = true;
79192
- }
79193
- }
79194
- }
79195
- let i = 0;
79196
- while (i < pageItems.length) {
79197
- pages.byIndex.set(pageStart + i, pageItems[i]);
79198
- staleRanksNow.delete(pageStart + i);
79199
- i++;
79200
- }
79201
- pages.count = pageCount;
79202
- // Which rank holds which id, kept by the source so a list drawing
79203
- // this collection again finds it drawn (see readComposition above).
79204
- if (itemsAction.writeComposition) {
79205
- itemsAction.writeComposition({
79206
- byIndex: pages.byIndex,
79207
- count: pageCount,
79208
- replace: dropped
79209
- });
79210
- }
79211
- listItems.pagesSignal.value = listItems.pagesSignal.peek() + 1;
79432
+ pagesLandedRef.current.push(pageLanded);
79212
79433
  setPageVersion(version => version + 1);
79213
79434
  };
79214
79435
  const failed = error => {
@@ -79572,8 +79793,9 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
79572
79793
  * @param {number} [props.virtualItemSize]
79573
79794
  * The room an item never drawn is held at, in px along the scroll axis: what
79574
79795
  * the fillers hold for it outside the render window, what a scroll position
79575
- * inside them is read with, and the least an item on its way takes (see
79576
- * `renderSkeleton`). An item the list has drawn is held at the room it took
79796
+ * inside them is read with, and the least room an item on its way takes
79797
+ * (see `renderSkeleton`) — a room counts what stands before the item, a
79798
+ * 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
79577
79799
  * then, and an item whose run says its room (`<List.Items itemSize>`) at
79578
79800
  * that room. Left out, it is the average of the items measured so far — once when
79579
79801
  * the list mounts, again when a popup around it opens, and after each commit