@jsenv/navi 0.29.417 → 0.29.418

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.
@@ -11254,6 +11254,28 @@ const suspendScrollRecording = () => {
11254
11254
  };
11255
11255
  };
11256
11256
 
11257
+ // A list scrolling the document that opens on a row puts the document where
11258
+ // that row is, by measuring it (see placeWhereHeld in list.jsx). The offset kept
11259
+ // for the url is pixels of the page as it was drawn — and a list draws a window
11260
+ // of its rows and holds the room of the others with fillers of an estimated
11261
+ // height, so the same pixels now fall on other rows. Put back after the list
11262
+ // has placed itself, they would undo it, in the very picture a route transition
11263
+ // takes. So the list holds the document while it is placing itself, and the
11264
+ // url's offset is not put back meanwhile. Counted: two lists can share one
11265
+ // document.
11266
+ let documentHoldCount = 0;
11267
+ const holdDocumentScroll = () => {
11268
+ documentHoldCount++;
11269
+ let released = false;
11270
+ return () => {
11271
+ if (released) {
11272
+ return;
11273
+ }
11274
+ released = true;
11275
+ documentHoldCount--;
11276
+ };
11277
+ };
11278
+
11257
11279
  let installed = false;
11258
11280
  const installScrollRestoration = () => {
11259
11281
  if (installed) {
@@ -11288,6 +11310,9 @@ const installScrollRestoration = () => {
11288
11310
  if (positionOnLoad && (positionOnLoad.x || positionOnLoad.y)) {
11289
11311
  const stopListening = observeRouteRender(() => {
11290
11312
  stopListening();
11313
+ if (documentHoldCount) {
11314
+ return;
11315
+ }
11291
11316
  scrollTo(positionOnLoad);
11292
11317
  });
11293
11318
  }
@@ -11302,6 +11327,11 @@ const restoreScrollPosition = (url) => {
11302
11327
  if (!position) {
11303
11328
  return false;
11304
11329
  }
11330
+ if (documentHoldCount) {
11331
+ // There is a place to go back to, and the list holding the document is
11332
+ // putting it back (see holdDocumentScroll).
11333
+ return true;
11334
+ }
11305
11335
  scrollTo(position);
11306
11336
  return true;
11307
11337
  };
@@ -72626,9 +72656,17 @@ const ListUI = props => {
72626
72656
  // why not an effect), and the floor of 30 is its: it protects the scrolling
72627
72657
  // window, not a picture nobody has seen yet.
72628
72658
  const {
72629
- initial: initialRenderBudget,
72659
+ initial: initialRenderBudgetProp,
72630
72660
  after: renderBudgetAfterPaint
72631
72661
  } = resolveRenderBudget(renderBudgetProp);
72662
+ // Opening on a position that says how many rows were on screen from its row
72663
+ // on (the way back to where the list was, see reportPosition): that is what
72664
+ // the first picture shows, whatever `initial` guessed — with the row above
72665
+ // when it stood partly in view. The rest waits for the paint like any
72666
+ // `initial`: a page coming back in a route transition builds it inside the
72667
+ // update callback, and every row below the fold delays the movement.
72668
+ const openingPosition = scrolled ?? defaultScrolled;
72669
+ const initialRenderBudget = openingPosition && typeof openingPosition === "object" && typeof openingPosition.visibleCount === "number" ? openingPosition.visibleCount + (openingPosition.offset > 0 ? 1 : 0) : initialRenderBudgetProp;
72632
72670
  let renderBudget = renderBudgetAfterPaint;
72633
72671
  if (renderBudget < 30 && !renderBudgetSkipCheck) {
72634
72672
  console.warn(`List: renderBudget=${renderBudget} is too low. A renderBudget below 30 is not supported: on large screens or when the list grows, items outside the window would appear as blank space instead of rendered content. Use a value of at least 30, or omit the prop to use the default (${RENDER_BUDGET_DEFAULT}).`);
@@ -73091,11 +73129,23 @@ const useListScrollSync = ({
73091
73129
  horizontal
73092
73130
  });
73093
73131
  };
73132
+
73133
+ // How many rows the window keeps above the row the list is held at. Around it
73134
+ // for the scrolling to come, from the first paint on; before, the window is
73135
+ // the first picture's, and a picture of the list opening on a row shows that
73136
+ // row and what is below it — with the one above when a remembered position
73137
+ // says it stood partly in view.
73138
+ const rowsAboveOpening = (openAt, windowSize) => {
73139
+ if (renderBudget !== renderBudgetSteady) {
73140
+ return openAt && typeof openAt === "object" && openAt.offset > 0 ? 1 : 0;
73141
+ }
73142
+ return Math.floor(windowSize / 2);
73143
+ };
73094
73144
  const [renderWindow, setRenderWindow] = useState(() => {
73095
73145
  // Opening somewhere else than the beginning starts by framing there: the
73096
73146
  // rows the list will draw are the rows it will ask for.
73097
73147
  const openAt = scrolled ?? defaultScrolled;
73098
- const start = typeof openAt === "number" ? openAt - Math.floor(renderBudget / 2) : 0;
73148
+ const start = typeof openAt === "number" ? openAt - rowsAboveOpening(openAt, renderBudget) : 0;
73099
73149
  const startClamped = start < 0 ? 0 : start;
73100
73150
  return {
73101
73151
  start: startClamped,
@@ -73169,8 +73219,12 @@ const useListScrollSync = ({
73169
73219
  // Held somewhere it has not reached yet: what the window frames right now
73170
73220
  // is not what it will frame, so nothing should be fetched for it.
73171
73221
  listRows.holdPending = scrolledWanted !== "start" && scrolledWanted !== undefined;
73222
+ // Zero until the runs have rendered — they count their rows as they do,
73223
+ // after the list — which is not a collection fitting in the window: it is
73224
+ // one whose end is not known yet, and the window then frames the row the
73225
+ // list opens on, already in the first commit.
73172
73226
  const total = listRows.totalSignal.peek();
73173
- if (total <= renderBudget) {
73227
+ if (total > 0 && total <= renderBudget) {
73174
73228
  // The whole collection is what the list draws: wherever in it the list is
73175
73229
  // held, the window is already its place. Nowhere to move to means nothing
73176
73230
  // to wait for — a hold left standing here is a list that never asks for
@@ -73178,21 +73232,21 @@ const useListScrollSync = ({
73178
73232
  listRows.holdPending = false;
73179
73233
  return;
73180
73234
  }
73181
- const half = Math.floor(renderBudget / 2);
73235
+ const above = rowsAboveOpening(scrolledWanted, renderBudget);
73182
73236
  let wantedStart = null;
73183
73237
  if (scrolledWanted === "end") {
73184
73238
  wantedStart = total - renderBudget;
73185
73239
  } else if (typeof scrolledWanted === "number") {
73186
- wantedStart = scrolledWanted - half;
73240
+ wantedStart = scrolledWanted - above;
73187
73241
  } else if (scrolledWanted && scrolledWanted.id !== undefined) {
73188
73242
  const rowIndex = listRows.locateRow(scrolledWanted.id);
73189
73243
  if (rowIndex !== null) {
73190
- wantedStart = rowIndex - half;
73244
+ wantedStart = rowIndex - above;
73191
73245
  } else if (typeof scrolledWanted.index === "number") {
73192
73246
  // The row has not come back yet, but where it stood is known: near
73193
73247
  // enough to frame, and to put the scrollbar roughly where it will end
73194
73248
  // up rather than at the top.
73195
- wantedStart = scrolledWanted.index - half;
73249
+ wantedStart = scrolledWanted.index - above;
73196
73250
  }
73197
73251
  }
73198
73252
  if (wantedStart === null) {
@@ -73201,12 +73255,12 @@ const useListScrollSync = ({
73201
73255
  // back — the run asks for it by name (see useRequestMissing).
73202
73256
  return;
73203
73257
  }
73258
+ if (total > 0 && wantedStart + renderBudget > total) {
73259
+ wantedStart = total - renderBudget;
73260
+ }
73204
73261
  if (wantedStart < 0) {
73205
73262
  wantedStart = 0;
73206
73263
  }
73207
- if (wantedStart + renderBudget > total) {
73208
- wantedStart = total - renderBudget;
73209
- }
73210
73264
  const {
73211
73265
  start,
73212
73266
  end
@@ -73543,8 +73597,8 @@ const useListScrollSync = ({
73543
73597
  end
73544
73598
  } = renderWindowRef.current;
73545
73599
  if (rowIndex < start || rowIndex >= end) {
73546
- const half = Math.floor((end - start) / 2);
73547
- const wantedStart = rowIndex - half < 0 ? 0 : rowIndex - half;
73600
+ const above = rowsAboveOpening(scrolledWanted, end - start);
73601
+ const wantedStart = rowIndex - above < 0 ? 0 : rowIndex - above;
73548
73602
  updateRenderWindow(wantedStart, wantedStart + (end - start), `opening on row ${scrolledWanted.id}`);
73549
73603
  return;
73550
73604
  }
@@ -73590,6 +73644,15 @@ const useListScrollSync = ({
73590
73644
  }
73591
73645
  };
73592
73646
  useLayoutEffect(placeWhereHeld);
73647
+ // Held on a row of a list scrolling the document: where the document goes is
73648
+ // this list's to say, measured on the row, not the url's offset in pixels
73649
+ // (see holdDocumentScroll).
73650
+ useLayoutEffect(() => {
73651
+ if (!heldSomewhere || getScroller() !== document.scrollingElement) {
73652
+ return undefined;
73653
+ }
73654
+ return holdDocumentScroll();
73655
+ }, [heldSomewhere, scrollerElResolved]);
73593
73656
  // What to do when the list's own geometry moves under it, kept fresh for the
73594
73657
  // observer below (which is installed once).
73595
73658
  const onGeometryChangeRef = useRef(null);
@@ -73632,31 +73695,40 @@ const useListScrollSync = ({
73632
73695
  // Where the list was at the last thing that moved it. Kept whether anyone
73633
73696
  // asked for it or not: it is what a resize needs to put things back.
73634
73697
  const positionRef = useRef(null);
73698
+ const captureListAnchor = countVisible => captureScrollAnchor({
73699
+ scrollerEl: getScroller(),
73700
+ listEl: getListEl(),
73701
+ items: listRows.visibleItemsSignal.peek(),
73702
+ horizontal,
73703
+ countVisible
73704
+ });
73705
+ // Said from where the row lands on its own, and with `visibleCount`: the
73706
+ // rows on screen from that one on, which is what a list given this position
73707
+ // back draws first (see ListUI).
73708
+ const toScrolledPosition = position => {
73709
+ const rowEl = findRowElement(getListEl(), position.id);
73710
+ return {
73711
+ id: position.id,
73712
+ index: position.index,
73713
+ offset: position.offset - getRowScrollInset(getScroller(), rowEl, horizontal),
73714
+ visibleCount: position.visibleCount
73715
+ };
73716
+ };
73635
73717
  const reportPosition = () => {
73636
73718
  if (!ref.current) {
73637
73719
  return;
73638
73720
  }
73639
- const position = captureScrollAnchor({
73640
- scrollerEl: getScroller(),
73641
- listEl: getListEl(),
73642
- items: listRows.visibleItemsSignal.peek(),
73643
- horizontal
73644
- });
73721
+ const remember = rememberScrollRef.current;
73722
+ const onScrolledChange = onScrolledChangeRef.current;
73723
+ const position = captureListAnchor(remember || Boolean(onScrolledChange));
73645
73724
  if (!position) {
73646
73725
  return;
73647
73726
  }
73648
73727
  positionRef.current = position;
73649
- const remember = rememberScrollRef.current;
73650
- const onScrolledChange = onScrolledChangeRef.current;
73651
73728
  if (!remember && !onScrolledChange) {
73652
73729
  return;
73653
73730
  }
73654
- const rowEl = findRowElement(getListEl(), position.id);
73655
- const scrolledNow = {
73656
- id: position.id,
73657
- index: position.index,
73658
- offset: position.offset - getRowScrollInset(getScroller(), rowEl, horizontal)
73659
- };
73731
+ const scrolledNow = toScrolledPosition(position);
73660
73732
  if (remember) {
73661
73733
  rememberScrollerPosition(listId, scrolledNow);
73662
73734
  }
@@ -73664,6 +73736,20 @@ const useListScrollSync = ({
73664
73736
  onScrolledChange(scrolledNow);
73665
73737
  }
73666
73738
  };
73739
+ // Nothing scrolled, yet what is on screen changed: the list has just been
73740
+ // laid out, its rows have arrived, one of them grew. A list read where it
73741
+ // opened would otherwise come back with nothing kept — opening again as a
73742
+ // fresh arrival does, all of its window drawn in the first commit instead of
73743
+ // the rows that were on screen. Kept, and told to nobody: nobody scrolled.
73744
+ const rememberPosition = () => {
73745
+ if (!rememberScrollRef.current || !ref.current) {
73746
+ return;
73747
+ }
73748
+ const position = captureListAnchor(true);
73749
+ if (position) {
73750
+ rememberScrollerPosition(listId, toScrolledPosition(position));
73751
+ }
73752
+ };
73667
73753
  // Leaving: with its page, or alone (see forgetScrollerUnlessPageLeft).
73668
73754
  useLayoutEffect(() => {
73669
73755
  return () => {
@@ -73689,6 +73775,7 @@ const useListScrollSync = ({
73689
73775
  // list which has outgrown a bounded ancestor stops holding on to the
73690
73776
  // page.
73691
73777
  resolveScroller();
73778
+ rememberPosition();
73692
73779
  // Two things resize here, and they call for opposite answers. The LIST
73693
73780
  // growing is its own content settling: only a list holding itself
73694
73781
  // somewhere cares (the end it aims at has moved), and a list the user is
@@ -74477,12 +74564,15 @@ const getRowName = rowEl => rowEl.getAttribute("navi-list-item-real");
74477
74564
  // the list is rebuilt around it. Read off the rows' own boxes, not by
74478
74565
  // hit-testing the screen: a scrolling list takes its rows out of hit-testing
74479
74566
  // (see the navi-scrolling rule in the css above), and the scroll event is
74480
- // precisely when this is asked.
74567
+ // precisely when this is asked. `countVisible` adds how many rows are on
74568
+ // screen from that one on: what a list coming back there has to draw before
74569
+ // anything else (see the first paint in ListUI).
74481
74570
  const captureScrollAnchor = ({
74482
74571
  scrollerEl,
74483
74572
  listEl,
74484
74573
  items,
74485
- horizontal
74574
+ horizontal,
74575
+ countVisible
74486
74576
  }) => {
74487
74577
  if (!scrollerEl || !listEl) {
74488
74578
  return null;
@@ -74518,6 +74608,9 @@ const captureScrollAnchor = ({
74518
74608
  offset
74519
74609
  };
74520
74610
  if (offset >= 0) {
74611
+ if (countVisible) {
74612
+ anchor.visibleCount = countRowsStartingBefore(rowEls, i, range.to, horizontal);
74613
+ }
74521
74614
  return anchor;
74522
74615
  }
74523
74616
  // The row under the top of the view starts above it. Good enough to hold
@@ -74526,10 +74619,26 @@ const captureScrollAnchor = ({
74526
74619
  // top" reads, and can be drawn. Keep looking; this one is the fallback.
74527
74620
  if (!fallbackAnchor) {
74528
74621
  fallbackAnchor = anchor;
74622
+ if (countVisible) {
74623
+ anchor.visibleCount = countRowsStartingBefore(rowEls, i, range.to, horizontal);
74624
+ }
74529
74625
  }
74530
74626
  }
74531
74627
  return fallbackAnchor;
74532
74628
  };
74629
+ // How many of the rows from `fromIndex` on start before `to` along the scroll
74630
+ // axis — the ones on screen, when `to` is where the view ends.
74631
+ const countRowsStartingBefore = (rowEls, fromIndex, to, horizontal) => {
74632
+ let index = fromIndex;
74633
+ while (index < rowEls.length) {
74634
+ const rect = rowEls[index].getBoundingClientRect();
74635
+ if ((horizontal ? rect.left : rect.top) >= to) {
74636
+ break;
74637
+ }
74638
+ index++;
74639
+ }
74640
+ return index - fromIndex;
74641
+ };
74533
74642
  // The part of the list that is on screen, along the scrolling axis. Both edges
74534
74643
  // matter: the scroller may be larger than the list (scroller="parent") as well
74535
74644
  // as smaller (the list scrolls inside its own box).
@@ -76897,7 +77006,7 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
76897
77006
  * are skeletons, the others hold their room. `0` says the list is already
76898
77007
  * known to be empty: the empty `fallback` shows right away rather than an
76899
77008
  * empty frame.
76900
- * @param {"start"|"end"|number|{id: string, offset?: number}} [props.defaultScrolled="start"]
77009
+ * @param {"start"|"end"|number|{id: string, index?: number, offset?: number, visibleCount?: number}} [props.defaultScrolled="start"]
76901
77010
  * Where the list opens, after which the user owns the scroll — unless it is
76902
77011
  * being come back to (see `scrollResetOnNavigation`). `"end"` is a
76903
77012
  * thread read backwards — the last rows are the ones to show, and the ones
@@ -76909,8 +77018,10 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
76909
77018
  * the screen it was saved on. `offset: 0` is where a `scrollIntoView()` puts
76910
77019
  * it — in front of the fixed bar the scroller gives room for, below the
76911
77020
  * sticky header and the group label the row lives under — so nothing of that
76912
- * room has to be restated as a number by whoever asks.
76913
- * @param {"start"|"end"|number|{id: string, offset?: number}} [props.scrolled]
77021
+ * room has to be restated as a number by whoever asks. The `index` and
77022
+ * `visibleCount` it also hands out say where to aim before the row is found,
77023
+ * and how many rows to draw before the first paint (see `renderBudget`).
77024
+ * @param {"start"|"end"|number|{id: string, index?: number, offset?: number, visibleCount?: number}} [props.scrolled]
76914
77025
  * The same, but held: the list goes back there every time this changes, even
76915
77026
  * after the user has scrolled — the caller owns where the list is (see
76916
77027
  * `defaultScrolled` for the uncontrolled form, and `open`/`defaultOpen`
@@ -76921,12 +77032,12 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
76921
77032
  * In every form the list holds itself there while it is still finding out
76922
77033
  * how many rows there are and how tall one is, and lets go the moment the
76923
77034
  * user reaches for the list.
76924
- * @param {(scrolled: {id: string, index: number, offset: number}) => void} [props.onScrolledChange]
77035
+ * @param {(scrolled: {id: string, index: number, offset: number, visibleCount: number}) => void} [props.onScrolledChange]
76925
77036
  * Where the list is, as the user scrolls: the row at the top of the view and
76926
77037
  * how far below the place a row lands on its own (see `defaultScrolled`) it
76927
- * starts. Keep it to come back to it
76928
- * later through `scrolled`/`defaultScrolled` — an index would not do, since
76929
- * rows get inserted while a list is being read.
77038
+ * starts, and how many rows are on screen from that one on. Keep it whole to
77039
+ * come back to it later through `scrolled`/`defaultScrolled` — an index alone
77040
+ * would not do, since rows get inserted while a list is being read.
76930
77041
  * @param {number|{initial: number, after: number}} [props.renderBudget=100]
76931
77042
  * How many rows of a `<List.Items>` run are in the DOM at once: the render
76932
77043
  * window, which slides as the user scrolls while fillers hold the room of
@@ -76938,11 +77049,15 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
76938
77049
  * once, with room for that lookahead — the list warns below 30, and when a
76939
77050
  * budget leaves fewer than two rows beyond the screen.
76940
77051
  *
76941
- * `{ initial, after }` for a list drawn in the click that opens it (a popup):
76942
- * `initial` rows in the commit the browser paints first — about what a phone
76943
- * screen shows — and `after` from the paint on, where the floor of 30
76944
- * applies. The runs ask their source for `after` rows from the start, so the
76945
- * first picture costs no second request.
77052
+ * `{ initial, after }` for a list whose first picture is taken as it is
77053
+ * built — drawn in the click that opens a popup, or in the update callback
77054
+ * of a route transition bringing its page back: `initial` rows in the commit
77055
+ * the browser paints first — about what a phone screen shows — counted from
77056
+ * the row the list opens on, and `after` from the paint on, around it, where
77057
+ * the floor of 30 applies. Opening on a position that says how many rows were
77058
+ * on screen (`visibleCount`, see `onScrolledChange`) draws those first,
77059
+ * whatever `initial` says. The runs ask their source for `after` rows from
77060
+ * the start, so the first picture costs no second request.
76946
77061
  * @param {number} [props.virtualItemSize]
76947
77062
  * The size of one row along the scroll axis, in px, when every row has the
76948
77063
  * same: what the fillers are sized with and what a scroll position is