@jsenv/navi 0.29.413 → 0.29.415

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.
@@ -6,8 +6,8 @@ import { installImportMetaCssBuild, coarsePointerSignal, windowHeightSignal, win
6
6
  import { createPubSub, elementIsFocusable, createIterableWeakSet, dispatchInternalCustomEvent, dispatchCustomEvent, findEvent, mergeOneStyle, findFocusable, getElementSignature, chainEvent, isTouchDrivenEvent, getKeyboardEventDefaultAction, isPressDrivenClick, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, createValueEffect, findFocusDelegateTarget, scrollIntoViewThroughScrollables, allowWheelThrough, dispatchPublicCustomEvent, resolveCSSColor, getScrollContainerSet, ELEMENT_SIZE_CHANGE, findSelfOrAncestorFixedPosition, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, applyNewPosition, measureLongestVisualLineWidth, keepTouchRefusable, waitForTap, waitForPressHeld, suppressClickAfterGesture, startDragToTravel, dragSourceThatStoodDown, markDragSource, refuseDragTo, startDragTo, installPanZoom, createInternalCustomEvent, activeElementSignal, normalizeStyle, getPositionedParent, normalizeStyles, createGroupTransitionController, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, getScrollContainer, watchWheelTravel, scrollRoomTowards, trapScrollInside, scrollIntoViewScoped, closestOpenableAncestor, isAncestorOpen, isDisplayedDespiteClosedAncestor, observeAncestorOpenState, getAncestorOpenType, findBefore, findAfter, resolveCSSSize, hasCSSSizeUnit, releaseWheelGesture, getScrollIntoViewScopedOffsets, wheelGestureIsTakenFrom, claimWheelGesture, initFocusGroup, stringifyStyle as stringifyStyle$1, isScrollable, resolveOklchLightness, contrastColor, parsePositionArea, snapToPixel, trapFocusInside, onAncestorReopen, isPressDisputedByDrag, canScroll, measureWidestChildRow, performTabNavigation, dragAfterIntent, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement, setVirtualKeyboardOverlaysContent, subscribeVirtualKeyboardGeometryChange, getVirtualKeyboardOverlayHeight } from "@jsenv/dom";
7
7
  export { chainEvent, clickIsSuppressed, contrastColor, createDragGestureController, dragAfterIntent, findEvent, markDragSource, startDragTo } from "@jsenv/dom";
8
8
  import { signal, computed, effect, untracked, batch, useComputed, useSignal } from "@preact/signals";
9
- import { options, isValidElement, createContext, render, h, toChildArray, cloneElement, createElement, Fragment as Fragment$1 } from "preact";
10
- import { useErrorBoundary, useLayoutEffect, useContext, useMemo, useRef, useEffect, useCallback, useState, useId } from "preact/hooks";
9
+ import { options, isValidElement, createContext, render, h, toChildArray, createRef, cloneElement, createElement, Fragment as Fragment$1 } from "preact";
10
+ import { useErrorBoundary, useLayoutEffect, useContext, useMemo, useRef, useEffect, useCallback, useState, useReducer, useId } from "preact/hooks";
11
11
  import { humanizeI18n, setRuntimeLangSource, formatDuration, formatMonth, formatDay, resolveTimeRangePrecision, formatDatePlaceholder, toDate, getRelativeDay, formatDayRelative, formatMonthPlaceholder, formatWeekPlaceholder, formatDatetimePlaceholder, formatDatetime, toTimeOfDay, formatTimeOfDay, formatTime, formatMinuteDuration, formatSecondDuration, formatHourDuration, formatTimeRelative, formatNumber, interpolateText, installInterpolateJsx } from "@jsenv/humanize";
12
12
  export { createI18n, formatDatetime, formatDay, formatDayRelative, formatDuration, formatHourDuration, formatMinuteDuration, formatMonth, formatNumber, formatSecondDuration, formatTime, formatTimeOfDay, formatTimeRange, formatTimeRelative, interpolateText } from "@jsenv/humanize";
13
13
  import { jsxs, jsx, Fragment } from "preact/jsx-runtime";
@@ -79,7 +79,7 @@ installImportMetaCssBuild(import.meta);
79
79
  * any of these, and a number is the last resort, not the first tool.
80
80
  */
81
81
 
82
- const css$17 = /* css */ `@layer navi {
82
+ const css$18 = /* css */ `@layer navi {
83
83
  :root {
84
84
  --navi-z-index-control-hovered: 1;
85
85
  --navi-z-index-control-focused: 2;
@@ -92,7 +92,7 @@ const css$17 = /* css */ `@layer navi {
92
92
  }
93
93
  }
94
94
  `;
95
- import.meta.css = [css$17, "@jsenv/navi/src/navi_z_indexes.js"];
95
+ import.meta.css = [css$18, "@jsenv/navi/src/navi_z_indexes.js"];
96
96
 
97
97
  /**
98
98
  * A navigation is ABOUT to be applied — said before its very first write.
@@ -430,7 +430,7 @@ installImportMetaCssBuild(import.meta);
430
430
 
431
431
  const URL_TARGET_ATTRIBUTE = "data-url-target";
432
432
 
433
- const css$16 = /* css */ `@layer navi {
433
+ const css$17 = /* css */ `@layer navi {
434
434
  [data-url-target] {
435
435
  animation: navi_url_target var(--navi-url-target-duration, 2s)
436
436
  ease-out;
@@ -448,7 +448,7 @@ const css$16 = /* css */ `@layer navi {
448
448
  }
449
449
  }
450
450
  `;
451
- import.meta.css = [css$16, "@jsenv/navi/src/nav/url_target/url_target.js"];
451
+ import.meta.css = [css$17, "@jsenv/navi/src/nav/url_target/url_target.js"];
452
452
 
453
453
  let urlTargetOptions = {
454
454
  block: "start",
@@ -2048,7 +2048,7 @@ const useActionStatus = (action) => {
2048
2048
  };
2049
2049
 
2050
2050
  installImportMetaCssBuild(import.meta);
2051
- const css$15 = /* css */`.action_error {
2051
+ const css$16 = /* css */`.action_error {
2052
2052
  background: #fdd;
2053
2053
  border: 1px solid red;
2054
2054
  margin-top: 0;
@@ -2072,7 +2072,7 @@ const ActionRenderer = ({
2072
2072
  children,
2073
2073
  disabled
2074
2074
  }) => {
2075
- import.meta.css = [css$15, "@jsenv/navi/src/action/action_renderer.jsx"];
2075
+ import.meta.css = [css$16, "@jsenv/navi/src/action/action_renderer.jsx"];
2076
2076
  if (action === undefined) {
2077
2077
  throw new Error("ActionRenderer requires an action to render, but none was provided.");
2078
2078
  }
@@ -14110,6 +14110,31 @@ const moveFocusTo = (
14110
14110
  });
14111
14111
  };
14112
14112
 
14113
+ /**
14114
+ * Gives the focus back to `target` once the commit a close is part of is over:
14115
+ * for a close caused by an unmount (UNMOUNT_EVENT_TYPE in
14116
+ * layout/open_controller.js), which runs from a cleanup while the tree leaving
14117
+ * with it is still in the document. Only its end tells a target leaving with
14118
+ * it — disconnected by then, and skipped — from one staying, and a focused
14119
+ * element that leaves makes the removal of its whole subtree restyle it.
14120
+ *
14121
+ * And only into a void: the close took the focus with it, so something holding
14122
+ * the focus by then — a page arriving that focused a field of its own — took it
14123
+ * after, and keeps it.
14124
+ */
14125
+ const giveFocusBackAfterCommit = (target, giveFocus) => {
14126
+ queueMicrotask(() => {
14127
+ if (!target || !target.isConnected) {
14128
+ return;
14129
+ }
14130
+ const focusedNow = document.activeElement;
14131
+ if (focusedNow && focusedNow !== document.body) {
14132
+ return;
14133
+ }
14134
+ giveFocus();
14135
+ });
14136
+ };
14137
+
14113
14138
  const focusTransferTarget = (target, focusVisible) => {
14114
14139
  moveFocusTo(target, { focusVisible });
14115
14140
  if (target.hasAttribute("navi-autofocus-select")) {
@@ -14664,6 +14689,12 @@ const createOpenController = (
14664
14689
  closeEvent,
14665
14690
  `closed by focusout -> let focus go away`,
14666
14691
  );
14692
+ } else if (findEvent(closeEvent, UNMOUNT_EVENT_TYPE)) {
14693
+ // Left the tree: what held the focus before the opening may be
14694
+ // leaving with it (see giveFocusBackAfterCommit).
14695
+ giveFocusBackAfterCommit(focusTransfer.focusedElement, () => {
14696
+ focusTransfer.restoreFocus(closeEvent);
14697
+ });
14667
14698
  } else {
14668
14699
  // Only the mousedown, deliberately: a popup with no backdrop is
14669
14700
  // closed by a `pointerdown` that belongs to the page (see
@@ -15336,7 +15367,7 @@ installImportMetaCssBuild(import.meta);
15336
15367
  * (see calloutTemplate in callout.js).
15337
15368
  */
15338
15369
 
15339
- const css$14 = /* css */`.navi_callout_status_icon {
15370
+ const css$15 = /* css */`.navi_callout_status_icon {
15340
15371
  --x-callout-status-icon-color: var(--navi-callout-neutral-color);
15341
15372
  box-sizing: border-box;
15342
15373
  aspect-ratio: 1;
@@ -15407,7 +15438,7 @@ const CalloutStatusIcon = ({
15407
15438
  status = "info",
15408
15439
  shape = "square"
15409
15440
  }) => {
15410
- import.meta.css = [css$14, "@jsenv/navi/src/control/rules/callout/callout_status_icon.jsx"];
15441
+ import.meta.css = [css$15, "@jsenv/navi/src/control/rules/callout/callout_status_icon.jsx"];
15411
15442
  return jsx("span", {
15412
15443
  className: "navi_callout_status_icon",
15413
15444
  "data-status": status === "none" ? undefined : status,
@@ -15434,7 +15465,7 @@ let calloutCount = 0;
15434
15465
  // large enough to survive the pixel or two a trackpad rests on.
15435
15466
  const CLOSE_BY_SCROLL_DISTANCE = 10;
15436
15467
 
15437
- const css$13 = /* css */ `
15468
+ const css$14 = /* css */ `
15438
15469
  /* jsenv-css-opaque: same as dialog.jsx, cascade order. */
15439
15470
  @layer navi {
15440
15471
  .navi_callout {
@@ -15795,7 +15826,7 @@ const openCallout = (
15795
15826
  debug = () => {},
15796
15827
  } = {},
15797
15828
  ) => {
15798
- import.meta.css = [css$13, "@jsenv/navi/src/control/rules/callout/callout.js"];
15829
+ import.meta.css = [css$14, "@jsenv/navi/src/control/rules/callout/callout.js"];
15799
15830
  if (debug === true) {
15800
15831
  debug = (e, ...args) => console.debug(`"${e.type}" -> `, ...args);
15801
15832
  }
@@ -17425,14 +17456,8 @@ const createCalloutManager = (
17425
17456
  };
17426
17457
  if (findEvent(closeEvent, UNMOUNT_EVENT_TYPE)) {
17427
17458
  // Closed by a popup leaving the tree: the control may be leaving
17428
- // in the same commit, which only its end tells. A leaving control
17429
- // must not get the focus — the removal of a subtree holding it
17430
- // restyles the whole subtree.
17431
- queueMicrotask(() => {
17432
- if (focusTarget.isConnected) {
17433
- giveFocusBack();
17434
- }
17435
- });
17459
+ // with it (see giveFocusBackAfterCommit).
17460
+ giveFocusBackAfterCommit(focusTarget, giveFocusBack);
17436
17461
  } else {
17437
17462
  // Given before the callout leaves the document: removed while it
17438
17463
  // holds the focus, it sends a focusout with nowhere to go up
@@ -20787,7 +20812,9 @@ const swipeTypeOf = (axis, pulled) => {
20787
20812
  * meantime, nothing is picked up. The carry is the element's own, outcomes and
20788
20813
  * refusal included, without the wait for intent (the hold was the proof), and
20789
20814
  * the copy is brought to the finger rather than following it from wherever the
20790
- * element stands. A copy or nothing: `move` and `moving` carry the thing itself,
20815
+ * element stands. Let go of there before it has travelled — the hold only meant
20816
+ * to open the popup — it goes back and nothing is answered, whatever it was
20817
+ * brought over. A copy or nothing: `move` and `moving` carry the thing itself,
20791
20818
  * which would leave its place just for being handed a press. When it starts is
20792
20819
  * navi's — after a popup still being lifted (see `--navi-grab` in commands.js).
20793
20820
  *
@@ -30886,6 +30913,12 @@ const useUrlSearchParam = (paramName, defaultValue) => {
30886
30913
  return [value, setSearchParamValue];
30887
30914
  };
30888
30915
 
30916
+ // The title belongs to the last Head to arrive, whatever order they leave in:
30917
+ // the page being left by a route transition is taken down AFTER the page
30918
+ // arriving is up (see keepLeavingPages in route.jsx), and putting back the
30919
+ // title it found would put its own page's title over the new one.
30920
+ const titleEntries = [];
30921
+ let titleWithoutHead = null;
30889
30922
  const Head = ({
30890
30923
  children
30891
30924
  }) => {
@@ -30894,15 +30927,17 @@ const Head = ({
30894
30927
  return undefined;
30895
30928
  }
30896
30929
  const childArray = Array.isArray(children) ? children : [children];
30897
- const previousTitle = document.title;
30898
30930
  const appendedElements = [];
30931
+ let titleEntry = null;
30899
30932
  for (const child of childArray) {
30900
30933
  if (!child) {
30901
30934
  continue;
30902
30935
  }
30903
30936
  if (child.type === "title") {
30904
30937
  const titleChildren = child.props.children;
30905
- document.title = Array.isArray(titleChildren) ? titleChildren.join("") : titleChildren ?? "";
30938
+ titleEntry = {
30939
+ title: Array.isArray(titleChildren) ? titleChildren.join("") : titleChildren ?? ""
30940
+ };
30906
30941
  continue;
30907
30942
  }
30908
30943
  const el = document.createElement(child.type);
@@ -30913,8 +30948,19 @@ const Head = ({
30913
30948
  document.head.appendChild(el);
30914
30949
  appendedElements.push(el);
30915
30950
  }
30951
+ if (titleEntry) {
30952
+ if (titleEntries.length === 0) {
30953
+ titleWithoutHead = document.title;
30954
+ }
30955
+ titleEntries.push(titleEntry);
30956
+ document.title = titleEntry.title;
30957
+ }
30916
30958
  return () => {
30917
- document.title = previousTitle;
30959
+ if (titleEntry) {
30960
+ titleEntries.splice(titleEntries.indexOf(titleEntry), 1);
30961
+ const lastEntry = titleEntries[titleEntries.length - 1];
30962
+ document.title = lastEntry ? lastEntry.title : titleWithoutHead;
30963
+ }
30918
30964
  for (const el of appendedElements) {
30919
30965
  el.remove();
30920
30966
  }
@@ -31077,75 +31123,7 @@ const samePages = (pages, otherPages) => {
31077
31123
  return true;
31078
31124
  };
31079
31125
 
31080
- /**
31081
- * Route is the single primitive for URL-based rendering.
31082
- *
31083
- * ## Layout pattern
31084
- * Use this when multiple routes share a common layout but have no shared URL prefix,
31085
- * making it impossible to set a guard route on the parent container.
31086
- * For example, `/profile` and `/settings` both live inside `AuthLayout` but there
31087
- * is no `/auth/` prefix to match on. A container Route wraps them: the active
31088
- * child's element is injected as `children` into the layout element.
31089
- * If a page needs state owned by the layout, the layout must expose it via context.
31090
- *
31091
- * ```jsx
31092
- * const PROFILE_ROUTE = route("/profile");
31093
- * const SETTINGS_ROUTE = route("/settings");
31094
- *
31095
- * <Route element={AuthLayout}>
31096
- * <Route route={PROFILE_ROUTE} element={ProfilePage} />
31097
- * <Route route={SETTINGS_ROUTE} element={SettingsPage} />
31098
- * </Route>
31099
- * ```
31100
- *
31101
- * Such a container holds no fallback, and cannot: what makes this one the right
31102
- * layout is a child matching, and having no prefix is the very reason it was
31103
- * reached for — so there is no address that belongs to it and to none of its
31104
- * children. An address nothing claims is the router's to answer; write the
31105
- * fallback there.
31106
- *
31107
- * ## Self-contained section pattern
31108
- * Use this when routes share a common URL prefix (e.g. `/dashboard/`).
31109
- * A single leaf Route in the top-level router matches the prefix; the component
31110
- * it renders owns its sub-router and all related routes internally.
31111
- * Everything about the section — routes, structure, sub-pages — is co-located.
31112
- * The component is not a reusable layout; it is the section.
31113
- *
31114
- * Compared to the layout pattern, a dedicated section component is more powerful:
31115
- * - Wrapper elements (chrome, nav, containers) are part of the component's render,
31116
- * not injected via `children`, so a layout is not needed.
31117
- * - Local state can be passed directly via `elementProps` to sub-pages. With the
31118
- * layout pattern, sub-pages receive state only through `children` or context.
31119
- *
31120
- * The layout pattern remains necessary when a shared prefix does not exist
31121
- * (see "Layout pattern" above).
31122
- *
31123
- * ```jsx
31124
- * const DASHBOARD_SECTION_ROUTE = route("/dashboard/");
31125
- * const DASHBOARD_HOME_ROUTE = route("/dashboard");
31126
- * const DASHBOARD_POSTS_ROUTE = route("/dashboard/posts");
31127
- *
31128
- * // top-level router — only knows about the prefix
31129
- * <Route route={DASHBOARD_SECTION_ROUTE} element={DashboardSection} />
31130
- *
31131
- * // Dashboard owns the rest
31132
- * const DashboardSection = () => {
31133
- * const [sidebarOpen, setSidebarOpen] = useState(false);
31134
- *
31135
- * return <div
31136
- * style="background: lightblue; padding: 10px;"
31137
- * onClick={() => setSidebarOpen(o => !o)}
31138
- * >
31139
- * <Route>
31140
- * <Route route={DASHBOARD_HOME_ROUTE} element={DashboardHomePage} elementProps={{ sidebarOpen}} />
31141
- * <Route route={DASHBOARD_POSTS_ROUTE} element={DashboardPostsPage} elementProps={{ sidebarOpen }} />
31142
- * <Route fallback element={DashboardNotFound} />
31143
- * </Route>
31144
- * </div>;
31145
- * }
31146
- * ```
31147
- */
31148
-
31126
+ installImportMetaCssBuild(import.meta);
31149
31127
 
31150
31128
  /**
31151
31129
  * Keep every container showing the page it is showing, whatever the routes say.
@@ -31169,6 +31147,56 @@ const freezeRouteRender = () => {
31169
31147
  routeRenderFrozenSignal.value++;
31170
31148
  };
31171
31149
  };
31150
+
31151
+ /**
31152
+ * Keep the page a container leaves, hidden, until its route transition is over.
31153
+ *
31154
+ * The browser photographs the page being left before the change, and the page
31155
+ * arriving inside the transition's update callback — where everything done
31156
+ * delays the movement. Taking the page being left down is most of that: every
31157
+ * hook of every component in it is cleaned up, and a page of thirty cards
31158
+ * takes longer to take down than the page arriving takes to build. Its picture
31159
+ * is already taken, so it only has to stay out of the second one: a container
31160
+ * that changes page while this is held keeps the page it was showing, hidden,
31161
+ * and drops it once the movement is over (see route_transition.jsx).
31162
+ *
31163
+ * Kept only where Preact would have unmounted it: a page rendered by the same
31164
+ * element on both sides is the same component instance carried over, exactly
31165
+ * as without a movement.
31166
+ */
31167
+ let leavingPagesHold = null;
31168
+ const keepLeavingPages = () => {
31169
+ const hold = {
31170
+ released: false,
31171
+ containers: new Set()
31172
+ };
31173
+ leavingPagesHold = hold;
31174
+ return () => {
31175
+ if (hold.released) {
31176
+ return;
31177
+ }
31178
+ hold.released = true;
31179
+ if (leavingPagesHold === hold) {
31180
+ leavingPagesHold = null;
31181
+ }
31182
+ for (const rerender of hold.containers) {
31183
+ rerender();
31184
+ }
31185
+ hold.containers.clear();
31186
+ };
31187
+ };
31188
+
31189
+ // What a page can learn about its place from the container rendering it:
31190
+ // whether it is a page kept while leaving (see keepLeavingPages). Such a page
31191
+ // is still mounted, and whatever it fills OUTSIDE its own nodes — a slot in a
31192
+ // bar, say — would otherwise go on showing with the page arriving (see
31193
+ // layout/slot.jsx).
31194
+ const RoutePageContext = createContext(null);
31195
+ const LEAVING_ATTRIBUTE = "data-navi-route-leaving";
31196
+ const css$13 = /* css */`[data-navi-route-leaving] {
31197
+ display: none !important;
31198
+ }
31199
+ `;
31172
31200
  const debug$1 = (...args) => {
31173
31201
  {
31174
31202
  return;
@@ -31288,6 +31316,7 @@ const RouteContainer = ({
31288
31316
  shownBranchRef.current = activeBranch;
31289
31317
  }
31290
31318
  const branch = shownBranchRef.current || activeBranch;
31319
+ const content = useContentKeepingLeavingPages(branch);
31291
31320
 
31292
31321
  // The two things this component knows that nobody outside can find out: the
31293
31322
  // pages a named fallback is the absence of — the children it was written
@@ -31309,7 +31338,6 @@ const RouteContainer = ({
31309
31338
  };
31310
31339
  }, [fallbackPage]);
31311
31340
  debug$1(`[container "${id}"] RENDER, active=${branch ? branch.type : "none"}`);
31312
- const content = branch ? branch.node : null;
31313
31341
  if (!content) {
31314
31342
  return null;
31315
31343
  }
@@ -31318,12 +31346,152 @@ const RouteContainer = ({
31318
31346
  }
31319
31347
  return content;
31320
31348
  };
31349
+ // What a container renders: the page it shows and, while a route transition
31350
+ // keeps them (see keepLeavingPages), the pages it has left — each the very
31351
+ // vnode rendered last time, which Preact does not look into, between two
31352
+ // markers that say which nodes to hide. Always an array, even of one page: the
31353
+ // page being left must stay the child it was when a page is added beside it,
31354
+ // or Preact takes it down and builds it again.
31355
+ let leavingKeyCount = 0;
31356
+ const useContentKeepingLeavingPages = branch => {
31357
+ const [, rerender] = useReducer(countRenders, 0);
31358
+ const shownRef = useRef(null);
31359
+ const leavingRef = useRef([]);
31360
+ const shown = shownRef.current;
31361
+ const element = branchElement(branch);
31362
+ let key;
31363
+ let page = null;
31364
+ if (shown && branch && (shown.id === branch.id || shown.element === element)) {
31365
+ // The same page, or one Preact carries over from it: the key it was
31366
+ // mounted with, or it would be mounted again.
31367
+ key = shown.key;
31368
+ page = shown.page;
31369
+ } else if (shown && leavingPagesHold && !document.querySelector(":modal")) {
31370
+ // Never while a modal dialog is open: it keeps everything but itself out
31371
+ // of reach — the page arriving could not even take the focus — for as long
31372
+ // as it is open, and one in a page kept while leaving stays open for the
31373
+ // whole movement. The page is then taken down at once, as without one.
31374
+ leavingRef.current.push({
31375
+ vnode: shown.vnode,
31376
+ page: shown.page,
31377
+ key: `navi-route-leaving-${++leavingKeyCount}`,
31378
+ hold: leavingPagesHold,
31379
+ startRef: createRef(),
31380
+ endRef: createRef(),
31381
+ hidden: false
31382
+ });
31383
+ leavingPagesHold.containers.add(rerender);
31384
+ // Keyed apart from the page it takes the place of, which is still there.
31385
+ key = `navi-route-${++leavingKeyCount}`;
31386
+ }
31387
+ let vnode = null;
31388
+ if (branch) {
31389
+ if (!page) {
31390
+ page = {
31391
+ leavingSignal: signal(false)
31392
+ };
31393
+ }
31394
+ vnode = h(RoutePageContext.Provider, {
31395
+ key,
31396
+ value: page
31397
+ }, branch.node);
31398
+ }
31399
+ shownRef.current = branch ? {
31400
+ id: branch.id,
31401
+ element,
31402
+ key,
31403
+ vnode,
31404
+ page
31405
+ } : null;
31406
+ const leaving = leavingRef.current.filter(entry => !entry.hold.released);
31407
+ leavingRef.current = leaving;
31408
+
31409
+ // Hidden as soon as it is in the DOM next to the page arriving: this runs in
31410
+ // the update callback's render, before the second picture is taken.
31411
+ useLayoutEffect(() => {
31412
+ for (const entry of leavingRef.current) {
31413
+ const start = entry.startRef.current;
31414
+ const end = entry.endRef.current;
31415
+ if (entry.hidden || !start || !end) {
31416
+ continue;
31417
+ }
31418
+ entry.hidden = true;
31419
+ entry.page.leavingSignal.value = true;
31420
+ const focusedElement = document.activeElement;
31421
+ let node = start.nextSibling;
31422
+ while (node && node !== end) {
31423
+ if (node.nodeType === Node.ELEMENT_NODE) {
31424
+ node.setAttribute(LEAVING_ATTRIBUTE, "");
31425
+ // What the keyboard held in it goes where the page's removal would
31426
+ // have sent it: a hidden element keeps the focus, and would go on
31427
+ // receiving the keys pressed during the movement.
31428
+ if (node.contains(focusedElement)) {
31429
+ focusedElement.blur();
31430
+ }
31431
+ }
31432
+ node = node.nextSibling;
31433
+ }
31434
+ }
31435
+ });
31436
+ useLayoutEffect(() => {
31437
+ return () => {
31438
+ for (const entry of leavingRef.current) {
31439
+ entry.hold.containers.delete(rerender);
31440
+ }
31441
+ };
31442
+ }, []);
31443
+ if (leaving.length === 0) {
31444
+ return vnode ? [vnode] : null;
31445
+ }
31446
+ import.meta.css = [css$13, "@jsenv/navi/src/nav/route.jsx"];
31447
+ const children = vnode ? [vnode] : [];
31448
+ for (const entry of leaving) {
31449
+ children.push(h("template", {
31450
+ key: `${entry.key}-start`,
31451
+ ref: entry.startRef
31452
+ }), entry.vnode, h("template", {
31453
+ key: `${entry.key}-end`,
31454
+ ref: entry.endRef
31455
+ }));
31456
+ }
31457
+ return children;
31458
+ };
31459
+ const countRenders = count => count + 1;
31460
+ // What Preact compares when a container changes page, below the <Route> it
31461
+ // renders either way: the element the page is rendered with — a layout, for a
31462
+ // container branch.
31463
+ const branchElement = branch => {
31464
+ if (!branch) {
31465
+ return null;
31466
+ }
31467
+ let node = branch.node;
31468
+ while (node && node.type !== Route) {
31469
+ // A wrapper around the branch (see wrapBranch).
31470
+ node = node.props.children;
31471
+ }
31472
+ if (!node) {
31473
+ return null;
31474
+ }
31475
+ const {
31476
+ element
31477
+ } = node.props;
31478
+ if (element === undefined || typeof element === "function") {
31479
+ return element;
31480
+ }
31481
+ return element.type;
31482
+ };
31483
+
31321
31484
  // Walk JSX children vnodes (without rendering) to build a branch list and
31322
31485
  // find the active one in the same pass. Anything that is not a <Route> is read
31323
31486
  // through and kept around the branch it holds (see below).
31324
31487
  // Returns { matchingBranch, fallbackBranch, activeBranch, pages }.
31325
- const collectBranches = children => {
31488
+ //
31489
+ // Each branch is given an id from where it is written among the children,
31490
+ // which is what tells a container it changed page: the vnodes themselves are
31491
+ // new at every render of the parent.
31492
+ const collectBranches = (children, path = "") => {
31326
31493
  let matchingBranch = null;
31494
+ let childIndex = 0;
31327
31495
  let fallbackBranch = null;
31328
31496
  // Every page a branch can be on, at any depth: what a named fallback is the
31329
31497
  // ABSENCE of (see route_fallback.js). A container with a guard route counts
@@ -31341,6 +31509,7 @@ const collectBranches = children => {
31341
31509
  }
31342
31510
  return;
31343
31511
  }
31512
+ const id = `${path}${childIndex++}`;
31344
31513
  if (child.type !== Route) {
31345
31514
  // Anything else is a wrapper around branches, and the two that matter are
31346
31515
  // navi's own: a page says what it renders and delegates what it cannot —
@@ -31360,7 +31529,7 @@ const collectBranches = children => {
31360
31529
  matchingBranch: matchingInside,
31361
31530
  fallbackBranch: fallbackInside,
31362
31531
  pages: pagesInside
31363
- } = collectBranches(wrapperChildren);
31532
+ } = collectBranches(wrapperChildren, `${id}.`);
31364
31533
  for (const pageInside of pagesInside) {
31365
31534
  pages.push(pageInside);
31366
31535
  }
@@ -31383,7 +31552,7 @@ const collectBranches = children => {
31383
31552
  matchingBranch: matchingChild,
31384
31553
  fallbackBranch: fallbackChild,
31385
31554
  pages: pagesChild
31386
- } = collectBranches(nodeChildren);
31555
+ } = collectBranches(nodeChildren, `${id}.`);
31387
31556
  if (fallbackChild && !route) {
31388
31557
  // A fallback is the branch taken for an address none of its siblings
31389
31558
  // claim — so something has to make that address BELONG to this
@@ -31407,6 +31576,7 @@ const collectBranches = children => {
31407
31576
  }
31408
31577
  const branch = {
31409
31578
  type: "container",
31579
+ id,
31410
31580
  node: child
31411
31581
  };
31412
31582
  const guardMatching = route ? route.matchingSignal.value : false;
@@ -31424,6 +31594,7 @@ const collectBranches = children => {
31424
31594
  if (!fallbackBranch) {
31425
31595
  fallbackBranch = {
31426
31596
  type: "fallback",
31597
+ id,
31427
31598
  node: child,
31428
31599
  page: fallback.isRouteFallback ? fallback : null
31429
31600
  };
@@ -31431,6 +31602,7 @@ const collectBranches = children => {
31431
31602
  } else {
31432
31603
  const branch = {
31433
31604
  type: "leaf",
31605
+ id,
31434
31606
  node: child
31435
31607
  };
31436
31608
  pages.push({
@@ -31878,7 +32050,7 @@ const holdTransitionFurniture = (owner, areaElement) => {
31878
32050
  }
31879
32051
  const namesLeaving = [];
31880
32052
  for (const element of namedElements) {
31881
- if (!element.isConnected) {
32053
+ if (!isRendered(element)) {
31882
32054
  namesLeaving.push(nameByElement.get(element));
31883
32055
  } else if (element.matches(FIXED_BAR_SELECTOR$1)) {
31884
32056
  element.setAttribute(FURNITURE_ATTRIBUTE, "shared");
@@ -31997,11 +32169,16 @@ const travelRule = (side, names, movementProperty) => {
31997
32169
  * The pictures are up and about to move: what no picture covers heads to the
31998
32170
  * state arriving, on the same clock.
31999
32171
  */
32000
- const startTransitionFurniture = (owner) => {
32172
+ const startTransitionFurniture = (owner, { duration } = {}) => {
32001
32173
  if (owner !== furnitureOwner) {
32002
32174
  return;
32003
32175
  }
32004
32176
  for (const restWall of restWallElements) {
32177
+ // A duration of the movement's own is written for its pictures alone (see
32178
+ // transition_values.js); the stand-in is an element, and is told.
32179
+ if (duration !== undefined) {
32180
+ restWall.style.setProperty("--navi-route-transition-duration", duration);
32181
+ }
32005
32182
  restWall.style.opacity = restWallOpacity;
32006
32183
  }
32007
32184
  };
@@ -32032,7 +32209,9 @@ const releaseTransitionFurniture = (owner) => {
32032
32209
  // area's stand-in answers for it.
32033
32210
  const paintTransitionWalls = (areaElement) => {
32034
32211
  removeTransitionWalls();
32035
- const sources = document.querySelectorAll(TOP_LAYER_WALL_SELECTOR);
32212
+ const sources = Array.from(
32213
+ document.querySelectorAll(TOP_LAYER_WALL_SELECTOR),
32214
+ ).filter(isRendered);
32036
32215
  if (sources.length === 0) {
32037
32216
  return sources;
32038
32217
  }
@@ -32234,6 +32413,14 @@ const subtractRect = (rect, hole) => {
32234
32413
  return pieces;
32235
32414
  };
32236
32415
 
32416
+ // Out of the state arriving: taken out of the document, or in a page kept
32417
+ // hidden until its movement is over (see keepLeavingPages in route.jsx) —
32418
+ // where a dialog is still :modal and a popover still :popover-open, and
32419
+ // neither is painted or photographed.
32420
+ const isRendered = (element) => {
32421
+ return element.isConnected && element.checkVisibility();
32422
+ };
32423
+
32237
32424
  const removeRestWalls = () => {
32238
32425
  for (const restWall of restWallElements) {
32239
32426
  restWall.remove();
@@ -32243,6 +32430,52 @@ const removeRestWalls = () => {
32243
32430
  leavingRects = new Map();
32244
32431
  };
32245
32432
 
32433
+ /**
32434
+ * The values a movement hands to its pictures, written where only the pictures
32435
+ * read them: on ::view-transition, the root of the pseudo-element tree, rather
32436
+ * than on the document's root.
32437
+ *
32438
+ * A custom property written on :root is inherited by every element of the
32439
+ * document, and writing one restyles all of them. A movement writes its
32440
+ * numbers as its pages are held, again whenever a scroll moves the window, and
32441
+ * takes them off as it ends — each time a whole-document restyle of a page that
32442
+ * can be thousands of elements long. On ::view-transition they reach every
32443
+ * picture by plain inheritance, and no element at all.
32444
+ *
32445
+ * An inline style cannot target a pseudo-element, so they are declarations of
32446
+ * one rule, in a sheet of their own. Written only: what a pseudo-element that
32447
+ * is not on screen computes is not reliably readable, so nothing reads them
32448
+ * back from there.
32449
+ */
32450
+
32451
+ let valuesStyle = null;
32452
+ const getValuesStyle = () => {
32453
+ if (valuesStyle) {
32454
+ return valuesStyle;
32455
+ }
32456
+ const sheet = new CSSStyleSheet();
32457
+ sheet.replaceSync(":root::view-transition {}");
32458
+ const [rule] = sheet.cssRules;
32459
+ if (rule) {
32460
+ document.adoptedStyleSheets.push(sheet);
32461
+ valuesStyle = rule.style;
32462
+ } else {
32463
+ // A browser without view transitions drops the rule: the movement runs its
32464
+ // steps all the same (see start_view_transition_polyfill.js), with no
32465
+ // pictures to hand anything to.
32466
+ valuesStyle = document.createElement("div").style;
32467
+ }
32468
+ return valuesStyle;
32469
+ };
32470
+
32471
+ const setTransitionValue = (name, value) => {
32472
+ getValuesStyle().setProperty(name, value);
32473
+ };
32474
+
32475
+ const removeTransitionValue = (name) => {
32476
+ getValuesStyle().removeProperty(name);
32477
+ };
32478
+
32246
32479
  installImportMetaCssBuild(import.meta);
32247
32480
 
32248
32481
  const TRANSITION_WINDOW_CSS = /* css */ `@property --navi-transition-cover-top {
@@ -32277,96 +32510,6 @@ const TRANSITION_WINDOW_CSS = /* css */ `@property --navi-transition-cover-top {
32277
32510
  --navi-transition-cover-left: 0px;
32278
32511
  }
32279
32512
  }
32280
-
32281
- @property --navi-transition-window-top {
32282
- syntax: "*";
32283
- inherits: false
32284
- }
32285
-
32286
- @property --navi-transition-window-left {
32287
- syntax: "*";
32288
- inherits: false
32289
- }
32290
-
32291
- @property --navi-transition-window-width {
32292
- syntax: "*";
32293
- inherits: false
32294
- }
32295
-
32296
- @property --navi-transition-window-height {
32297
- syntax: "*";
32298
- inherits: false
32299
- }
32300
-
32301
- @property --navi-transition-window-old-top {
32302
- syntax: "*";
32303
- inherits: false
32304
- }
32305
-
32306
- @property --navi-transition-window-old-left {
32307
- syntax: "*";
32308
- inherits: false
32309
- }
32310
-
32311
- @property --navi-transition-window-new-top {
32312
- syntax: "*";
32313
- inherits: false
32314
- }
32315
-
32316
- @property --navi-transition-window-new-left {
32317
- syntax: "*";
32318
- inherits: false
32319
- }
32320
-
32321
- @property --navi-transition-old-band-top {
32322
- syntax: "*";
32323
- inherits: false
32324
- }
32325
-
32326
- @property --navi-transition-old-band-right {
32327
- syntax: "*";
32328
- inherits: false
32329
- }
32330
-
32331
- @property --navi-transition-old-band-bottom {
32332
- syntax: "*";
32333
- inherits: false
32334
- }
32335
-
32336
- @property --navi-transition-old-band-left {
32337
- syntax: "*";
32338
- inherits: false
32339
- }
32340
-
32341
- :root::view-transition, :root::view-transition-group(*), :root::view-transition-image-pair(*), :root::view-transition-old(*), :root::view-transition-new(*) {
32342
- --navi-transition-window-top: inherit;
32343
- --navi-transition-window-left: inherit;
32344
- --navi-transition-window-width: inherit;
32345
- --navi-transition-window-height: inherit;
32346
- --navi-transition-window-old-top: inherit;
32347
- --navi-transition-window-old-left: inherit;
32348
- --navi-transition-window-new-top: inherit;
32349
- --navi-transition-window-new-left: inherit;
32350
- --navi-transition-old-band-top: inherit;
32351
- --navi-transition-old-band-right: inherit;
32352
- --navi-transition-old-band-bottom: inherit;
32353
- --navi-transition-old-band-left: inherit;
32354
- }
32355
-
32356
- :root::view-transition-group-children(*) {
32357
- --navi-transition-window-top: inherit;
32358
- --navi-transition-window-left: inherit;
32359
- --navi-transition-window-width: inherit;
32360
- --navi-transition-window-height: inherit;
32361
- --navi-transition-window-old-top: inherit;
32362
- --navi-transition-window-old-left: inherit;
32363
- --navi-transition-window-new-top: inherit;
32364
- --navi-transition-window-new-left: inherit;
32365
- --navi-transition-old-band-top: inherit;
32366
- --navi-transition-old-band-right: inherit;
32367
- --navi-transition-old-band-bottom: inherit;
32368
- --navi-transition-old-band-left: inherit;
32369
- }
32370
32513
  `;
32371
32514
 
32372
32515
  // Called by whatever needs the window — a render, a movement declared or
@@ -32464,22 +32607,21 @@ const holdTransitionWindow = (
32464
32607
  const right =
32465
32608
  rectBefore.right > rectAfter.right ? rectBefore.right : rectAfter.right;
32466
32609
  windowOwner = owner;
32467
- const { style } = document.documentElement;
32468
- style.setProperty(WINDOW_TOP_PROPERTY, `${top}px`);
32469
- style.setProperty(WINDOW_LEFT_PROPERTY, `${left}px`);
32470
- style.setProperty(WINDOW_WIDTH_PROPERTY, `${right - left}px`);
32471
- style.setProperty(WINDOW_HEIGHT_PROPERTY, `${bottom - top}px`);
32472
- style.setProperty(WINDOW_OLD_TOP_PROPERTY, `${rectBefore.top}px`);
32473
- style.setProperty(WINDOW_OLD_LEFT_PROPERTY, `${rectBefore.left}px`);
32474
- style.setProperty(WINDOW_NEW_TOP_PROPERTY, `${rectAfter.top}px`);
32475
- style.setProperty(WINDOW_NEW_LEFT_PROPERTY, `${rectAfter.left}px`);
32610
+ setTransitionValue(WINDOW_TOP_PROPERTY, `${top}px`);
32611
+ setTransitionValue(WINDOW_LEFT_PROPERTY, `${left}px`);
32612
+ setTransitionValue(WINDOW_WIDTH_PROPERTY, `${right - left}px`);
32613
+ setTransitionValue(WINDOW_HEIGHT_PROPERTY, `${bottom - top}px`);
32614
+ setTransitionValue(WINDOW_OLD_TOP_PROPERTY, `${rectBefore.top}px`);
32615
+ setTransitionValue(WINDOW_OLD_LEFT_PROPERTY, `${rectBefore.left}px`);
32616
+ setTransitionValue(WINDOW_NEW_TOP_PROPERTY, `${rectAfter.top}px`);
32617
+ setTransitionValue(WINDOW_NEW_LEFT_PROPERTY, `${rectAfter.left}px`);
32476
32618
  const oldBand = furnitureLive
32477
32619
  ? bandTheCutMayBeRelaxedTo(bandBefore, bandAfter, rectAfter)
32478
32620
  : bandBefore;
32479
- style.setProperty(OLD_BAND_TOP_PROPERTY, `${oldBand.top}px`);
32480
- style.setProperty(OLD_BAND_RIGHT_PROPERTY, `${oldBand.right}px`);
32481
- style.setProperty(OLD_BAND_BOTTOM_PROPERTY, `${oldBand.bottom}px`);
32482
- style.setProperty(OLD_BAND_LEFT_PROPERTY, `${oldBand.left}px`);
32621
+ setTransitionValue(OLD_BAND_TOP_PROPERTY, `${oldBand.top}px`);
32622
+ setTransitionValue(OLD_BAND_RIGHT_PROPERTY, `${oldBand.right}px`);
32623
+ setTransitionValue(OLD_BAND_BOTTOM_PROPERTY, `${oldBand.bottom}px`);
32624
+ setTransitionValue(OLD_BAND_LEFT_PROPERTY, `${oldBand.left}px`);
32483
32625
  followArrivingState(element, rectAfter, {
32484
32626
  outerScroll,
32485
32627
  windowTop: top,
@@ -32501,9 +32643,8 @@ const releaseTransitionWindow = (owner) => {
32501
32643
  if (unfollowArrivingState) {
32502
32644
  unfollowArrivingState();
32503
32645
  }
32504
- const { style } = document.documentElement;
32505
32646
  for (const property of WINDOW_PROPERTIES) {
32506
- style.removeProperty(property);
32647
+ removeTransitionValue(property);
32507
32648
  }
32508
32649
  };
32509
32650
 
@@ -32512,12 +32653,11 @@ const releaseTransitionWindow = (owner) => {
32512
32653
  // file) — and with it the offset outside the box, which carries the window
32513
32654
  // and the corner of the state being left along. Requested, never awaited: the
32514
32655
  // first call runs inside the update callback, where a frame cannot come.
32515
- // Written only when it moved: every write restyles the root and the pictures.
32656
+ // Written only when it moved: every write restyles the pictures.
32516
32657
  const followArrivingState = (element, rectAtHold, heldAt) => {
32517
32658
  if (unfollowArrivingState) {
32518
32659
  unfollowArrivingState();
32519
32660
  }
32520
- const { style } = document.documentElement;
32521
32661
  let top = rectAtHold.top;
32522
32662
  let left = rectAtHold.left;
32523
32663
  let outerTop = heldAt.outerScroll.top;
@@ -32526,24 +32666,27 @@ const followArrivingState = (element, rectAtHold, heldAt) => {
32526
32666
  const rect = element.getBoundingClientRect();
32527
32667
  if (rect.top !== top) {
32528
32668
  top = rect.top;
32529
- style.setProperty(WINDOW_NEW_TOP_PROPERTY, `${top}px`);
32669
+ setTransitionValue(WINDOW_NEW_TOP_PROPERTY, `${top}px`);
32530
32670
  }
32531
32671
  if (rect.left !== left) {
32532
32672
  left = rect.left;
32533
- style.setProperty(WINDOW_NEW_LEFT_PROPERTY, `${left}px`);
32673
+ setTransitionValue(WINDOW_NEW_LEFT_PROPERTY, `${left}px`);
32534
32674
  }
32535
32675
  const outerScroll = readOuterScroll(element);
32536
32676
  if (outerScroll.top !== outerTop) {
32537
32677
  outerTop = outerScroll.top;
32538
32678
  const shift = heldAt.outerScroll.top - outerTop;
32539
- style.setProperty(WINDOW_TOP_PROPERTY, `${heldAt.windowTop + shift}px`);
32540
- style.setProperty(WINDOW_OLD_TOP_PROPERTY, `${heldAt.oldTop + shift}px`);
32679
+ setTransitionValue(WINDOW_TOP_PROPERTY, `${heldAt.windowTop + shift}px`);
32680
+ setTransitionValue(WINDOW_OLD_TOP_PROPERTY, `${heldAt.oldTop + shift}px`);
32541
32681
  }
32542
32682
  if (outerScroll.left !== outerLeft) {
32543
32683
  outerLeft = outerScroll.left;
32544
32684
  const shift = heldAt.outerScroll.left - outerLeft;
32545
- style.setProperty(WINDOW_LEFT_PROPERTY, `${heldAt.windowLeft + shift}px`);
32546
- style.setProperty(
32685
+ setTransitionValue(
32686
+ WINDOW_LEFT_PROPERTY,
32687
+ `${heldAt.windowLeft + shift}px`,
32688
+ );
32689
+ setTransitionValue(
32547
32690
  WINDOW_OLD_LEFT_PROPERTY,
32548
32691
  `${heldAt.oldLeft + shift}px`,
32549
32692
  );
@@ -32906,58 +33049,9 @@ const css$12 = /* css */`@property --navi-route-transition-leave {
32906
33049
  inherits: false
32907
33050
  }
32908
33051
 
32909
- @property --navi-route-transition-clip-top {
32910
- syntax: "*";
32911
- inherits: false
32912
- }
32913
-
32914
- @property --navi-route-transition-clip-right {
32915
- syntax: "*";
32916
- inherits: false
32917
- }
32918
-
32919
- @property --navi-route-transition-clip-bottom {
32920
- syntax: "*";
32921
- inherits: false
32922
- }
32923
-
32924
- @property --navi-route-transition-clip-left {
32925
- syntax: "*";
32926
- inherits: false
32927
- }
32928
-
32929
- @property --navi-route-transition-travel-x {
32930
- syntax: "*";
32931
- inherits: false
32932
- }
32933
-
32934
- @property --navi-route-transition-travel-y {
32935
- syntax: "*";
32936
- inherits: false
32937
- }
32938
-
32939
33052
  :root::view-transition, :root::view-transition-group(*), :root::view-transition-image-pair(*), :root::view-transition-old(*), :root::view-transition-new(*) {
32940
33053
  --navi-route-transition-leave: inherit;
32941
33054
  --navi-route-transition-enter: inherit;
32942
- --navi-route-transition-covered: inherit;
32943
- --navi-route-transition-clip-top: inherit;
32944
- --navi-route-transition-clip-right: inherit;
32945
- --navi-route-transition-clip-bottom: inherit;
32946
- --navi-route-transition-clip-left: inherit;
32947
- --navi-route-transition-travel-x: inherit;
32948
- --navi-route-transition-travel-y: inherit;
32949
- }
32950
-
32951
- :root::view-transition-group-children(*) {
32952
- --navi-route-transition-leave: inherit;
32953
- --navi-route-transition-enter: inherit;
32954
- --navi-route-transition-covered: inherit;
32955
- --navi-route-transition-clip-top: inherit;
32956
- --navi-route-transition-clip-right: inherit;
32957
- --navi-route-transition-clip-bottom: inherit;
32958
- --navi-route-transition-clip-left: inherit;
32959
- --navi-route-transition-travel-x: inherit;
32960
- --navi-route-transition-travel-y: inherit;
32961
33055
  }
32962
33056
 
32963
33057
  :root[data-navi-route-transition] [data-navi-route-transition-area] {
@@ -32978,28 +33072,32 @@ const css$12 = /* css */`@property --navi-route-transition-leave {
32978
33072
 
32979
33073
  &[data-navi-route-transition-target="area"] {
32980
33074
  view-transition-name: none;
32981
- --navi-route-transition-clip-top: max(0px,
32982
- min(var(--navi-safe-area-inset-top) + var(--navi-transition-cover-top),
32983
- var(--navi-transition-old-band-top)) - var(--navi-transition-window-top));
32984
- --navi-route-transition-clip-left: max(0px,
32985
- min(var(--navi-safe-area-inset-left) + var(--navi-transition-cover-left),
32986
- var(--navi-transition-old-band-left)) - var(--navi-transition-window-left));
32987
- --navi-route-transition-clip-bottom: max(0px,
32988
- var(--navi-transition-window-top) +
32989
- var(--navi-transition-window-height) +
32990
- min(var(--navi-safe-area-inset-bottom) +
32991
- var(--navi-transition-cover-bottom),
32992
- var(--navi-transition-old-band-bottom)) -
32993
- 100dvh);
32994
- --navi-route-transition-clip-right: max(0px,
32995
- var(--navi-transition-window-left) +
32996
- var(--navi-transition-window-width) +
32997
- min(var(--navi-safe-area-inset-right) +
32998
- var(--navi-transition-cover-right),
32999
- var(--navi-transition-old-band-right)) -
33000
- 100dvw);
33001
- --navi-route-transition-travel-x: calc(var(--navi-transition-window-width) - var(--navi-route-transition-clip-left) - var(--navi-route-transition-clip-right));
33002
- --navi-route-transition-travel-y: calc(var(--navi-transition-window-height) - var(--navi-route-transition-clip-top) - var(--navi-route-transition-clip-bottom));
33075
+
33076
+ &::view-transition {
33077
+ --navi-route-transition-clip-top: max(0px,
33078
+ min(var(--navi-safe-area-inset-top) + var(--navi-transition-cover-top),
33079
+ var(--navi-transition-old-band-top)) - var(--navi-transition-window-top));
33080
+ --navi-route-transition-clip-left: max(0px,
33081
+ min(var(--navi-safe-area-inset-left) +
33082
+ var(--navi-transition-cover-left),
33083
+ var(--navi-transition-old-band-left)) - var(--navi-transition-window-left));
33084
+ --navi-route-transition-clip-bottom: max(0px,
33085
+ var(--navi-transition-window-top) +
33086
+ var(--navi-transition-window-height) +
33087
+ min(var(--navi-safe-area-inset-bottom) +
33088
+ var(--navi-transition-cover-bottom),
33089
+ var(--navi-transition-old-band-bottom)) -
33090
+ 100dvh);
33091
+ --navi-route-transition-clip-right: max(0px,
33092
+ var(--navi-transition-window-left) +
33093
+ var(--navi-transition-window-width) +
33094
+ min(var(--navi-safe-area-inset-right) +
33095
+ var(--navi-transition-cover-right),
33096
+ var(--navi-transition-old-band-right)) -
33097
+ 100dvw);
33098
+ --navi-route-transition-travel-x: calc(var(--navi-transition-window-width) - var(--navi-route-transition-clip-left) - var(--navi-route-transition-clip-right));
33099
+ --navi-route-transition-travel-y: calc(var(--navi-transition-window-height) - var(--navi-route-transition-clip-top) - var(--navi-route-transition-clip-bottom));
33100
+ }
33003
33101
 
33004
33102
  &::view-transition-group(*) {
33005
33103
  z-index: 0;
@@ -34027,7 +34125,9 @@ const beginTransition = ({
34027
34125
  decision: navigationDecision,
34028
34126
  walkHome: null,
34029
34127
  releaseReverting: null,
34030
- restoreDuration: null
34128
+ duration: undefined,
34129
+ restoreDuration: null,
34130
+ releaseLeavingPages: null
34031
34131
  };
34032
34132
  // The document wears one transition at a time, and the one this interrupts
34033
34133
  // ends only after this one has begun — too late to take off what it wears
@@ -34080,18 +34180,13 @@ const beginTransition = ({
34080
34180
  if (areaElement) {
34081
34181
  documentElement.setAttribute(TRANSITION_TARGET_ATTRIBUTE, "area");
34082
34182
  }
34083
- // A duration of this relation's own, worn for the length of the transition —
34084
- // and whatever the application had written inline put back afterwards, not
34085
- // erased.
34183
+ // A duration of this relation's own, worn by the pictures for the length of
34184
+ // the transition, over whatever the application says on the root.
34086
34185
  if (duration !== undefined) {
34087
- const durationBefore = documentElement.style.getPropertyValue(TRANSITION_DURATION_PROPERTY);
34088
- documentElement.style.setProperty(TRANSITION_DURATION_PROPERTY, typeof duration === "number" ? `${duration}ms` : duration);
34186
+ transition.duration = typeof duration === "number" ? `${duration}ms` : duration;
34187
+ setTransitionValue(TRANSITION_DURATION_PROPERTY, transition.duration);
34089
34188
  transition.restoreDuration = () => {
34090
- if (durationBefore) {
34091
- documentElement.style.setProperty(TRANSITION_DURATION_PROPERTY, durationBefore);
34092
- } else {
34093
- documentElement.style.removeProperty(TRANSITION_DURATION_PROPERTY);
34094
- }
34189
+ removeTransitionValue(TRANSITION_DURATION_PROPERTY);
34095
34190
  };
34096
34191
  }
34097
34192
  const releaseRendering = takeoverRoutingRenderingHold();
@@ -34104,7 +34199,9 @@ const beginTransition = ({
34104
34199
  // the only place the silent misconfigurations show. They are all about the
34105
34200
  // same thing — a movement playing on pictures that are not the pages.
34106
34201
  const viewTransitionReady = () => {
34107
- startTransitionFurniture(transition);
34202
+ startTransitionFurniture(transition, {
34203
+ duration: transition.duration
34204
+ });
34108
34205
  const capturedNames = capturedViewTransitionNames();
34109
34206
  if (areaElements.length > 0) {
34110
34207
  if (!capturedNames.has(AREA_NAME)) {
@@ -34130,6 +34227,10 @@ const beginTransition = ({
34130
34227
  // is awaited here must be able to resolve without a frame: the document
34131
34228
  // is frozen for the whole of this callback.
34132
34229
  try {
34230
+ // The page being left is photographed already: it only has to stay out
34231
+ // of the second picture, not be taken down before it (see
34232
+ // keepLeavingPages in route.jsx).
34233
+ transition.releaseLeavingPages = keepLeavingPages();
34133
34234
  // Releasing flushes the held render synchronously, so a route that
34134
34235
  // rendered has already resolved the wait by the next line.
34135
34236
  releaseRendering();
@@ -34182,6 +34283,11 @@ const beginTransition = ({
34182
34283
  // must not strip what that one is wearing.
34183
34284
  renderWait.stop();
34184
34285
  releaseRendering();
34286
+ // Taken down once nobody sees it any more.
34287
+ if (transition.releaseLeavingPages) {
34288
+ transition.releaseLeavingPages();
34289
+ transition.releaseLeavingPages = null;
34290
+ }
34185
34291
  // The hold a way back took, when it is still standing: the pictures were
34186
34292
  // turned round a second time and played out, or something else took the
34187
34293
  // document over mid-walk. A hold nobody gives back freezes the page.
@@ -43888,8 +43994,12 @@ const useInteractiveProps = (props, {
43888
43994
  }
43889
43995
  // Read by READONLY_CONSTRAINT to say how many runs are already out, and by
43890
43996
  // the action handlers below, which are where a run joins and leaves the
43891
- // count (a labelled block away, hence through the controller).
43892
- uiStateController.parallelGuard = parallelGuard;
43997
+ // count (a labelled block away, hence through the controller). None for a
43998
+ // control without an action of its own, by the same rule as being held
43999
+ // back: what it runs (the noop every such control has, a link's
44000
+ // navigation) is nothing the guard counts, and would hold a slot for its
44001
+ // length.
44002
+ uiStateController.parallelGuard = props.action ? parallelGuard : null;
43893
44003
  if (heldByParallelGuard) {
43894
44004
  controlHostProps["data-readonly-reason"] = "parallel-guard";
43895
44005
  }
@@ -44163,7 +44273,11 @@ const useInteractiveProps = (props, {
44163
44273
  // carried by every navi_action_* event of that run).
44164
44274
  uiStateController.pendingActionEvent = e.detail.event;
44165
44275
  uiStateController.actionInFlight = true;
44166
- uiStateController.parallelGuard?.claim(uiStateController);
44276
+ // Given back to the guard it was counted in, whatever the control
44277
+ // holds by then: one that lost its action mid-run has no guard left
44278
+ // to name, and the slot would stay taken.
44279
+ const countedIn = uiStateController.parallelGuard;
44280
+ countedIn?.claim(uiStateController);
44167
44281
  if (optimistic) {
44168
44282
  syncOptimisticAttribute(true);
44169
44283
  }
@@ -44181,7 +44295,7 @@ const useInteractiveProps = (props, {
44181
44295
  e.detail.addSideEffect((outcome, outcomeEvent) => {
44182
44296
  uiStateController.actionInFlight = false;
44183
44297
  uiStateController.runningActionSignal.value = null;
44184
- uiStateController.parallelGuard?.release(uiStateController);
44298
+ countedIn?.release(uiStateController);
44185
44299
  // The outcome callbacks are told from here, the run's side effect,
44186
44300
  // and not from the onnavi_action_* handlers below: the run can
44187
44301
  // outlive this control (its own answer, written to a store the
@@ -47713,6 +47827,90 @@ const BinderItemContext = createContext(null);
47713
47827
  */
47714
47828
  const NavContext = createContext(null);
47715
47829
 
47830
+ /**
47831
+ * Whether the page one is on is `route` with `params` — what a link or a button
47832
+ * pointing at a route shows as current.
47833
+ *
47834
+ * Read through a computed of that one answer, so the component reading it
47835
+ * re-renders when the answer flips, and not whenever the route's status moves:
47836
+ * a page arriving on the route with other params — one profile opening, under a
47837
+ * list of links to a hundred other profiles — starts the route matching and
47838
+ * changes its params, and leaves every one of those links exactly as it was.
47839
+ *
47840
+ * `except`: routes whose matching vetoes the answer — a route named there is
47841
+ * somewhere else, whatever the rest of the reading says. `also`: routes whose
47842
+ * matching grants it. Both take a route or a list of routes.
47843
+ */
47844
+ const useRouteCurrent = (route, params, { except, also } = {}) => {
47845
+ const exceptRoutes = toRouteList(except);
47846
+ const alsoRoutes = toRouteList(also);
47847
+ const paramsKey = getParamsCacheKey(params);
47848
+ const exceptKey = routeListKey(exceptRoutes);
47849
+ const alsoKey = routeListKey(alsoRoutes);
47850
+ const currentSignal = useMemo(() => {
47851
+ if (paramsKey === null) {
47852
+ return null;
47853
+ }
47854
+ return computed(() =>
47855
+ readRouteCurrent(route, params, exceptRoutes, alsoRoutes),
47856
+ );
47857
+ }, [route, paramsKey, exceptKey, alsoKey]);
47858
+ if (!currentSignal) {
47859
+ // Params no key can be made of (a date, an object): read on the spot, the
47860
+ // component then re-renders with the status of every route it reads.
47861
+ return readRouteCurrent(route, params, exceptRoutes, alsoRoutes);
47862
+ }
47863
+ return currentSignal.value;
47864
+ };
47865
+
47866
+ // Every signal is read even once the answer is known: reading is what
47867
+ // subscribes, and a route skipped today is the one whose change has to be heard
47868
+ // tomorrow.
47869
+ const readRouteCurrent = (route, params, exceptRoutes, alsoRoutes) => {
47870
+ const matching = route.matchingSignal.value;
47871
+ const paramsMatching = route.matchesParams(params);
47872
+ let someExceptMatching = false;
47873
+ for (const exceptRoute of exceptRoutes) {
47874
+ if (exceptRoute.matchingSignal.value) {
47875
+ someExceptMatching = true;
47876
+ }
47877
+ }
47878
+ let someAlsoMatching = false;
47879
+ for (const alsoRoute of alsoRoutes) {
47880
+ if (alsoRoute.matchingSignal.value) {
47881
+ someAlsoMatching = true;
47882
+ }
47883
+ }
47884
+ if (someExceptMatching) {
47885
+ return false;
47886
+ }
47887
+ return (matching && paramsMatching) || someAlsoMatching;
47888
+ };
47889
+
47890
+ const toRouteList = (routes) => {
47891
+ if (!routes) {
47892
+ return [];
47893
+ }
47894
+ return Array.isArray(routes) ? routes : [routes];
47895
+ };
47896
+
47897
+ // A list of routes as a key, since an inline `currentAlso={[A, B]}` is a new
47898
+ // array at every render of the parent.
47899
+ const routeIdByRoute = new WeakMap();
47900
+ let routeIdCount = 0;
47901
+ const routeListKey = (routes) => {
47902
+ let key = "";
47903
+ for (const route of routes) {
47904
+ let id = routeIdByRoute.get(route);
47905
+ if (id === undefined) {
47906
+ id = ++routeIdCount;
47907
+ routeIdByRoute.set(route, id);
47908
+ }
47909
+ key += `${id},`;
47910
+ }
47911
+ return key;
47912
+ };
47913
+
47716
47914
  /*
47717
47915
  * Custom hook to apply semi-transparent color when an element should be dimmed.
47718
47916
  *
@@ -48256,15 +48454,10 @@ const LinkWithRoute = ({
48256
48454
  ...rest
48257
48455
  }) => {
48258
48456
  const url = route.buildUrl(routeParams);
48259
- const {
48260
- matching
48261
- } = useRouteStatus(route);
48262
- const paramsAreMatching = route.matchesParams(routeParams);
48263
- const someExceptedRouteMatching = useSomeRouteMatching(currentExcept);
48264
- const someAlsoRouteMatching = useSomeRouteMatching(currentAlso);
48265
- // "Except" is a veto: a route named there is somewhere else, whatever the
48266
- // rest of the reading says.
48267
- const linkMatching = !someExceptedRouteMatching && (matching && paramsAreMatching || someAlsoRouteMatching);
48457
+ const linkMatching = useRouteCurrent(route, routeParams, {
48458
+ except: currentExcept,
48459
+ also: currentAlso
48460
+ });
48268
48461
  const innerCurrent = current || linkMatching;
48269
48462
  return jsx(Link, {
48270
48463
  href: url,
@@ -48273,22 +48466,6 @@ const LinkWithRoute = ({
48273
48466
  children: children || route.buildRelativeUrl(routeParams)
48274
48467
  });
48275
48468
  };
48276
- // A route, or a list of them, read as one answer: does any of them match.
48277
- // Reading matchingSignal during the render is what subscribes the component to
48278
- // it, so every route is read even once one has answered yes — stopping early
48279
- // would leave the component deaf to the ones it skipped.
48280
- const useSomeRouteMatching = routes => {
48281
- if (!routes) {
48282
- return false;
48283
- }
48284
- let someMatching = false;
48285
- for (const route of Array.isArray(routes) ? routes : [routes]) {
48286
- if (route.matchingSignal.value) {
48287
- someMatching = true;
48288
- }
48289
- }
48290
- return someMatching;
48291
- };
48292
48469
  const LinkPlain = props => {
48293
48470
  const defaultRef = useRef();
48294
48471
  props.ref = props.ref || defaultRef;
@@ -53372,11 +53549,7 @@ const ButtonWithRoute = props => {
53372
53549
  ...rest
53373
53550
  } = props;
53374
53551
  const url = route.buildUrl(routeParams);
53375
- const {
53376
- matching
53377
- } = useRouteStatus(route);
53378
- const paramsAreMatching = route.matchesParams(routeParams);
53379
- const linkMatching = matching && paramsAreMatching;
53552
+ const linkMatching = useRouteCurrent(route, routeParams);
53380
53553
 
53381
53554
  // Merged into whatever the caller already holds: a button can be forced into
53382
53555
  // a state for a demo and still learn its own current-ness from its route.
@@ -64674,10 +64847,24 @@ const useDialogProps = props => {
64674
64847
  // whichever show call ran, not just when a stray authored display
64675
64848
  // property is actually present — harmless the rest of the time.
64676
64849
  dialogEl.setAttribute("navi-hidden", "");
64677
- if (isTopLayerPopover) {
64678
- dialogEl.hidePopover();
64850
+ const hideNatively = () => {
64851
+ if (isTopLayerPopover) {
64852
+ dialogEl.hidePopover();
64853
+ } else {
64854
+ dialogEl.close();
64855
+ }
64856
+ };
64857
+ if (findEvent(closeEvent, UNMOUNT_EVENT_TYPE)) {
64858
+ // Leaving the tree: hidden natively once the commit is over. Its
64859
+ // removal takes it out of the top layer and hands the focus to
64860
+ // nobody, where closing it now hands the focus to whatever held it
64861
+ // before the opening — often a trigger leaving in the same commit
64862
+ // (see giveFocusBackAfterCommit). What is left to close then is its
64863
+ // state: a <dialog> removed while open keeps its `open` attribute, and
64864
+ // a subtree <Suspense> parks and puts back would show it again.
64865
+ queueMicrotask(hideNatively);
64679
64866
  } else {
64680
- dialogEl.close();
64867
+ hideNatively();
64681
64868
  }
64682
64869
  // Held at the size it has right now, for the whole way out. cleanup()
64683
64870
  // below already stops the JS repositioning, but the size is CSS-driven
@@ -66346,7 +66533,13 @@ const usePopoverProps = props => {
66346
66533
  // reliably sufficient once a consumer's own CSS also sets display.
66347
66534
  popoverEl.setAttribute("navi-hidden", "");
66348
66535
  if (isTopLayer) {
66349
- popoverEl.hidePopover();
66536
+ // Left to its removal when the popover leaves the tree: that hides it
66537
+ // and hands the focus to nobody, where hidePopover() hands the focus
66538
+ // to whatever held it before the opening — often a trigger leaving in
66539
+ // the same commit (see giveFocusBackAfterCommit).
66540
+ if (!findEvent(closeEvent, UNMOUNT_EVENT_TYPE)) {
66541
+ popoverEl.hidePopover();
66542
+ }
66350
66543
  } else {
66351
66544
  openLocalPopoverCount = Math.max(0, openLocalPopoverCount - 1);
66352
66545
  }
@@ -89869,8 +90062,7 @@ const createSlot = (SlotRenderer = Box) => {
89869
90062
  };
89870
90063
  const SlotFill = props => {
89871
90064
  const fillerRef = useRef();
89872
- filler = fillerRef;
89873
- slotPropsSignal.value = props;
90065
+ const routePage = useContext(RoutePageContext);
89874
90066
  useLayoutEffect(() => {
89875
90067
  return () => {
89876
90068
  // Within one diff preact mounts the newcomer before unmounting the
@@ -89883,6 +90075,26 @@ const createSlot = (SlotRenderer = Box) => {
89883
90075
  slotPropsSignal.value = null;
89884
90076
  };
89885
90077
  }, []);
90078
+ // In a page kept while leaving: out of the slot as if unmounted, since
90079
+ // the page arriving is what the slot must show — or nothing, when it
90080
+ // fills none.
90081
+ if (routePage && routePage.leavingSignal.value) {
90082
+ if (filler === fillerRef) {
90083
+ filler = null;
90084
+ slotPropsSignal.value = null;
90085
+ }
90086
+ return null;
90087
+ }
90088
+ if (!fillerRef.current) {
90089
+ fillerRef.current = true;
90090
+ filler = fillerRef;
90091
+ }
90092
+ if (filler === null) {
90093
+ filler = fillerRef;
90094
+ }
90095
+ if (filler === fillerRef) {
90096
+ slotPropsSignal.value = props;
90097
+ }
89886
90098
  return null;
89887
90099
  };
89888
90100
  return [Slot, SlotFill];