@jsenv/navi 0.29.433 → 0.29.435

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
@@ -11117,9 +11124,20 @@ let documentOffset = null;
11117
11124
  // url -> (scroller name -> position). The position is whatever the scroller
11118
11125
  // handed in: what it can put itself back on, in its own terms.
11119
11126
  const scrollerPositionsByUrl = new Map();
11120
- // The url each named scroller last spoke under: what tells a scroller leaving
11121
- // a page that stays from one leaving with its page.
11122
- 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
+ };
11123
11141
 
11124
11142
  // Read once, the first time anyone needs the positions: the document's
11125
11143
  // restoration is installed by the routing, and a list remembering itself may
@@ -11220,11 +11238,12 @@ const holdDocumentScroll = () => {
11220
11238
  };
11221
11239
 
11222
11240
  let installed = false;
11223
- const installScrollRestoration = () => {
11241
+ const installScrollRestoration = ({ readAddress }) => {
11224
11242
  if (installed) {
11225
11243
  return;
11226
11244
  }
11227
11245
  installed = true;
11246
+ readRoutingAddress = readAddress;
11228
11247
  if (!("scrollRestoration" in window.history)) {
11229
11248
  return;
11230
11249
  }
@@ -11241,14 +11260,16 @@ const installScrollRestoration = () => {
11241
11260
  if (suspendCount) {
11242
11261
  return;
11243
11262
  }
11244
- positionByUrl.set(window.location.href, documentOffset);
11263
+ positionByUrl.set(readShownUrl(), documentOffset);
11245
11264
  },
11246
11265
  { passive: true },
11247
11266
  );
11248
- const positionOnLoad = positionByUrl.get(window.location.href);
11267
+ const urlOnLoad = readShownUrl();
11268
+ const positionOnLoad = positionByUrl.get(urlOnLoad);
11249
11269
  if (!positionOnLoad) {
11250
- // The first page, where the browser landed it.
11251
- stayInPlace(window.location.href);
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
  }
@@ -11343,11 +11366,13 @@ const startAtTop = (url, { from } = {}) => {
11343
11366
  return;
11344
11367
  }
11345
11368
  arrivalCount++;
11369
+ const href = new URL(url, window.location.href).href;
11370
+ carryScrollersOnceRendered(href);
11346
11371
  // Written down as well as done: a document already at its top fires no
11347
11372
  // scroll event, and a page left without ever being scrolled would have no
11348
11373
  // position to come back to — the return would keep the offset of the page
11349
11374
  // being left.
11350
- positionByUrl.set(new URL(url, window.location.href).href, { x: 0, y: 0 });
11375
+ positionByUrl.set(href, { x: 0, y: 0 });
11351
11376
  window.scrollTo({ top: 0, left: 0, behavior: "instant" });
11352
11377
  };
11353
11378
  const isArrival = (url, { from }) => {
@@ -11361,21 +11386,51 @@ const isArrival = (url, { from }) => {
11361
11386
  return true;
11362
11387
  };
11363
11388
 
11364
- // A url the document is shown under without moving: a replace, a push that
11365
- // keeps the page (see startAtTop), the first page loaded. No scroll event says
11366
- // where it stands, so it is written down here, or a return to it would find
11367
- // nothing and keep the offset of the page being left. The offset the scroll
11368
- // events last told, not read again: a read here would lay the page out in the
11369
- // middle of a routing, once per address a settling param writes.
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.
11370
11393
  //
11371
- // Deaf with the recording: a row of tabs replaces the url as it travels, and
11372
- // the offset belongs to nobody until the row says where its tab lands (see
11373
- // arriveAtScrollPosition).
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).
11374
11399
  const stayInPlace = (url) => {
11375
- if (documentOffset === null || suspendCount) {
11376
- return;
11400
+ const href = new URL(url, window.location.href).href;
11401
+ carryScrollersOnceRendered(href);
11402
+ if (documentOffset !== null && !suspendCount) {
11403
+ positionByUrl.set(href, documentOffset);
11377
11404
  }
11378
- positionByUrl.set(new URL(url, window.location.href).href, documentOffset);
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
+ });
11379
11434
  };
11380
11435
 
11381
11436
  // The same arrival, for the page's own scrollers. The document is scrolled to
@@ -11418,19 +11473,21 @@ const scrollTo = ({ x, y }) => {
11418
11473
 
11419
11474
  const rememberScrollerPosition = (name, position) => {
11420
11475
  loadStore();
11421
- const url = window.location.href;
11476
+ writeScrollerPosition(readShownUrl(), name, position);
11477
+ };
11478
+ const writeScrollerPosition = (url, name, position) => {
11422
11479
  let positionByName = scrollerPositionsByUrl.get(url);
11423
11480
  if (!positionByName) {
11424
11481
  positionByName = new Map();
11425
11482
  scrollerPositionsByUrl.set(url, positionByName);
11426
11483
  }
11427
11484
  positionByName.set(name, position);
11428
- urlByScrollerName.set(name, url);
11485
+ saidByScrollerName.set(name, { url, position });
11429
11486
  };
11430
11487
 
11431
11488
  const recallScrollerPosition = (name) => {
11432
11489
  loadStore();
11433
- const positionByName = scrollerPositionsByUrl.get(window.location.href);
11490
+ const positionByName = scrollerPositionsByUrl.get(readShownUrl());
11434
11491
  if (!positionByName) {
11435
11492
  return undefined;
11436
11493
  }
@@ -11440,17 +11497,18 @@ const recallScrollerPosition = (name) => {
11440
11497
  // Said by a scroller as it unmounts. Its page is being left when the url is
11441
11498
  // already another one — the history is written before the page it names is
11442
11499
  // taken down — and then its position is kept for the way back. The url still
11443
- // being the one it spoke under means the page stays and the scroller alone
11444
- // goes (a popup closing, a section folding): there is no coming back to a
11445
- // 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
11446
11503
  // under this address as a return.
11447
11504
  const forgetScrollerUnlessPageLeft = (name) => {
11448
- const url = urlByScrollerName.get(name);
11449
- if (url === undefined) {
11505
+ const said = saidByScrollerName.get(name);
11506
+ if (said === undefined) {
11450
11507
  return;
11451
11508
  }
11452
- urlByScrollerName.delete(name);
11453
- if (url !== window.location.href) {
11509
+ saidByScrollerName.delete(name);
11510
+ const { url } = said;
11511
+ if (url !== readShownUrl()) {
11454
11512
  return;
11455
11513
  }
11456
11514
  const positionByName = scrollerPositionsByUrl.get(url);
@@ -11978,7 +12036,7 @@ const setupBrowserIntegrationViaHistory = ({
11978
12036
  // The browser's own scroll restoration is taken over here rather than left
11979
12037
  // to whoever navigates: it is a decision about the document, and the entry
11980
12038
  // being left must be recorded from the first pixel scrolled.
11981
- installScrollRestoration();
12039
+ installScrollRestoration({ readAddress });
11982
12040
 
11983
12041
  // The document is leaving, or may be killed in the background: what the
11984
12042
  // entry says about it has to be what the document says (see
@@ -32492,6 +32550,20 @@ const toHex = ([r, g, b]) =>
32492
32550
  // sure to be understood in, whatever getComputedStyle answered with.
32493
32551
  let colorContext = null;
32494
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) => {
32495
32567
  if (!colorContext) {
32496
32568
  const canvas = document.createElement("canvas");
32497
32569
  canvas.width = 1;
@@ -32504,8 +32576,7 @@ const parseColor = (color) => {
32504
32576
  colorContext.fillStyle = "transparent";
32505
32577
  colorContext.fillStyle = color;
32506
32578
  colorContext.fillRect(0, 0, 1, 1);
32507
- const [r, g, b, a] = colorContext.getImageData(0, 0, 1, 1).data;
32508
- return [r, g, b, a / 255];
32579
+ return colorContext.getImageData(0, 0, 1, 1).data;
32509
32580
  };
32510
32581
 
32511
32582
  // The title belongs to the last Head to arrive, whatever order they leave in:
@@ -48480,6 +48551,15 @@ const applySpacingOnTextChildren = (children, spacing, defaultSpace) => {
48480
48551
  }
48481
48552
  return childrenWithGap;
48482
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
+ };
48483
48563
  const outsideTextFlowSet = new Set();
48484
48564
  const markAsOutsideTextFlow = jsxElement => {
48485
48565
  outsideTextFlowSet.add(jsxElement);
@@ -48747,7 +48827,11 @@ const TextUI = props => {
48747
48827
  }
48748
48828
  const shouldPreserveSpacing = rest.as === "pre" || rest.flex || rest.grid;
48749
48829
  if (shouldPreserveSpacing) {
48750
- 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
+ }
48751
48835
  } else {
48752
48836
  children = applySpacingOnTextChildren(children, resolvedSpacing, defaultSpace);
48753
48837
  }
@@ -53383,6 +53467,26 @@ const Expandable = props => {
53383
53467
  return stopRide;
53384
53468
  };
53385
53469
 
53470
+ // A silent change is a state the page was already in (see openEffect): it
53471
+ // lands at once. Not playing a reveal is not enough — the stylesheet's own
53472
+ // transition plays it whenever the previous state's style was resolved
53473
+ // before the render: an expandable mounted closed and measured in the same
53474
+ // commit (a List sizing its items) before its deferred mount-time open, or
53475
+ // one kept on screen across the page change.
53476
+ const renderWithoutMovement = (contentContainer, render) => {
53477
+ if (!animation || !contentContainer) {
53478
+ flushSyncRendering(render);
53479
+ return;
53480
+ }
53481
+ contentContainer.style.transitionProperty = "none";
53482
+ flushSyncRendering(render);
53483
+ // The new style must be resolved while transitions are off — re-enabled
53484
+ // in the same recalc, the change would transition after all.
53485
+ // eslint-disable-next-line no-unused-expressions
53486
+ getComputedStyle(contentContainer).transitionProperty;
53487
+ contentContainer.style.transitionProperty = "";
53488
+ };
53489
+
53386
53490
  // What opening LOOKS like here, and how to undo it — the one thing an
53387
53491
  // expandable owns that a popup does not (see open_controller.js). Reassigned
53388
53492
  // on every render so it always closes over the latest props.
@@ -53403,10 +53507,15 @@ const Expandable = props => {
53403
53507
  // reopening during a collapse — read while the DOM still says closed.
53404
53508
  const startRect = revealing ? contentContainer.getBoundingClientRect() : null;
53405
53509
  cancelSettleWatch();
53406
- flushSyncRendering(() => {
53510
+ const renderOpened = () => {
53407
53511
  setOpened(true);
53408
53512
  setSettled(!revealing);
53409
- });
53513
+ };
53514
+ if (silent) {
53515
+ renderWithoutMovement(contentContainer, renderOpened);
53516
+ } else {
53517
+ flushSyncRendering(renderOpened);
53518
+ }
53410
53519
  if (hasAction) {
53411
53520
  runUnwatched(() => effectiveAction.run());
53412
53521
  }
@@ -53445,16 +53554,22 @@ const Expandable = props => {
53445
53554
  cancelSettleWatch();
53446
53555
  // `silent`: closed by the navigation that puts another page on screen,
53447
53556
  // which is its movement (see page_change.js).
53448
- const collapsing = animation && !closeEvent.detail.silent && Boolean(contentContainerAtClose);
53557
+ const silentClose = Boolean(closeEvent.detail.silent);
53558
+ const collapsing = animation && !silentClose && Boolean(contentContainerAtClose);
53449
53559
  if (collapsing) {
53450
53560
  // Now, while the content is still fully laid out — the collapsing
53451
53561
  // track uncovers a content frozen at that size.
53452
53562
  freezeContentSize();
53453
53563
  }
53454
- flushSyncRendering(() => {
53564
+ const renderClosed = () => {
53455
53565
  setOpened(false);
53456
53566
  setSettled(!collapsing);
53457
- });
53567
+ };
53568
+ if (silentClose) {
53569
+ renderWithoutMovement(contentContainerAtClose, renderClosed);
53570
+ } else {
53571
+ flushSyncRendering(renderClosed);
53572
+ }
53458
53573
  if (restoreFocus) {
53459
53574
  restoreFocus(closeEvent);
53460
53575
  } else if (focusedBeforeClose && contentContainerAtClose && contentContainerAtClose.contains(focusedBeforeClose)) {
@@ -78086,9 +78201,11 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
78086
78201
  * does, and for a list that scrolls itself the page's own restoration cannot
78087
78202
  * see. The position is kept under the list's `id` and the page's url, for
78088
78203
  * the session (a reload comes back too); a list without an `id` of its own
78089
- * has nothing to be remembered by. A fresh arrival at the page opens at
78090
- * `defaultScrolled` either way, and so does a list the caller holds through
78091
- * `scrolled`.
78204
+ * has nothing to be remembered by, and two lists mounted at once under one
78205
+ * `id` — a popup over the page, the page a route transition is leaving —
78206
+ * overwrite each other (see docs/scroll.md). A fresh arrival at the page
78207
+ * opens at `defaultScrolled` either way, and so does a list the caller holds
78208
+ * through `scrolled`.
78092
78209
  * @param {boolean} [props.deselectable]
78093
78210
  * A single-select list allowed to hold nothing: the selected item, pressed
78094
78211
  * again, lets go. Without it the list is a radio group — a choice, once