@jsenv/navi 0.29.430 → 0.29.431

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.
@@ -6750,6 +6750,55 @@ const applyNavigationToNavDepth = (navigationType, state) => {
6750
6750
  canNavForwardSignal.value = navDepth < navDepthMax;
6751
6751
  };
6752
6752
 
6753
+ /**
6754
+ * Whether the render running now is the one putting a new page on screen.
6755
+ *
6756
+ * A layer drawn over the pages — a popup bound to the address, the slides
6757
+ * inside it — can open, close or change place in the very navigation that
6758
+ * replaces the page beneath it: a link inside the layer leads to another page
6759
+ * and the layer's weak params are dropped, a back to the page it was open over
6760
+ * brings them back. For the layer that is not a movement of its own: the page
6761
+ * changing IS the movement. A route transition already carries it, photographed
6762
+ * as furniture with the page it belongs to (transition_furniture.js), and with
6763
+ * no transition the page cuts and the layer cuts with it. Played on its own as
6764
+ * well, it moves twice — an entrance or an exit over a page sliding past, or
6765
+ * appearing at once.
6766
+ *
6767
+ * So whatever reacts to such a change in a render — a popup's open state, a
6768
+ * slide container's current slide — asks here whether that render carries a
6769
+ * page change, and if so arrives where it goes without travelling there.
6770
+ *
6771
+ * A page is what navi animates between (route_page.js): a route, and the
6772
+ * params telling apart branches of one route. Which routes match is decided by
6773
+ * the path alone, so a navigation that changes the path changes the page; one
6774
+ * that keeps it changes the page only where a param IS a page — and navi knows
6775
+ * those only through the relation or the RouteTravel row that names them, so
6776
+ * that is who says so. A search param a page merely reads is not one: writing
6777
+ * it is the page answering, and a layer opened by it (the `?settings` its door
6778
+ * pushes, the back button taking it out) moves on its own.
6779
+ *
6780
+ * The answer lasts until the render applying the navigation is over: the held
6781
+ * one when somebody photographs the change, Preact's own otherwise, over by the
6782
+ * next frame (see whenPageRendered). Asked from a layout effect it is exact; an
6783
+ * opening deferred past the commit takes the answer with it, read when the
6784
+ * effect ran.
6785
+ */
6786
+
6787
+
6788
+ let renderingPageChange = false;
6789
+
6790
+ const notePageChange = () => {
6791
+ if (renderingPageChange) {
6792
+ return;
6793
+ }
6794
+ renderingPageChange = true;
6795
+ whenPageRendered(() => {
6796
+ renderingPageChange = false;
6797
+ });
6798
+ };
6799
+
6800
+ const isRenderingPageChange = () => renderingPageChange;
6801
+
6753
6802
  // Global signal registry for route template detection
6754
6803
  const globalSignalRegistry = new Map();
6755
6804
 
@@ -10333,8 +10382,12 @@ This prevents cross-test pollution and ensures clean state.`,
10333
10382
  // read again.
10334
10383
  const path = urlWithoutSearchAndHash(url);
10335
10384
  const pathKept = path === previousPath;
10385
+ // A new path is a new page: what the integration tells whatever reacts to
10386
+ // this change in the render it causes (see page_change.js). Not the first
10387
+ // one — there was no page before it, and what it mounts mounts as it is.
10388
+ const pageChanged = previousPath !== null && !pathKept;
10336
10389
  previousPath = path;
10337
- const returnValue = {};
10390
+ const returnValue = { pageChanged };
10338
10391
  const routeMatchInfoSet = new Set();
10339
10392
  for (const route of routeSet) {
10340
10393
  const routePrivateProperties = getRoutePrivateProperties(route);
@@ -12148,11 +12201,14 @@ const applyRouting = (
12148
12201
  // The routes' actions follow the address by themselves (see routeAction):
12149
12202
  // what a navigation leaves to the routing is a reload, the one that asks
12150
12203
  // them again with the address unchanged.
12151
- const { reloadSet } = updateRoutes(url, {
12204
+ const { pageChanged, reloadSet } = updateRoutes(url, {
12152
12205
  navigationType,
12153
12206
  isVisited,
12154
12207
  // state,
12155
12208
  });
12209
+ if (pageChanged) {
12210
+ notePageChange();
12211
+ }
12156
12212
  if (!reloadSet || reloadSet.size === 0) {
12157
12213
  return {
12158
12214
  allResult: undefined,
@@ -15499,8 +15555,8 @@ const useOpenPropsEffectOnOpenController = (
15499
15555
  // Tracks whether the effect below has ever run before — only the very
15500
15556
  // first run gets the "mount already open" treatment (`open` truthy from
15501
15557
  // the start, or the uncontrolled, mount-only `defaultOpen`); every
15502
- // subsequent `open` change is a real, later toggle and should animate
15503
- // normally like any other interactive open/close.
15558
+ // subsequent `open` change is a real, later toggle and animates like any
15559
+ // interactive open/close — unless a page change carries it (see below).
15504
15560
  const isFirstRunRef = useRef(true);
15505
15561
  // What `open` was on the previous run of the effect below. preact re-runs
15506
15562
  // an effect for a change of its deps — or for none at all, when a
@@ -15520,6 +15576,12 @@ const useOpenPropsEffectOnOpenController = (
15520
15576
  isFirstRunRef.current = false;
15521
15577
  const openChanged = open !== lastRunOpenRef.current;
15522
15578
  lastRunOpenRef.current = open;
15579
+ // Opened or closed by the navigation that puts another page on screen: a
15580
+ // link inside the popup leading away, a back to the page it stood over.
15581
+ // The page changing is its movement, so it has none of its own — a route
15582
+ // transition carries it with its page, and without one it cuts with the
15583
+ // page (see page_change.js).
15584
+ const carriedByPageChange = isRenderingPageChange();
15523
15585
 
15524
15586
  if (isFirstRun) {
15525
15587
  // "interaction" is said even over what the caller holds reading open: a
@@ -15538,10 +15600,12 @@ const useOpenPropsEffectOnOpenController = (
15538
15600
  // other truthy value means it was simply already open: nothing was ever
15539
15601
  // shown as "closed" for the user to see it transition away from, so the
15540
15602
  // entrance is skipped (`silent`, see popover.jsx's own openEffect).
15541
- //
15603
+ // A popup built by an opening a page change carries is silent too,
15604
+ // whatever built it: read now, the answer only holds for this render.
15605
+ const silent = mountOpenReason !== "interaction" || carriedByPageChange;
15542
15606
  mountOpenOwedRef.current = () =>
15543
15607
  openController.open(new CustomEvent("open_by_prop", { detail: {} }), {
15544
- silent: mountOpenReason !== "interaction",
15608
+ silent,
15545
15609
  });
15546
15610
  }
15547
15611
  return undefined;
@@ -15573,11 +15637,13 @@ const useOpenPropsEffectOnOpenController = (
15573
15637
  });
15574
15638
  return undefined;
15575
15639
  }
15576
- openController.open(new CustomEvent("open_by_prop", { detail: {} }));
15640
+ openController.open(new CustomEvent("open_by_prop", { detail: {} }), {
15641
+ silent: carriedByPageChange,
15642
+ });
15577
15643
  } else {
15578
15644
  openController.requestClose(
15579
15645
  new CustomEvent("close_by_prop", { detail: {} }),
15580
- { isCancel: true },
15646
+ { isCancel: true, silent: carriedByPageChange },
15581
15647
  );
15582
15648
  }
15583
15649
  // The request can be refused (a busy form denying the close): the popup
@@ -35185,6 +35251,9 @@ const rebuildWatcher = () => {
35185
35251
  if (fromIndex === index) {
35186
35252
  return;
35187
35253
  }
35254
+ // A page of a relation can be a param: a move between two of them is a
35255
+ // page change the path does not show (see page_change.js).
35256
+ notePageChange();
35188
35257
  if (navigationAnimated) {
35189
35258
  // The movement on screen was turned round for this very navigation (see
35190
35259
  // turnRunningTransitionRound): it is already being answered, by the
@@ -36549,6 +36618,10 @@ const RouteTravel = ({
36549
36618
  }
36550
36619
  return;
36551
36620
  }
36621
+ // Whoever asked for it: a tab of this row can be a param, and a move
36622
+ // between two of them is a page change the path does not show (see
36623
+ // page_change.js).
36624
+ notePageChange();
36552
36625
  const page = pages[index];
36553
36626
  // A page this box asked for itself — a travel's own navigation, or one
36554
36627
  // it had given up waiting on: what arrives here is the answer to a
@@ -53059,11 +53132,13 @@ const Expandable = props => {
53059
53132
  openController.openEffect = openEvent => {
53060
53133
  const contentContainer = contentContainerRef.current;
53061
53134
  // `silent`: the expandable was already open when the page appeared
53062
- // (`open`/`defaultOpen` at mount). Nothing changed for anyone to be told
53063
- // about, nothing was ever shown closed to move away from, and a page must
53064
- // not have its focus stolen by a section that was simply already open — so
53065
- // an opening that was never an opening plays nothing, says nothing and
53066
- // takes nothing. The action it carries still runs: its content is due.
53135
+ // (`open`/`defaultOpen` at mount, or the navigation putting that page on
53136
+ // screen opening it — see page_change.js). Nothing changed for anyone to
53137
+ // be told about, nothing was ever shown closed to move away from, and a
53138
+ // page must not have its focus stolen by a section that was simply
53139
+ // already open — so an opening that was never an opening plays nothing,
53140
+ // says nothing and takes nothing. The action it carries still runs: its
53141
+ // content is due.
53067
53142
  const silent = Boolean(openEvent.detail.silent);
53068
53143
  const revealing = animation && !silent && Boolean(contentContainer);
53069
53144
  // Where the last paint left the track — 0 when fully closed, partway when
@@ -53110,7 +53185,9 @@ const Expandable = props => {
53110
53185
  effectiveAction.abort();
53111
53186
  }
53112
53187
  cancelSettleWatch();
53113
- const collapsing = animation && Boolean(contentContainerAtClose);
53188
+ // `silent`: closed by the navigation that puts another page on screen,
53189
+ // which is its movement (see page_change.js).
53190
+ const collapsing = animation && !closeEvent.detail.silent && Boolean(contentContainerAtClose);
53114
53191
  if (collapsing) {
53115
53192
  // Now, while the content is still fully laid out — the collapsing
53116
53193
  // track uncovers a content frozen at that size.
@@ -60430,10 +60507,23 @@ const SlideContainer = ({
60430
60507
  y: 0
60431
60508
  };
60432
60509
  const durationMs = durationToMs(duration);
60510
+ // A change of slide the page changing carries is no travel: the page is
60511
+ // the movement, and the slides land where the address puts them (see
60512
+ // page_change.js). A travel still playing is cut on the spot, and the
60513
+ // indicator riding it with it.
60514
+ const landsWithPage = isRenderingPageChange() && (stageRef.current ? stageRef.current.area : drawnAreaRef.current) !== currentArea;
60515
+ if (landsWithPage && trackAnimationRef.current) {
60516
+ trackAnimationRef.current.cancel();
60517
+ trackAnimationRef.current = null;
60518
+ movingRef.current = null;
60519
+ cancelTravelProgressAnimation();
60520
+ paintTravelProgress(0);
60521
+ }
60522
+ const still = noTravel || landsWithPage;
60433
60523
  // Nothing is travelling, so nothing is staged: the picture to paint is the
60434
60524
  // map itself, and a stage left over from a travel that has just been given
60435
60525
  // up on would be painted instead of it.
60436
- if (noTravel) {
60526
+ if (still) {
60437
60527
  stageRef.current = null;
60438
60528
  }
60439
60529
  let stage = stageRef.current;
@@ -60441,7 +60531,7 @@ const SlideContainer = ({
60441
60531
  // between the picture and the slide arriving, on the axis it walks.
60442
60532
  let travelStep = null;
60443
60533
  const drawnArea = stage ? stage.area : drawnAreaRef.current;
60444
- const travelStarts = !noTravel && durationMs > 0 && drawnArea !== undefined && drawnArea !== currentArea && slideElements.some(slideElement => readArea(slideElement) === drawnArea);
60534
+ const travelStarts = !still && durationMs > 0 && drawnArea !== undefined && drawnArea !== currentArea && slideElements.some(slideElement => readArea(slideElement) === drawnArea);
60445
60535
  if (travelStarts) {
60446
60536
  // Where the slide being left IS — the place the stage gave it if a travel
60447
60537
  // was already playing (a press landing mid-travel departs from where the
@@ -60568,7 +60658,7 @@ const SlideContainer = ({
60568
60658
  // duration (the ask is zero, so ratioOfOneTravel answers 1), slowing the
60569
60659
  // travel at every re-render.
60570
60660
  const sameTravelPlaying = travelInFlight && !offsetDragged && offsetTargetBefore === offset;
60571
- const travels = !sameTravelPlaying && !noTravel && durationMs > 0 && offsetBefore !== undefined && offsetBefore !== offset;
60661
+ const travels = !sameTravelPlaying && !still && durationMs > 0 && offsetBefore !== undefined && offsetBefore !== offset;
60572
60662
  if (travels) {
60573
60663
  // The time it takes is the distance it has left to cover: a travel picked
60574
60664
  // up a fifth of the way through goes back in a fifth of the time, so what
@@ -64530,6 +64620,12 @@ const css$E = /* css */`
64530
64620
  }
64531
64621
  }
64532
64622
  }
64623
+ /* Off while the dialog's own transitions are off (switchTransitionsOff):
64624
+ the inline style that turns them off cannot reach a pseudo-element.
64625
+ After the rules above, which it must win over at equal weight. */
64626
+ &[navi-transitions-off]::backdrop {
64627
+ transition-property: none;
64628
+ }
64533
64629
 
64534
64630
  &[data-focus-visible],
64535
64631
  /* …or something filling it holds the keyboard and offers its ring to
@@ -65744,8 +65840,9 @@ const useDialogProps = props => {
65744
65840
  const dialogEl = ref.current;
65745
65841
  // A mount-time opening was never seen closed (see openEffect's own
65746
65842
  // `silent`): there is no box it comes from, because nothing was shown
65747
- // before it.
65748
- if (!dialogEl || opened && (!lifting || event.detail.silent)) {
65843
+ // before it. And a change a page change carries moves with that page,
65844
+ // in and out of no anchor (see page_change.js).
65845
+ if (!dialogEl || event.detail.silent || opened && !lifting) {
65749
65846
  applyChange();
65750
65847
  return;
65751
65848
  }
@@ -65785,8 +65882,9 @@ const useDialogProps = props => {
65785
65882
  }
65786
65883
 
65787
65884
  // Set by useOpenControllerByProps for the very first open triggered by
65788
- // `open`/`defaultOpen` already being truthy at mount — see popover.jsx's
65789
- // own openEffect for the full reasoning, mirrored here identically.
65885
+ // `open`/`defaultOpen` already being truthy at mount, and for an open a
65886
+ // page change carries — see popover.jsx's own openEffect for the full
65887
+ // reasoning, mirrored here identically.
65790
65888
  const silent = Boolean(e.detail.silent);
65791
65889
 
65792
65890
  // document.documentElement — the shared "no real container, use the
@@ -65830,7 +65928,7 @@ const useDialogProps = props => {
65830
65928
  // comment), even though Dialog never needs to measure/flip anything: it
65831
65929
  // still needs a genuinely rendered "closed" frame to transition from, not a
65832
65930
  // jump straight from not-shown to aria-expanded="true".
65833
- dialogEl.style.transitionProperty = "none";
65931
+ switchTransitionsOff(dialogEl);
65834
65932
  if (backdropEl) {
65835
65933
  disarmBackdropHideRef.current?.();
65836
65934
  disarmBackdropHideRef.current = null;
@@ -66063,12 +66161,12 @@ const useDialogProps = props => {
66063
66161
  dialogEl.setAttribute("aria-expanded", "true");
66064
66162
  backdropEl?.setAttribute("aria-expanded", "true");
66065
66163
  dialogEl.getBoundingClientRect();
66066
- dialogEl.style.transitionProperty = "";
66164
+ switchTransitionsOn(dialogEl);
66067
66165
  if (backdropEl) {
66068
66166
  backdropEl.style.transitionProperty = "";
66069
66167
  }
66070
66168
  } else {
66071
- dialogEl.style.transitionProperty = "";
66169
+ switchTransitionsOn(dialogEl);
66072
66170
  dialogEl.setAttribute("aria-expanded", "true");
66073
66171
  backdropEl?.setAttribute("aria-expanded", "true");
66074
66172
  if (backdropEl) {
@@ -66104,6 +66202,15 @@ const useDialogProps = props => {
66104
66202
  return closeEvent => {
66105
66203
  debugPopup(`"${closeEvent.type}" on ${getElementSignature(closeEvent.target)} -> closeDialog`);
66106
66204
  clearTextSelectionInside(dialogEl);
66205
+ // Closed by the navigation that puts another page on screen: it leaves
66206
+ // with that page, and plays no exit of its own (see page_change.js).
66207
+ const silent = Boolean(closeEvent.detail.silent);
66208
+ if (silent) {
66209
+ switchTransitionsOff(dialogEl);
66210
+ if (backdropEl) {
66211
+ backdropEl.style.transitionProperty = "none";
66212
+ }
66213
+ }
66107
66214
  dialogEl.setAttribute("aria-expanded", "false");
66108
66215
  if (!isTopLayer) {
66109
66216
  openLocalDialogCount = Math.max(0, openLocalDialogCount - 1);
@@ -66143,7 +66250,7 @@ const useDialogProps = props => {
66143
66250
  // next opening clears it (see openEffect's own unfreezeSize).
66144
66251
  freezeSize(dialogEl);
66145
66252
  cancelOpenInteractionSuppression?.();
66146
- if (hasCssTransitionAnimation) {
66253
+ if (hasCssTransitionAnimation && !silent) {
66147
66254
  suppressPointerEventsDuringTransition(dialogEl);
66148
66255
  }
66149
66256
  if (backdropEl) {
@@ -66154,6 +66261,15 @@ const useDialogProps = props => {
66154
66261
  }
66155
66262
  restoreFocus(closeEvent);
66156
66263
  cleanup();
66264
+ if (silent) {
66265
+ // The closed frame committed with nothing transitioning, before
66266
+ // transitions come back for the next opening.
66267
+ dialogEl.getBoundingClientRect();
66268
+ switchTransitionsOn(dialogEl);
66269
+ if (backdropEl) {
66270
+ backdropEl.style.transitionProperty = "";
66271
+ }
66272
+ }
66157
66273
  };
66158
66274
  };
66159
66275
  const onKeyDownShortcuts = createOnKeyDownForShortcuts({
@@ -66396,6 +66512,19 @@ const useDialogProps = props => {
66396
66512
  }
66397
66513
  return [!isTopLayer && backdrop ? backdropProps : null, contentProps];
66398
66514
  };
66515
+
66516
+ // The dialog's own transitions, off while a frame is pinned (the closed one an
66517
+ // entrance starts from, the open one a silent opening lands on, the closed one
66518
+ // a silent close leaves) and back on after it. A modal's wall is its native
66519
+ // ::backdrop, which takes no inline style: it reads the attribute instead.
66520
+ const switchTransitionsOff = dialogEl => {
66521
+ dialogEl.style.transitionProperty = "none";
66522
+ dialogEl.setAttribute("navi-transitions-off", "");
66523
+ };
66524
+ const switchTransitionsOn = dialogEl => {
66525
+ dialogEl.style.transitionProperty = "";
66526
+ dialogEl.removeAttribute("navi-transitions-off");
66527
+ };
66399
66528
  const DIALOG_PSEUDO_CLASSES = [":hover", ":active", ":focus", ":focus-visible", ":focus-within"];
66400
66529
 
66401
66530
  // Lets consumers pass animationDuration="0.5s" as a regular prop; Box maps
@@ -67320,8 +67449,10 @@ const usePopoverProps = props => {
67320
67449
  // `open`/`defaultOpen` already being truthy at mount — there's nothing
67321
67450
  // to visually transition away from (nothing was ever shown as "closed"
67322
67451
  // to the user), so this open skips the animation entirely instead of
67323
- // playing it against a closed frame that was never actually seen. See
67324
- // the final commit step below for how that's done.
67452
+ // playing it against a closed frame that was never actually seen. Set
67453
+ // too for an open the page changing carries: that page is its movement
67454
+ // (see page_change.js). See the final commit step below for how that's
67455
+ // done.
67325
67456
  const silent = Boolean(e.detail.silent);
67326
67457
  const [cleanup, addCleanup] = createPubSub(true);
67327
67458
 
@@ -67757,7 +67888,7 @@ const usePopoverProps = props => {
67757
67888
  // focus in. Inlined rather than a standalone function since it only
67758
67889
  // has this one call site.
67759
67890
  //
67760
- // `silent` (mounting already open via `open`/`defaultOpen`) flips the
67891
+ // `silent` (mounting already open, or carried by a page change) flips the
67761
67892
  // order instead: aria-expanded first, *then* re-enable transitions, with
67762
67893
  // its own forced reflow in between the two — without that reflow, the
67763
67894
  // browser coalesces "flip aria-expanded" and "re-enable transitions"
@@ -67803,6 +67934,15 @@ const usePopoverProps = props => {
67803
67934
  // reason: only ever built here.
67804
67935
  return closeEvent => {
67805
67936
  clearTextSelectionInside(popoverEl);
67937
+ // Closed by the navigation that puts another page on screen: it leaves
67938
+ // with that page, and plays no exit of its own (see page_change.js).
67939
+ const silent = Boolean(closeEvent.detail.silent);
67940
+ if (silent) {
67941
+ popoverEl.style.transitionProperty = "none";
67942
+ if (backdropEl) {
67943
+ backdropEl.style.transitionProperty = "none";
67944
+ }
67945
+ }
67806
67946
  popoverEl.setAttribute("aria-expanded", "false");
67807
67947
  // Set regardless of isTopLayer — see the open side's own identical
67808
67948
  // comment (openEffect above) for why hidePopover() alone isn't
@@ -67829,7 +67969,7 @@ const usePopoverProps = props => {
67829
67969
  // open is its own separate call with no way to reach back into this
67830
67970
  // one).
67831
67971
  cancelOpenInteractionSuppression?.();
67832
- if (hasCssTransitionAnimation) {
67972
+ if (hasCssTransitionAnimation && !silent) {
67833
67973
  suppressPointerEventsDuringTransition(popoverEl);
67834
67974
  }
67835
67975
  if (backdropEl) {
@@ -67864,6 +68004,15 @@ const usePopoverProps = props => {
67864
68004
  popoverEl.removeAttribute("data-position-y-current");
67865
68005
  popoverEl.removeAttribute("data-position-x-current");
67866
68006
  cleanup();
68007
+ if (silent) {
68008
+ // The closed frame committed with nothing transitioning, before
68009
+ // transitions come back for the next opening.
68010
+ popoverEl.getBoundingClientRect();
68011
+ popoverEl.style.transitionProperty = "";
68012
+ if (backdropEl) {
68013
+ backdropEl.style.transitionProperty = "";
68014
+ }
68015
+ }
67867
68016
  };
67868
68017
  };
67869
68018
  const onKeyDownShortcuts = createOnKeyDownForShortcuts({