@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.
@@ -489,32 +489,33 @@ const MountAfterPaint = ({
489
489
  /**
490
490
  * Builds, now, every box still waiting whose top is above `bottom` (in the
491
491
  * document's coordinates): what the document will show once scrolled so that
492
- * `bottom` is the bottom of the screen. Boxes built push those below them
493
- * further down, so the ones measured above `bottom` are all the ones that can
494
- * be shown there, and a few more at worst.
492
+ * `bottom` is the bottom of the screen.
493
+ *
494
+ * Each box is read once those before it have their height. A box has no height
495
+ * until it is built, so read together they would all stand at the end of what
496
+ * is built, above `bottom` as soon as the offset is worth a screen: a page of
497
+ * sections would be built whole before its first frame. A box found below
498
+ * `bottom` waits: a build only pushes it further down, and where it stands
499
+ * already makes the document tall enough for the offset.
500
+ *
501
+ * The walk follows the set live, in mount order: a box mounted by a build is
502
+ * read too. One standing higher than boxes built before it (mounted later) can
503
+ * leave a few built that the offset does not show, never one missing that it
504
+ * does.
495
505
  *
496
506
  * Renders synchronously, so it must be called outside a Preact commit: a render
497
507
  * flushed from a layout effect runs inside the commit still walking the tree.
498
508
  */
499
509
  const buildDeferredBoxesAbove = bottom => {
500
- if (deferredBoxes.size === 0) {
501
- return;
502
- }
503
- const toBuild = [];
504
510
  for (const deferredBox of deferredBoxes) {
505
511
  const top = readTopInWindow(deferredBox.getElement());
506
- if (top !== null && top + window.scrollY < bottom) {
507
- toBuild.push(deferredBox);
512
+ if (top === null || top + window.scrollY >= bottom) {
513
+ continue;
508
514
  }
509
- }
510
- if (toBuild.length === 0) {
511
- return;
512
- }
513
- flushSyncRendering(() => {
514
- for (const deferredBox of toBuild) {
515
+ flushSyncRendering(() => {
515
516
  deferredBox.build();
516
- }
517
- });
517
+ });
518
+ }
518
519
  };
519
520
 
520
521
  // null for a box nothing draws (a closed popup, a hidden tab): no frame shows
@@ -10839,6 +10840,12 @@ const startAtTop = (url, { from } = {}) => {
10839
10840
  // position to come back to — the return would keep the offset of the page
10840
10841
  // being left.
10841
10842
  positionByUrl.set(href, { x: 0, y: 0 });
10843
+ // A list holding the document on the page arriving has placed it already: a
10844
+ // navigation holding the rendering gets here after that render, and nothing
10845
+ // owes the list another commit to put it back (see holdDocumentScroll).
10846
+ if (isDocumentHeld()) {
10847
+ return;
10848
+ }
10842
10849
  window.scrollTo({ top: 0, left: 0, behavior: "instant" });
10843
10850
  };
10844
10851
  // Also what decides whether a push carries the state of the page it keeps
@@ -10973,6 +10980,19 @@ const recallScrollerPosition = (name) => {
10973
10980
  return positionByName.get(name);
10974
10981
  };
10975
10982
 
10983
+ // Said by a scroller sent back to its start while it stays mounted: where it
10984
+ // was is a place in what it no longer shows, so nothing is carried and nothing
10985
+ // is come back to under this address until it says where it is again. What it
10986
+ // said under another address stays, for the way back to that address.
10987
+ const forgetScrollerPosition = (name) => {
10988
+ loadStore();
10989
+ saidByScrollerName.delete(name);
10990
+ const positionByName = scrollerPositionsByUrl.get(readShownUrl());
10991
+ if (positionByName) {
10992
+ positionByName.delete(name);
10993
+ }
10994
+ };
10995
+
10976
10996
  // Said by a scroller as it unmounts. Its page is being left when the url is
10977
10997
  // already another one — the history is written before the page it names is
10978
10998
  // taken down — and then its position is kept for the way back. The url still
@@ -14010,10 +14030,17 @@ const isRenderingPageChange = () => renderingPageChange;
14010
14030
  * outside any render — by a subscription deciding whether to re-render, by
14011
14031
  * the scroll restoration when a position is due — and it reads the routes
14012
14032
  * untracked, so asking subscribes nobody to them.
14013
- * - `leavingSignal` — true once the page is hidden and kept while its movement
14014
- * plays: still mounted, so what it fills or moves outside its own nodes (a
14015
- * slot in a bar, the document a list scrolls) steps out rather than showing
14016
- * with, or moving, the page arriving. Every box of it measures 0×0 by then.
14033
+ * - `hasLeft()` — whether the page, or a page it sits in, is kept while leaving.
14034
+ * Written by the container's render, before anything below it renders, and
14035
+ * final. `isShown()` can answer false for a page that stays on screen (the
14036
+ * address coming back, a container first rendered while the routes are
14037
+ * frozen), so what cannot be undone asks this one.
14038
+ * - `leavingSignal` — true once the page, or a page it sits in, is hidden and
14039
+ * kept while the movement plays: still mounted, so what it fills or moves
14040
+ * outside its own nodes (a slot in a bar, the document a list scrolls) steps
14041
+ * out rather than showing with, or moving, the page arriving. Every box of it
14042
+ * measures 0×0 by then. Set after the render keeping the page, so that render
14043
+ * and what it builds can only ask `hasLeft()`.
14017
14044
  */
14018
14045
  const RoutePageContext = createContext(null);
14019
14046
 
@@ -37873,7 +37900,7 @@ const useContentKeepingLeavingPages = (branch, readShownBranch, parentPage) => {
37873
37900
  continue;
37874
37901
  }
37875
37902
  entry.hidden = true;
37876
- entry.page.leavingSignal.value = true;
37903
+ entry.page.ownLeavingSignal.value = true;
37877
37904
  // The page renders on its own while kept (a component reading a signal
37878
37905
  // the address moved), never through this container: a top node it
37879
37906
  // replaces lands between the markers unmarked, a text it writes shows
@@ -37961,11 +37988,21 @@ const hideNodesBetween = (start, end, observer) => {
37961
37988
  // page it sits in is shown itself. Never again once kept while leaving: coming
37962
37989
  // back to its route mounts the page anew.
37963
37990
  const createRoutePage = (readShownBranch, parentPage) => {
37991
+ const ownLeavingSignal = signal(false);
37964
37992
  const page = {
37965
37993
  branchId: null,
37966
37994
  element: undefined,
37967
37995
  left: false,
37968
- leavingSignal: signal(false),
37996
+ ownLeavingSignal,
37997
+ // A page nested in one that left is hidden with it, even while its own
37998
+ // container goes on picking it (a route matching on both sides).
37999
+ leavingSignal: parentPage ? computed(() => ownLeavingSignal.value || parentPage.leavingSignal.value) : ownLeavingSignal,
38000
+ hasLeft: () => {
38001
+ if (page.left) {
38002
+ return true;
38003
+ }
38004
+ return parentPage ? parentPage.hasLeft() : false;
38005
+ },
37969
38006
  isShown: () => {
37970
38007
  if (page.left) {
37971
38008
  return false;
@@ -85860,6 +85897,7 @@ const ListUI = props => {
85860
85897
  scroller = "self",
85861
85898
  hoverWhileScrolling = false,
85862
85899
  scrollResetOnNavigation = false,
85900
+ scrollResetKey,
85863
85901
  lockSize,
85864
85902
  columns,
85865
85903
  itemColumns,
@@ -85978,6 +86016,7 @@ const ListUI = props => {
85978
86016
  scrolled,
85979
86017
  defaultScrolled,
85980
86018
  onScrolledChange,
86019
+ scrollResetKey,
85981
86020
  rememberScroll,
85982
86021
  listId: rest.id,
85983
86022
  scroller,
@@ -86348,6 +86387,7 @@ const useListScrollSync = ({
86348
86387
  scrolled,
86349
86388
  defaultScrolled,
86350
86389
  onScrolledChange,
86390
+ scrollResetKey,
86351
86391
  rememberScroll,
86352
86392
  listId,
86353
86393
  scroller,
@@ -86356,6 +86396,8 @@ const useListScrollSync = ({
86356
86396
  columns
86357
86397
  }) => {
86358
86398
  const debugScroll = useDebugScroll();
86399
+ // The offset a search gives back once emptied (see below).
86400
+ const savedScrollRef = useRef(null);
86359
86401
  // Where the list must be: what the caller holds it at (`scrolled`), or where
86360
86402
  // it opens and then lets go (`defaultScrolled`). A `scrolled` that changes is
86361
86403
  // the caller moving the list, so the hold is armed again — that is what makes
@@ -86364,7 +86406,9 @@ const useListScrollSync = ({
86364
86406
  // one kept from the previous visit): followed for as long as nobody has
86365
86407
  // moved the list, and compared by value, since it is said at every render.
86366
86408
  // Nothing to hold it at (a position not saved yet) is not a position: the
86367
- // list opens where it opens.
86409
+ // list opens where it opens. A `scrollResetKey` that changes sends it back to
86410
+ // its start ("back_to_start", see placeBackAtStart), unless a `scrolled`
86411
+ // changing in the same render says where it goes.
86368
86412
  const startPlaceRef = useRef(null);
86369
86413
  if (startPlaceRef.current === null) {
86370
86414
  startPlaceRef.current = {
@@ -86372,6 +86416,7 @@ const useListScrollSync = ({
86372
86416
  wanted: scrolled ?? defaultScrolled,
86373
86417
  scrolled,
86374
86418
  defaultScrolled,
86419
+ scrollResetKey,
86375
86420
  way: null,
86376
86421
  arrived: false,
86377
86422
  // Whether the hold's writes glide (--navi-scroll:smooth).
@@ -86379,6 +86424,20 @@ const useListScrollSync = ({
86379
86424
  };
86380
86425
  } else {
86381
86426
  const place = startPlaceRef.current;
86427
+ if (!compareTwoJsValues(place.scrollResetKey, scrollResetKey)) {
86428
+ place.scrollResetKey = scrollResetKey;
86429
+ place.wanted = "back_to_start";
86430
+ place.userTookOver = false;
86431
+ place.way = null;
86432
+ place.arrived = false;
86433
+ place.smooth = false;
86434
+ // What the list said it was reading, and what a search would give back,
86435
+ // are places in what it no longer shows.
86436
+ savedScrollRef.current = null;
86437
+ if (rememberScroll) {
86438
+ forgetScrollerPosition(listId);
86439
+ }
86440
+ }
86382
86441
  if (place.scrolled !== scrolled) {
86383
86442
  place.scrolled = scrolled;
86384
86443
  place.defaultScrolled = defaultScrolled;
@@ -86387,7 +86446,7 @@ const useListScrollSync = ({
86387
86446
  place.smooth = false;
86388
86447
  } else if ((scrolled === undefined || scrolled === null) && !compareTwoJsValues(place.defaultScrolled, defaultScrolled)) {
86389
86448
  place.defaultScrolled = defaultScrolled;
86390
- if (!place.userTookOver) {
86449
+ if (!place.userTookOver && place.wanted !== "back_to_start") {
86391
86450
  place.wanted = defaultScrolled;
86392
86451
  // Placed again from where it stands: the way to the previous place
86393
86452
  // says nothing about whose the next scrolls are.
@@ -86731,6 +86790,8 @@ const useListScrollSync = ({
86731
86790
  wantedStart = total - windowSize;
86732
86791
  } else if (typeof scrolledWanted === "number") {
86733
86792
  heldItem = scrolledWanted;
86793
+ } else if (scrolledWanted === "back_to_start") {
86794
+ heldItem = 0;
86734
86795
  } else if (scrolledWanted && scrolledWanted.id !== undefined) {
86735
86796
  const itemIndex = listItems.locateItem(scrolledWanted.id);
86736
86797
  if (itemIndex !== null) {
@@ -86966,7 +87027,6 @@ const useListScrollSync = ({
86966
87027
  // Lorsqu'on selectionne quelque chose pendant une recherche, alors ensuite meme si on clear
86967
87028
  // on veut pas revenir a la position scroll précédente car on veut garde l'item qu'on a selectionné visible
86968
87029
  // (pour l'instant pas grave car on travaille pour le mode select qui fermera le dialog au select)
86969
- const savedScrollRef = useRef(null);
86970
87030
  const topMatchScoresKeyRef = useRef("");
86971
87031
  const restoreScrollRafRef = useRef(null);
86972
87032
  useLayoutEffect(() => {
@@ -87076,6 +87136,10 @@ const useListScrollSync = ({
87076
87136
  if (scrolledWanted === "start" || scrolledWanted === undefined || startPlaceRef.current.userTookOver || !ref.current) {
87077
87137
  return;
87078
87138
  }
87139
+ if (scrolledWanted === "back_to_start") {
87140
+ placeBackAtStart();
87141
+ return;
87142
+ }
87079
87143
  if (listItems.totalSignal.peek() === 0 || virtualItemSizeSignal.peek() === 0) {
87080
87144
  return;
87081
87145
  }
@@ -87174,6 +87238,24 @@ const useListScrollSync = ({
87174
87238
  }
87175
87239
  sendHeldScroll(scrollNow + delta);
87176
87240
  };
87241
+ // The start of the list, what comes before its first item included, not an
87242
+ // item: a box of its own goes to 0. A box the list is further down of also
87243
+ // shows what stands above it, the control that asked for this among others, so
87244
+ // the list is brought back to the box's start and never sent forward to it.
87245
+ const placeBackAtStart = () => {
87246
+ const scrollerEl = getScroller();
87247
+ if (ref.current.contains(scrollerEl)) {
87248
+ sendHeldScroll(0);
87249
+ return;
87250
+ }
87251
+ const scale = readScrollerScale(scrollerEl, horizontal);
87252
+ const viewportRect = getScrollerViewportRect(scrollerEl);
87253
+ const listRect = ref.current.getBoundingClientRect();
87254
+ const offsetNow = (horizontal ? listRect.left - viewportRect.left : listRect.top - viewportRect.top) / scale;
87255
+ const delta = offsetNow - getItemScrollInset(scrollerEl, ref.current, horizontal);
87256
+ const scrollNow = horizontal ? scrollerEl.scrollLeft : scrollerEl.scrollTop;
87257
+ sendHeldScroll(delta < 0 ? scrollNow + delta : scrollNow);
87258
+ };
87177
87259
  // Every placement goes through here, the ones that find the list in place
87178
87260
  // too: it keeps the way from where the scroller was to where it is sent,
87179
87261
  // which is how a scroll event tells the hold's own scrolls from anyone
@@ -87396,27 +87478,35 @@ const useListScrollSync = ({
87396
87478
  // landing or resizing — and said again when it reads differently (`ifChanged`):
87397
87479
  // the same scroll, the screen said right.
87398
87480
  const announcedRef = useRef(null);
87481
+ // A scroll landing past everything drawn (a key sending the list to its end,
87482
+ // a hand on the scrollbar) has no item to read a position from: it is said by
87483
+ // that re-read, even when it is the first thing the list has to say.
87484
+ const reportOwedRef = useRef(false);
87399
87485
  const reportPosition = ({
87400
87486
  ifChanged
87401
87487
  } = {}) => {
87402
87488
  if (!ref.current) {
87403
87489
  return;
87404
87490
  }
87405
- if (ifChanged && !announcedRef.current) {
87491
+ if (ifChanged && !announcedRef.current && !reportOwedRef.current) {
87406
87492
  return;
87407
87493
  }
87408
87494
  const remember = rememberScrollRef.current;
87409
87495
  const onScrolledChange = onScrolledChangeRef.current;
87410
87496
  const position = captureListAnchor(remember || Boolean(onScrolledChange));
87411
87497
  if (!position) {
87498
+ if (!ifChanged) {
87499
+ reportOwedRef.current = true;
87500
+ }
87412
87501
  return;
87413
87502
  }
87503
+ reportOwedRef.current = false;
87414
87504
  positionRef.current = position;
87415
87505
  if (!remember && !onScrolledChange) {
87416
87506
  return;
87417
87507
  }
87418
87508
  const scrolledNow = toScrolledPosition(position);
87419
- if (ifChanged && isSamePosition(scrolledNow, announcedRef.current)) {
87509
+ if (ifChanged && announcedRef.current && isSamePosition(scrolledNow, announcedRef.current)) {
87420
87510
  return;
87421
87511
  }
87422
87512
  announcedRef.current = scrolledNow;
@@ -87972,10 +88062,9 @@ const useListScrollSync = ({
87972
88062
  // ends the hold like a wheel does — the ones that come with no input event
87973
88063
  // at all: the status bar tap on iOS, find in page, a script.
87974
88064
  // Only once the list has stood where it holds itself, though. Until then
87975
- // the page is still settling around it, and some of that is written after
87976
- // the list placed itself: a navigation holding the rendering for its
87977
- // transition takes the arriving page to its top once the page rendered
87978
- // (whenRenderingResumes), and the hold puts it back at its next commit.
88065
+ // the page is still settling around it, and what that moves (a document
88066
+ // still too short for the offset clamps it) the hold puts back at its next
88067
+ // commit.
87979
88068
  const letGoUnlessHeldScroll = () => {
87980
88069
  const place = startPlaceRef.current;
87981
88070
  const way = place.way;
@@ -92044,6 +92133,7 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
92044
92133
  * scroller?: "self" | "parent" | "document" | Element | {current: Element},
92045
92134
  * hoverWhileScrolling?: boolean,
92046
92135
  * scrollResetOnNavigation?: boolean,
92136
+ * scrollResetKey?: any,
92047
92137
  * fallback?: import("preact").ComponentChildren,
92048
92138
  * searchFallback?: import("preact").ComponentChildren,
92049
92139
  * searchText?: string,
@@ -92288,6 +92378,15 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
92288
92378
  * overwrite each other (see docs/scroll.md). A fresh arrival at the page
92289
92379
  * opens at `defaultScrolled` either way, and so does a list the caller holds
92290
92380
  * through `scrolled`.
92381
+ * @param {any} [props.scrollResetKey]
92382
+ * What the list shows, said as a value: another order, another filter. When
92383
+ * it changes after mount (compared by value: `[order, filter]` can be written
92384
+ * at every render) the list goes back to its start before the paint, in
92385
+ * whichever box `scroller` names, and is held there until the user reaches
92386
+ * for it. A list whose start is in view in a box that also shows what stands
92387
+ * above it does not move. A `scrolled` changing in the same render wins. The
92388
+ * position remembered under the `id` for the address shown is dropped; see
92389
+ * docs/scroll.md.
92291
92390
  * @param {boolean} [props.deselectable]
92292
92391
  * A single-select list allowed to hold nothing: the selected item, pressed
92293
92392
  * again, lets go. Without it the list is a radio group — a choice, once
@@ -108993,9 +109092,9 @@ SidePanel.Foot = SidePanelFoot;
108993
109092
  * mount, or the first to render into an empty slot; later renders of a
108994
109093
  * `SlotFill` that is not the filler leave the slot alone. A `SlotFill`
108995
109094
  * unmounting empties the slot only while it is still that one, and so does one
108996
- * in a page a route transition keeps while leaving (see keepLeavingPages in
108997
- * nav/route.jsx): still mounted, hidden, it must neither stay in the slot nor
108998
- * take it back from the page arriving.
109095
+ * in or below a page a route transition keeps while leaving (see
109096
+ * keepLeavingPages in nav/route.jsx): still mounted, hidden, it must neither
109097
+ * stay in the slot nor take it back from the page arriving.
108999
109098
  *
109000
109099
  * **One `SlotFill`, rendered where the choice is made.** `isFilled` follows
109001
109100
  * the fills as they mount and unmount, in the order the tree walks them. A
@@ -109060,8 +109159,10 @@ const createSlot = (SlotRenderer = Box) => {
109060
109159
  }, []);
109061
109160
  // In a page kept while leaving: out of the slot as if unmounted, since
109062
109161
  // the page arriving is what the slot must show — or nothing, when it
109063
- // fills none.
109064
- if (routePage && routePage.leavingSignal.value) {
109162
+ // fills none. The signal brings the render that steps out; hasLeft() refuses
109163
+ // a fill built in a kept page (a wrapper changing shape rebuilds what it
109164
+ // holds), which renders before that signal is set.
109165
+ if (routePage && (routePage.leavingSignal.value || routePage.hasLeft())) {
109065
109166
  if (filler === fillerRef) {
109066
109167
  filler = null;
109067
109168
  slotPropsSignal.value = null;