@jsenv/navi 0.29.514 → 0.29.516

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.
@@ -462,32 +462,33 @@ const MountAfterPaint = ({
462
462
  /**
463
463
  * Builds, now, every box still waiting whose top is above `bottom` (in the
464
464
  * document's coordinates): what the document will show once scrolled so that
465
- * `bottom` is the bottom of the screen. Boxes built push those below them
466
- * further down, so the ones measured above `bottom` are all the ones that can
467
- * be shown there, and a few more at worst.
465
+ * `bottom` is the bottom of the screen.
466
+ *
467
+ * Each box is read once those before it have their height. A box has no height
468
+ * until it is built, so read together they would all stand at the end of what
469
+ * is built, above `bottom` as soon as the offset is worth a screen: a page of
470
+ * sections would be built whole before its first frame. A box found below
471
+ * `bottom` waits: a build only pushes it further down, and where it stands
472
+ * already makes the document tall enough for the offset.
473
+ *
474
+ * The walk follows the set live, in mount order: a box mounted by a build is
475
+ * read too. One standing higher than boxes built before it (mounted later) can
476
+ * leave a few built that the offset does not show, never one missing that it
477
+ * does.
468
478
  *
469
479
  * Renders synchronously, so it must be called outside a Preact commit: a render
470
480
  * flushed from a layout effect runs inside the commit still walking the tree.
471
481
  */
472
482
  const buildDeferredBoxesAbove = bottom => {
473
- if (deferredBoxes.size === 0) {
474
- return;
475
- }
476
- const toBuild = [];
477
483
  for (const deferredBox of deferredBoxes) {
478
484
  const top = readTopInWindow(deferredBox.getElement());
479
- if (top !== null && top + window.scrollY < bottom) {
480
- toBuild.push(deferredBox);
485
+ if (top === null || top + window.scrollY >= bottom) {
486
+ continue;
481
487
  }
482
- }
483
- if (toBuild.length === 0) {
484
- return;
485
- }
486
- flushSyncRendering(() => {
487
- for (const deferredBox of toBuild) {
488
+ flushSyncRendering(() => {
488
489
  deferredBox.build();
489
- }
490
- });
490
+ });
491
+ }
491
492
  };
492
493
 
493
494
  // null for a box nothing draws (a closed popup, a hidden tab): no frame shows
@@ -10434,6 +10435,12 @@ const startAtTop = (url, { from } = {}) => {
10434
10435
  // position to come back to — the return would keep the offset of the page
10435
10436
  // being left.
10436
10437
  positionByUrl.set(href, { x: 0, y: 0 });
10438
+ // A list holding the document on the page arriving has placed it already: a
10439
+ // navigation holding the rendering gets here after that render, and nothing
10440
+ // owes the list another commit to put it back (see holdDocumentScroll).
10441
+ if (isDocumentHeld()) {
10442
+ return;
10443
+ }
10437
10444
  window.scrollTo({ top: 0, left: 0, behavior: "instant" });
10438
10445
  };
10439
10446
  // Also what decides whether a push carries the state of the page it keeps
@@ -10568,6 +10575,19 @@ const recallScrollerPosition = (name) => {
10568
10575
  return positionByName.get(name);
10569
10576
  };
10570
10577
 
10578
+ // Said by a scroller sent back to its start while it stays mounted: where it
10579
+ // was is a place in what it no longer shows, so nothing is carried and nothing
10580
+ // is come back to under this address until it says where it is again. What it
10581
+ // said under another address stays, for the way back to that address.
10582
+ const forgetScrollerPosition = (name) => {
10583
+ loadStore();
10584
+ saidByScrollerName.delete(name);
10585
+ const positionByName = scrollerPositionsByUrl.get(readShownUrl());
10586
+ if (positionByName) {
10587
+ positionByName.delete(name);
10588
+ }
10589
+ };
10590
+
10571
10591
  // Said by a scroller as it unmounts. Its page is being left when the url is
10572
10592
  // already another one — the history is written before the page it names is
10573
10593
  // taken down — and then its position is kept for the way back. The url still
@@ -13409,10 +13429,17 @@ const isRenderingPageChange = () => renderingPageChange;
13409
13429
  * outside any render — by a subscription deciding whether to re-render, by
13410
13430
  * the scroll restoration when a position is due — and it reads the routes
13411
13431
  * untracked, so asking subscribes nobody to them.
13412
- * - `leavingSignal` — true once the page is hidden and kept while its movement
13413
- * plays: still mounted, so what it fills or moves outside its own nodes (a
13414
- * slot in a bar, the document a list scrolls) steps out rather than showing
13415
- * with, or moving, the page arriving. Every box of it measures 0×0 by then.
13432
+ * - `hasLeft()` — whether the page, or a page it sits in, is kept while leaving.
13433
+ * Written by the container's render, before anything below it renders, and
13434
+ * final. `isShown()` can answer false for a page that stays on screen (the
13435
+ * address coming back, a container first rendered while the routes are
13436
+ * frozen), so what cannot be undone asks this one.
13437
+ * - `leavingSignal` — true once the page, or a page it sits in, is hidden and
13438
+ * kept while the movement plays: still mounted, so what it fills or moves
13439
+ * outside its own nodes (a slot in a bar, the document a list scrolls) steps
13440
+ * out rather than showing with, or moving, the page arriving. Every box of it
13441
+ * measures 0×0 by then. Set after the render keeping the page, so that render
13442
+ * and what it builds can only ask `hasLeft()`.
13416
13443
  */
13417
13444
  const RoutePageContext = createContext(null);
13418
13445
 
@@ -36622,7 +36649,7 @@ const useContentKeepingLeavingPages = (branch, readShownBranch, parentPage) => {
36622
36649
  continue;
36623
36650
  }
36624
36651
  entry.hidden = true;
36625
- entry.page.leavingSignal.value = true;
36652
+ entry.page.ownLeavingSignal.value = true;
36626
36653
  // The page renders on its own while kept (a component reading a signal
36627
36654
  // the address moved), never through this container: a top node it
36628
36655
  // replaces lands between the markers unmarked, a text it writes shows
@@ -36710,11 +36737,21 @@ const hideNodesBetween = (start, end, observer) => {
36710
36737
  // page it sits in is shown itself. Never again once kept while leaving: coming
36711
36738
  // back to its route mounts the page anew.
36712
36739
  const createRoutePage = (readShownBranch, parentPage) => {
36740
+ const ownLeavingSignal = signal(false);
36713
36741
  const page = {
36714
36742
  branchId: null,
36715
36743
  element: undefined,
36716
36744
  left: false,
36717
- leavingSignal: signal(false),
36745
+ ownLeavingSignal,
36746
+ // A page nested in one that left is hidden with it, even while its own
36747
+ // container goes on picking it (a route matching on both sides).
36748
+ leavingSignal: parentPage ? computed(() => ownLeavingSignal.value || parentPage.leavingSignal.value) : ownLeavingSignal,
36749
+ hasLeft: () => {
36750
+ if (page.left) {
36751
+ return true;
36752
+ }
36753
+ return parentPage ? parentPage.hasLeft() : false;
36754
+ },
36718
36755
  isShown: () => {
36719
36756
  if (page.left) {
36720
36757
  return false;
@@ -84055,6 +84092,7 @@ const ListUI = props => {
84055
84092
  scroller = "self",
84056
84093
  hoverWhileScrolling = false,
84057
84094
  scrollResetOnNavigation = false,
84095
+ scrollResetKey,
84058
84096
  lockSize,
84059
84097
  columns,
84060
84098
  itemColumns,
@@ -84173,6 +84211,7 @@ const ListUI = props => {
84173
84211
  scrolled,
84174
84212
  defaultScrolled,
84175
84213
  onScrolledChange,
84214
+ scrollResetKey,
84176
84215
  rememberScroll,
84177
84216
  listId: rest.id,
84178
84217
  scroller,
@@ -84504,6 +84543,7 @@ const useListScrollSync = ({
84504
84543
  scrolled,
84505
84544
  defaultScrolled,
84506
84545
  onScrolledChange,
84546
+ scrollResetKey,
84507
84547
  rememberScroll,
84508
84548
  listId,
84509
84549
  scroller,
@@ -84512,6 +84552,8 @@ const useListScrollSync = ({
84512
84552
  columns
84513
84553
  }) => {
84514
84554
  const debugScroll = useDebugScroll();
84555
+ // The offset a search gives back once emptied (see below).
84556
+ const savedScrollRef = useRef(null);
84515
84557
  // Where the list must be: what the caller holds it at (`scrolled`), or where
84516
84558
  // it opens and then lets go (`defaultScrolled`). A `scrolled` that changes is
84517
84559
  // the caller moving the list, so the hold is armed again — that is what makes
@@ -84520,7 +84562,9 @@ const useListScrollSync = ({
84520
84562
  // one kept from the previous visit): followed for as long as nobody has
84521
84563
  // moved the list, and compared by value, since it is said at every render.
84522
84564
  // Nothing to hold it at (a position not saved yet) is not a position: the
84523
- // list opens where it opens.
84565
+ // list opens where it opens. A `scrollResetKey` that changes sends it back to
84566
+ // its start ("back_to_start", see placeBackAtStart), unless a `scrolled`
84567
+ // changing in the same render says where it goes.
84524
84568
  const startPlaceRef = useRef(null);
84525
84569
  if (startPlaceRef.current === null) {
84526
84570
  startPlaceRef.current = {
@@ -84528,6 +84572,7 @@ const useListScrollSync = ({
84528
84572
  wanted: scrolled ?? defaultScrolled,
84529
84573
  scrolled,
84530
84574
  defaultScrolled,
84575
+ scrollResetKey,
84531
84576
  way: null,
84532
84577
  arrived: false,
84533
84578
  // Whether the hold's writes glide (--navi-scroll:smooth).
@@ -84535,6 +84580,20 @@ const useListScrollSync = ({
84535
84580
  };
84536
84581
  } else {
84537
84582
  const place = startPlaceRef.current;
84583
+ if (!compareTwoJsValues(place.scrollResetKey, scrollResetKey)) {
84584
+ place.scrollResetKey = scrollResetKey;
84585
+ place.wanted = "back_to_start";
84586
+ place.userTookOver = false;
84587
+ place.way = null;
84588
+ place.arrived = false;
84589
+ place.smooth = false;
84590
+ // What the list said it was reading, and what a search would give back,
84591
+ // are places in what it no longer shows.
84592
+ savedScrollRef.current = null;
84593
+ if (rememberScroll) {
84594
+ forgetScrollerPosition(listId);
84595
+ }
84596
+ }
84538
84597
  if (place.scrolled !== scrolled) {
84539
84598
  place.scrolled = scrolled;
84540
84599
  place.defaultScrolled = defaultScrolled;
@@ -84543,7 +84602,7 @@ const useListScrollSync = ({
84543
84602
  place.smooth = false;
84544
84603
  } else if ((scrolled === undefined || scrolled === null) && !compareTwoJsValues(place.defaultScrolled, defaultScrolled)) {
84545
84604
  place.defaultScrolled = defaultScrolled;
84546
- if (!place.userTookOver) {
84605
+ if (!place.userTookOver && place.wanted !== "back_to_start") {
84547
84606
  place.wanted = defaultScrolled;
84548
84607
  // Placed again from where it stands: the way to the previous place
84549
84608
  // says nothing about whose the next scrolls are.
@@ -84876,6 +84935,8 @@ const useListScrollSync = ({
84876
84935
  wantedStart = total - windowSize;
84877
84936
  } else if (typeof scrolledWanted === "number") {
84878
84937
  heldItem = scrolledWanted;
84938
+ } else if (scrolledWanted === "back_to_start") {
84939
+ heldItem = 0;
84879
84940
  } else if (scrolledWanted && scrolledWanted.id !== undefined) {
84880
84941
  const itemIndex = listItems.locateItem(scrolledWanted.id);
84881
84942
  if (itemIndex !== null) {
@@ -85110,7 +85171,6 @@ const useListScrollSync = ({
85110
85171
  // Lorsqu'on selectionne quelque chose pendant une recherche, alors ensuite meme si on clear
85111
85172
  // on veut pas revenir a la position scroll précédente car on veut garde l'item qu'on a selectionné visible
85112
85173
  // (pour l'instant pas grave car on travaille pour le mode select qui fermera le dialog au select)
85113
- const savedScrollRef = useRef(null);
85114
85174
  const topMatchScoresKeyRef = useRef("");
85115
85175
  const restoreScrollRafRef = useRef(null);
85116
85176
  useLayoutEffect(() => {
@@ -85219,6 +85279,10 @@ const useListScrollSync = ({
85219
85279
  if (scrolledWanted === "start" || scrolledWanted === undefined || startPlaceRef.current.userTookOver || !ref.current) {
85220
85280
  return;
85221
85281
  }
85282
+ if (scrolledWanted === "back_to_start") {
85283
+ placeBackAtStart();
85284
+ return;
85285
+ }
85222
85286
  if (listItems.totalSignal.peek() === 0 || virtualItemSizeSignal.peek() === 0) {
85223
85287
  return;
85224
85288
  }
@@ -85317,6 +85381,24 @@ const useListScrollSync = ({
85317
85381
  }
85318
85382
  sendHeldScroll(scrollNow + delta);
85319
85383
  };
85384
+ // The start of the list, what comes before its first item included, not an
85385
+ // item: a box of its own goes to 0. A box the list is further down of also
85386
+ // shows what stands above it, the control that asked for this among others, so
85387
+ // the list is brought back to the box's start and never sent forward to it.
85388
+ const placeBackAtStart = () => {
85389
+ const scrollerEl = getScroller();
85390
+ if (ref.current.contains(scrollerEl)) {
85391
+ sendHeldScroll(0);
85392
+ return;
85393
+ }
85394
+ const scale = readScrollerScale(scrollerEl, horizontal);
85395
+ const viewportRect = getScrollerViewportRect(scrollerEl);
85396
+ const listRect = ref.current.getBoundingClientRect();
85397
+ const offsetNow = (horizontal ? listRect.left - viewportRect.left : listRect.top - viewportRect.top) / scale;
85398
+ const delta = offsetNow - getItemScrollInset(scrollerEl, ref.current, horizontal);
85399
+ const scrollNow = horizontal ? scrollerEl.scrollLeft : scrollerEl.scrollTop;
85400
+ sendHeldScroll(delta < 0 ? scrollNow + delta : scrollNow);
85401
+ };
85320
85402
  // Every placement goes through here, the ones that find the list in place
85321
85403
  // too: it keeps the way from where the scroller was to where it is sent,
85322
85404
  // which is how a scroll event tells the hold's own scrolls from anyone
@@ -85539,27 +85621,35 @@ const useListScrollSync = ({
85539
85621
  // landing or resizing — and said again when it reads differently (`ifChanged`):
85540
85622
  // the same scroll, the screen said right.
85541
85623
  const announcedRef = useRef(null);
85624
+ // A scroll landing past everything drawn (a key sending the list to its end,
85625
+ // a hand on the scrollbar) has no item to read a position from: it is said by
85626
+ // that re-read, even when it is the first thing the list has to say.
85627
+ const reportOwedRef = useRef(false);
85542
85628
  const reportPosition = ({
85543
85629
  ifChanged
85544
85630
  } = {}) => {
85545
85631
  if (!ref.current) {
85546
85632
  return;
85547
85633
  }
85548
- if (ifChanged && !announcedRef.current) {
85634
+ if (ifChanged && !announcedRef.current && !reportOwedRef.current) {
85549
85635
  return;
85550
85636
  }
85551
85637
  const remember = rememberScrollRef.current;
85552
85638
  const onScrolledChange = onScrolledChangeRef.current;
85553
85639
  const position = captureListAnchor(remember || Boolean(onScrolledChange));
85554
85640
  if (!position) {
85641
+ if (!ifChanged) {
85642
+ reportOwedRef.current = true;
85643
+ }
85555
85644
  return;
85556
85645
  }
85646
+ reportOwedRef.current = false;
85557
85647
  positionRef.current = position;
85558
85648
  if (!remember && !onScrolledChange) {
85559
85649
  return;
85560
85650
  }
85561
85651
  const scrolledNow = toScrolledPosition(position);
85562
- if (ifChanged && isSamePosition(scrolledNow, announcedRef.current)) {
85652
+ if (ifChanged && announcedRef.current && isSamePosition(scrolledNow, announcedRef.current)) {
85563
85653
  return;
85564
85654
  }
85565
85655
  announcedRef.current = scrolledNow;
@@ -86105,10 +86195,9 @@ const useListScrollSync = ({
86105
86195
  // ends the hold like a wheel does — the ones that come with no input event
86106
86196
  // at all: the status bar tap on iOS, find in page, a script.
86107
86197
  // Only once the list has stood where it holds itself, though. Until then
86108
- // the page is still settling around it, and some of that is written after
86109
- // the list placed itself: a navigation holding the rendering for its
86110
- // transition takes the arriving page to its top once the page rendered
86111
- // (whenRenderingResumes), and the hold puts it back at its next commit.
86198
+ // the page is still settling around it, and what that moves (a document
86199
+ // still too short for the offset clamps it) the hold puts back at its next
86200
+ // commit.
86112
86201
  const letGoUnlessHeldScroll = () => {
86113
86202
  const place = startPlaceRef.current;
86114
86203
  const way = place.way;
@@ -90034,6 +90123,7 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
90034
90123
  * scroller?: "self" | "parent" | "document" | Element | {current: Element},
90035
90124
  * hoverWhileScrolling?: boolean,
90036
90125
  * scrollResetOnNavigation?: boolean,
90126
+ * scrollResetKey?: any,
90037
90127
  * fallback?: import("preact").ComponentChildren,
90038
90128
  * searchFallback?: import("preact").ComponentChildren,
90039
90129
  * searchText?: string,
@@ -90278,6 +90368,15 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
90278
90368
  * overwrite each other (see docs/scroll.md). A fresh arrival at the page
90279
90369
  * opens at `defaultScrolled` either way, and so does a list the caller holds
90280
90370
  * through `scrolled`.
90371
+ * @param {any} [props.scrollResetKey]
90372
+ * What the list shows, said as a value: another order, another filter. When
90373
+ * it changes after mount (compared by value: `[order, filter]` can be written
90374
+ * at every render) the list goes back to its start before the paint, in
90375
+ * whichever box `scroller` names, and is held there until the user reaches
90376
+ * for it. A list whose start is in view in a box that also shows what stands
90377
+ * above it does not move. A `scrolled` changing in the same render wins. The
90378
+ * position remembered under the `id` for the address shown is dropped; see
90379
+ * docs/scroll.md.
90281
90380
  * @param {boolean} [props.deselectable]
90282
90381
  * A single-select list allowed to hold nothing: the selected item, pressed
90283
90382
  * again, lets go. Without it the list is a radio group — a choice, once
@@ -106851,9 +106950,9 @@ SidePanel.Foot = SidePanelFoot;
106851
106950
  * mount, or the first to render into an empty slot; later renders of a
106852
106951
  * `SlotFill` that is not the filler leave the slot alone. A `SlotFill`
106853
106952
  * unmounting empties the slot only while it is still that one, and so does one
106854
- * in a page a route transition keeps while leaving (see keepLeavingPages in
106855
- * nav/route.jsx): still mounted, hidden, it must neither stay in the slot nor
106856
- * take it back from the page arriving.
106953
+ * in or below a page a route transition keeps while leaving (see
106954
+ * keepLeavingPages in nav/route.jsx): still mounted, hidden, it must neither
106955
+ * stay in the slot nor take it back from the page arriving.
106857
106956
  *
106858
106957
  * **One `SlotFill`, rendered where the choice is made.** `isFilled` follows
106859
106958
  * the fills as they mount and unmount, in the order the tree walks them. A
@@ -106918,8 +107017,10 @@ const createSlot = (SlotRenderer = Box) => {
106918
107017
  }, []);
106919
107018
  // In a page kept while leaving: out of the slot as if unmounted, since
106920
107019
  // the page arriving is what the slot must show — or nothing, when it
106921
- // fills none.
106922
- if (routePage && routePage.leavingSignal.value) {
107020
+ // fills none. The signal brings the render that steps out; hasLeft() refuses
107021
+ // a fill built in a kept page (a wrapper changing shape rebuilds what it
107022
+ // holds), which renders before that signal is set.
107023
+ if (routePage && (routePage.leavingSignal.value || routePage.hasLeft())) {
106923
107024
  if (filler === fillerRef) {
106924
107025
  filler = null;
106925
107026
  slotPropsSignal.value = null;