@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.
@@ -11086,9 +11086,16 @@ const [publishRouteRender, observeRouteRender] = createPubSub();
11086
11086
  * (recallScrollerPosition). What a push means for them is what it means for
11087
11087
  * the document — an arrival opens at the top (see startAtTop): the page
11088
11088
  * arrived at has its named positions dropped before its lists render, so a
11089
- * list recalls only on the way back. A scroller that goes while its page
11090
- * stays — a popup closing over the same address — has nothing to come back
11091
- * to, and says so as it leaves (forgetScrollerUnlessPageLeft).
11089
+ * list recalls only on the way back. A scroller that outlives a navigation —
11090
+ * its page stays under a new address, or it lives outside the page that
11091
+ * changed — is shown under the new address too, and is carried to it (see
11092
+ * carryScrollersOnceRendered). A scroller that goes while its page stays — a
11093
+ * popup closing over the same address — has nothing to come back to, and says
11094
+ * so as it leaves (forgetScrollerUnlessPageLeft).
11095
+ *
11096
+ * Every position is kept under the address the routing shows, which can be
11097
+ * ahead of the browser's: a replace waiting for its state to settle has moved
11098
+ * the page and not yet `window.location` (see readShownUrl).
11092
11099
  *
11093
11100
  * What is NOT covered, and cannot be from here: a page whose height depends on
11094
11101
  * something still loading. Its content is not there at the moment it is put
@@ -11109,12 +11116,28 @@ const [publishRouteRender, observeRouteRender] = createPubSub();
11109
11116
  const STORAGE_KEY = "navi_scroll_positions";
11110
11117
 
11111
11118
  const positionByUrl = new Map();
11119
+ // Where the document stands, as its scroll events last said, whoever it
11120
+ // belongs to: what a url shown without the document moving is given (see
11121
+ // stayInPlace). Null until the restoration is installed — nothing is recorded
11122
+ // before.
11123
+ let documentOffset = null;
11112
11124
  // url -> (scroller name -> position). The position is whatever the scroller
11113
11125
  // handed in: what it can put itself back on, in its own terms.
11114
11126
  const scrollerPositionsByUrl = new Map();
11115
- // The url each named scroller last spoke under: what tells a scroller leaving
11116
- // a page that stays from one leaving with its page.
11117
- const urlByScrollerName = new Map();
11127
+ // What each named scroller still mounted last said: its position and the url
11128
+ // it said it under. What tells a scroller leaving a page that stays from one
11129
+ // leaving with its page, and what one outliving a navigation is carried with.
11130
+ const saidByScrollerName = new Map();
11131
+
11132
+ // The address the page is shown under, as the routing knows it (see
11133
+ // installScrollRestoration): `window.location` lags behind a replace waiting
11134
+ // for its state to settle (replaceAddressWhenSettled in either integration),
11135
+ // and a scroll keyed by it meanwhile is written for the address being left.
11136
+ // Resolved, as an address handed to navTo can be kept as it was given.
11137
+ let readRoutingAddress = () => window.location.href;
11138
+ const readShownUrl = () => {
11139
+ return new URL(readRoutingAddress(), window.location.href).href;
11140
+ };
11118
11141
 
11119
11142
  // Read once, the first time anyone needs the positions: the document's
11120
11143
  // restoration is installed by the routing, and a list remembering itself may
@@ -11215,40 +11238,38 @@ const holdDocumentScroll = () => {
11215
11238
  };
11216
11239
 
11217
11240
  let installed = false;
11218
- const installScrollRestoration = () => {
11241
+ const installScrollRestoration = ({ readAddress }) => {
11219
11242
  if (installed) {
11220
11243
  return;
11221
11244
  }
11222
11245
  installed = true;
11246
+ readRoutingAddress = readAddress;
11223
11247
  if (!("scrollRestoration" in window.history)) {
11224
11248
  return;
11225
11249
  }
11226
11250
  window.history.scrollRestoration = "manual";
11227
11251
  loadStore();
11252
+ documentOffset = { x: window.scrollX, y: window.scrollY };
11228
11253
  // Read as it happens rather than when leaving: a traverse changes the url
11229
11254
  // before anything here is told, so a position read then would be read for
11230
11255
  // the wrong page.
11231
11256
  window.addEventListener(
11232
11257
  "scroll",
11233
11258
  () => {
11259
+ documentOffset = { x: window.scrollX, y: window.scrollY };
11234
11260
  if (suspendCount) {
11235
11261
  return;
11236
11262
  }
11237
- positionByUrl.set(window.location.href, {
11238
- x: window.scrollX,
11239
- y: window.scrollY,
11240
- });
11263
+ positionByUrl.set(readShownUrl(), documentOffset);
11241
11264
  },
11242
11265
  { passive: true },
11243
11266
  );
11244
- const positionOnLoad = positionByUrl.get(window.location.href);
11267
+ const urlOnLoad = readShownUrl();
11268
+ const positionOnLoad = positionByUrl.get(urlOnLoad);
11245
11269
  if (!positionOnLoad) {
11246
- // Where the browser landed the first page, written down for the reason
11247
- // startAtTop writes its top: no scroll event says so.
11248
- positionByUrl.set(window.location.href, {
11249
- x: window.scrollX,
11250
- y: window.scrollY,
11251
- });
11270
+ // The first page, where the browser landed it: no scroll event says so
11271
+ // (see stayInPlace).
11272
+ positionByUrl.set(urlOnLoad, documentOffset);
11252
11273
  return;
11253
11274
  }
11254
11275
  // What a reload asks for, now that the browser has been told not to do it.
@@ -11301,7 +11322,9 @@ const restoreScrollPosition = (url) => {
11301
11322
  // clamp is a scroll recorded under the url already returned to.
11302
11323
  const restoreScrollPositionOnReturn = (url) => {
11303
11324
  const arrival = ++arrivalCount;
11304
- const position = positionByUrl.get(new URL(url, window.location.href).href);
11325
+ const href = new URL(url, window.location.href).href;
11326
+ carryScrollersOnceRendered(href);
11327
+ const position = positionByUrl.get(href);
11305
11328
  if (!position) {
11306
11329
  return;
11307
11330
  }
@@ -11339,14 +11362,17 @@ const restoreScrollPositionOnReturn = (url) => {
11339
11362
  // app that scrolls an element of its own scrolls it itself.
11340
11363
  const startAtTop = (url, { from } = {}) => {
11341
11364
  if (!isArrival(url, { from })) {
11365
+ stayInPlace(url);
11342
11366
  return;
11343
11367
  }
11344
11368
  arrivalCount++;
11369
+ const href = new URL(url, window.location.href).href;
11370
+ carryScrollersOnceRendered(href);
11345
11371
  // Written down as well as done: a document already at its top fires no
11346
11372
  // scroll event, and a page left without ever being scrolled would have no
11347
11373
  // position to come back to — the return would keep the offset of the page
11348
11374
  // being left.
11349
- positionByUrl.set(new URL(url, window.location.href).href, { x: 0, y: 0 });
11375
+ positionByUrl.set(href, { x: 0, y: 0 });
11350
11376
  window.scrollTo({ top: 0, left: 0, behavior: "instant" });
11351
11377
  };
11352
11378
  const isArrival = (url, { from }) => {
@@ -11360,6 +11386,53 @@ const isArrival = (url, { from }) => {
11360
11386
  return true;
11361
11387
  };
11362
11388
 
11389
+ // A url the page is shown under without changing: a replace, a push that
11390
+ // keeps the page (see startAtTop). Nothing scrolls, so nothing says where the
11391
+ // document stands under it — written down here, or a return to it would find
11392
+ // nothing and keep the offset of the page being left.
11393
+ //
11394
+ // The offset as its scroll events last told it, not read again: a read here
11395
+ // would lay the page out in the middle of a routing, once per address a
11396
+ // settling param writes. Deaf with the recording: a row of tabs replaces the
11397
+ // url as it travels, and the offset belongs to nobody until the row says where
11398
+ // its tab lands (see arriveAtScrollPosition).
11399
+ const stayInPlace = (url) => {
11400
+ const href = new URL(url, window.location.href).href;
11401
+ carryScrollersOnceRendered(href);
11402
+ if (documentOffset !== null && !suspendCount) {
11403
+ positionByUrl.set(href, documentOffset);
11404
+ }
11405
+ };
11406
+
11407
+ // A scroller that outlives a navigation — its page stays under the new url, or
11408
+ // it lives outside the page that changed (a sidebar list kept by every page) —
11409
+ // is shown under that url as well, and says nothing there unless it moves: a
11410
+ // return to it would find no position and open the list at its top. So it is
11411
+ // carried, with the position it last said.
11412
+ //
11413
+ // Told apart once the navigation has rendered: until then a list about to be
11414
+ // swapped out is as mounted as one that stays, and carried, its position is
11415
+ // what a list of the same name arriving would read. By then one that left has
11416
+ // said so (forgetScrollerUnlessPageLeft), and one that arrived has read its
11417
+ // own and speaks under this url. One still mounted on its way out (a page kept
11418
+ // hidden while its route transition plays) is carried, and forgets the copy as
11419
+ // it unmounts under this url.
11420
+ const carryScrollersOnceRendered = (url) => {
11421
+ whenPageRendered(() => {
11422
+ if (readShownUrl() !== url) {
11423
+ // Another navigation came first: what is mounted is shown under its url,
11424
+ // and its own carry says so.
11425
+ return;
11426
+ }
11427
+ for (const [name, said] of saidByScrollerName) {
11428
+ if (said.url === url) {
11429
+ continue;
11430
+ }
11431
+ writeScrollerPosition(url, name, said.position);
11432
+ }
11433
+ });
11434
+ };
11435
+
11363
11436
  // The same arrival, for the page's own scrollers. The document is scrolled to
11364
11437
  // its top once the page is there; a list opens where it decides to in its
11365
11438
  // first render, so what it must not find is dropped before the routing
@@ -11400,19 +11473,21 @@ const scrollTo = ({ x, y }) => {
11400
11473
 
11401
11474
  const rememberScrollerPosition = (name, position) => {
11402
11475
  loadStore();
11403
- const url = window.location.href;
11476
+ writeScrollerPosition(readShownUrl(), name, position);
11477
+ };
11478
+ const writeScrollerPosition = (url, name, position) => {
11404
11479
  let positionByName = scrollerPositionsByUrl.get(url);
11405
11480
  if (!positionByName) {
11406
11481
  positionByName = new Map();
11407
11482
  scrollerPositionsByUrl.set(url, positionByName);
11408
11483
  }
11409
11484
  positionByName.set(name, position);
11410
- urlByScrollerName.set(name, url);
11485
+ saidByScrollerName.set(name, { url, position });
11411
11486
  };
11412
11487
 
11413
11488
  const recallScrollerPosition = (name) => {
11414
11489
  loadStore();
11415
- const positionByName = scrollerPositionsByUrl.get(window.location.href);
11490
+ const positionByName = scrollerPositionsByUrl.get(readShownUrl());
11416
11491
  if (!positionByName) {
11417
11492
  return undefined;
11418
11493
  }
@@ -11422,17 +11497,18 @@ const recallScrollerPosition = (name) => {
11422
11497
  // Said by a scroller as it unmounts. Its page is being left when the url is
11423
11498
  // already another one — the history is written before the page it names is
11424
11499
  // taken down — and then its position is kept for the way back. The url still
11425
- // being the one it spoke under means the page stays and the scroller alone
11426
- // goes (a popup closing, a section folding): there is no coming back to a
11427
- // place that was not left, and a position kept would greet the next mount
11500
+ // being the one it was last kept under means the page stays and the scroller
11501
+ // alone goes (a popup closing, a section folding): there is no coming back to
11502
+ // a place that was not left, and a position kept would greet the next mount
11428
11503
  // under this address as a return.
11429
11504
  const forgetScrollerUnlessPageLeft = (name) => {
11430
- const url = urlByScrollerName.get(name);
11431
- if (url === undefined) {
11505
+ const said = saidByScrollerName.get(name);
11506
+ if (said === undefined) {
11432
11507
  return;
11433
11508
  }
11434
- urlByScrollerName.delete(name);
11435
- if (url !== window.location.href) {
11509
+ saidByScrollerName.delete(name);
11510
+ const { url } = said;
11511
+ if (url !== readShownUrl()) {
11436
11512
  return;
11437
11513
  }
11438
11514
  const positionByName = scrollerPositionsByUrl.get(url);
@@ -11835,14 +11911,18 @@ const setupBrowserIntegrationViaHistory = ({
11835
11911
  // also waits for the page it returns to: an offset is clamped to whatever
11836
11912
  // page the document holds when it is written.
11837
11913
  //
11838
- // A replace gets neither: it is the same place said differently — a param
11839
- // settling, a state written — and moving the reader for it would throw
11840
- // them out of a page they never left. A push that keeps the pathname is
11841
- // read the same way, by startAtTop itself. The one replace that IS an
11842
- // arrival is a row of tabs travelling, and the row says so for itself (see
11843
- // route_travel.jsx).
11914
+ // A replace leaves the document where it is: it is the same place said
11915
+ // differently — a param settling, a state written — and moving the reader
11916
+ // for it would throw them out of a page they never left. Where they stay
11917
+ // is written down for the new address (see stayInPlace). A push that keeps
11918
+ // the pathname is read the same way, by startAtTop itself. The one replace
11919
+ // that IS an arrival is a row of tabs travelling, and the row says so for
11920
+ // itself (see route_travel.jsx) — from inside the routing above, which is
11921
+ // why the replace is written down after it.
11844
11922
  if (navigationType === "push") {
11845
11923
  whenRenderingResumes(() => startAtTop(url, { from: urlLeft }));
11924
+ } else if (navigationType === "replace") {
11925
+ stayInPlace(url);
11846
11926
  } else if (navigationType === "traverse") {
11847
11927
  restoreScrollPositionOnReturn(url);
11848
11928
  }
@@ -11956,7 +12036,7 @@ const setupBrowserIntegrationViaHistory = ({
11956
12036
  // The browser's own scroll restoration is taken over here rather than left
11957
12037
  // to whoever navigates: it is a decision about the document, and the entry
11958
12038
  // being left must be recorded from the first pixel scrolled.
11959
- installScrollRestoration();
12039
+ installScrollRestoration({ readAddress });
11960
12040
 
11961
12041
  // The document is leaving, or may be killed in the background: what the
11962
12042
  // entry says about it has to be what the document says (see
@@ -32470,6 +32550,20 @@ const toHex = ([r, g, b]) =>
32470
32550
  // sure to be understood in, whatever getComputedStyle answered with.
32471
32551
  let colorContext = null;
32472
32552
  const parseColor = (color) => {
32553
+ const [r, g, b, a] = drawColor(color);
32554
+ if (a === 0 || a === 255) {
32555
+ return [r, g, b, a / 255];
32556
+ }
32557
+ // The canvas keeps a pixel premultiplied by its alpha, in bytes: the
32558
+ // channels of a translucent colour come back rounded at that alpha —
32559
+ // rgba(12, 23, 44, 0.45) reads 11, 22, 44. Made opaque, they read whole.
32560
+ const [rOpaque, gOpaque, bOpaque] = drawColor(
32561
+ `color(from ${color} srgb r g b / 1)`,
32562
+ );
32563
+ return [rOpaque, gOpaque, bOpaque, a / 255];
32564
+ };
32565
+
32566
+ const drawColor = (color) => {
32473
32567
  if (!colorContext) {
32474
32568
  const canvas = document.createElement("canvas");
32475
32569
  canvas.width = 1;
@@ -32482,8 +32576,7 @@ const parseColor = (color) => {
32482
32576
  colorContext.fillStyle = "transparent";
32483
32577
  colorContext.fillStyle = color;
32484
32578
  colorContext.fillRect(0, 0, 1, 1);
32485
- const [r, g, b, a] = colorContext.getImageData(0, 0, 1, 1).data;
32486
- return [r, g, b, a / 255];
32579
+ return colorContext.getImageData(0, 0, 1, 1).data;
32487
32580
  };
32488
32581
 
32489
32582
  // The title belongs to the last Head to arrive, whatever order they leave in:
@@ -48458,6 +48551,15 @@ const applySpacingOnTextChildren = (children, spacing, defaultSpace) => {
48458
48551
  }
48459
48552
  return childrenWithGap;
48460
48553
  };
48554
+ const isSpacingWidth = spacing => {
48555
+ if (typeof spacing === "number") {
48556
+ return true;
48557
+ }
48558
+ if (typeof spacing !== "string") {
48559
+ return false;
48560
+ }
48561
+ return isSizeSpacingKey(spacing) || hasCSSSizeUnit(spacing) || spacing.startsWith("var(");
48562
+ };
48461
48563
  const outsideTextFlowSet = new Set();
48462
48564
  const markAsOutsideTextFlow = jsxElement => {
48463
48565
  outsideTextFlowSet.add(jsxElement);
@@ -48725,7 +48827,11 @@ const TextUI = props => {
48725
48827
  }
48726
48828
  const shouldPreserveSpacing = rest.as === "pre" || rest.flex || rest.grid;
48727
48829
  if (shouldPreserveSpacing) {
48728
- boxProps.spacing = resolvedSpacing;
48830
+ // Between flex/grid items spacing is a gap, so only a width carries over: a
48831
+ // separator (the default space, a text, an element) or "pre" has no gap.
48832
+ if (isSpacingWidth(spacing)) {
48833
+ boxProps.spacing = spacing;
48834
+ }
48729
48835
  } else {
48730
48836
  children = applySpacingOnTextChildren(children, resolvedSpacing, defaultSpace);
48731
48837
  }
@@ -70336,32 +70442,43 @@ document.addEventListener(
70336
70442
  );
70337
70443
 
70338
70444
  /*
70339
- * Whether a scroller is gliding: moving on its own toward a place it was sent
70340
- * to, with no hand on it — a key (Home, PageDown, Space), a smooth `scrollTo`
70341
- * or `scrollIntoView`, the status bar tap on iOS. The other scrolls follow a
70342
- * hand: a finger on the screen, a wheel turning.
70343
- *
70344
- * It matters to whoever writes a scroll position while one plays: the write is
70345
- * an instant scroll, and the browser abandons the glide for it, short of where
70346
- * it was going. A hand is not abandoned: its next report moves the scroller on
70347
- * from wherever the write left it.
70348
- *
70349
- * A scroll is followed from its first "scroll" event to its "scrollend". It is
70350
- * a hand's when a finger was down as it started (the fling a finger leaves
70351
- * behind is still its own), when one is down now, or when a wheel turned
70352
- * around the time it started or since. A finger is down from its
70353
- * touchstart to its touchend, heard on what it touched: a list removes the
70354
- * items scrolled out of its window while the finger is still on one of them,
70355
- * and the touchend of a removed element never reaches the document.
70356
- *
70357
- * In a browser without "scrollend", nothing is said to glide: a scroll would
70358
- * never be seen ending.
70445
+ * Whether writing a scroll position now would cost the user the scroll in
70446
+ * progress. A write is an instant scroll, and the browser abandons for it any
70447
+ * scroll it is playing on its own, short of where that one was going: a key
70448
+ * (Home, PageDown, Space), a smooth `scrollTo` or `scrollIntoView`, the status
70449
+ * bar tap on iOS, the fling a finger leaves behind once it lifts. A scroll
70450
+ * following a hand is not abandoned: the wheel's next turn carries on, and on
70451
+ * most engines a finger keeps dragging from wherever the write left it.
70452
+ *
70453
+ * Not on iOS. Its finger places the content from where the drag began, so a
70454
+ * write under it is undone at the finger's next move — or, when the finger
70455
+ * lifts first, lands after the lift and stops the fling dead. And a fling
70456
+ * stops on any write during its momentum, as the status bar's scroll to the
70457
+ * top does (Safari 26.5 on the simulator: flings of 1700px stopped 130 to 340px
70458
+ * after the lift, 3 in 3; a write 50ms before the lift stopped 2 flings in 3).
70459
+ * No finger there is a hand a write could hold to. Chrome flings on through a
70460
+ * write, and loses nothing by not getting one.
70461
+ *
70462
+ * A scroll is followed from its first "scroll" event to its "scrollend", and
70463
+ * is a hand's while a finger is down, or when a wheel turned around the time it
70464
+ * started or since. A finger is down from its touchstart to its touchend,
70465
+ * heard on what it touched: a list removes the items scrolled out of its window
70466
+ * while the finger is still on one of them, and the touchend of a removed
70467
+ * element never reaches the document.
70468
+ *
70469
+ * In a browser without "scrollend", no write is said to interrupt anything: a
70470
+ * scroll would never be seen ending.
70359
70471
  */
70360
70472
 
70361
70473
  // The scroll a wheel turn causes starts in the frame the turn is read, or the
70362
70474
  // next one. A turn further from it moved nothing (a wheel at an edge) and is no
70363
70475
  // hand on a scroll that came from elsewhere.
70364
70476
  const WHEEL_REACH = 100;
70477
+ // iOS's touch handling, the one engine whose finger drops writes (see above).
70478
+ const FINGER_KEEPS_WRITES = !window.CSS.supports(
70479
+ "-webkit-touch-callout",
70480
+ "none",
70481
+ );
70365
70482
 
70366
70483
  const flightMap = new WeakMap();
70367
70484
  let fingersDown = 0;
@@ -70371,13 +70488,13 @@ let wheelTimeStamp = -Infinity;
70371
70488
  * @param {Element} scrollerEl
70372
70489
  * @returns {boolean}
70373
70490
  */
70374
- const isScrollGliding = (scrollerEl) => {
70491
+ const wouldInterruptScroll = (scrollerEl) => {
70375
70492
  const flight = flightMap.get(scrollerEl);
70376
70493
  if (!flight) {
70377
70494
  return false;
70378
70495
  }
70379
- if (flight.fingerDown || fingersDown > 0) {
70380
- return false;
70496
+ if (fingersDown > 0) {
70497
+ return !FINGER_KEEPS_WRITES;
70381
70498
  }
70382
70499
  if (wheelTimeStamp > flight.timeStamp - WHEEL_REACH) {
70383
70500
  return false;
@@ -70407,10 +70524,7 @@ if ("onscrollend" in window) {
70407
70524
  if (flightMap.has(scroller)) {
70408
70525
  return;
70409
70526
  }
70410
- flightMap.set(scroller, {
70411
- timeStamp: e.timeStamp,
70412
- fingerDown: fingersDown > 0,
70413
- });
70527
+ flightMap.set(scroller, { timeStamp: e.timeStamp });
70414
70528
  },
70415
70529
  { capture: true, passive: true },
70416
70530
  );
@@ -74097,6 +74211,13 @@ const useListScrollSync = ({
74097
74211
  if (delta > -0.5 && delta < 0.5) {
74098
74212
  return;
74099
74213
  }
74214
+ if (wouldInterruptScroll(scrollerEl)) {
74215
+ // As the anchoring's write would (see holdAnchorStill). The document's
74216
+ // box is the scroller of a list scrolling the page, and it resizes with
74217
+ // every item drawn taller than its filler: while the page scrolls, that
74218
+ // is at every window move.
74219
+ return;
74220
+ }
74100
74221
  anchorRef.current = null;
74101
74222
  if (horizontal) {
74102
74223
  scrollerEl.scrollLeft += delta;
@@ -74194,13 +74315,12 @@ const useListScrollSync = ({
74194
74315
  return;
74195
74316
  }
74196
74317
  anchorRef.current = null;
74197
- if (isScrollGliding(scrollerEl)) {
74318
+ if (wouldInterruptScroll(scrollerEl)) {
74198
74319
  // Nobody is looking at items standing still: they are passing by, on
74199
- // their way somewhere the browser was asked to take them (a key, a smooth
74200
- // scrollTo, the status bar). The write would be an instant scroll, and
74201
- // the browser abandons the glide for it, short of where it was going.
74202
- // Left alone, the items drawn above push the rest by `drift` while all
74203
- // of it moves, and the glide lands.
74320
+ // their way somewhere the browser takes them (a key, a smooth scrollTo,
74321
+ // the status bar, a fling). The write would stop that scroll short of
74322
+ // where it was going. Left alone, the items drawn above push the rest by
74323
+ // `drift` while all of it moves, and the scroll lands.
74204
74324
  return;
74205
74325
  }
74206
74326
  scrolledByListRef.current = true;
@@ -78050,9 +78170,11 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
78050
78170
  * does, and for a list that scrolls itself the page's own restoration cannot
78051
78171
  * see. The position is kept under the list's `id` and the page's url, for
78052
78172
  * the session (a reload comes back too); a list without an `id` of its own
78053
- * has nothing to be remembered by. A fresh arrival at the page opens at
78054
- * `defaultScrolled` either way, and so does a list the caller holds through
78055
- * `scrolled`.
78173
+ * has nothing to be remembered by, and two lists mounted at once under one
78174
+ * `id` — a popup over the page, the page a route transition is leaving —
78175
+ * overwrite each other (see docs/scroll.md). A fresh arrival at the page
78176
+ * opens at `defaultScrolled` either way, and so does a list the caller holds
78177
+ * through `scrolled`.
78056
78178
  * @param {boolean} [props.deselectable]
78057
78179
  * A single-select list allowed to hold nothing: the selected item, pressed
78058
78180
  * again, lets go. Without it the list is a radio group — a choice, once