@jsenv/navi 0.29.432 → 0.29.434

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.
@@ -11514,9 +11514,16 @@ const [publishRouteRender, observeRouteRender] = createPubSub();
11514
11514
  * (recallScrollerPosition). What a push means for them is what it means for
11515
11515
  * the document — an arrival opens at the top (see startAtTop): the page
11516
11516
  * arrived at has its named positions dropped before its lists render, so a
11517
- * list recalls only on the way back. A scroller that goes while its page
11518
- * stays — a popup closing over the same address — has nothing to come back
11519
- * to, and says so as it leaves (forgetScrollerUnlessPageLeft).
11517
+ * list recalls only on the way back. A scroller that outlives a navigation —
11518
+ * its page stays under a new address, or it lives outside the page that
11519
+ * changed — is shown under the new address too, and is carried to it (see
11520
+ * carryScrollersOnceRendered). A scroller that goes while its page stays — a
11521
+ * popup closing over the same address — has nothing to come back to, and says
11522
+ * so as it leaves (forgetScrollerUnlessPageLeft).
11523
+ *
11524
+ * Every position is kept under the address the routing shows, which can be
11525
+ * ahead of the browser's: a replace waiting for its state to settle has moved
11526
+ * the page and not yet `window.location` (see readShownUrl).
11520
11527
  *
11521
11528
  * What is NOT covered, and cannot be from here: a page whose height depends on
11522
11529
  * something still loading. Its content is not there at the moment it is put
@@ -11537,12 +11544,28 @@ const [publishRouteRender, observeRouteRender] = createPubSub();
11537
11544
  const STORAGE_KEY = "navi_scroll_positions";
11538
11545
 
11539
11546
  const positionByUrl = new Map();
11547
+ // Where the document stands, as its scroll events last said, whoever it
11548
+ // belongs to: what a url shown without the document moving is given (see
11549
+ // stayInPlace). Null until the restoration is installed — nothing is recorded
11550
+ // before.
11551
+ let documentOffset = null;
11540
11552
  // url -> (scroller name -> position). The position is whatever the scroller
11541
11553
  // handed in: what it can put itself back on, in its own terms.
11542
11554
  const scrollerPositionsByUrl = new Map();
11543
- // The url each named scroller last spoke under: what tells a scroller leaving
11544
- // a page that stays from one leaving with its page.
11545
- const urlByScrollerName = new Map();
11555
+ // What each named scroller still mounted last said: its position and the url
11556
+ // it said it under. What tells a scroller leaving a page that stays from one
11557
+ // leaving with its page, and what one outliving a navigation is carried with.
11558
+ const saidByScrollerName = new Map();
11559
+
11560
+ // The address the page is shown under, as the routing knows it (see
11561
+ // installScrollRestoration): `window.location` lags behind a replace waiting
11562
+ // for its state to settle (replaceAddressWhenSettled in either integration),
11563
+ // and a scroll keyed by it meanwhile is written for the address being left.
11564
+ // Resolved, as an address handed to navTo can be kept as it was given.
11565
+ let readRoutingAddress = () => window.location.href;
11566
+ const readShownUrl = () => {
11567
+ return new URL(readRoutingAddress(), window.location.href).href;
11568
+ };
11546
11569
 
11547
11570
  // Read once, the first time anyone needs the positions: the document's
11548
11571
  // restoration is installed by the routing, and a list remembering itself may
@@ -11643,40 +11666,38 @@ const holdDocumentScroll = () => {
11643
11666
  };
11644
11667
 
11645
11668
  let installed = false;
11646
- const installScrollRestoration = () => {
11669
+ const installScrollRestoration = ({ readAddress }) => {
11647
11670
  if (installed) {
11648
11671
  return;
11649
11672
  }
11650
11673
  installed = true;
11674
+ readRoutingAddress = readAddress;
11651
11675
  if (!("scrollRestoration" in window.history)) {
11652
11676
  return;
11653
11677
  }
11654
11678
  window.history.scrollRestoration = "manual";
11655
11679
  loadStore();
11680
+ documentOffset = { x: window.scrollX, y: window.scrollY };
11656
11681
  // Read as it happens rather than when leaving: a traverse changes the url
11657
11682
  // before anything here is told, so a position read then would be read for
11658
11683
  // the wrong page.
11659
11684
  window.addEventListener(
11660
11685
  "scroll",
11661
11686
  () => {
11687
+ documentOffset = { x: window.scrollX, y: window.scrollY };
11662
11688
  if (suspendCount) {
11663
11689
  return;
11664
11690
  }
11665
- positionByUrl.set(window.location.href, {
11666
- x: window.scrollX,
11667
- y: window.scrollY,
11668
- });
11691
+ positionByUrl.set(readShownUrl(), documentOffset);
11669
11692
  },
11670
11693
  { passive: true },
11671
11694
  );
11672
- const positionOnLoad = positionByUrl.get(window.location.href);
11695
+ const urlOnLoad = readShownUrl();
11696
+ const positionOnLoad = positionByUrl.get(urlOnLoad);
11673
11697
  if (!positionOnLoad) {
11674
- // Where the browser landed the first page, written down for the reason
11675
- // startAtTop writes its top: no scroll event says so.
11676
- positionByUrl.set(window.location.href, {
11677
- x: window.scrollX,
11678
- y: window.scrollY,
11679
- });
11698
+ // The first page, where the browser landed it: no scroll event says so
11699
+ // (see stayInPlace).
11700
+ positionByUrl.set(urlOnLoad, documentOffset);
11680
11701
  return;
11681
11702
  }
11682
11703
  // What a reload asks for, now that the browser has been told not to do it.
@@ -11729,7 +11750,9 @@ const restoreScrollPosition = (url) => {
11729
11750
  // clamp is a scroll recorded under the url already returned to.
11730
11751
  const restoreScrollPositionOnReturn = (url) => {
11731
11752
  const arrival = ++arrivalCount;
11732
- const position = positionByUrl.get(new URL(url, window.location.href).href);
11753
+ const href = new URL(url, window.location.href).href;
11754
+ carryScrollersOnceRendered(href);
11755
+ const position = positionByUrl.get(href);
11733
11756
  if (!position) {
11734
11757
  return;
11735
11758
  }
@@ -11767,14 +11790,17 @@ const restoreScrollPositionOnReturn = (url) => {
11767
11790
  // app that scrolls an element of its own scrolls it itself.
11768
11791
  const startAtTop = (url, { from } = {}) => {
11769
11792
  if (!isArrival(url, { from })) {
11793
+ stayInPlace(url);
11770
11794
  return;
11771
11795
  }
11772
11796
  arrivalCount++;
11797
+ const href = new URL(url, window.location.href).href;
11798
+ carryScrollersOnceRendered(href);
11773
11799
  // Written down as well as done: a document already at its top fires no
11774
11800
  // scroll event, and a page left without ever being scrolled would have no
11775
11801
  // position to come back to — the return would keep the offset of the page
11776
11802
  // being left.
11777
- positionByUrl.set(new URL(url, window.location.href).href, { x: 0, y: 0 });
11803
+ positionByUrl.set(href, { x: 0, y: 0 });
11778
11804
  window.scrollTo({ top: 0, left: 0, behavior: "instant" });
11779
11805
  };
11780
11806
  const isArrival = (url, { from }) => {
@@ -11788,6 +11814,53 @@ const isArrival = (url, { from }) => {
11788
11814
  return true;
11789
11815
  };
11790
11816
 
11817
+ // A url the page is shown under without changing: a replace, a push that
11818
+ // keeps the page (see startAtTop). Nothing scrolls, so nothing says where the
11819
+ // document stands under it — written down here, or a return to it would find
11820
+ // nothing and keep the offset of the page being left.
11821
+ //
11822
+ // The offset as its scroll events last told it, not read again: a read here
11823
+ // would lay the page out in the middle of a routing, once per address a
11824
+ // settling param writes. Deaf with the recording: a row of tabs replaces the
11825
+ // url as it travels, and the offset belongs to nobody until the row says where
11826
+ // its tab lands (see arriveAtScrollPosition).
11827
+ const stayInPlace = (url) => {
11828
+ const href = new URL(url, window.location.href).href;
11829
+ carryScrollersOnceRendered(href);
11830
+ if (documentOffset !== null && !suspendCount) {
11831
+ positionByUrl.set(href, documentOffset);
11832
+ }
11833
+ };
11834
+
11835
+ // A scroller that outlives a navigation — its page stays under the new url, or
11836
+ // it lives outside the page that changed (a sidebar list kept by every page) —
11837
+ // is shown under that url as well, and says nothing there unless it moves: a
11838
+ // return to it would find no position and open the list at its top. So it is
11839
+ // carried, with the position it last said.
11840
+ //
11841
+ // Told apart once the navigation has rendered: until then a list about to be
11842
+ // swapped out is as mounted as one that stays, and carried, its position is
11843
+ // what a list of the same name arriving would read. By then one that left has
11844
+ // said so (forgetScrollerUnlessPageLeft), and one that arrived has read its
11845
+ // own and speaks under this url. One still mounted on its way out (a page kept
11846
+ // hidden while its route transition plays) is carried, and forgets the copy as
11847
+ // it unmounts under this url.
11848
+ const carryScrollersOnceRendered = (url) => {
11849
+ whenPageRendered(() => {
11850
+ if (readShownUrl() !== url) {
11851
+ // Another navigation came first: what is mounted is shown under its url,
11852
+ // and its own carry says so.
11853
+ return;
11854
+ }
11855
+ for (const [name, said] of saidByScrollerName) {
11856
+ if (said.url === url) {
11857
+ continue;
11858
+ }
11859
+ writeScrollerPosition(url, name, said.position);
11860
+ }
11861
+ });
11862
+ };
11863
+
11791
11864
  // The same arrival, for the page's own scrollers. The document is scrolled to
11792
11865
  // its top once the page is there; a list opens where it decides to in its
11793
11866
  // first render, so what it must not find is dropped before the routing
@@ -11828,19 +11901,21 @@ const scrollTo = ({ x, y }) => {
11828
11901
 
11829
11902
  const rememberScrollerPosition = (name, position) => {
11830
11903
  loadStore();
11831
- const url = window.location.href;
11904
+ writeScrollerPosition(readShownUrl(), name, position);
11905
+ };
11906
+ const writeScrollerPosition = (url, name, position) => {
11832
11907
  let positionByName = scrollerPositionsByUrl.get(url);
11833
11908
  if (!positionByName) {
11834
11909
  positionByName = new Map();
11835
11910
  scrollerPositionsByUrl.set(url, positionByName);
11836
11911
  }
11837
11912
  positionByName.set(name, position);
11838
- urlByScrollerName.set(name, url);
11913
+ saidByScrollerName.set(name, { url, position });
11839
11914
  };
11840
11915
 
11841
11916
  const recallScrollerPosition = (name) => {
11842
11917
  loadStore();
11843
- const positionByName = scrollerPositionsByUrl.get(window.location.href);
11918
+ const positionByName = scrollerPositionsByUrl.get(readShownUrl());
11844
11919
  if (!positionByName) {
11845
11920
  return undefined;
11846
11921
  }
@@ -11850,17 +11925,18 @@ const recallScrollerPosition = (name) => {
11850
11925
  // Said by a scroller as it unmounts. Its page is being left when the url is
11851
11926
  // already another one — the history is written before the page it names is
11852
11927
  // taken down — and then its position is kept for the way back. The url still
11853
- // being the one it spoke under means the page stays and the scroller alone
11854
- // goes (a popup closing, a section folding): there is no coming back to a
11855
- // place that was not left, and a position kept would greet the next mount
11928
+ // being the one it was last kept under means the page stays and the scroller
11929
+ // alone goes (a popup closing, a section folding): there is no coming back to
11930
+ // a place that was not left, and a position kept would greet the next mount
11856
11931
  // under this address as a return.
11857
11932
  const forgetScrollerUnlessPageLeft = (name) => {
11858
- const url = urlByScrollerName.get(name);
11859
- if (url === undefined) {
11933
+ const said = saidByScrollerName.get(name);
11934
+ if (said === undefined) {
11860
11935
  return;
11861
11936
  }
11862
- urlByScrollerName.delete(name);
11863
- if (url !== window.location.href) {
11937
+ saidByScrollerName.delete(name);
11938
+ const { url } = said;
11939
+ if (url !== readShownUrl()) {
11864
11940
  return;
11865
11941
  }
11866
11942
  const positionByName = scrollerPositionsByUrl.get(url);
@@ -12263,14 +12339,18 @@ const setupBrowserIntegrationViaHistory = ({
12263
12339
  // also waits for the page it returns to: an offset is clamped to whatever
12264
12340
  // page the document holds when it is written.
12265
12341
  //
12266
- // A replace gets neither: it is the same place said differently — a param
12267
- // settling, a state written — and moving the reader for it would throw
12268
- // them out of a page they never left. A push that keeps the pathname is
12269
- // read the same way, by startAtTop itself. The one replace that IS an
12270
- // arrival is a row of tabs travelling, and the row says so for itself (see
12271
- // route_travel.jsx).
12342
+ // A replace leaves the document where it is: it is the same place said
12343
+ // differently — a param settling, a state written — and moving the reader
12344
+ // for it would throw them out of a page they never left. Where they stay
12345
+ // is written down for the new address (see stayInPlace). A push that keeps
12346
+ // the pathname is read the same way, by startAtTop itself. The one replace
12347
+ // that IS an arrival is a row of tabs travelling, and the row says so for
12348
+ // itself (see route_travel.jsx) — from inside the routing above, which is
12349
+ // why the replace is written down after it.
12272
12350
  if (navigationType === "push") {
12273
12351
  whenRenderingResumes(() => startAtTop(url, { from: urlLeft }));
12352
+ } else if (navigationType === "replace") {
12353
+ stayInPlace(url);
12274
12354
  } else if (navigationType === "traverse") {
12275
12355
  restoreScrollPositionOnReturn(url);
12276
12356
  }
@@ -12384,7 +12464,7 @@ const setupBrowserIntegrationViaHistory = ({
12384
12464
  // The browser's own scroll restoration is taken over here rather than left
12385
12465
  // to whoever navigates: it is a decision about the document, and the entry
12386
12466
  // being left must be recorded from the first pixel scrolled.
12387
- installScrollRestoration();
12467
+ installScrollRestoration({ readAddress });
12388
12468
 
12389
12469
  // The document is leaving, or may be killed in the background: what the
12390
12470
  // entry says about it has to be what the document says (see
@@ -33418,6 +33498,20 @@ const toHex = ([r, g, b]) =>
33418
33498
  // sure to be understood in, whatever getComputedStyle answered with.
33419
33499
  let colorContext = null;
33420
33500
  const parseColor = (color) => {
33501
+ const [r, g, b, a] = drawColor(color);
33502
+ if (a === 0 || a === 255) {
33503
+ return [r, g, b, a / 255];
33504
+ }
33505
+ // The canvas keeps a pixel premultiplied by its alpha, in bytes: the
33506
+ // channels of a translucent colour come back rounded at that alpha —
33507
+ // rgba(12, 23, 44, 0.45) reads 11, 22, 44. Made opaque, they read whole.
33508
+ const [rOpaque, gOpaque, bOpaque] = drawColor(
33509
+ `color(from ${color} srgb r g b / 1)`,
33510
+ );
33511
+ return [rOpaque, gOpaque, bOpaque, a / 255];
33512
+ };
33513
+
33514
+ const drawColor = (color) => {
33421
33515
  if (!colorContext) {
33422
33516
  const canvas = document.createElement("canvas");
33423
33517
  canvas.width = 1;
@@ -33430,8 +33524,7 @@ const parseColor = (color) => {
33430
33524
  colorContext.fillStyle = "transparent";
33431
33525
  colorContext.fillStyle = color;
33432
33526
  colorContext.fillRect(0, 0, 1, 1);
33433
- const [r, g, b, a] = colorContext.getImageData(0, 0, 1, 1).data;
33434
- return [r, g, b, a / 255];
33527
+ return colorContext.getImageData(0, 0, 1, 1).data;
33435
33528
  };
33436
33529
 
33437
33530
  // The title belongs to the last Head to arrive, whatever order they leave in:
@@ -49640,6 +49733,15 @@ const applySpacingOnTextChildren = (children, spacing, defaultSpace) => {
49640
49733
  }
49641
49734
  return childrenWithGap;
49642
49735
  };
49736
+ const isSpacingWidth = spacing => {
49737
+ if (typeof spacing === "number") {
49738
+ return true;
49739
+ }
49740
+ if (typeof spacing !== "string") {
49741
+ return false;
49742
+ }
49743
+ return isSizeSpacingKey(spacing) || hasCSSSizeUnit(spacing) || spacing.startsWith("var(");
49744
+ };
49643
49745
  const outsideTextFlowSet = new Set();
49644
49746
  const markAsOutsideTextFlow = jsxElement => {
49645
49747
  outsideTextFlowSet.add(jsxElement);
@@ -49907,7 +50009,11 @@ const TextUI = props => {
49907
50009
  }
49908
50010
  const shouldPreserveSpacing = rest.as === "pre" || rest.flex || rest.grid;
49909
50011
  if (shouldPreserveSpacing) {
49910
- boxProps.spacing = resolvedSpacing;
50012
+ // Between flex/grid items spacing is a gap, so only a width carries over: a
50013
+ // separator (the default space, a text, an element) or "pre" has no gap.
50014
+ if (isSpacingWidth(spacing)) {
50015
+ boxProps.spacing = spacing;
50016
+ }
49911
50017
  } else {
49912
50018
  children = applySpacingOnTextChildren(children, resolvedSpacing, defaultSpace);
49913
50019
  }
@@ -71723,32 +71829,43 @@ document.addEventListener(
71723
71829
  );
71724
71830
 
71725
71831
  /*
71726
- * Whether a scroller is gliding: moving on its own toward a place it was sent
71727
- * to, with no hand on it — a key (Home, PageDown, Space), a smooth `scrollTo`
71728
- * or `scrollIntoView`, the status bar tap on iOS. The other scrolls follow a
71729
- * hand: a finger on the screen, a wheel turning.
71730
- *
71731
- * It matters to whoever writes a scroll position while one plays: the write is
71732
- * an instant scroll, and the browser abandons the glide for it, short of where
71733
- * it was going. A hand is not abandoned: its next report moves the scroller on
71734
- * from wherever the write left it.
71735
- *
71736
- * A scroll is followed from its first "scroll" event to its "scrollend". It is
71737
- * a hand's when a finger was down as it started (the fling a finger leaves
71738
- * behind is still its own), when one is down now, or when a wheel turned
71739
- * around the time it started or since. A finger is down from its
71740
- * touchstart to its touchend, heard on what it touched: a list removes the
71741
- * items scrolled out of its window while the finger is still on one of them,
71742
- * and the touchend of a removed element never reaches the document.
71743
- *
71744
- * In a browser without "scrollend", nothing is said to glide: a scroll would
71745
- * never be seen ending.
71832
+ * Whether writing a scroll position now would cost the user the scroll in
71833
+ * progress. A write is an instant scroll, and the browser abandons for it any
71834
+ * scroll it is playing on its own, short of where that one was going: a key
71835
+ * (Home, PageDown, Space), a smooth `scrollTo` or `scrollIntoView`, the status
71836
+ * bar tap on iOS, the fling a finger leaves behind once it lifts. A scroll
71837
+ * following a hand is not abandoned: the wheel's next turn carries on, and on
71838
+ * most engines a finger keeps dragging from wherever the write left it.
71839
+ *
71840
+ * Not on iOS. Its finger places the content from where the drag began, so a
71841
+ * write under it is undone at the finger's next move — or, when the finger
71842
+ * lifts first, lands after the lift and stops the fling dead. And a fling
71843
+ * stops on any write during its momentum, as the status bar's scroll to the
71844
+ * top does (Safari 26.5 on the simulator: flings of 1700px stopped 130 to 340px
71845
+ * after the lift, 3 in 3; a write 50ms before the lift stopped 2 flings in 3).
71846
+ * No finger there is a hand a write could hold to. Chrome flings on through a
71847
+ * write, and loses nothing by not getting one.
71848
+ *
71849
+ * A scroll is followed from its first "scroll" event to its "scrollend", and
71850
+ * is a hand's while a finger is down, or when a wheel turned around the time it
71851
+ * started or since. A finger is down from its touchstart to its touchend,
71852
+ * heard on what it touched: a list removes the items scrolled out of its window
71853
+ * while the finger is still on one of them, and the touchend of a removed
71854
+ * element never reaches the document.
71855
+ *
71856
+ * In a browser without "scrollend", no write is said to interrupt anything: a
71857
+ * scroll would never be seen ending.
71746
71858
  */
71747
71859
 
71748
71860
  // The scroll a wheel turn causes starts in the frame the turn is read, or the
71749
71861
  // next one. A turn further from it moved nothing (a wheel at an edge) and is no
71750
71862
  // hand on a scroll that came from elsewhere.
71751
71863
  const WHEEL_REACH = 100;
71864
+ // iOS's touch handling, the one engine whose finger drops writes (see above).
71865
+ const FINGER_KEEPS_WRITES = !window.CSS.supports(
71866
+ "-webkit-touch-callout",
71867
+ "none",
71868
+ );
71752
71869
 
71753
71870
  const flightMap = new WeakMap();
71754
71871
  let fingersDown = 0;
@@ -71758,13 +71875,13 @@ let wheelTimeStamp = -Infinity;
71758
71875
  * @param {Element} scrollerEl
71759
71876
  * @returns {boolean}
71760
71877
  */
71761
- const isScrollGliding = (scrollerEl) => {
71878
+ const wouldInterruptScroll = (scrollerEl) => {
71762
71879
  const flight = flightMap.get(scrollerEl);
71763
71880
  if (!flight) {
71764
71881
  return false;
71765
71882
  }
71766
- if (flight.fingerDown || fingersDown > 0) {
71767
- return false;
71883
+ if (fingersDown > 0) {
71884
+ return !FINGER_KEEPS_WRITES;
71768
71885
  }
71769
71886
  if (wheelTimeStamp > flight.timeStamp - WHEEL_REACH) {
71770
71887
  return false;
@@ -71794,10 +71911,7 @@ if ("onscrollend" in window) {
71794
71911
  if (flightMap.has(scroller)) {
71795
71912
  return;
71796
71913
  }
71797
- flightMap.set(scroller, {
71798
- timeStamp: e.timeStamp,
71799
- fingerDown: fingersDown > 0,
71800
- });
71914
+ flightMap.set(scroller, { timeStamp: e.timeStamp });
71801
71915
  },
71802
71916
  { capture: true, passive: true },
71803
71917
  );
@@ -75487,6 +75601,13 @@ const useListScrollSync = ({
75487
75601
  if (delta > -0.5 && delta < 0.5) {
75488
75602
  return;
75489
75603
  }
75604
+ if (wouldInterruptScroll(scrollerEl)) {
75605
+ // As the anchoring's write would (see holdAnchorStill). The document's
75606
+ // box is the scroller of a list scrolling the page, and it resizes with
75607
+ // every item drawn taller than its filler: while the page scrolls, that
75608
+ // is at every window move.
75609
+ return;
75610
+ }
75490
75611
  anchorRef.current = null;
75491
75612
  if (horizontal) {
75492
75613
  scrollerEl.scrollLeft += delta;
@@ -75584,13 +75705,12 @@ const useListScrollSync = ({
75584
75705
  return;
75585
75706
  }
75586
75707
  anchorRef.current = null;
75587
- if (isScrollGliding(scrollerEl)) {
75708
+ if (wouldInterruptScroll(scrollerEl)) {
75588
75709
  // Nobody is looking at items standing still: they are passing by, on
75589
- // their way somewhere the browser was asked to take them (a key, a smooth
75590
- // scrollTo, the status bar). The write would be an instant scroll, and
75591
- // the browser abandons the glide for it, short of where it was going.
75592
- // Left alone, the items drawn above push the rest by `drift` while all
75593
- // of it moves, and the glide lands.
75710
+ // their way somewhere the browser takes them (a key, a smooth scrollTo,
75711
+ // the status bar, a fling). The write would stop that scroll short of
75712
+ // where it was going. Left alone, the items drawn above push the rest by
75713
+ // `drift` while all of it moves, and the scroll lands.
75594
75714
  return;
75595
75715
  }
75596
75716
  scrolledByListRef.current = true;
@@ -79546,9 +79666,11 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
79546
79666
  * does, and for a list that scrolls itself the page's own restoration cannot
79547
79667
  * see. The position is kept under the list's `id` and the page's url, for
79548
79668
  * the session (a reload comes back too); a list without an `id` of its own
79549
- * has nothing to be remembered by. A fresh arrival at the page opens at
79550
- * `defaultScrolled` either way, and so does a list the caller holds through
79551
- * `scrolled`.
79669
+ * has nothing to be remembered by, and two lists mounted at once under one
79670
+ * `id` — a popup over the page, the page a route transition is leaving —
79671
+ * overwrite each other (see docs/scroll.md). A fresh arrival at the page
79672
+ * opens at `defaultScrolled` either way, and so does a list the caller holds
79673
+ * through `scrolled`.
79552
79674
  * @param {boolean} [props.deselectable]
79553
79675
  * A single-select list allowed to hold nothing: the selected item, pressed
79554
79676
  * again, lets go. Without it the list is a radio group — a choice, once