@jsenv/navi 0.29.24 → 0.29.26
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.
- package/README.md +3 -1
- package/dist/jsenv_navi.js +960 -343
- package/dist/jsenv_navi.js.map +117 -62
- package/docs/AI_INSTRUCTIONS.md +7 -3
- package/docs/drag_to_travel.md +24 -2
- package/docs/navigation.md +253 -0
- package/docs/popup_open.md +185 -0
- package/package.json +2 -2
package/dist/jsenv_navi.js
CHANGED
|
@@ -7,7 +7,7 @@ import { createContext, isValidElement, h, Fragment, toChildArray, render, optio
|
|
|
7
7
|
import { useContext, useLayoutEffect, useRef, useCallback, useState, useMemo, useId, useEffect, useErrorBoundary } from "preact/hooks";
|
|
8
8
|
import { jsx, jsxs, Fragment as Fragment$1 } from "preact/jsx-runtime";
|
|
9
9
|
import { computed, signal, effect, batch, useSignal } from "@preact/signals";
|
|
10
|
-
import { createPubSub, normalizeStyle, mergeOneStyle, getPositionedParent, dispatchInternalCustomEvent, dispatchCustomEvent, findEvent, mergeTwoStyles, normalizeStyles, resolveCSSSize, measureLongestVisualLineWidth, hasCSSSizeUnit, resolveOklchLightness, contrastColor, createIterableWeakSet, getElementSignature, createValueEffect, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, findFocusDelegateTarget, findFocusable, allowWheelThrough, dispatchPublicCustomEvent, resolveCSSColor, ELEMENT_SIZE_CHANGE, findSelfOrAncestorFixedPosition, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, applyNewPosition, createEventGroupLogger, closestOpenableAncestor, isAncestorOpen, observeAncestorOpenState, getAncestorOpenType, getKeyboardEventDefaultAction, chainEvent, activeElementSignal, parsePositionArea, snapToPixel, trapFocusInside, trapScrollInside, onAncestorReopen, createGroupTransitionController, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, watchWheelTravel, startDragToTravel, scrollRoomTowards, findBefore, findAfter, initFocusGroup, elementIsFocusable, scrollIntoViewScoped, getScrollContainer, canScroll, measureWidestChildRow, performTabNavigation, dragAfterIntent, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement, stringifyStyle as stringifyStyle$1 } from "@jsenv/dom";
|
|
10
|
+
import { createPubSub, normalizeStyle, mergeOneStyle, getPositionedParent, dispatchInternalCustomEvent, dispatchCustomEvent, findEvent, mergeTwoStyles, normalizeStyles, resolveCSSSize, measureLongestVisualLineWidth, hasCSSSizeUnit, resolveOklchLightness, contrastColor, createIterableWeakSet, getElementSignature, createValueEffect, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, findFocusDelegateTarget, findFocusable, allowWheelThrough, dispatchPublicCustomEvent, resolveCSSColor, ELEMENT_SIZE_CHANGE, findSelfOrAncestorFixedPosition, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, applyNewPosition, createEventGroupLogger, closestOpenableAncestor, isAncestorOpen, observeAncestorOpenState, getAncestorOpenType, getKeyboardEventDefaultAction, chainEvent, activeElementSignal, parsePositionArea, snapToPixel, trapFocusInside, trapScrollInside, onAncestorReopen, createGroupTransitionController, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, watchWheelTravel, startDragToTravel, scrollRoomTowards, findBefore, findAfter, initFocusGroup, elementIsFocusable, scrollIntoViewScoped, getScrollContainer, canScroll, measureWidestChildRow, performTabNavigation, wheelGestureIsTakenFrom, releaseWheelGesture, claimWheelGesture, dragAfterIntent, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement, stringifyStyle as stringifyStyle$1 } from "@jsenv/dom";
|
|
11
11
|
export { contrastColor, startDragToReorder } from "@jsenv/dom";
|
|
12
12
|
import { createValidity, parseDuration, durationContainsNaN, compareTwoDurations, durationToSeconds, durationToISOString } from "@jsenv/validity";
|
|
13
13
|
export { compareTwoDurations, durationContainsNaN, durationToHours, durationToISOString, durationToMinutes, durationToNumber, durationToSeconds, durationToString, parseDuration } from "@jsenv/validity";
|
|
@@ -25120,6 +25120,9 @@ const createOpenController = (
|
|
|
25120
25120
|
openEffectCleanup = null;
|
|
25121
25121
|
closeHandlers?.onClose?.(closeEvent);
|
|
25122
25122
|
closeHandlers = null;
|
|
25123
|
+
// Last: the close effects above are what starts the exit transition the
|
|
25124
|
+
// content must outlive (see popup_content_mount.js).
|
|
25125
|
+
controller.unmountContent?.();
|
|
25123
25126
|
};
|
|
25124
25127
|
const controller = {
|
|
25125
25128
|
opened: false,
|
|
@@ -25128,6 +25131,9 @@ const createOpenController = (
|
|
|
25128
25131
|
// content is still waiting for a first open to be built. Called below,
|
|
25129
25132
|
// before openEffect, so the popup measures and positions the real thing.
|
|
25130
25133
|
mountContent: null,
|
|
25134
|
+
// The counterpart, set only when the popup was told to throw its content
|
|
25135
|
+
// away on close (`unmountWhenClosed`). Called from performClose above.
|
|
25136
|
+
unmountContent: null,
|
|
25131
25137
|
open: (e, detail) => {
|
|
25132
25138
|
if (controller.opened || !controller.openEffect) {
|
|
25133
25139
|
return;
|
|
@@ -25407,7 +25413,166 @@ const flushSyncRendering = (fn) => {
|
|
|
25407
25413
|
};
|
|
25408
25414
|
|
|
25409
25415
|
/**
|
|
25410
|
-
*
|
|
25416
|
+
* Small, renderer-agnostic helpers shared by Popover and Dialog's own custom
|
|
25417
|
+
* (non-top-layer) renderers — operate on a plain DOM element, no knowledge
|
|
25418
|
+
* of which of the two owns it.
|
|
25419
|
+
*/
|
|
25420
|
+
|
|
25421
|
+
|
|
25422
|
+
/**
|
|
25423
|
+
* Calls `onSettled` once `el`'s current CSS transition is over — via
|
|
25424
|
+
* `transitionend`, with a safety `setTimeout` fallback matching the longest
|
|
25425
|
+
* `transition-duration`, in case nothing actually transitions or an event is
|
|
25426
|
+
* missed.
|
|
25427
|
+
*
|
|
25428
|
+
* Returns a "cancel" function, so a caller whose instance has been superseded
|
|
25429
|
+
* (a fresh open/close about to set its own state) can keep this stale one from
|
|
25430
|
+
* firing later. Cancelling only stops `onSettled`: undoing whatever the caller
|
|
25431
|
+
* did up front is that fresh call's business, not this one's.
|
|
25432
|
+
*/
|
|
25433
|
+
const whenTransitionSettles = (el, onSettled) => {
|
|
25434
|
+
let settled = false;
|
|
25435
|
+
const onTransitionEnd = (transitionEvent) => {
|
|
25436
|
+
if (transitionEvent.target === el) {
|
|
25437
|
+
finish();
|
|
25438
|
+
}
|
|
25439
|
+
};
|
|
25440
|
+
const stopWatching = () => {
|
|
25441
|
+
settled = true;
|
|
25442
|
+
el.removeEventListener("transitionend", onTransitionEnd);
|
|
25443
|
+
clearTimeout(safetyTimeoutId);
|
|
25444
|
+
};
|
|
25445
|
+
const finish = () => {
|
|
25446
|
+
if (settled) {
|
|
25447
|
+
return;
|
|
25448
|
+
}
|
|
25449
|
+
stopWatching();
|
|
25450
|
+
onSettled();
|
|
25451
|
+
};
|
|
25452
|
+
el.addEventListener("transitionend", onTransitionEnd);
|
|
25453
|
+
const durationsInSeconds = getComputedStyle(el)
|
|
25454
|
+
.transitionDuration.split(",")
|
|
25455
|
+
.map((value) => parseFloat(value) || 0);
|
|
25456
|
+
const longestDurationMs = Math.max(0, ...durationsInSeconds) * 1000;
|
|
25457
|
+
const safetyTimeoutId = setTimeout(finish, longestDurationMs + 50);
|
|
25458
|
+
return () => {
|
|
25459
|
+
if (settled) {
|
|
25460
|
+
return;
|
|
25461
|
+
}
|
|
25462
|
+
stopWatching();
|
|
25463
|
+
};
|
|
25464
|
+
};
|
|
25465
|
+
|
|
25466
|
+
/**
|
|
25467
|
+
* Disables pointer-events on `el` until its current CSS transition settles —
|
|
25468
|
+
* avoids the cursor changing/something becoming clickable while the popup is
|
|
25469
|
+
* still visually moving into or out of place.
|
|
25470
|
+
*
|
|
25471
|
+
* Returns whenTransitionSettles' own "cancel" function: it doesn't restore
|
|
25472
|
+
* pointer-events, since a fresh call for the next open/close is about to set
|
|
25473
|
+
* its own state.
|
|
25474
|
+
*/
|
|
25475
|
+
const suppressPointerEventsDuringTransition = (el) => {
|
|
25476
|
+
el.style.pointerEvents = "none";
|
|
25477
|
+
return whenTransitionSettles(el, () => {
|
|
25478
|
+
el.style.pointerEvents = "";
|
|
25479
|
+
});
|
|
25480
|
+
};
|
|
25481
|
+
|
|
25482
|
+
/**
|
|
25483
|
+
* Hides the backdrop, deferring until the browser's matching "click" fires
|
|
25484
|
+
* when `closeEvent` was triggered by a mousedown (see popover.jsx's top
|
|
25485
|
+
* comment for why) — same capture-phase-on-document pattern as
|
|
25486
|
+
* armSuppressNextOpenRequest in open_controller.js, which a plain timeout
|
|
25487
|
+
* can't safely replace: mouseup (and the click that follows it) can land an
|
|
25488
|
+
* arbitrarily long time after mousedown (the user is still holding the
|
|
25489
|
+
* button down), so a short timeout can fire first and hide the backdrop
|
|
25490
|
+
* before its own click ever arrives. A capture-phase listener on document
|
|
25491
|
+
* fires for every click regardless of what any bubble-phase handler does
|
|
25492
|
+
* downstream, so no fallback timer is needed.
|
|
25493
|
+
*
|
|
25494
|
+
* `hide` is the caller's own way to actually hide the backdrop
|
|
25495
|
+
* (`hidePopover()` for a top-layer backdrop, a plain `style.display = "none"`
|
|
25496
|
+
* for a plain div) — this helper only owns the mousedown/click timing.
|
|
25497
|
+
*
|
|
25498
|
+
* Returns a disarm function (or undefined if hidden immediately), so a
|
|
25499
|
+
* fresh open can cancel a pending hide it's about to make redundant.
|
|
25500
|
+
*/
|
|
25501
|
+
const armPointerDownOutsideClose = (closeEvent, hide) => {
|
|
25502
|
+
const mousedownEvent = findEvent(closeEvent, "mousedown");
|
|
25503
|
+
if (!mousedownEvent) {
|
|
25504
|
+
hide();
|
|
25505
|
+
return undefined;
|
|
25506
|
+
}
|
|
25507
|
+
const onClick = () => {
|
|
25508
|
+
document.removeEventListener("click", onClick, { capture: true });
|
|
25509
|
+
hide();
|
|
25510
|
+
};
|
|
25511
|
+
document.addEventListener("click", onClick, { capture: true });
|
|
25512
|
+
return () => {
|
|
25513
|
+
document.removeEventListener("click", onClick, { capture: true });
|
|
25514
|
+
};
|
|
25515
|
+
};
|
|
25516
|
+
|
|
25517
|
+
/**
|
|
25518
|
+
* Maps a positionArea y/x pair to a concrete `navi-animation` value (a
|
|
25519
|
+
* `prefix` plus a direction word), or `null` if both axes overlap the anchor
|
|
25520
|
+
* (no direction at all — that's `resolvedAnimationKind === "scaling"`
|
|
25521
|
+
* territory instead, see resolveAutoAnimationKind below).
|
|
25522
|
+
*
|
|
25523
|
+
* `prefix: "slide-from"` (used with no real anchor — Dialog always, Popover
|
|
25524
|
+
* when docked) keeps the word as the compass direction the popup comes
|
|
25525
|
+
* from: placed "top" (a point/corner), it slides in from the top.
|
|
25526
|
+
* `prefix: "expand"` (a real anchor, Popover-only) uses the motion/growth
|
|
25527
|
+
* direction instead, the opposite compass point: placed "top" of the
|
|
25528
|
+
* anchor, it moves/grows up, away from the anchor (which sits below it).
|
|
25529
|
+
*
|
|
25530
|
+
* "inset-*"/"center" contribute no direction on their axis either way.
|
|
25531
|
+
*/
|
|
25532
|
+
const resolveDirectionValue = (y, x, { prefix }) => {
|
|
25533
|
+
const yWord =
|
|
25534
|
+
y === "top"
|
|
25535
|
+
? prefix === "expand"
|
|
25536
|
+
? "up"
|
|
25537
|
+
: "top"
|
|
25538
|
+
: y === "bottom"
|
|
25539
|
+
? prefix === "expand"
|
|
25540
|
+
? "down"
|
|
25541
|
+
: "bottom"
|
|
25542
|
+
: null;
|
|
25543
|
+
const xWord = x === "left" ? "left" : x === "right" ? "right" : null;
|
|
25544
|
+
if (!yWord && !xWord) {
|
|
25545
|
+
return null;
|
|
25546
|
+
}
|
|
25547
|
+
return yWord && xWord
|
|
25548
|
+
? `${prefix}-${yWord}-${xWord}`
|
|
25549
|
+
: `${prefix}-${yWord || xWord}`;
|
|
25550
|
+
};
|
|
25551
|
+
|
|
25552
|
+
/**
|
|
25553
|
+
* Shared `animation="auto"`/`true` resolution: "scaling" reads best overall
|
|
25554
|
+
* — picked for any real anchor, or for a point/corner placed dead-center
|
|
25555
|
+
* (both positionArea axes overlapping — there's no sensible direction to
|
|
25556
|
+
* slide from in that case). "sliding" otherwise. `anchor` is `undefined`
|
|
25557
|
+
* for any no-anchor/docked case (Dialog always, Popover's own custom
|
|
25558
|
+
* renderer when there's no real anchor), so this collapses to "scaling"
|
|
25559
|
+
* there only for the dead-center case, "sliding" otherwise. The two
|
|
25560
|
+
* "overlapping" booleans below describe the *positionArea* itself (a bare
|
|
25561
|
+
* word vs. "inset-"/"center"), not anything about the anchor — they'd
|
|
25562
|
+
* mean exactly the same thing even with no anchor at all, since it's the
|
|
25563
|
+
* position strategy, not the anchor, that decides whether there's a
|
|
25564
|
+
* direction to slide from.
|
|
25565
|
+
*/
|
|
25566
|
+
const resolveAutoAnimationKind = (anchor, parsedPositionArea) => {
|
|
25567
|
+
const yIsOverlapping =
|
|
25568
|
+
parsedPositionArea.y !== "top" && parsedPositionArea.y !== "bottom";
|
|
25569
|
+
const xIsOverlapping =
|
|
25570
|
+
parsedPositionArea.x !== "left" && parsedPositionArea.x !== "right";
|
|
25571
|
+
return anchor || (yIsOverlapping && xIsOverlapping) ? "scaling" : "sliding";
|
|
25572
|
+
};
|
|
25573
|
+
|
|
25574
|
+
/**
|
|
25575
|
+
* When a popup builds what it holds, and when it throws it away.
|
|
25411
25576
|
*
|
|
25412
25577
|
* A closed popup shows nothing, focuses nothing, and answers nothing: what it
|
|
25413
25578
|
* holds is out of reach until it opens. Building that content at mount time
|
|
@@ -25430,12 +25595,18 @@ const flushSyncRendering = (fn) => {
|
|
|
25430
25595
|
* `mountWhenClosed` is for content something else depends on before any of
|
|
25431
25596
|
* this: a value the popup's owner reads off its own children, fields a form
|
|
25432
25597
|
* around it collects on submit, a size measured from outside.
|
|
25598
|
+
*
|
|
25599
|
+
* `unmountWhenClosed` is the opposite end: content that must be rebuilt from
|
|
25600
|
+
* scratch every time, because what it shows is read once at build time and can
|
|
25601
|
+
* change while the popup is closed — an uncontrolled field seeded from a
|
|
25602
|
+
* `defaultValue`, a form whose fresh state is its initial state.
|
|
25433
25603
|
*/
|
|
25434
25604
|
|
|
25435
25605
|
|
|
25436
25606
|
const usePopupContentMount = (
|
|
25437
25607
|
openController,
|
|
25438
|
-
|
|
25608
|
+
ref,
|
|
25609
|
+
{ children, mountWhenClosed, unmountWhenClosed },
|
|
25439
25610
|
) => {
|
|
25440
25611
|
const [contentMounted, setContentMounted] = useState(
|
|
25441
25612
|
() => Boolean(mountWhenClosed) || openController.opened,
|
|
@@ -25447,6 +25618,27 @@ const usePopupContentMount = (
|
|
|
25447
25618
|
setContentMounted(true);
|
|
25448
25619
|
});
|
|
25449
25620
|
};
|
|
25621
|
+
openController.unmountContent =
|
|
25622
|
+
unmountWhenClosed && !mountWhenClosed
|
|
25623
|
+
? () => {
|
|
25624
|
+
const element = ref?.current;
|
|
25625
|
+
if (!element) {
|
|
25626
|
+
setContentMounted(false);
|
|
25627
|
+
return;
|
|
25628
|
+
}
|
|
25629
|
+
// The popup is still on screen while it plays its exit transition;
|
|
25630
|
+
// emptying it right away would show that transition running on a
|
|
25631
|
+
// blank surface.
|
|
25632
|
+
whenTransitionSettles(element, () => {
|
|
25633
|
+
if (openController.opened) {
|
|
25634
|
+
// reopened while it was leaving — the content it holds is the
|
|
25635
|
+
// one that open just asked for
|
|
25636
|
+
return;
|
|
25637
|
+
}
|
|
25638
|
+
setContentMounted(false);
|
|
25639
|
+
});
|
|
25640
|
+
}
|
|
25641
|
+
: null;
|
|
25450
25642
|
useLayoutEffect(() => {
|
|
25451
25643
|
if (mountWhenClosed) {
|
|
25452
25644
|
setContentMounted(true);
|
|
@@ -25760,150 +25952,6 @@ const unfreezeSize = (el) => {
|
|
|
25760
25952
|
el.style.height = "";
|
|
25761
25953
|
};
|
|
25762
25954
|
|
|
25763
|
-
/**
|
|
25764
|
-
* Small, renderer-agnostic helpers shared by Popover and Dialog's own custom
|
|
25765
|
-
* (non-top-layer) renderers — operate on a plain DOM element, no knowledge
|
|
25766
|
-
* of which of the two owns it.
|
|
25767
|
-
*/
|
|
25768
|
-
|
|
25769
|
-
|
|
25770
|
-
/**
|
|
25771
|
-
* Disables pointer-events on `el` until its current CSS transition settles
|
|
25772
|
-
* (via `transitionend`, with a safety `setTimeout` fallback matching the
|
|
25773
|
-
* longest `transition-duration` in case nothing actually transitions or an
|
|
25774
|
-
* event is missed) — avoids the cursor changing/something becoming
|
|
25775
|
-
* clickable while the popup is still visually moving into or out of place.
|
|
25776
|
-
*
|
|
25777
|
-
* Returns a "cancel" function: doesn't restore pointer-events (a fresh call
|
|
25778
|
-
* for the next open/close is about to set its own state) — only prevents
|
|
25779
|
-
* this stale instance's `transitionend` listener/timeout from firing later
|
|
25780
|
-
* and clobbering that fresh state.
|
|
25781
|
-
*/
|
|
25782
|
-
const suppressPointerEventsDuringTransition = (el) => {
|
|
25783
|
-
el.style.pointerEvents = "none";
|
|
25784
|
-
let settled = false;
|
|
25785
|
-
const onTransitionEnd = (transitionEvent) => {
|
|
25786
|
-
if (transitionEvent.target === el) {
|
|
25787
|
-
finish();
|
|
25788
|
-
}
|
|
25789
|
-
};
|
|
25790
|
-
const finish = () => {
|
|
25791
|
-
if (settled) {
|
|
25792
|
-
return;
|
|
25793
|
-
}
|
|
25794
|
-
settled = true;
|
|
25795
|
-
el.style.pointerEvents = "";
|
|
25796
|
-
el.removeEventListener("transitionend", onTransitionEnd);
|
|
25797
|
-
clearTimeout(safetyTimeoutId);
|
|
25798
|
-
};
|
|
25799
|
-
el.addEventListener("transitionend", onTransitionEnd);
|
|
25800
|
-
const durationsInSeconds = getComputedStyle(el)
|
|
25801
|
-
.transitionDuration.split(",")
|
|
25802
|
-
.map((value) => parseFloat(value) || 0);
|
|
25803
|
-
const longestDurationMs = Math.max(0, ...durationsInSeconds) * 1000;
|
|
25804
|
-
const safetyTimeoutId = setTimeout(finish, longestDurationMs + 50);
|
|
25805
|
-
return () => {
|
|
25806
|
-
if (settled) {
|
|
25807
|
-
return;
|
|
25808
|
-
}
|
|
25809
|
-
settled = true;
|
|
25810
|
-
el.removeEventListener("transitionend", onTransitionEnd);
|
|
25811
|
-
clearTimeout(safetyTimeoutId);
|
|
25812
|
-
};
|
|
25813
|
-
};
|
|
25814
|
-
|
|
25815
|
-
/**
|
|
25816
|
-
* Hides the backdrop, deferring until the browser's matching "click" fires
|
|
25817
|
-
* when `closeEvent` was triggered by a mousedown (see popover.jsx's top
|
|
25818
|
-
* comment for why) — same capture-phase-on-document pattern as
|
|
25819
|
-
* armSuppressNextOpenRequest in open_controller.js, which a plain timeout
|
|
25820
|
-
* can't safely replace: mouseup (and the click that follows it) can land an
|
|
25821
|
-
* arbitrarily long time after mousedown (the user is still holding the
|
|
25822
|
-
* button down), so a short timeout can fire first and hide the backdrop
|
|
25823
|
-
* before its own click ever arrives. A capture-phase listener on document
|
|
25824
|
-
* fires for every click regardless of what any bubble-phase handler does
|
|
25825
|
-
* downstream, so no fallback timer is needed.
|
|
25826
|
-
*
|
|
25827
|
-
* `hide` is the caller's own way to actually hide the backdrop
|
|
25828
|
-
* (`hidePopover()` for a top-layer backdrop, a plain `style.display = "none"`
|
|
25829
|
-
* for a plain div) — this helper only owns the mousedown/click timing.
|
|
25830
|
-
*
|
|
25831
|
-
* Returns a disarm function (or undefined if hidden immediately), so a
|
|
25832
|
-
* fresh open can cancel a pending hide it's about to make redundant.
|
|
25833
|
-
*/
|
|
25834
|
-
const armPointerDownOutsideClose = (closeEvent, hide) => {
|
|
25835
|
-
const mousedownEvent = findEvent(closeEvent, "mousedown");
|
|
25836
|
-
if (!mousedownEvent) {
|
|
25837
|
-
hide();
|
|
25838
|
-
return undefined;
|
|
25839
|
-
}
|
|
25840
|
-
const onClick = () => {
|
|
25841
|
-
document.removeEventListener("click", onClick, { capture: true });
|
|
25842
|
-
hide();
|
|
25843
|
-
};
|
|
25844
|
-
document.addEventListener("click", onClick, { capture: true });
|
|
25845
|
-
return () => {
|
|
25846
|
-
document.removeEventListener("click", onClick, { capture: true });
|
|
25847
|
-
};
|
|
25848
|
-
};
|
|
25849
|
-
|
|
25850
|
-
/**
|
|
25851
|
-
* Maps a positionArea y/x pair to a concrete `navi-animation` value (a
|
|
25852
|
-
* `prefix` plus a direction word), or `null` if both axes overlap the anchor
|
|
25853
|
-
* (no direction at all — that's `resolvedAnimationKind === "scaling"`
|
|
25854
|
-
* territory instead, see resolveAutoAnimationKind below).
|
|
25855
|
-
*
|
|
25856
|
-
* `prefix: "slide-from"` (used with no real anchor — Dialog always, Popover
|
|
25857
|
-
* when docked) keeps the word as the compass direction the popup comes
|
|
25858
|
-
* from: placed "top" (a point/corner), it slides in from the top.
|
|
25859
|
-
* `prefix: "expand"` (a real anchor, Popover-only) uses the motion/growth
|
|
25860
|
-
* direction instead, the opposite compass point: placed "top" of the
|
|
25861
|
-
* anchor, it moves/grows up, away from the anchor (which sits below it).
|
|
25862
|
-
*
|
|
25863
|
-
* "inset-*"/"center" contribute no direction on their axis either way.
|
|
25864
|
-
*/
|
|
25865
|
-
const resolveDirectionValue = (y, x, { prefix }) => {
|
|
25866
|
-
const yWord =
|
|
25867
|
-
y === "top"
|
|
25868
|
-
? prefix === "expand"
|
|
25869
|
-
? "up"
|
|
25870
|
-
: "top"
|
|
25871
|
-
: y === "bottom"
|
|
25872
|
-
? prefix === "expand"
|
|
25873
|
-
? "down"
|
|
25874
|
-
: "bottom"
|
|
25875
|
-
: null;
|
|
25876
|
-
const xWord = x === "left" ? "left" : x === "right" ? "right" : null;
|
|
25877
|
-
if (!yWord && !xWord) {
|
|
25878
|
-
return null;
|
|
25879
|
-
}
|
|
25880
|
-
return yWord && xWord
|
|
25881
|
-
? `${prefix}-${yWord}-${xWord}`
|
|
25882
|
-
: `${prefix}-${yWord || xWord}`;
|
|
25883
|
-
};
|
|
25884
|
-
|
|
25885
|
-
/**
|
|
25886
|
-
* Shared `animation="auto"`/`true` resolution: "scaling" reads best overall
|
|
25887
|
-
* — picked for any real anchor, or for a point/corner placed dead-center
|
|
25888
|
-
* (both positionArea axes overlapping — there's no sensible direction to
|
|
25889
|
-
* slide from in that case). "sliding" otherwise. `anchor` is `undefined`
|
|
25890
|
-
* for any no-anchor/docked case (Dialog always, Popover's own custom
|
|
25891
|
-
* renderer when there's no real anchor), so this collapses to "scaling"
|
|
25892
|
-
* there only for the dead-center case, "sliding" otherwise. The two
|
|
25893
|
-
* "overlapping" booleans below describe the *positionArea* itself (a bare
|
|
25894
|
-
* word vs. "inset-"/"center"), not anything about the anchor — they'd
|
|
25895
|
-
* mean exactly the same thing even with no anchor at all, since it's the
|
|
25896
|
-
* position strategy, not the anchor, that decides whether there's a
|
|
25897
|
-
* direction to slide from.
|
|
25898
|
-
*/
|
|
25899
|
-
const resolveAutoAnimationKind = (anchor, parsedPositionArea) => {
|
|
25900
|
-
const yIsOverlapping =
|
|
25901
|
-
parsedPositionArea.y !== "top" && parsedPositionArea.y !== "bottom";
|
|
25902
|
-
const xIsOverlapping =
|
|
25903
|
-
parsedPositionArea.x !== "left" && parsedPositionArea.x !== "right";
|
|
25904
|
-
return anchor || (yIsOverlapping && xIsOverlapping) ? "scaling" : "sliding";
|
|
25905
|
-
};
|
|
25906
|
-
|
|
25907
25955
|
installImportMetaCssBuild(import.meta);/**
|
|
25908
25956
|
* A dialog is a surface, not a control: it holds no value, has no action and
|
|
25909
25957
|
* aggregates nothing. Fields and a submit go in a `<Form>` inside it, exactly
|
|
@@ -26384,6 +26432,11 @@ const css$V = /* css */`
|
|
|
26384
26432
|
* content something depends on while the popup is still closed: a value read
|
|
26385
26433
|
* off it, fields a surrounding form collects on submit, a size measured from
|
|
26386
26434
|
* outside.
|
|
26435
|
+
* @param {boolean} [props.unmountWhenClosed] - Throws `children` away once the
|
|
26436
|
+
* popup has finished closing (see popup_content_mount.js). For content whose
|
|
26437
|
+
* fresh state is its initial state: an uncontrolled field seeded from a
|
|
26438
|
+
* `defaultValue` that changed while the popup was closed. Ignored when
|
|
26439
|
+
* `mountWhenClosed` is set.
|
|
26387
26440
|
* @param {import("ignore:preact").ComponentChildren} props.children
|
|
26388
26441
|
*/
|
|
26389
26442
|
const Dialog = props => {
|
|
@@ -26580,11 +26633,13 @@ const useDialogProps = props => {
|
|
|
26580
26633
|
onKeyDown,
|
|
26581
26634
|
children: childrenProp,
|
|
26582
26635
|
mountWhenClosed,
|
|
26636
|
+
unmountWhenClosed,
|
|
26583
26637
|
...rest
|
|
26584
26638
|
} = props;
|
|
26585
|
-
const children = usePopupContentMount(openController, {
|
|
26639
|
+
const children = usePopupContentMount(openController, props.ref, {
|
|
26586
26640
|
children: childrenProp,
|
|
26587
|
-
mountWhenClosed
|
|
26641
|
+
mountWhenClosed,
|
|
26642
|
+
unmountWhenClosed
|
|
26588
26643
|
});
|
|
26589
26644
|
const isModal = layer === "top";
|
|
26590
26645
|
const ref = props.ref;
|
|
@@ -27632,6 +27687,11 @@ const css$U = /* css */`
|
|
|
27632
27687
|
* content something depends on while the popup is still closed: a value read
|
|
27633
27688
|
* off it, fields a surrounding form collects on submit, a size measured from
|
|
27634
27689
|
* outside.
|
|
27690
|
+
* @param {boolean} [props.unmountWhenClosed] - Throws `children` away once the
|
|
27691
|
+
* popup has finished closing (see popup_content_mount.js). For content whose
|
|
27692
|
+
* fresh state is its initial state: an uncontrolled field seeded from a
|
|
27693
|
+
* `defaultValue` that changed while the popup was closed. Ignored when
|
|
27694
|
+
* `mountWhenClosed` is set.
|
|
27635
27695
|
* @param {import("ignore:preact").ComponentChildren} props.children
|
|
27636
27696
|
*/
|
|
27637
27697
|
const Popover = props => {
|
|
@@ -27821,11 +27881,13 @@ const usePopoverProps = props => {
|
|
|
27821
27881
|
onKeyDown,
|
|
27822
27882
|
children: childrenProp,
|
|
27823
27883
|
mountWhenClosed,
|
|
27884
|
+
unmountWhenClosed,
|
|
27824
27885
|
...rest
|
|
27825
27886
|
} = props;
|
|
27826
|
-
const children = usePopupContentMount(openController, {
|
|
27887
|
+
const children = usePopupContentMount(openController, props.ref, {
|
|
27827
27888
|
children: childrenProp,
|
|
27828
|
-
mountWhenClosed
|
|
27889
|
+
mountWhenClosed,
|
|
27890
|
+
unmountWhenClosed
|
|
27829
27891
|
});
|
|
27830
27892
|
const isTopLayer = layer === "top";
|
|
27831
27893
|
const ref = props.ref;
|
|
@@ -34592,17 +34654,25 @@ const Route = props => {
|
|
|
34592
34654
|
});
|
|
34593
34655
|
};
|
|
34594
34656
|
/**
|
|
34595
|
-
* The
|
|
34657
|
+
* The pages a tree of <Route> children is made of, in the order they are
|
|
34596
34658
|
* written. Reading them is what turns a router into a row one can walk: "one
|
|
34597
34659
|
* step that way" is a fact about the order the branches were declared in, and
|
|
34598
34660
|
* nothing in a URL says it.
|
|
34599
34661
|
*
|
|
34662
|
+
* A page is `{ route, params }`, never the route alone: a section of a page is
|
|
34663
|
+
* as often a PARAM as it is a route of its own — `<Route route={PAGE}
|
|
34664
|
+
* routeParams={{ section: "done" }}>` is how this very file selects a branch on
|
|
34665
|
+
* one — and three branches of the same route are then the same object three
|
|
34666
|
+
* times. Told apart by their params, they are three pages one walks between;
|
|
34667
|
+
* told apart by identity, they are one page and there is nowhere to walk.
|
|
34668
|
+
* `params` is undefined for a branch that is a route on its own.
|
|
34669
|
+
*
|
|
34600
34670
|
* The same walk the container does to find the active branch (collectBranches),
|
|
34601
34671
|
* except that it keeps every leaf rather than the one that matches — and reads
|
|
34602
34672
|
* no signal, so asking does not subscribe the asker to anything.
|
|
34603
34673
|
*/
|
|
34604
|
-
const
|
|
34605
|
-
const
|
|
34674
|
+
const collectRoutePages = children => {
|
|
34675
|
+
const pages = [];
|
|
34606
34676
|
const visit = child => {
|
|
34607
34677
|
if (!child || child === true || child === false) {
|
|
34608
34678
|
return;
|
|
@@ -34618,18 +34688,22 @@ const collectRoutes = children => {
|
|
|
34618
34688
|
}
|
|
34619
34689
|
const {
|
|
34620
34690
|
children: nodeChildren,
|
|
34621
|
-
route
|
|
34691
|
+
route,
|
|
34692
|
+
routeParams
|
|
34622
34693
|
} = child.props;
|
|
34623
34694
|
if (nodeChildren) {
|
|
34624
34695
|
visit(nodeChildren);
|
|
34625
34696
|
return;
|
|
34626
34697
|
}
|
|
34627
34698
|
if (route) {
|
|
34628
|
-
|
|
34699
|
+
pages.push({
|
|
34700
|
+
route,
|
|
34701
|
+
params: routeParams
|
|
34702
|
+
});
|
|
34629
34703
|
}
|
|
34630
34704
|
};
|
|
34631
34705
|
visit(children);
|
|
34632
|
-
return
|
|
34706
|
+
return pages;
|
|
34633
34707
|
};
|
|
34634
34708
|
|
|
34635
34709
|
// RouteContainer: traverses children statically per render, finds the active branch,
|
|
@@ -34835,14 +34909,19 @@ const DRAGGED_ATTRIBUTE = "data-navi-route-travel-dragged";
|
|
|
34835
34909
|
// transition carries was measured once, at the start, against a destination
|
|
34836
34910
|
// this travel is no longer going to.
|
|
34837
34911
|
const TURNED_ATTRIBUTE = "data-navi-route-travel-turned";
|
|
34912
|
+
// The name the box wears while it travels, and only then (see nameForTravel).
|
|
34913
|
+
const TRAVEL_NAME = "navi-route-travel";
|
|
34838
34914
|
const css$R = /* css */`
|
|
34915
|
+
/* The name that makes the page inside this box a picture of its own during a
|
|
34916
|
+
transition — rather than part of the one big picture the document takes, so
|
|
34917
|
+
the two pages can move past each other while everything else stays where it
|
|
34918
|
+
is — is not written here: it is worn only for the length of a travel (see
|
|
34919
|
+
nameForTravel). A name belongs to ONE element at a time, and a page can hold
|
|
34920
|
+
several of these boxes at once — a section of the url and a search param of
|
|
34921
|
+
the root route are two rows of tabs, both live, and only one of them is ever
|
|
34922
|
+
travelling. */
|
|
34839
34923
|
.navi_route_travel {
|
|
34840
34924
|
position: relative;
|
|
34841
|
-
/* Named, so the page inside this box is a picture of its own during a
|
|
34842
|
-
transition rather than part of the one big picture the document takes:
|
|
34843
|
-
the two pages can then move past each other while everything else stays
|
|
34844
|
-
where it is. */
|
|
34845
|
-
view-transition-name: navi-route-travel;
|
|
34846
34925
|
/* The gesture takes the axis the pages travel on and leaves the other one
|
|
34847
34926
|
to the page, so a list still scrolls under the same finger. */
|
|
34848
34927
|
touch-action: pan-y;
|
|
@@ -34884,7 +34963,14 @@ const css$R = /* css */`
|
|
|
34884
34963
|
|
|
34885
34964
|
&::view-transition-old(navi-route-travel),
|
|
34886
34965
|
&::view-transition-new(navi-route-travel) {
|
|
34887
|
-
|
|
34966
|
+
/* Each picture at the size it was taken at: a page is not resized by the
|
|
34967
|
+
page it crosses. Told to fill a box whose height is being animated, a
|
|
34968
|
+
picture is STRETCHED with it — the page leaving is then seen squashing
|
|
34969
|
+
upwards, or zooming, over the length of the travel, when all it is
|
|
34970
|
+
doing is walking off the edge. */
|
|
34971
|
+
height: auto;
|
|
34972
|
+
object-fit: none;
|
|
34973
|
+
object-position: top left;
|
|
34888
34974
|
/* The default cross-fade, dropped: two pages sliding past each other are
|
|
34889
34975
|
two solid things, and seeing through one to the other says they are the
|
|
34890
34976
|
same page changing its mind. */
|
|
@@ -34900,7 +34986,21 @@ const css$R = /* css */`
|
|
|
34900
34986
|
overflow: clip;
|
|
34901
34987
|
}
|
|
34902
34988
|
&::view-transition-group(navi-route-travel) {
|
|
34989
|
+
/* The window the two pictures are seen through, held still for the whole
|
|
34990
|
+
travel at the taller of the two boxes (see holdTravelHeight): the group
|
|
34991
|
+
is what CLIPS, and the browser animates its height from the box being
|
|
34992
|
+
left to the box arriving — so the window shrinks under the pictures and
|
|
34993
|
+
cuts the page leaving from the bottom, progressively. The box does end
|
|
34994
|
+
up at the arriving page's height, and that is right; what must not
|
|
34995
|
+
happen is the user watching it get there.
|
|
34996
|
+
|
|
34997
|
+
The height is held by dropping the group's animation rather than by
|
|
34998
|
+
winning against it with !important — which also drops its position
|
|
34999
|
+
animation, fine while a travel box stands in the same place from one
|
|
35000
|
+
route to the next. */
|
|
35001
|
+
height: var(--navi-route-travel-height);
|
|
34903
35002
|
animation-duration: var(--navi-route-travel-duration, 300ms);
|
|
35003
|
+
animation-name: none;
|
|
34904
35004
|
}
|
|
34905
35005
|
}
|
|
34906
35006
|
|
|
@@ -35019,21 +35119,30 @@ const css$R = /* css */`
|
|
|
35019
35119
|
|
|
35020
35120
|
/**
|
|
35021
35121
|
* @type {import("ignore:preact").FunctionComponent<{
|
|
35022
|
-
* routes?: Array<object>,
|
|
35122
|
+
* routes?: Array<object|{route: object, params?: object}>,
|
|
35023
35123
|
* axis?: "x"|"y",
|
|
35024
35124
|
* travelByDrag?: boolean,
|
|
35025
|
-
* onTravel?: (detail: {route: object, cause: string}) => void|Promise<void>,
|
|
35125
|
+
* onTravel?: (detail: {route: object, params: object|undefined, cause: string}) => void|Promise<void>,
|
|
35026
35126
|
* }>}
|
|
35027
|
-
* @param {Array<object>} [props.routes] - the
|
|
35028
|
-
* Read from the <Route> children by
|
|
35029
|
-
* the router already holds that list,
|
|
35030
|
-
* asking
|
|
35031
|
-
* pages are not children of this box
|
|
35127
|
+
* @param {Array<object|{route: object, params?: object}>} [props.routes] - the
|
|
35128
|
+
* tabs, in the order they are shown. Read from the <Route> children by
|
|
35129
|
+
* default, in the order they are written: the router already holds that list,
|
|
35130
|
+
* and asking a caller to write it twice is asking for the two to disagree.
|
|
35131
|
+
* Pass it to say another order, when the pages are not children of this box,
|
|
35132
|
+
* or to name a tab the children cannot — the section a <Route fallback> shows
|
|
35133
|
+
* is a tab like the others, and only its params say which one.
|
|
35134
|
+
*
|
|
35135
|
+
* An entry is a route, or `{ route, params }` when the tabs of the row are a
|
|
35136
|
+
* PARAM of one route rather than routes of their own (the form
|
|
35137
|
+
* `<Route routeParams>` selects a branch on, and the form that lets a link
|
|
35138
|
+
* with no params reopen the section one was looking at). Written as bare
|
|
35139
|
+
* routes, three tabs of one route are the same object three times: there is
|
|
35140
|
+
* then one tab, and nowhere to travel.
|
|
35032
35141
|
* @param {"x"|"y"} [props.axis="x"] - which way the pages are laid out.
|
|
35033
35142
|
* @param {boolean} [props.travelByDrag=true] - whether a pointer dragging the
|
|
35034
35143
|
* page travels. Off where the gesture belongs to the content.
|
|
35035
|
-
* @param {(detail: {route: object, cause: "drag"|"wheel"|"revert"}) => void|Promise<void>} [props.onTravel]
|
|
35036
|
-
* - how to go to a
|
|
35144
|
+
* @param {(detail: {route: object, params: object|undefined, cause: "drag"|"wheel"|"revert"}) => void|Promise<void>} [props.onTravel]
|
|
35145
|
+
* - how to go to a tab. The default REPLACES the current history entry
|
|
35037
35146
|
* rather than pushing one: a swipe is how one browses a page, not a place one
|
|
35038
35147
|
* aimed at, and three swipes back and forth must not bury the way out of the
|
|
35039
35148
|
* page under six entries. A tab pressed is the other case and pushes, which
|
|
@@ -35056,8 +35165,9 @@ const RouteTravel = ({
|
|
|
35056
35165
|
axis = "x",
|
|
35057
35166
|
travelByDrag = true,
|
|
35058
35167
|
onTravel = ({
|
|
35059
|
-
route
|
|
35060
|
-
|
|
35168
|
+
route,
|
|
35169
|
+
params
|
|
35170
|
+
}) => route.redirectTo(params),
|
|
35061
35171
|
className,
|
|
35062
35172
|
children,
|
|
35063
35173
|
...rest
|
|
@@ -35069,51 +35179,58 @@ const RouteTravel = ({
|
|
|
35069
35179
|
// left, the animations the finger drives, and what to do with them once the
|
|
35070
35180
|
// browser has them ready. Null when no page is on its way anywhere.
|
|
35071
35181
|
const travelRef = useRef(null);
|
|
35072
|
-
// The
|
|
35182
|
+
// The page this box has ASKED for and is still waiting to see arrive.
|
|
35073
35183
|
// Routing is asynchronous: a travel's own navigation lands well after the
|
|
35074
35184
|
// travel decided anything about it — sometimes after the travel was undone —
|
|
35075
|
-
// and read back as "the
|
|
35185
|
+
// and read back as "the page changed" it would start a second travel nobody
|
|
35076
35186
|
// asked for, over pictures that are already showing something else.
|
|
35077
|
-
const
|
|
35187
|
+
const pageAskedForRef = useRef(null);
|
|
35078
35188
|
// What a press stopped in flight, until the gesture says what it is about.
|
|
35079
35189
|
const caughtAtPressRef = useRef(null);
|
|
35080
35190
|
// The latest way to answer a gesture, for a watcher that outlives every
|
|
35081
35191
|
// render (see the wheel effect below).
|
|
35082
35192
|
const travelHandlersRef = useRef(null);
|
|
35083
35193
|
const pointerDownRef = useRef(null);
|
|
35084
|
-
const
|
|
35085
|
-
const
|
|
35086
|
-
|
|
35087
|
-
|
|
35088
|
-
// is
|
|
35089
|
-
|
|
35090
|
-
for
|
|
35091
|
-
|
|
35092
|
-
|
|
35093
|
-
}
|
|
35094
|
-
}
|
|
35194
|
+
const pagesFromChildren = useMemo(() => collectRoutePages(children), [children]);
|
|
35195
|
+
const pagesFromProp = useMemo(() => routesProp && routesProp.map(normalizePage), [routesProp]);
|
|
35196
|
+
const pages = pagesFromProp || pagesFromChildren;
|
|
35197
|
+
|
|
35198
|
+
// Which page is on screen, read from the pages themselves: every one of them
|
|
35199
|
+
// is read, so this re-renders when any of them starts or stops matching — and
|
|
35200
|
+
// for a row whose tabs are params of one route, when the params move from one
|
|
35201
|
+
// tab to the next (see pageIsCurrent).
|
|
35202
|
+
const currentIndex = currentPageIndex(pages);
|
|
35095
35203
|
// The page that was on screen when the change now happening was asked for:
|
|
35096
35204
|
// a travel is between two of them, and by the time anything renders the first
|
|
35097
35205
|
// one is already gone. Written after each render (below), so a subscriber
|
|
35098
35206
|
// reading it — they all run before Preact flushes — reads the one being left.
|
|
35099
35207
|
const currentIndexRef = useRef(currentIndex);
|
|
35100
35208
|
|
|
35209
|
+
// Where a page is asked for, whoever asks: a page is a route AND the params
|
|
35210
|
+
// that say which of its tabs, and a caller told only the route would send the
|
|
35211
|
+
// row back to whichever tab the URL already says (see redirectTo).
|
|
35212
|
+
const travelTo = (page, cause) => onTravel({
|
|
35213
|
+
route: page.route,
|
|
35214
|
+
params: page.params,
|
|
35215
|
+
cause
|
|
35216
|
+
});
|
|
35217
|
+
|
|
35101
35218
|
// One travel, whoever asked for it: a finger, a tab pressed, the browser's
|
|
35102
35219
|
// own back button. What differs is only who moves it — the finger drives it
|
|
35103
35220
|
// frame by frame (`scrub`), everything else lets it play.
|
|
35104
35221
|
const beginTravel = ({
|
|
35105
|
-
|
|
35106
|
-
|
|
35222
|
+
page,
|
|
35223
|
+
fromPage,
|
|
35107
35224
|
direction,
|
|
35108
35225
|
scrub,
|
|
35109
35226
|
change
|
|
35110
35227
|
}) => {
|
|
35111
35228
|
const travel = {
|
|
35112
|
-
|
|
35229
|
+
page,
|
|
35113
35230
|
// The page this set off from, kept rather than looked up again: the URL
|
|
35114
35231
|
// changes at the first pixel, so a moment later nothing on screen
|
|
35115
35232
|
// remembers where it started.
|
|
35116
|
-
|
|
35233
|
+
fromPage,
|
|
35117
35234
|
direction,
|
|
35118
35235
|
scrub,
|
|
35119
35236
|
ratio: 0,
|
|
@@ -35124,24 +35241,36 @@ const RouteTravel = ({
|
|
|
35124
35241
|
ended: false
|
|
35125
35242
|
};
|
|
35126
35243
|
travelRef.current = travel;
|
|
35244
|
+
// Taken before the picture is: the browser reads the name off the DOM as it
|
|
35245
|
+
// stands when the transition starts, and this box is only a picture of its
|
|
35246
|
+
// own for as long as it is the one travelling.
|
|
35247
|
+
nameForTravel(elementRef.current);
|
|
35127
35248
|
document.documentElement.setAttribute(TRAVEL_ATTRIBUTE, direction);
|
|
35128
35249
|
if (scrub) {
|
|
35129
35250
|
holdPictures(travel);
|
|
35130
35251
|
document.documentElement.setAttribute(DRAGGED_ATTRIBUTE, "");
|
|
35131
35252
|
}
|
|
35132
|
-
|
|
35253
|
+
pageAskedForRef.current = page;
|
|
35254
|
+
// The box as it stands before anything moves: rendering is held, so this is
|
|
35255
|
+
// still the page being left (see holdTravelHeight).
|
|
35256
|
+
const heightBefore = elementRef.current.getBoundingClientRect().height;
|
|
35133
35257
|
// The hold a navigation already took, if this travel is the answer to one:
|
|
35134
35258
|
// taking another would be taking a hold on a page that is holding still.
|
|
35135
35259
|
const releaseRendering = renderingHeldForRouting || holdRendering();
|
|
35136
35260
|
renderingHeldForRouting = null;
|
|
35137
35261
|
// The picture the browser is about to take must be of the page that was
|
|
35138
35262
|
// asked for, and a route matching is not yet a page rendered.
|
|
35139
|
-
const viewTransition = startViewTransition(
|
|
35140
|
-
|
|
35141
|
-
|
|
35142
|
-
|
|
35143
|
-
|
|
35144
|
-
|
|
35263
|
+
const viewTransition = startViewTransition(async () => {
|
|
35264
|
+
await whilePageRenders(page, async () => {
|
|
35265
|
+
releaseRendering();
|
|
35266
|
+
if (change) {
|
|
35267
|
+
await change();
|
|
35268
|
+
}
|
|
35269
|
+
});
|
|
35270
|
+
// The page arriving is in the DOM and the transition has not started
|
|
35271
|
+
// playing: the one moment both boxes can be known.
|
|
35272
|
+
holdTravelHeight(elementRef.current, heightBefore);
|
|
35273
|
+
});
|
|
35145
35274
|
travel.viewTransition = viewTransition;
|
|
35146
35275
|
if (scrub) {
|
|
35147
35276
|
// Said only now: the release has to have something to let go of, and the
|
|
@@ -35185,21 +35314,41 @@ const RouteTravel = ({
|
|
|
35185
35314
|
};
|
|
35186
35315
|
|
|
35187
35316
|
// A page change nobody here asked for: a tab pressed, a key, the back button.
|
|
35188
|
-
// The transition is started from the
|
|
35189
|
-
//
|
|
35190
|
-
//
|
|
35317
|
+
// The transition is started from what the router SAYS rather than from a
|
|
35318
|
+
// render, because a render is one flush too late — by then the DOM holds the
|
|
35319
|
+
// new page and the picture of the old one cannot be taken anymore.
|
|
35320
|
+
//
|
|
35321
|
+
// Watched as a position in the row rather than route by route. A route
|
|
35322
|
+
// announces its own status, and the row's tabs can all be one route: the
|
|
35323
|
+
// announcement then says a section changed without saying which is on screen,
|
|
35324
|
+
// and it says it about things this row does not move for (a route that has
|
|
35325
|
+
// now been visited, a param of its own that is not a tab of this row). Worse,
|
|
35326
|
+
// a status is published from inside the routing and the params it carries are
|
|
35327
|
+
// the ones known at that instant — a section that lands as a signal settles
|
|
35328
|
+
// is announced late, or not at all. The signals ARE the position, so the
|
|
35329
|
+
// position is read from them: one computed over the whole row, notified once
|
|
35330
|
+
// per move, whichever route moved and whether by matching or by params.
|
|
35191
35331
|
useLayoutEffect(() => {
|
|
35192
|
-
const
|
|
35193
|
-
|
|
35194
|
-
|
|
35195
|
-
if (!matching) {
|
|
35332
|
+
const currentIndexSignal = computed(() => currentPageIndex(pages));
|
|
35333
|
+
const onRowMove = index => {
|
|
35334
|
+
if (index === -1) {
|
|
35196
35335
|
return;
|
|
35197
35336
|
}
|
|
35337
|
+
if (index === currentIndexRef.current) {
|
|
35338
|
+
// Where the row already was — the first reading of all, and any move
|
|
35339
|
+
// this box has already taken note of (a render writes it too). What it
|
|
35340
|
+
// was still waiting for is here nonetheless, so the wait is called off.
|
|
35341
|
+
if (samePage(pageAskedForRef.current, pages[index])) {
|
|
35342
|
+
pageAskedForRef.current = null;
|
|
35343
|
+
}
|
|
35344
|
+
return;
|
|
35345
|
+
}
|
|
35346
|
+
const page = pages[index];
|
|
35198
35347
|
// A page this box asked for itself — a travel's own navigation, or one
|
|
35199
35348
|
// it had given up waiting on: what arrives here is the answer to a
|
|
35200
35349
|
// question already answered, not somebody going somewhere.
|
|
35201
|
-
if (
|
|
35202
|
-
|
|
35350
|
+
if (samePage(pageAskedForRef.current, page)) {
|
|
35351
|
+
pageAskedForRef.current = null;
|
|
35203
35352
|
currentIndexRef.current = index;
|
|
35204
35353
|
return;
|
|
35205
35354
|
}
|
|
@@ -35207,13 +35356,13 @@ const RouteTravel = ({
|
|
|
35207
35356
|
// is not coming, or no longer means anything. Forgotten here rather
|
|
35208
35357
|
// than kept, or the next press on that very tab would be taken for the
|
|
35209
35358
|
// late answer to a question nobody remembers asking.
|
|
35210
|
-
|
|
35359
|
+
pageAskedForRef.current = null;
|
|
35211
35360
|
// Asked for a page while one was already on its way: the travel in
|
|
35212
35361
|
// flight is the answer, aimed somewhere else. Starting a second one on
|
|
35213
35362
|
// top would leave this one's pictures to be dropped mid-slide.
|
|
35214
35363
|
if (travelRef.current) {
|
|
35215
35364
|
currentIndexRef.current = index;
|
|
35216
|
-
retargetTravel(travelRef.current,
|
|
35365
|
+
retargetTravel(travelRef.current, page);
|
|
35217
35366
|
return;
|
|
35218
35367
|
}
|
|
35219
35368
|
const fromIndex = currentIndexRef.current;
|
|
@@ -35224,18 +35373,18 @@ const RouteTravel = ({
|
|
|
35224
35373
|
return;
|
|
35225
35374
|
}
|
|
35226
35375
|
beginTravel({
|
|
35227
|
-
|
|
35228
|
-
|
|
35376
|
+
page,
|
|
35377
|
+
fromPage: pages[fromIndex],
|
|
35229
35378
|
direction: index > fromIndex ? "forward" : "back",
|
|
35230
35379
|
scrub: false
|
|
35231
35380
|
});
|
|
35232
|
-
}));
|
|
35233
|
-
return () => {
|
|
35234
|
-
for (const unsubscribe of unsubscribes) {
|
|
35235
|
-
unsubscribe();
|
|
35236
|
-
}
|
|
35237
35381
|
};
|
|
35238
|
-
|
|
35382
|
+
// `subscribe` rather than `effect`: it hands the value to a callback that
|
|
35383
|
+
// is NOT being tracked, and what this one does — navigate, ask the router
|
|
35384
|
+
// for another page — reads and writes the very signals the row is watched
|
|
35385
|
+
// through.
|
|
35386
|
+
return currentIndexSignal.subscribe(onRowMove);
|
|
35387
|
+
}, [pages]);
|
|
35239
35388
|
|
|
35240
35389
|
// Rendering is held for the length of a navigation, so that whatever picture
|
|
35241
35390
|
// this box is about to take is of the page being LEFT (see holdRendering).
|
|
@@ -35272,6 +35421,13 @@ const RouteTravel = ({
|
|
|
35272
35421
|
// Let go of far enough: the movement carries on from under the finger, at its
|
|
35273
35422
|
// own pace, to the end.
|
|
35274
35423
|
const finishTravel = travel => {
|
|
35424
|
+
// Nobody is driving it anymore. `scrub` is who MOVES the pictures, not what
|
|
35425
|
+
// set them off: left standing after the release, this travel would go on
|
|
35426
|
+
// claiming a hand that is no longer there — and everything that asks
|
|
35427
|
+
// "is somebody holding this?" before touching it (a press on the tab one
|
|
35428
|
+
// came from, a wheel push) would be answered yes and do nothing, while the
|
|
35429
|
+
// pages carry on to a page the router has already left.
|
|
35430
|
+
travel.scrub = false;
|
|
35275
35431
|
releaseHold();
|
|
35276
35432
|
travel.viewTransition.finished.then(() => endTravel(travel), () => endTravel(travel));
|
|
35277
35433
|
};
|
|
@@ -35324,19 +35480,20 @@ const RouteTravel = ({
|
|
|
35324
35480
|
Promise.resolve();
|
|
35325
35481
|
backAtTheStart.then(async () => {
|
|
35326
35482
|
try {
|
|
35327
|
-
|
|
35483
|
+
pageAskedForRef.current = travel.fromPage;
|
|
35328
35484
|
// The page that was left is put back UNDER the picture before the
|
|
35329
35485
|
// picture is dropped, so the two are the same thing at the moment they
|
|
35330
35486
|
// are swapped: that only holds once the page is really back.
|
|
35331
|
-
if (travel.
|
|
35487
|
+
if (pageIsCurrent(travel.fromPage)) {
|
|
35332
35488
|
// It never left: the press that set this revert off put it back
|
|
35333
35489
|
// there, and the pages have been held where they were until now.
|
|
35334
35490
|
// Nothing to ask for, and nothing to wait for — waiting anyway is a
|
|
35335
35491
|
// render that never comes and a page frozen under its own pictures.
|
|
35336
35492
|
releaseRendering();
|
|
35337
35493
|
} else {
|
|
35338
|
-
await
|
|
35339
|
-
route: travel.
|
|
35494
|
+
await whilePageRenders(travel.fromPage, () => onTravel({
|
|
35495
|
+
route: travel.fromPage.route,
|
|
35496
|
+
params: travel.fromPage.params,
|
|
35340
35497
|
cause: "revert"
|
|
35341
35498
|
}));
|
|
35342
35499
|
}
|
|
@@ -35375,8 +35532,8 @@ const RouteTravel = ({
|
|
|
35375
35532
|
// change — only what is being brought in against it, and that one is LIVE:
|
|
35376
35533
|
// pointing the router elsewhere is all it takes for the picture to show that
|
|
35377
35534
|
// page instead.
|
|
35378
|
-
const redirectTravel = (travel,
|
|
35379
|
-
travel.
|
|
35535
|
+
const redirectTravel = (travel, page, direction) => {
|
|
35536
|
+
travel.page = page;
|
|
35380
35537
|
// Everything the transition carries that is NOT the pages was measured
|
|
35381
35538
|
// against a destination this travel is no longer going to (see the CSS).
|
|
35382
35539
|
document.documentElement.setAttribute(TURNED_ATTRIBUTE, "");
|
|
@@ -35397,28 +35554,28 @@ const RouteTravel = ({
|
|
|
35397
35554
|
|
|
35398
35555
|
// Somebody asked for a page while one was on its way. Where they asked for
|
|
35399
35556
|
// decides what that means.
|
|
35400
|
-
const retargetTravel = (travel,
|
|
35557
|
+
const retargetTravel = (travel, page) => {
|
|
35401
35558
|
if (travel.scrub || travel.reverting || travel.ended || travel.noPicture) {
|
|
35402
35559
|
// A hand is holding the pages, or they are already on their way back:
|
|
35403
35560
|
// either way this travel's end is decided by somebody else.
|
|
35404
35561
|
return;
|
|
35405
35562
|
}
|
|
35406
|
-
if (
|
|
35563
|
+
if (samePage(page, travel.page)) {
|
|
35407
35564
|
// Already on its way there.
|
|
35408
35565
|
return;
|
|
35409
35566
|
}
|
|
35410
|
-
if (
|
|
35567
|
+
if (samePage(page, travel.fromPage)) {
|
|
35411
35568
|
// Back where it set off from: that is not another travel, it is this one
|
|
35412
35569
|
// undone — the same pictures, run backwards.
|
|
35413
35570
|
revertTravel(travel);
|
|
35414
35571
|
return;
|
|
35415
35572
|
}
|
|
35416
|
-
const fromIndex =
|
|
35417
|
-
const toIndex =
|
|
35573
|
+
const fromIndex = pageIndexOf(pages, travel.fromPage);
|
|
35574
|
+
const toIndex = pageIndexOf(pages, page);
|
|
35418
35575
|
if (fromIndex === -1 || toIndex === -1) {
|
|
35419
35576
|
return;
|
|
35420
35577
|
}
|
|
35421
|
-
redirectTravel(travel,
|
|
35578
|
+
redirectTravel(travel, page, toIndex > fromIndex ? "forward" : "back");
|
|
35422
35579
|
};
|
|
35423
35580
|
const endTravel = travel => {
|
|
35424
35581
|
if (travel.ended) {
|
|
@@ -35432,9 +35589,14 @@ const RouteTravel = ({
|
|
|
35432
35589
|
releaseHold(travel);
|
|
35433
35590
|
if (travelRef.current === travel) {
|
|
35434
35591
|
travelRef.current = null;
|
|
35592
|
+
// Given back, so another box may wear it: kept, two of them on a page
|
|
35593
|
+
// would both answer to it and the browser refuses the whole transition
|
|
35594
|
+
// rather than pick.
|
|
35595
|
+
unnameAfterTravel(elementRef.current);
|
|
35435
35596
|
document.documentElement.removeAttribute(TRAVEL_ATTRIBUTE);
|
|
35436
35597
|
document.documentElement.removeAttribute(DRAGGED_ATTRIBUTE);
|
|
35437
35598
|
document.documentElement.removeAttribute(TURNED_ATTRIBUTE);
|
|
35599
|
+
releaseTravelHeight();
|
|
35438
35600
|
}
|
|
35439
35601
|
};
|
|
35440
35602
|
|
|
@@ -35495,24 +35657,21 @@ const RouteTravel = ({
|
|
|
35495
35657
|
}
|
|
35496
35658
|
// Dragging the page towards the end of the axis brings in what is
|
|
35497
35659
|
// BEFORE it, the way pushing a sheet to the right reveals its left.
|
|
35498
|
-
const
|
|
35499
|
-
if (!
|
|
35660
|
+
const page = sign > 0 ? pages[currentIndex - 1] : pages[currentIndex + 1];
|
|
35661
|
+
if (!page || !size || scrollRoomTowards(target, elementRef.current, axis, sign)) {
|
|
35500
35662
|
return false;
|
|
35501
35663
|
}
|
|
35502
35664
|
if (CAN_KEEP_PICTURE) {
|
|
35503
35665
|
beginTravel({
|
|
35504
|
-
|
|
35505
|
-
|
|
35666
|
+
page,
|
|
35667
|
+
fromPage: pages[currentIndex],
|
|
35506
35668
|
direction: sign > 0 ? "back" : "forward",
|
|
35507
35669
|
scrub: true,
|
|
35508
|
-
change: () =>
|
|
35509
|
-
route,
|
|
35510
|
-
cause: "drag"
|
|
35511
|
-
})
|
|
35670
|
+
change: () => travelTo(page, "drag")
|
|
35512
35671
|
});
|
|
35513
35672
|
} else {
|
|
35514
35673
|
travelRef.current = {
|
|
35515
|
-
|
|
35674
|
+
page,
|
|
35516
35675
|
noPicture: true,
|
|
35517
35676
|
ended: false
|
|
35518
35677
|
};
|
|
@@ -35564,9 +35723,9 @@ const RouteTravel = ({
|
|
|
35564
35723
|
// Where we are is what THIS travel was bringing in — the URL changed at
|
|
35565
35724
|
// the first pixel, so `currentIndex` belongs to a render this gesture
|
|
35566
35725
|
// is older than.
|
|
35567
|
-
const fromIndex =
|
|
35568
|
-
const
|
|
35569
|
-
if (fromIndex === -1 || !
|
|
35726
|
+
const fromIndex = pageIndexOf(pages, travel.page);
|
|
35727
|
+
const page = direction === "back" ? pages[fromIndex - 1] : pages[fromIndex + 1];
|
|
35728
|
+
if (fromIndex === -1 || !page) {
|
|
35570
35729
|
return false;
|
|
35571
35730
|
}
|
|
35572
35731
|
// At their very end before they are let go of: what ends the travel in
|
|
@@ -35576,14 +35735,11 @@ const RouteTravel = ({
|
|
|
35576
35735
|
scrubTravel(travel, 1);
|
|
35577
35736
|
travel.ratio = 1;
|
|
35578
35737
|
beginTravel({
|
|
35579
|
-
|
|
35580
|
-
|
|
35738
|
+
page,
|
|
35739
|
+
fromPage: travel.page,
|
|
35581
35740
|
direction,
|
|
35582
35741
|
scrub: true,
|
|
35583
|
-
change: () =>
|
|
35584
|
-
route,
|
|
35585
|
-
cause: "drag"
|
|
35586
|
-
})
|
|
35742
|
+
change: () => travelTo(page, "drag")
|
|
35587
35743
|
});
|
|
35588
35744
|
return {
|
|
35589
35745
|
size: boxSizeOnAxis(),
|
|
@@ -35598,17 +35754,14 @@ const RouteTravel = ({
|
|
|
35598
35754
|
// other neighbour is enough for it to show that one instead. The travel
|
|
35599
35755
|
// turns around where it stands, on the same transition and under the same
|
|
35600
35756
|
// hand, and there is no gap at all.
|
|
35601
|
-
const fromIndex =
|
|
35602
|
-
const
|
|
35603
|
-
if (fromIndex === -1 || !
|
|
35757
|
+
const fromIndex = pageIndexOf(pages, travel.fromPage);
|
|
35758
|
+
const page = direction === "back" ? pages[fromIndex - 1] : pages[fromIndex + 1];
|
|
35759
|
+
if (fromIndex === -1 || !page) {
|
|
35604
35760
|
return false;
|
|
35605
35761
|
}
|
|
35606
|
-
redirectTravel(travel,
|
|
35607
|
-
|
|
35608
|
-
|
|
35609
|
-
route,
|
|
35610
|
-
cause: "drag"
|
|
35611
|
-
});
|
|
35762
|
+
redirectTravel(travel, page, direction);
|
|
35763
|
+
pageAskedForRef.current = page;
|
|
35764
|
+
travelTo(page, "drag");
|
|
35612
35765
|
return {
|
|
35613
35766
|
size: boxSizeOnAxis(),
|
|
35614
35767
|
travelBack: sign > 0,
|
|
@@ -35631,10 +35784,7 @@ const RouteTravel = ({
|
|
|
35631
35784
|
if (travel.noPicture) {
|
|
35632
35785
|
travelRef.current = null;
|
|
35633
35786
|
if (travels) {
|
|
35634
|
-
|
|
35635
|
-
route: travel.route,
|
|
35636
|
-
cause: "drag"
|
|
35637
|
-
});
|
|
35787
|
+
travelTo(travel.page, "drag");
|
|
35638
35788
|
}
|
|
35639
35789
|
return;
|
|
35640
35790
|
}
|
|
@@ -35666,6 +35816,16 @@ const RouteTravel = ({
|
|
|
35666
35816
|
if (currentIndex === -1) {
|
|
35667
35817
|
return;
|
|
35668
35818
|
}
|
|
35819
|
+
// A press that never became a gesture: whatever it stopped goes on its way,
|
|
35820
|
+
// from where the finger caught it.
|
|
35821
|
+
const giveUp = () => {
|
|
35822
|
+
gestureRef.current = null;
|
|
35823
|
+
const caught = caughtAtPressRef.current;
|
|
35824
|
+
caughtAtPressRef.current = null;
|
|
35825
|
+
if (caught && !caught.ended) {
|
|
35826
|
+
releaseHold(caught);
|
|
35827
|
+
}
|
|
35828
|
+
};
|
|
35669
35829
|
const gesture = startDragToTravel(pointerDownEvent, {
|
|
35670
35830
|
element: elementRef.current,
|
|
35671
35831
|
axes: axis,
|
|
@@ -35673,17 +35833,14 @@ const RouteTravel = ({
|
|
|
35673
35833
|
// is answered from its first pixel, on the axis the pages travel.
|
|
35674
35834
|
immediate: caughtAtPressRef.current ? axis : false,
|
|
35675
35835
|
...travelHandlers,
|
|
35676
|
-
onGiveUp:
|
|
35677
|
-
gestureRef.current = null;
|
|
35678
|
-
// A press that never became a gesture: whatever it stopped goes on its
|
|
35679
|
-
// way, from where the finger caught it.
|
|
35680
|
-
const caught = caughtAtPressRef.current;
|
|
35681
|
-
caughtAtPressRef.current = null;
|
|
35682
|
-
if (caught && !caught.ended) {
|
|
35683
|
-
releaseHold(caught);
|
|
35684
|
-
}
|
|
35685
|
-
}
|
|
35836
|
+
onGiveUp: giveUp
|
|
35686
35837
|
});
|
|
35838
|
+
if (!gesture) {
|
|
35839
|
+
// Not a press this box can be about — something that reads the pointer
|
|
35840
|
+
// itself, a box below it that travels the same way.
|
|
35841
|
+
giveUp();
|
|
35842
|
+
return;
|
|
35843
|
+
}
|
|
35687
35844
|
gestureRef.current = gesture;
|
|
35688
35845
|
};
|
|
35689
35846
|
|
|
@@ -35735,13 +35892,13 @@ const RouteTravel = ({
|
|
|
35735
35892
|
}
|
|
35736
35893
|
// Where the box is going, which is not where it is: a step asked for while
|
|
35737
35894
|
// a travel plays is the page after the one on its way.
|
|
35738
|
-
const
|
|
35739
|
-
const fromIndex =
|
|
35895
|
+
const fromPage = travelInFlight ? travelInFlight.page : pages[currentIndex];
|
|
35896
|
+
const fromIndex = pageIndexOf(pages, fromPage);
|
|
35740
35897
|
if (fromIndex === -1) {
|
|
35741
35898
|
return;
|
|
35742
35899
|
}
|
|
35743
|
-
const
|
|
35744
|
-
if (!
|
|
35900
|
+
const page = sign > 0 ? pages[fromIndex - 1] : pages[fromIndex + 1];
|
|
35901
|
+
if (!page) {
|
|
35745
35902
|
return;
|
|
35746
35903
|
}
|
|
35747
35904
|
if (travelInFlight) {
|
|
@@ -35752,14 +35909,11 @@ const RouteTravel = ({
|
|
|
35752
35909
|
travelInFlight.ratio = 1;
|
|
35753
35910
|
}
|
|
35754
35911
|
beginTravel({
|
|
35755
|
-
|
|
35756
|
-
|
|
35912
|
+
page,
|
|
35913
|
+
fromPage,
|
|
35757
35914
|
direction: sign > 0 ? "back" : "forward",
|
|
35758
35915
|
scrub: false,
|
|
35759
|
-
change: () =>
|
|
35760
|
-
route,
|
|
35761
|
-
cause: "wheel"
|
|
35762
|
-
})
|
|
35916
|
+
change: () => travelTo(page, "wheel")
|
|
35763
35917
|
});
|
|
35764
35918
|
};
|
|
35765
35919
|
|
|
@@ -35790,7 +35944,15 @@ const RouteTravel = ({
|
|
|
35790
35944
|
// page (see drag_to_travel.js).
|
|
35791
35945
|
,
|
|
35792
35946
|
|
|
35793
|
-
"data-drag-travel": travelByDrag ? axis : undefined
|
|
35947
|
+
"data-drag-travel": travelByDrag ? axis : undefined
|
|
35948
|
+
// The same fact said once per gesture, and for the other question the
|
|
35949
|
+
// DOM answers: a box that travels INSIDE this one — a row of slides in a
|
|
35950
|
+
// page — takes the axis it walks, and these are what it reads to know
|
|
35951
|
+
// this box walks it too.
|
|
35952
|
+
,
|
|
35953
|
+
|
|
35954
|
+
"data-travel-by-drag": travelByDrag ? axis : undefined,
|
|
35955
|
+
"data-travel-by-wheel": travelByDrag ? axis : undefined,
|
|
35794
35956
|
onPointerDown: onPointerDown,
|
|
35795
35957
|
children: children
|
|
35796
35958
|
});
|
|
@@ -35816,6 +35978,24 @@ const releaseHold = travel => {
|
|
|
35816
35978
|
travelHoldingPictures = null;
|
|
35817
35979
|
document.documentElement.removeAttribute(HOLD_ATTRIBUTE);
|
|
35818
35980
|
};
|
|
35981
|
+
const TRAVEL_HEIGHT_PROPERTY = "--navi-route-travel-height";
|
|
35982
|
+
// The height the group is held at for the whole travel: the taller of the two
|
|
35983
|
+
// boxes, so neither picture is ever cut. It cannot be said in CSS — neither box
|
|
35984
|
+
// is knowable there — and it cannot be measured from one side alone: a page
|
|
35985
|
+
// arriving shorter than the one it replaces would cut the one leaving, a page
|
|
35986
|
+
// arriving taller would be cut itself.
|
|
35987
|
+
const holdTravelHeight = (element, heightBefore) => {
|
|
35988
|
+
const heightAfter = element.getBoundingClientRect().height;
|
|
35989
|
+
const height = heightBefore > heightAfter ? heightBefore : heightAfter;
|
|
35990
|
+
document.documentElement.style.setProperty(TRAVEL_HEIGHT_PROPERTY, `${height}px`);
|
|
35991
|
+
};
|
|
35992
|
+
// The live layout takes the box back. A discontinuity by construction — the
|
|
35993
|
+
// group stands at the held height, the box is at the new one — and an invisible
|
|
35994
|
+
// one: the page arriving is fully in place, and the strip below it that the
|
|
35995
|
+
// group still covers shows the page leaving only while it is still on screen.
|
|
35996
|
+
const releaseTravelHeight = () => {
|
|
35997
|
+
document.documentElement.style.removeProperty(TRAVEL_HEIGHT_PROPERTY);
|
|
35998
|
+
};
|
|
35819
35999
|
|
|
35820
36000
|
// The browser does not take the picture of the page being left when a
|
|
35821
36001
|
// transition is ASKED for — it takes it at the next frame, just before running
|
|
@@ -35966,7 +36146,7 @@ const scrubTravel = (travel, ratio) => {
|
|
|
35966
36146
|
}
|
|
35967
36147
|
};
|
|
35968
36148
|
|
|
35969
|
-
// A
|
|
36149
|
+
// A page change, carried out and then waited for until the page it selects is
|
|
35970
36150
|
// really on screen. The container doing the swapping is the only one who knows
|
|
35971
36151
|
// when that is (observeRouteRender): a route matching is a signal changing, and
|
|
35972
36152
|
// how many passes Preact takes to answer it is its own business.
|
|
@@ -35976,7 +36156,7 @@ const scrubTravel = (travel, ratio) => {
|
|
|
35976
36156
|
// inside the callback of a view transition: the browser has stopped rendering
|
|
35977
36157
|
// and is waiting on this very promise to take its picture, so a wait that never
|
|
35978
36158
|
// ends is a page frozen under a transition that never became ready.
|
|
35979
|
-
const
|
|
36159
|
+
const whilePageRenders = async (page, change) => {
|
|
35980
36160
|
let stopListening;
|
|
35981
36161
|
const rendered = new Promise(resolve => {
|
|
35982
36162
|
// Listened for before the change, or a render landing while the change is
|
|
@@ -35985,7 +36165,7 @@ const whileRouteRenders = async (route, change) => {
|
|
|
35985
36165
|
});
|
|
35986
36166
|
try {
|
|
35987
36167
|
await change();
|
|
35988
|
-
if (
|
|
36168
|
+
if (pageIsCurrent(page)) {
|
|
35989
36169
|
await rendered;
|
|
35990
36170
|
}
|
|
35991
36171
|
} finally {
|
|
@@ -35993,9 +36173,76 @@ const whileRouteRenders = async (route, change) => {
|
|
|
35993
36173
|
}
|
|
35994
36174
|
};
|
|
35995
36175
|
|
|
36176
|
+
// A page of the row: a route, and the params that say which of its tabs when
|
|
36177
|
+
// several of them share it. Written as a bare route by a caller whose tabs are
|
|
36178
|
+
// routes of their own — which is the same page with nothing to tell apart.
|
|
36179
|
+
const normalizePage = page => page.isRoute ? {
|
|
36180
|
+
route: page,
|
|
36181
|
+
params: undefined
|
|
36182
|
+
} : page;
|
|
36183
|
+
|
|
36184
|
+
// Two pages are the same page when they select the same thing, not when they
|
|
36185
|
+
// were written by the same hand: the params of a tab are a literal in JSX, so
|
|
36186
|
+
// every render builds another object for what is plainly the same tab.
|
|
36187
|
+
const samePage = (a, b) => {
|
|
36188
|
+
if (a === b) {
|
|
36189
|
+
return true;
|
|
36190
|
+
}
|
|
36191
|
+
if (!a || !b) {
|
|
36192
|
+
return false;
|
|
36193
|
+
}
|
|
36194
|
+
return a.route === b.route && compareTwoJsValues(a.params, b.params);
|
|
36195
|
+
};
|
|
36196
|
+
const pageIndexOf = (pages, page) => pages.findIndex(candidate => samePage(candidate, page));
|
|
36197
|
+
|
|
36198
|
+
// Whether this page is the one on screen. `matchesParams` reads paramsSignal,
|
|
36199
|
+
// so a caller reading this during a render is subscribed to the param changes
|
|
36200
|
+
// that walk from one tab to the next — matchingSignal alone never moves there,
|
|
36201
|
+
// and a row whose tabs are params of one route would never re-render.
|
|
36202
|
+
//
|
|
36203
|
+
// The params are read only for a route that matches, and that is not a signal
|
|
36204
|
+
// left unread: a reader wakes on anything it read last time, so what matters is
|
|
36205
|
+
// that everything able to make this answer change is among them.
|
|
36206
|
+
// matchingSignal is read whatever happens, and it is a NECESSARY condition —
|
|
36207
|
+
// while it is false no param of that route can put this page on screen, and the
|
|
36208
|
+
// day one could, matchingSignal itself has to turn true to say so, which is the
|
|
36209
|
+
// read that brings the params back in. (Asking anyway would be worse than
|
|
36210
|
+
// useless: the params of a route that does not match are not params.)
|
|
36211
|
+
const pageIsCurrent = ({
|
|
36212
|
+
route,
|
|
36213
|
+
params
|
|
36214
|
+
}) => {
|
|
36215
|
+
if (!route.matchingSignal.value) {
|
|
36216
|
+
return false;
|
|
36217
|
+
}
|
|
36218
|
+
return params ? route.matchesParams(params) : true;
|
|
36219
|
+
};
|
|
36220
|
+
// Every page is read, never only up to the one that answers yes: a page that is
|
|
36221
|
+
// not the current one today is the one that must wake the reader tomorrow.
|
|
36222
|
+
const currentPageIndex = pages => {
|
|
36223
|
+
let currentIndex = -1;
|
|
36224
|
+
for (let i = 0; i < pages.length; i++) {
|
|
36225
|
+
if (pageIsCurrent(pages[i])) {
|
|
36226
|
+
currentIndex = i;
|
|
36227
|
+
}
|
|
36228
|
+
}
|
|
36229
|
+
return currentIndex;
|
|
36230
|
+
};
|
|
36231
|
+
|
|
35996
36232
|
// A transition skipped by another one starting is an outcome, not a failure.
|
|
35997
36233
|
const ignoreSkipped = () => {};
|
|
35998
36234
|
|
|
36235
|
+
// The name is lent to the box that is travelling and taken back afterwards.
|
|
36236
|
+
// There is one transition in a document at a time, so one box wears it at a
|
|
36237
|
+
// time — and the others, unnamed, are simply not captured: they stay live
|
|
36238
|
+
// under the pictures rather than being frozen with the page.
|
|
36239
|
+
const nameForTravel = element => {
|
|
36240
|
+
element.style.viewTransitionName = TRAVEL_NAME;
|
|
36241
|
+
};
|
|
36242
|
+
const unnameAfterTravel = element => {
|
|
36243
|
+
element.style.viewTransitionName = "";
|
|
36244
|
+
};
|
|
36245
|
+
|
|
35999
36246
|
const routeAction = (
|
|
36000
36247
|
routeOrRoutes,
|
|
36001
36248
|
action,
|
|
@@ -37958,8 +38205,10 @@ const BinderItemContext = createContext(null);
|
|
|
37958
38205
|
|
|
37959
38206
|
/**
|
|
37960
38207
|
* What a <Link> learns from the <Nav> around it: where to draw the bar that
|
|
37961
|
-
* says "you are here",
|
|
37962
|
-
*
|
|
38208
|
+
* says "you are here", the name under which the browser is to recognise that
|
|
38209
|
+
* bar from one page to the next, and — for a row of tabs that are slides — which
|
|
38210
|
+
* <SlideContainer> they are about and which of its slides is on screen (see
|
|
38211
|
+
* nav.jsx).
|
|
37963
38212
|
*/
|
|
37964
38213
|
const NavContext = createContext(null);
|
|
37965
38214
|
|
|
@@ -38392,6 +38641,12 @@ Object.assign(PSEUDO_CLASSES, {
|
|
|
38392
38641
|
* instead of a raw `href`: the URL is built from the route (see
|
|
38393
38642
|
* `routeParams`) and "current" is derived from whether the route matches.
|
|
38394
38643
|
* @param {object} [props.routeParams] - Params passed to `route.buildUrl`.
|
|
38644
|
+
* @param {string} [props.slide] - Makes this a tab for a slide rather than for
|
|
38645
|
+
* a URL: the area of a `<SlideContainer>` it goes to. The container is the one
|
|
38646
|
+
* the surrounding `<Nav slideContainer={id}>` names, and it is also what says
|
|
38647
|
+
* whether this tab is the current one. Nothing is written to the URL — these
|
|
38648
|
+
* are places within one screen, not pages of their own — so there is no href
|
|
38649
|
+
* and the tab behaves like a button.
|
|
38395
38650
|
* @param {string} [props.target] - Native anchor target; defaults from
|
|
38396
38651
|
* internal/external detection when omitted.
|
|
38397
38652
|
* @param {string} [props.rel] - Native anchor rel; defaults to
|
|
@@ -38481,6 +38736,7 @@ const LinkPlain = props => {
|
|
|
38481
38736
|
target,
|
|
38482
38737
|
rel,
|
|
38483
38738
|
anchor,
|
|
38739
|
+
slide,
|
|
38484
38740
|
value = href,
|
|
38485
38741
|
// visual
|
|
38486
38742
|
variant,
|
|
@@ -38531,7 +38787,9 @@ const LinkPlain = props => {
|
|
|
38531
38787
|
isAnchor,
|
|
38532
38788
|
isCurrent
|
|
38533
38789
|
} = getHrefTargetInfo(href);
|
|
38534
|
-
|
|
38790
|
+
// A tab that is a SLIDE is current when the container is on it — which the
|
|
38791
|
+
// <Nav> around reads off that container, so nothing here has to be told.
|
|
38792
|
+
const innerCurrent = current || (slide ? nav?.currentSlideArea === slide : isCurrent);
|
|
38535
38793
|
useReportCurrentToBinderItem(innerCurrent);
|
|
38536
38794
|
controlHostProps.basePseudoState = {
|
|
38537
38795
|
...basePseudoState,
|
|
@@ -38618,6 +38876,12 @@ const LinkPlain = props => {
|
|
|
38618
38876
|
onClick,
|
|
38619
38877
|
preventDefault
|
|
38620
38878
|
} = props;
|
|
38879
|
+
// Travelling there is the container's business, said as the command anything
|
|
38880
|
+
// else in the page would say it with: the tab knows the name of a slide and
|
|
38881
|
+
// the id of the box, and nothing more about either.
|
|
38882
|
+
const goToSlide = (element, event) => {
|
|
38883
|
+
triggerNaviCommand(element, `--navi-go-to-slide:${slide}`, event);
|
|
38884
|
+
};
|
|
38621
38885
|
return jsxs(Text, {
|
|
38622
38886
|
as: "a",
|
|
38623
38887
|
color: anchor && !innerChildren ? "inherit" : undefined,
|
|
@@ -38630,6 +38894,7 @@ const LinkPlain = props => {
|
|
|
38630
38894
|
// was handed.
|
|
38631
38895
|
preventDefault: undefined,
|
|
38632
38896
|
anchor: undefined,
|
|
38897
|
+
slide: undefined,
|
|
38633
38898
|
revealOnInteraction: undefined,
|
|
38634
38899
|
variant: undefined,
|
|
38635
38900
|
current: undefined,
|
|
@@ -38643,15 +38908,43 @@ const LinkPlain = props => {
|
|
|
38643
38908
|
hrefFallback: undefined,
|
|
38644
38909
|
onClick: e => {
|
|
38645
38910
|
onClick?.(e);
|
|
38911
|
+
if (slide) {
|
|
38912
|
+
goToSlide(e.currentTarget, e);
|
|
38913
|
+
}
|
|
38646
38914
|
if (preventDefault) {
|
|
38647
38915
|
e.preventDefault();
|
|
38648
38916
|
}
|
|
38917
|
+
}
|
|
38918
|
+
// A tab with no href is not a link the browser knows how to press: it is
|
|
38919
|
+
// focusable because it says so (tabIndex below) and it answers the two
|
|
38920
|
+
// keys a button answers, since that is what it behaves like.
|
|
38921
|
+
,
|
|
38922
|
+
|
|
38923
|
+
onKeyDown: e => {
|
|
38924
|
+
props.onKeyDown?.(e);
|
|
38925
|
+
if (!slide || e.defaultPrevented) {
|
|
38926
|
+
return;
|
|
38927
|
+
}
|
|
38928
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
38929
|
+
e.preventDefault();
|
|
38930
|
+
goToSlide(e.currentTarget, e);
|
|
38931
|
+
}
|
|
38649
38932
|
},
|
|
38650
38933
|
href: href,
|
|
38651
38934
|
rel: innerRel,
|
|
38652
|
-
target: innerTarget === "_self" ? undefined : target
|
|
38935
|
+
target: innerTarget === "_self" ? undefined : target
|
|
38936
|
+
// Which slide this tab is, and which box to say it to: read by the <Nav>
|
|
38937
|
+
// around it to place the row's own bar, and by the command above to find
|
|
38938
|
+
// the container across the document.
|
|
38939
|
+
,
|
|
38940
|
+
|
|
38941
|
+
"data-slide-target": slide,
|
|
38942
|
+
commandfor: slide ? nav?.slideContainer : undefined,
|
|
38943
|
+
"aria-controls": slide ? nav?.slideContainer : undefined,
|
|
38944
|
+
tabIndex: slide ? props.tabIndex ?? 0 : props.tabIndex,
|
|
38945
|
+
role: slide ? "tab" : props.role,
|
|
38653
38946
|
"aria-current": isCurrent ? "page" : undefined,
|
|
38654
|
-
"aria-selected": selectionContext ? selected : undefined,
|
|
38947
|
+
"aria-selected": slide ? innerCurrent : selectionContext ? selected : undefined,
|
|
38655
38948
|
"data-value-event": "navi_value",
|
|
38656
38949
|
onnavi_value: e => {
|
|
38657
38950
|
e.detail.setValue(value);
|
|
@@ -38714,6 +39007,75 @@ const css$N = /* css */`
|
|
|
38714
39007
|
--nav-padding: 0px;
|
|
38715
39008
|
--nav-border-radius: 0px;
|
|
38716
39009
|
--nav-background: transparent;
|
|
39010
|
+
--nav-current-indicator-size: 2px;
|
|
39011
|
+
--nav-current-indicator-color: var(--navi-link-current-indicator-color);
|
|
39012
|
+
}
|
|
39013
|
+
}
|
|
39014
|
+
|
|
39015
|
+
/* The bar of a nav whose tabs are SLIDES: one element for the whole row,
|
|
39016
|
+
placed over the current tab and interpolated towards the one the picture
|
|
39017
|
+
leans on (see paintIndicatorGeometry). The two ends are written in pixels
|
|
39018
|
+
as plain numbers, so the whole of the movement is a calc() the browser
|
|
39019
|
+
runs itself — the trait then follows a finger dragging the slides without a
|
|
39020
|
+
render per frame, and rides the same animation as the track when the travel
|
|
39021
|
+
was asked for rather than dragged.
|
|
39022
|
+
No named view transition here, unlike the bar of a nav made of routes:
|
|
39023
|
+
there is no transition to be part of — the slides travel under an animation
|
|
39024
|
+
of their own, which a finger can hold. */
|
|
39025
|
+
.navi_nav[data-nav-indicator] {
|
|
39026
|
+
position: relative;
|
|
39027
|
+
|
|
39028
|
+
> .navi_nav_indicator {
|
|
39029
|
+
--x-nav-indicator-position: calc(
|
|
39030
|
+
var(--nav-indicator-position) + var(--slide-travel-progress) *
|
|
39031
|
+
var(--nav-indicator-position-delta)
|
|
39032
|
+
);
|
|
39033
|
+
--x-nav-indicator-length: calc(
|
|
39034
|
+
var(--nav-indicator-length) + var(--slide-travel-progress) *
|
|
39035
|
+
var(--nav-indicator-length-delta)
|
|
39036
|
+
);
|
|
39037
|
+
|
|
39038
|
+
position: absolute;
|
|
39039
|
+
z-index: 1;
|
|
39040
|
+
background: var(--nav-current-indicator-color);
|
|
39041
|
+
border-radius: 0.1px;
|
|
39042
|
+
pointer-events: none;
|
|
39043
|
+
}
|
|
39044
|
+
/* Nothing to draw until the row has been measured: a tab bar whose current
|
|
39045
|
+
tab is not among its links (a container on a slide no tab names) has no
|
|
39046
|
+
place to put the trait. */
|
|
39047
|
+
&:not([data-nav-indicator-measured]) > .navi_nav_indicator {
|
|
39048
|
+
display: none;
|
|
39049
|
+
}
|
|
39050
|
+
|
|
39051
|
+
&[data-nav-indicator="top"],
|
|
39052
|
+
&[data-nav-indicator="bottom"] {
|
|
39053
|
+
> .navi_nav_indicator {
|
|
39054
|
+
left: calc(var(--x-nav-indicator-position) * 1px);
|
|
39055
|
+
width: calc(var(--x-nav-indicator-length) * 1px);
|
|
39056
|
+
height: var(--nav-current-indicator-size);
|
|
39057
|
+
}
|
|
39058
|
+
}
|
|
39059
|
+
&[data-nav-indicator="top"] > .navi_nav_indicator {
|
|
39060
|
+
top: 0;
|
|
39061
|
+
}
|
|
39062
|
+
&[data-nav-indicator="bottom"] > .navi_nav_indicator {
|
|
39063
|
+
bottom: 0;
|
|
39064
|
+
}
|
|
39065
|
+
|
|
39066
|
+
&[data-nav-indicator="left"],
|
|
39067
|
+
&[data-nav-indicator="right"] {
|
|
39068
|
+
> .navi_nav_indicator {
|
|
39069
|
+
top: calc(var(--x-nav-indicator-position) * 1px);
|
|
39070
|
+
width: var(--nav-current-indicator-size);
|
|
39071
|
+
height: calc(var(--x-nav-indicator-length) * 1px);
|
|
39072
|
+
}
|
|
39073
|
+
}
|
|
39074
|
+
&[data-nav-indicator="left"] > .navi_nav_indicator {
|
|
39075
|
+
left: 0;
|
|
39076
|
+
}
|
|
39077
|
+
&[data-nav-indicator="right"] > .navi_nav_indicator {
|
|
39078
|
+
right: 0;
|
|
38717
39079
|
}
|
|
38718
39080
|
}
|
|
38719
39081
|
|
|
@@ -38869,23 +39231,43 @@ const NavStyleCSSVars = {
|
|
|
38869
39231
|
paddingRight: "--nav-padding-right",
|
|
38870
39232
|
paddingBottom: "--nav-padding-bottom",
|
|
38871
39233
|
paddingLeft: "--nav-padding-left",
|
|
38872
|
-
background: "--nav-background"
|
|
39234
|
+
background: "--nav-background",
|
|
39235
|
+
currentIndicatorColor: "--nav-current-indicator-color",
|
|
39236
|
+
currentIndicatorSize: "--nav-current-indicator-size"
|
|
38873
39237
|
};
|
|
39238
|
+
const positionOfCurrentIndicator = (currentIndicator, vertical) => {
|
|
39239
|
+
if (currentIndicator === true) {
|
|
39240
|
+
return vertical ? "left" : "bottom";
|
|
39241
|
+
}
|
|
39242
|
+
if (currentIndicator === "top" || currentIndicator === "bottom" || currentIndicator === "left" || currentIndicator === "right") {
|
|
39243
|
+
return currentIndicator;
|
|
39244
|
+
}
|
|
39245
|
+
return null;
|
|
39246
|
+
};
|
|
39247
|
+
|
|
38874
39248
|
/**
|
|
38875
39249
|
* @type {import("ignore:preact").FunctionComponent<{
|
|
38876
39250
|
* currentIndicator?: boolean|"top"|"bottom"|"left"|"right",
|
|
38877
39251
|
* currentIndicatorSlides?: boolean,
|
|
39252
|
+
* slideContainer?: string,
|
|
38878
39253
|
* }>}
|
|
38879
39254
|
* @param {boolean|"top"|"bottom"|"left"|"right"} [props.currentIndicator] - the
|
|
38880
39255
|
* bar that says which tab one is on, said once here rather than on every
|
|
38881
39256
|
* `<Link>`. A link may still say otherwise for itself.
|
|
38882
39257
|
* @param {boolean} [props.currentIndicatorSlides=true] - whether that bar
|
|
38883
39258
|
* travels from the tab it was under to the tab it is under now, instead of
|
|
38884
|
-
* going out on one and coming back on the other.
|
|
38885
|
-
* which is all the browser needs: any change played as
|
|
38886
|
-
* animates it on the same clock as everything else in that
|
|
38887
|
-
* a `RouteTravel` that means it follows the pages, and the
|
|
38888
|
-
* them, without either of them being told about the other.
|
|
39259
|
+
* going out on one and coming back on the other. For a nav made of routes it
|
|
39260
|
+
* does so by being NAMED, which is all the browser needs: any change played as
|
|
39261
|
+
* a view transition animates it on the same clock as everything else in that
|
|
39262
|
+
* transition. Inside a `RouteTravel` that means it follows the pages, and the
|
|
39263
|
+
* thumb dragging them, without either of them being told about the other. For
|
|
39264
|
+
* a nav made of slides (`slideContainer`) the bar is one element for the whole
|
|
39265
|
+
* row, and it reads the travel the container publishes.
|
|
39266
|
+
* @param {string} [props.slideContainer] - the id of a `<SlideContainer>` these
|
|
39267
|
+
* tabs are about: each one says which slide it is (`<Link slide="…">`), the
|
|
39268
|
+
* container says which one is on screen, and pressing a tab travels there.
|
|
39269
|
+
* Tabs that are places in the same screen rather than pages of their own —
|
|
39270
|
+
* nothing is written to the URL and nothing is a link.
|
|
38889
39271
|
*/
|
|
38890
39272
|
const Nav = ({
|
|
38891
39273
|
children,
|
|
@@ -38898,21 +39280,132 @@ const Nav = ({
|
|
|
38898
39280
|
currentIndicatorSlides = true,
|
|
38899
39281
|
panelPosition,
|
|
38900
39282
|
// "before" or "after": which side the panel sits on, turning the nav into folder tabs
|
|
39283
|
+
slideContainer,
|
|
38901
39284
|
...props
|
|
38902
39285
|
}) => {
|
|
38903
39286
|
import.meta.css = [css$N, "@jsenv/navi/src/nav/link/nav.jsx"];
|
|
39287
|
+
const defaultRef = useRef();
|
|
39288
|
+
props.ref = props.ref || defaultRef;
|
|
39289
|
+
const navRef = props.ref;
|
|
38904
39290
|
const indicatorNameRef = useRef(null);
|
|
38905
39291
|
if (indicatorNameRef.current === null) {
|
|
38906
39292
|
indicatorNameRef.current = `navi-nav-indicator-${++navCount}`;
|
|
38907
39293
|
}
|
|
39294
|
+
const [currentSlideArea, setCurrentSlideArea] = useState(undefined);
|
|
39295
|
+
const slideContainerElementRef = useRef(null);
|
|
39296
|
+
const indicatorPosition = slideContainer ? positionOfCurrentIndicator(currentIndicator, vertical) : null;
|
|
39297
|
+
|
|
39298
|
+
// Where the trait is and where it is headed, as four numbers of pixels the
|
|
39299
|
+
// CSS above interpolates between (see the .navi_nav_indicator rules). Written
|
|
39300
|
+
// by hand rather than rendered: it is read off the row as it stands, and the
|
|
39301
|
+
// travel it must agree with starts in the same frame the container publishes
|
|
39302
|
+
// it — a render would land after the movement had begun.
|
|
39303
|
+
const paintIndicatorGeometry = () => {
|
|
39304
|
+
const navElement = navRef.current;
|
|
39305
|
+
const containerElement = slideContainerElementRef.current;
|
|
39306
|
+
if (!navElement || !containerElement || !indicatorPosition) {
|
|
39307
|
+
return;
|
|
39308
|
+
}
|
|
39309
|
+
const tabElements = Array.from(navElement.querySelectorAll("[data-slide-target]"));
|
|
39310
|
+
const areaOf = tabElement => tabElement.getAttribute("data-slide-target");
|
|
39311
|
+
const currentArea = containerElement.getAttribute("data-slide-current");
|
|
39312
|
+
const currentIndex = tabElements.findIndex(tabElement => areaOf(tabElement) === currentArea);
|
|
39313
|
+
if (currentIndex === -1) {
|
|
39314
|
+
// On a slide no tab in this row names: there is no tab to sit under.
|
|
39315
|
+
navElement.removeAttribute("data-nav-indicator-measured");
|
|
39316
|
+
return;
|
|
39317
|
+
}
|
|
39318
|
+
const measure = tabElement => vertical ? {
|
|
39319
|
+
position: tabElement.offsetTop,
|
|
39320
|
+
length: tabElement.offsetHeight
|
|
39321
|
+
} : {
|
|
39322
|
+
position: tabElement.offsetLeft,
|
|
39323
|
+
length: tabElement.offsetWidth
|
|
39324
|
+
};
|
|
39325
|
+
const currentMeasure = measure(tabElements[currentIndex]);
|
|
39326
|
+
const towardArea = containerElement.getAttribute("data-slide-travel-toward");
|
|
39327
|
+
const towardIndex = tabElements.findIndex(tabElement => areaOf(tabElement) === towardArea);
|
|
39328
|
+
let positionDelta = 0;
|
|
39329
|
+
let lengthDelta = 0;
|
|
39330
|
+
if (towardIndex !== -1 && towardIndex !== currentIndex) {
|
|
39331
|
+
const towardMeasure = measure(tabElements[towardIndex]);
|
|
39332
|
+
// What one box of travel is worth in pixels of this row, signed so that
|
|
39333
|
+
// the trait is exactly on the other tab when the progress is at its own
|
|
39334
|
+
// end: the container counts +1 when the picture leans on a slide sitting
|
|
39335
|
+
// BEFORE the current one and -1 when it sits after.
|
|
39336
|
+
const sign = towardIndex > currentIndex ? -1 : 1;
|
|
39337
|
+
positionDelta = (towardMeasure.position - currentMeasure.position) * sign;
|
|
39338
|
+
lengthDelta = (towardMeasure.length - currentMeasure.length) * sign;
|
|
39339
|
+
}
|
|
39340
|
+
const {
|
|
39341
|
+
style
|
|
39342
|
+
} = navElement;
|
|
39343
|
+
style.setProperty("--nav-indicator-position", currentMeasure.position);
|
|
39344
|
+
style.setProperty("--nav-indicator-length", currentMeasure.length);
|
|
39345
|
+
style.setProperty("--nav-indicator-position-delta", positionDelta);
|
|
39346
|
+
style.setProperty("--nav-indicator-length-delta", lengthDelta);
|
|
39347
|
+
navElement.setAttribute("data-nav-indicator-measured", "");
|
|
39348
|
+
};
|
|
39349
|
+
// Reached through a ref by everything watching the DOM below: those watchers
|
|
39350
|
+
// outlive a render, and what they must run is the version of this that knows
|
|
39351
|
+
// about the row as it is now.
|
|
39352
|
+
const paintIndicatorGeometryRef = useRef(null);
|
|
39353
|
+
paintIndicatorGeometryRef.current = paintIndicatorGeometry;
|
|
39354
|
+
useLayoutEffect(() => {
|
|
39355
|
+
if (!slideContainer) {
|
|
39356
|
+
return undefined;
|
|
39357
|
+
}
|
|
39358
|
+
const containerElement = document.getElementById(slideContainer);
|
|
39359
|
+
if (!containerElement) {
|
|
39360
|
+
console.warn(`<Nav slideContainer="${slideContainer}"> but no element with that id found`);
|
|
39361
|
+
return undefined;
|
|
39362
|
+
}
|
|
39363
|
+
slideContainerElementRef.current = containerElement;
|
|
39364
|
+
const readContainer = () => {
|
|
39365
|
+
setCurrentSlideArea(containerElement.getAttribute("data-slide-current") ?? undefined);
|
|
39366
|
+
paintIndicatorGeometryRef.current();
|
|
39367
|
+
};
|
|
39368
|
+
readContainer();
|
|
39369
|
+
// The container says where one is and what the picture leans on, and says
|
|
39370
|
+
// it in the DOM: nothing here is told, everything is read — which is what
|
|
39371
|
+
// lets this row sit anywhere on the page (above the box, in a fixed bar)
|
|
39372
|
+
// rather than inside it.
|
|
39373
|
+
const attributeObserver = new MutationObserver(readContainer);
|
|
39374
|
+
attributeObserver.observe(containerElement, {
|
|
39375
|
+
attributes: true,
|
|
39376
|
+
attributeFilter: ["data-slide-current", "data-slide-travel-toward"]
|
|
39377
|
+
});
|
|
39378
|
+
// A row whose tabs changed width — a badge count, a font that just
|
|
39379
|
+
// arrived, a window resized — is measured again: what was written is
|
|
39380
|
+
// pixels, and pixels go stale.
|
|
39381
|
+
const sizeObserver = new ResizeObserver(() => {
|
|
39382
|
+
paintIndicatorGeometryRef.current();
|
|
39383
|
+
});
|
|
39384
|
+
sizeObserver.observe(navRef.current);
|
|
39385
|
+
return () => {
|
|
39386
|
+
attributeObserver.disconnect();
|
|
39387
|
+
sizeObserver.disconnect();
|
|
39388
|
+
slideContainerElementRef.current = null;
|
|
39389
|
+
};
|
|
39390
|
+
}, [slideContainer]);
|
|
39391
|
+
|
|
39392
|
+
// Said after every commit: a tab added, removed or renamed moves the trait,
|
|
39393
|
+
// and no observer above watches this row's own children.
|
|
39394
|
+
useLayoutEffect(() => {
|
|
39395
|
+
paintIndicatorGeometry();
|
|
39396
|
+
});
|
|
38908
39397
|
const navContextValue = useMemo(() => ({
|
|
38909
|
-
|
|
39398
|
+
// The bar belongs to the row itself when the tabs are slides, so the
|
|
39399
|
+
// links draw none of their own.
|
|
39400
|
+
currentIndicator: slideContainer ? undefined : currentIndicator,
|
|
38910
39401
|
// Read by the link that is current, and by it alone: a name belongs to
|
|
38911
39402
|
// one element at a time, and the bar exists in every tab.
|
|
38912
|
-
indicatorName: currentIndicatorSlides ? indicatorNameRef.current : null
|
|
38913
|
-
|
|
39403
|
+
indicatorName: currentIndicatorSlides ? indicatorNameRef.current : null,
|
|
39404
|
+
slideContainer,
|
|
39405
|
+
currentSlideArea
|
|
39406
|
+
}), [currentIndicator, currentIndicatorSlides, slideContainer, currentSlideArea]);
|
|
38914
39407
|
children = toChildArray(children);
|
|
38915
|
-
return
|
|
39408
|
+
return jsxs(Box, {
|
|
38916
39409
|
as: "nav",
|
|
38917
39410
|
row: vertical,
|
|
38918
39411
|
column: !vertical,
|
|
@@ -38921,15 +39414,30 @@ const Nav = ({
|
|
|
38921
39414
|
"data-expand": expand || expandX ? "" : undefined,
|
|
38922
39415
|
"data-vertical": vertical ? "" : undefined,
|
|
38923
39416
|
"data-panel-position": panelPosition,
|
|
39417
|
+
"data-nav-indicator": indicatorPosition ?? undefined
|
|
39418
|
+
// "write your travel here too": a custom property cannot be read across
|
|
39419
|
+
// the DOM, so the container paints its progress onto this element and the
|
|
39420
|
+
// trait follows in CSS alone (see SlideContainer's followerElements).
|
|
39421
|
+
,
|
|
39422
|
+
|
|
39423
|
+
"data-slide-container-follows": slideContainer
|
|
39424
|
+
// Tabs over one screen, not links to pages: a screen reader is told so,
|
|
39425
|
+
// and told which way the row runs.
|
|
39426
|
+
,
|
|
39427
|
+
|
|
39428
|
+
role: slideContainer ? "tablist" : undefined,
|
|
39429
|
+
"aria-orientation": slideContainer && vertical ? "vertical" : undefined,
|
|
38924
39430
|
expand: expand,
|
|
38925
39431
|
expandX: expandX,
|
|
38926
39432
|
spacing: spacing,
|
|
38927
39433
|
...props,
|
|
38928
39434
|
styleCSSVars: NavStyleCSSVars,
|
|
38929
|
-
children: jsx(
|
|
39435
|
+
children: [indicatorPosition && jsx("span", {
|
|
39436
|
+
className: "navi_nav_indicator"
|
|
39437
|
+
}), jsx(NavContext.Provider, {
|
|
38930
39438
|
value: navContextValue,
|
|
38931
39439
|
children: children
|
|
38932
|
-
})
|
|
39440
|
+
})]
|
|
38933
39441
|
});
|
|
38934
39442
|
};
|
|
38935
39443
|
|
|
@@ -45812,7 +46320,16 @@ const SlideContainer = ({
|
|
|
45812
46320
|
// picture is from the slide ARRIVING when the travel starts, in boxes. Null
|
|
45813
46321
|
// for a travel nobody dragged, where a whole box is what is left to close.
|
|
45814
46322
|
const travelProgressFromRef = useRef(null);
|
|
45815
|
-
|
|
46323
|
+
// One per element painting the progress: the box, plus everything following
|
|
46324
|
+
// it (see followerElementsRef). All started together and with the same
|
|
46325
|
+
// options, so they are one movement said in several places.
|
|
46326
|
+
const progressAnimationsRef = useRef([]);
|
|
46327
|
+
// Elements outside the box that draw something about this travel — a tab bar
|
|
46328
|
+
// above it, most of all. A custom property cannot be read across the DOM, so
|
|
46329
|
+
// the progress is WRITTEN on each of them: they then interpolate whatever they
|
|
46330
|
+
// draw in CSS alone, at the pace of the travel and under the finger, with
|
|
46331
|
+
// nothing measured per frame.
|
|
46332
|
+
const followerElementsRef = useRef([]);
|
|
45816
46333
|
const current = rollingArea ?? provisionalArea ?? currentProp ?? currentAreaState;
|
|
45817
46334
|
const vertical = layout === "column";
|
|
45818
46335
|
// What the map has, and what each way of asking is allowed to use of it.
|
|
@@ -45938,6 +46455,28 @@ const SlideContainer = ({
|
|
|
45938
46455
|
...map
|
|
45939
46456
|
};
|
|
45940
46457
|
};
|
|
46458
|
+
|
|
46459
|
+
// Who is drawing something about this box from outside it: a tab bar saying
|
|
46460
|
+
// where one is, a row of dots. They name the box they follow by its id, the
|
|
46461
|
+
// way everything else that talks to it across the document does (commandfor).
|
|
46462
|
+
const readFollowerElements = () => {
|
|
46463
|
+
const containerEl = containerRef.current;
|
|
46464
|
+
const {
|
|
46465
|
+
id
|
|
46466
|
+
} = containerEl;
|
|
46467
|
+
if (!id) {
|
|
46468
|
+
return [];
|
|
46469
|
+
}
|
|
46470
|
+
return Array.from(document.querySelectorAll(`[data-slide-container-follows="${CSS.escape(id)}"]`));
|
|
46471
|
+
};
|
|
46472
|
+
|
|
46473
|
+
// Which slide is on screen, said in the DOM: it is what anything outside the
|
|
46474
|
+
// box reads to know where one is (a tab bar marking its current tab), and
|
|
46475
|
+
// there is nothing else for it to read — a slide the container holds by
|
|
46476
|
+
// itself is known to no one else.
|
|
46477
|
+
const paintCurrentArea = area => {
|
|
46478
|
+
containerRef.current?.setAttribute("data-slide-current", area);
|
|
46479
|
+
};
|
|
45941
46480
|
const markAnswered = area => {
|
|
45942
46481
|
const order = readMap().slideElements.map(readArea);
|
|
45943
46482
|
const rank = order.indexOf(area);
|
|
@@ -46020,6 +46559,10 @@ const SlideContainer = ({
|
|
|
46020
46559
|
// the children free — their shape says nothing about the arrangement, the map
|
|
46021
46560
|
// does — and it is also the only place that has to agree with itself.
|
|
46022
46561
|
useLayoutEffect(() => {
|
|
46562
|
+
// Read on every render rather than subscribed to: a follower says who it
|
|
46563
|
+
// follows in the DOM, and the render that mounted one is the render this
|
|
46564
|
+
// runs after.
|
|
46565
|
+
followerElementsRef.current = readFollowerElements();
|
|
46023
46566
|
const {
|
|
46024
46567
|
slideElements,
|
|
46025
46568
|
placeOf
|
|
@@ -46032,6 +46575,7 @@ const SlideContainer = ({
|
|
|
46032
46575
|
// shown, the way a stack of pages opens on its first page.
|
|
46033
46576
|
slideElements[0];
|
|
46034
46577
|
const currentArea = readArea(currentElement);
|
|
46578
|
+
paintCurrentArea(currentArea);
|
|
46035
46579
|
const realPlaceOf = area => placeOf.get(area) || {
|
|
46036
46580
|
x: 0,
|
|
46037
46581
|
y: 0
|
|
@@ -46177,7 +46721,9 @@ const SlideContainer = ({
|
|
|
46177
46721
|
// there was one, from a whole box away when the travel was asked for.
|
|
46178
46722
|
const progressFrom = travelProgressFromRef.current ?? (travelStep ? travelStep.x || travelStep.y : 0);
|
|
46179
46723
|
travelProgressFromRef.current = null;
|
|
46180
|
-
|
|
46724
|
+
// The slide the picture leans on for the length of it is the one being
|
|
46725
|
+
// LEFT: it is the second one in the frame until the travel is over.
|
|
46726
|
+
animateTravelProgress(progressFrom, durationMs * travelRatio, easing, drawnArea);
|
|
46181
46727
|
// Presses still waiting behind this one: it is already late, so it is
|
|
46182
46728
|
// sent home at once rather than played out at the pace of someone who
|
|
46183
46729
|
// has stopped pressing. Someone pressing → four times is asking to be
|
|
@@ -46597,6 +47143,30 @@ const SlideContainer = ({
|
|
|
46597
47143
|
paintTravelProgress(drag.progress, drag.areaPulled);
|
|
46598
47144
|
};
|
|
46599
47145
|
|
|
47146
|
+
// Everything that draws this travel: the box itself and whoever follows it.
|
|
47147
|
+
const travelPainters = () => {
|
|
47148
|
+
const containerEl = containerRef.current;
|
|
47149
|
+
if (!containerEl) {
|
|
47150
|
+
return [];
|
|
47151
|
+
}
|
|
47152
|
+
return [containerEl, ...followerElementsRef.current];
|
|
47153
|
+
};
|
|
47154
|
+
|
|
47155
|
+
// Which OTHER slide the picture leans on while it is not on the current one:
|
|
47156
|
+
// the slide being pulled in under a finger, the slide being left during a
|
|
47157
|
+
// travel. Two slides are in the frame and the number below says how far
|
|
47158
|
+
// between them one is — this says which the second one is, so a trait can be
|
|
47159
|
+
// drawn between two places rather than merely offset from one.
|
|
47160
|
+
const paintTravelToward = area => {
|
|
47161
|
+
for (const element of travelPainters()) {
|
|
47162
|
+
if (area) {
|
|
47163
|
+
element.setAttribute("data-slide-travel-toward", area);
|
|
47164
|
+
} else {
|
|
47165
|
+
element.removeAttribute("data-slide-travel-toward");
|
|
47166
|
+
}
|
|
47167
|
+
}
|
|
47168
|
+
};
|
|
47169
|
+
|
|
46600
47170
|
// Where the picture stands relative to the slide that is CURRENT, in boxes:
|
|
46601
47171
|
// 0 on it, +1 one whole box before it, -1 one box after. Written on the
|
|
46602
47172
|
// container so an indicator drawn inside the box — a tab bar, a dot row, a
|
|
@@ -46605,45 +47175,50 @@ const SlideContainer = ({
|
|
|
46605
47175
|
// gesture, so the number stays continuous when the travel commits and the
|
|
46606
47176
|
// current slide changes under it.
|
|
46607
47177
|
const paintTravelProgress = (progress, area) => {
|
|
46608
|
-
const
|
|
46609
|
-
|
|
46610
|
-
|
|
46611
|
-
|
|
46612
|
-
|
|
46613
|
-
|
|
46614
|
-
containerEl.removeAttribute("data-slide-travel-to");
|
|
46615
|
-
return;
|
|
47178
|
+
for (const element of travelPainters()) {
|
|
47179
|
+
if (progress) {
|
|
47180
|
+
element.style.setProperty("--slide-travel-progress", progress);
|
|
47181
|
+
} else {
|
|
47182
|
+
element.style.removeProperty("--slide-travel-progress");
|
|
47183
|
+
}
|
|
46616
47184
|
}
|
|
46617
|
-
|
|
46618
|
-
|
|
46619
|
-
|
|
46620
|
-
|
|
46621
|
-
|
|
47185
|
+
paintTravelToward(progress ? area : null);
|
|
47186
|
+
};
|
|
47187
|
+
const cancelTravelProgressAnimation = () => {
|
|
47188
|
+
for (const animation of progressAnimationsRef.current) {
|
|
47189
|
+
animation.cancel();
|
|
46622
47190
|
}
|
|
47191
|
+
progressAnimationsRef.current = [];
|
|
46623
47192
|
};
|
|
46624
47193
|
|
|
46625
47194
|
// The indicator, brought home at the pace of the travel it belongs to: the
|
|
46626
47195
|
// same duration and the same easing as the track, so the trait and the slides
|
|
46627
47196
|
// are one movement. The value it lands on is the one nothing writes (0), so
|
|
46628
|
-
// the animation is left to fall away on its own
|
|
46629
|
-
|
|
46630
|
-
|
|
46631
|
-
|
|
46632
|
-
|
|
47197
|
+
// the animation is left to fall away on its own — only the name of the slide
|
|
47198
|
+
// being leant on is taken back by hand, at the end.
|
|
47199
|
+
const animateTravelProgress = (from, durationMs, easing, area) => {
|
|
47200
|
+
// Read again at the start of every travel, not only at every render: a
|
|
47201
|
+
// follower appearing does not make this box render, and the travel it must
|
|
47202
|
+
// draw is the one about to start.
|
|
47203
|
+
followerElementsRef.current = readFollowerElements();
|
|
47204
|
+
cancelTravelProgressAnimation();
|
|
46633
47205
|
paintTravelProgress(0);
|
|
46634
|
-
|
|
47206
|
+
const painters = travelPainters();
|
|
47207
|
+
if (!painters.length || !from || !durationMs) {
|
|
46635
47208
|
return;
|
|
46636
47209
|
}
|
|
46637
|
-
|
|
47210
|
+
paintTravelToward(area);
|
|
47211
|
+
progressAnimationsRef.current = painters.map(element => element.animate([{
|
|
46638
47212
|
"--slide-travel-progress": from
|
|
46639
47213
|
}, {
|
|
46640
47214
|
"--slide-travel-progress": 0
|
|
46641
47215
|
}], {
|
|
46642
47216
|
duration: durationMs,
|
|
46643
47217
|
easing
|
|
46644
|
-
});
|
|
46645
|
-
|
|
46646
|
-
|
|
47218
|
+
}));
|
|
47219
|
+
progressAnimationsRef.current[0].finished.then(() => {
|
|
47220
|
+
progressAnimationsRef.current = [];
|
|
47221
|
+
paintTravelToward(null);
|
|
46647
47222
|
}, () => {
|
|
46648
47223
|
// cancelled by the next travel — that one says where the trait goes
|
|
46649
47224
|
});
|
|
@@ -46716,7 +47291,7 @@ const SlideContainer = ({
|
|
|
46716
47291
|
settleTravel();
|
|
46717
47292
|
return;
|
|
46718
47293
|
}
|
|
46719
|
-
animateTravelProgress(drag.progress, durationMs * (pulled / size), "ease-out");
|
|
47294
|
+
animateTravelProgress(drag.progress, durationMs * (pulled / size), "ease-out", drag.areaPulled);
|
|
46720
47295
|
const animation = track.animate([{
|
|
46721
47296
|
translate: drag.offset
|
|
46722
47297
|
}, {
|
|
@@ -46842,8 +47417,8 @@ const SlideContainer = ({
|
|
|
46842
47417
|
caughtTravel = null;
|
|
46843
47418
|
trackAnimationRef.current?.cancel();
|
|
46844
47419
|
trackAnimationRef.current = null;
|
|
46845
|
-
|
|
46846
|
-
|
|
47420
|
+
cancelTravelProgressAnimation();
|
|
47421
|
+
followerElementsRef.current = readFollowerElements();
|
|
46847
47422
|
drag.axis = axis;
|
|
46848
47423
|
drag.areaBack = areaBack;
|
|
46849
47424
|
drag.areaOn = areaOn;
|
|
@@ -46988,6 +47563,10 @@ const SlideContainer = ({
|
|
|
46988
47563
|
...handlers
|
|
46989
47564
|
});
|
|
46990
47565
|
if (!gesture) {
|
|
47566
|
+
// Not a press this box can be about — something that reads the pointer
|
|
47567
|
+
// itself, a box below it that travels the same way. Whatever the press
|
|
47568
|
+
// stopped on its way in goes back on its way.
|
|
47569
|
+
handlers.onGiveUp();
|
|
46991
47570
|
return;
|
|
46992
47571
|
}
|
|
46993
47572
|
handlers.drag.gesture = gesture;
|
|
@@ -47034,7 +47613,7 @@ const SlideContainer = ({
|
|
|
47034
47613
|
return () => {
|
|
47035
47614
|
dragRef.current?.gesture?.stop();
|
|
47036
47615
|
dragRef.current = null;
|
|
47037
|
-
|
|
47616
|
+
cancelTravelProgressAnimation();
|
|
47038
47617
|
};
|
|
47039
47618
|
}, []);
|
|
47040
47619
|
|
|
@@ -47099,10 +47678,16 @@ const SlideContainer = ({
|
|
|
47099
47678
|
"data-slide-container": ""
|
|
47100
47679
|
// Which axes a touch may travel on, said in the DOM: what the browser
|
|
47101
47680
|
// does with a finger is decided by CSS (touch-action) before any of this
|
|
47102
|
-
// has seen the gesture
|
|
47681
|
+
// has seen the gesture — and it is also what a box HOLDING this one reads
|
|
47682
|
+
// to know the gesture is not its own (see drag_to_travel.js).
|
|
47103
47683
|
,
|
|
47104
47684
|
|
|
47105
47685
|
"data-travel-by-drag": dragAxes ?? undefined
|
|
47686
|
+
// The same fact for a wheel, and only for that second reason: this box
|
|
47687
|
+
// takes the push, whatever the box around it also travels on.
|
|
47688
|
+
,
|
|
47689
|
+
|
|
47690
|
+
"data-travel-by-wheel": scrollAxes ?? undefined
|
|
47106
47691
|
// The same fact, read by the shared gesture stylesheet: what scrolls
|
|
47107
47692
|
// inside a box that travels must not spill onto the page behind it (see
|
|
47108
47693
|
// drag_to_travel.js).
|
|
@@ -48329,6 +48914,11 @@ const css$z = /* css */`
|
|
|
48329
48914
|
* content something depends on while the popup is still closed: a value read
|
|
48330
48915
|
* off it, fields a surrounding form collects on submit, a size measured from
|
|
48331
48916
|
* outside.
|
|
48917
|
+
* @param {boolean} [props.unmountWhenClosed] - Throws `children` away once the
|
|
48918
|
+
* popup has finished closing (see popup_content_mount.js). For content whose
|
|
48919
|
+
* fresh state is its initial state: an uncontrolled field seeded from a
|
|
48920
|
+
* `defaultValue` that changed while the popup was closed. Ignored when
|
|
48921
|
+
* `mountWhenClosed` is set.
|
|
48332
48922
|
* @param {import("ignore:preact").ComponentChildren} props.children
|
|
48333
48923
|
*/
|
|
48334
48924
|
const Popup = props => {
|
|
@@ -51323,10 +51913,17 @@ const ListUI = props => {
|
|
|
51323
51913
|
// search fallback), otherwise an empty list is the "empty" state.
|
|
51324
51914
|
const searchFallbackShown = (allNoMatch || searching && itemCount === 0) && !searchFallbackDisabled;
|
|
51325
51915
|
const emptyFallbackShown = !searching && itemCount === 0 && !fallbackDisabled;
|
|
51916
|
+
// A loading state only holds the list on screen when it has something to
|
|
51917
|
+
// draw. A count of 0 (or no loadingFallback at all) says the list is known to
|
|
51918
|
+
// be empty before the response arrives, so the empty state can already be
|
|
51919
|
+
// shown — nothing jumps when the response lands, exactly as three skeletons
|
|
51920
|
+
// become three rows.
|
|
51921
|
+
const loadingPlaceholderShown = Boolean(loading) && Boolean(loadingFallback) && (loadingFallback !== "skeleton" || loadingSkeletonCount > 0);
|
|
51326
51922
|
// Hide the whole list — border included — when there is genuinely nothing to
|
|
51327
|
-
// show: no visible items AND no fallback message. Never while loading
|
|
51328
|
-
//
|
|
51329
|
-
|
|
51923
|
+
// show: no visible items AND no fallback message. Never while a loading
|
|
51924
|
+
// placeholder or an error message is on screen (they ARE the content to
|
|
51925
|
+
// display).
|
|
51926
|
+
const nothingToDisplay = !loadingPlaceholderShown && !error && noVisibleItems && !searchFallbackShown && !emptyFallbackShown;
|
|
51330
51927
|
|
|
51331
51928
|
// Placeholder content replaces the real children: an error message when the
|
|
51332
51929
|
// load failed (takes precedence), otherwise — while loading — whatever
|
|
@@ -51421,7 +52018,7 @@ const ListUI = props => {
|
|
|
51421
52018
|
fallbackShown: emptyFallbackShown,
|
|
51422
52019
|
searchFallback: searchFallback,
|
|
51423
52020
|
searchFallbackShown: searchFallbackShown,
|
|
51424
|
-
|
|
52021
|
+
loadingPlaceholderShown: loadingPlaceholderShown,
|
|
51425
52022
|
error: error,
|
|
51426
52023
|
searchNoMatchMode: searchNoMatchMode,
|
|
51427
52024
|
separator: separator,
|
|
@@ -51521,6 +52118,10 @@ const ListFirstResolver = props => {
|
|
|
51521
52118
|
* displays nothing. A list that knows how many rows it will have has no use
|
|
51522
52119
|
* for this — see `<List.Items count>`, whose not-yet-loaded rows are drawn
|
|
51523
52120
|
* as skeletons in place, one per row, virtualized like the rest.
|
|
52121
|
+
* @param {number} [props.loadingSkeletonCount=3]
|
|
52122
|
+
* How many placeholder rows `loadingFallback="skeleton"` draws. `0` says the
|
|
52123
|
+
* list is already known to be empty: the empty `fallback` shows right away
|
|
52124
|
+
* rather than an empty frame, so nothing moves when the response arrives.
|
|
51524
52125
|
* @param {"start"|"end"|number|{id: string, offset?: number}} [props.defaultScrolled="start"]
|
|
51525
52126
|
* Where the list opens, after which the user owns the scroll. `"end"` is a
|
|
51526
52127
|
* thread read backwards — the last rows are the ones to show, and the ones
|
|
@@ -51581,7 +52182,7 @@ const ListContent = ({
|
|
|
51581
52182
|
fallbackShown,
|
|
51582
52183
|
searchFallback,
|
|
51583
52184
|
searchFallbackShown,
|
|
51584
|
-
|
|
52185
|
+
loadingPlaceholderShown,
|
|
51585
52186
|
error,
|
|
51586
52187
|
searchNoMatchMode,
|
|
51587
52188
|
separator,
|
|
@@ -51605,7 +52206,7 @@ const ListContent = ({
|
|
|
51605
52206
|
fallbackShown: fallbackShown,
|
|
51606
52207
|
searchFallback: searchFallback,
|
|
51607
52208
|
searchFallbackShown: searchFallbackShown,
|
|
51608
|
-
|
|
52209
|
+
loadingPlaceholderShown: loadingPlaceholderShown,
|
|
51609
52210
|
error: error,
|
|
51610
52211
|
searchNoMatchMode: searchNoMatchMode,
|
|
51611
52212
|
separator: separator,
|
|
@@ -52893,7 +53494,7 @@ const UnorderedList = ({
|
|
|
52893
53494
|
fallbackShown,
|
|
52894
53495
|
searchFallback,
|
|
52895
53496
|
searchFallbackShown,
|
|
52896
|
-
|
|
53497
|
+
loadingPlaceholderShown,
|
|
52897
53498
|
error,
|
|
52898
53499
|
searchNoMatchMode,
|
|
52899
53500
|
separator,
|
|
@@ -52904,9 +53505,11 @@ const UnorderedList = ({
|
|
|
52904
53505
|
children,
|
|
52905
53506
|
...rest
|
|
52906
53507
|
}) => {
|
|
52907
|
-
// No empty/no-match message while loading or
|
|
52908
|
-
//
|
|
52909
|
-
|
|
53508
|
+
// No empty/no-match message while a loading placeholder or an error message
|
|
53509
|
+
// is on screen — that IS the content, even though no items are tracked yet.
|
|
53510
|
+
// A loading state drawing nothing keeps the message: an announced count of 0
|
|
53511
|
+
// already tells us the list is empty (see ListUI's loadingPlaceholderShown).
|
|
53512
|
+
const suppressFallback = loadingPlaceholderShown || Boolean(error);
|
|
52910
53513
|
return jsxs(Box, {
|
|
52911
53514
|
as: "ul",
|
|
52912
53515
|
flex: columns ? undefined : horizontal ? "x" : "y",
|
|
@@ -59362,8 +59965,16 @@ const useWheelInteractions = ({
|
|
|
59362
59965
|
document.removeEventListener("wheel", onDocumentWheel, {
|
|
59363
59966
|
capture: true
|
|
59364
59967
|
});
|
|
59968
|
+
releaseWheelGesture(vp);
|
|
59365
59969
|
};
|
|
59366
59970
|
const keepClaimingGesture = () => {
|
|
59971
|
+
// Said out loud as well as swallowed: preventDefault only settles it with
|
|
59972
|
+
// the browser, and a box that travels with the wheel answers the burst
|
|
59973
|
+
// from a listener of its own — it asks who owns the gesture instead (see
|
|
59974
|
+
// wheel_gesture.js in @jsenv/dom).
|
|
59975
|
+
claimWheelGesture(vp, {
|
|
59976
|
+
delay: WHEEL_GESTURE_MAX_GAP
|
|
59977
|
+
});
|
|
59367
59978
|
if (!gestureGuardTimer) {
|
|
59368
59979
|
document.addEventListener("wheel", onDocumentWheel, {
|
|
59369
59980
|
capture: true,
|
|
@@ -59374,6 +59985,12 @@ const useWheelInteractions = ({
|
|
|
59374
59985
|
gestureGuardTimer = setTimeout(stopClaimingGesture, WHEEL_GESTURE_MAX_GAP);
|
|
59375
59986
|
};
|
|
59376
59987
|
const onWheel = e => {
|
|
59988
|
+
// The burst belongs to something else — a box that travels with the
|
|
59989
|
+
// wheel, another wheel the pointer has just left. It is theirs until the
|
|
59990
|
+
// events stop coming.
|
|
59991
|
+
if (wheelGestureIsTakenFrom(vp)) {
|
|
59992
|
+
return;
|
|
59993
|
+
}
|
|
59377
59994
|
const raw = isHorizontal ? e.deltaX || e.deltaY : e.deltaY;
|
|
59378
59995
|
if (!raw) {
|
|
59379
59996
|
return;
|
|
@@ -66817,5 +67434,5 @@ const UserSvg = () => jsx("svg", {
|
|
|
66817
67434
|
})
|
|
66818
67435
|
});
|
|
66819
67436
|
|
|
66820
|
-
export { ActionRenderer, ActiveKeyboardShortcuts, Address, Badge, BadgeCount, BadgeList, Binder, Box, Button, ButtonCopyToClipboard, Caption, CardLayout, CheckSvg, CheckboxGroup, CloseSvg, Code, Col, Colgroup, Color, ConstructionSvg, ControlGroup, DaySpin, Details, Dialog, Editable, ErrorBoundary, ErrorBoundaryContext, ExclamationSvg, EyeClosedSvg, EyeSvg, Field, FixedBar, Form, Group, Head, HeartSvg, HomeSvg, Icon, Image, Input, InputDuration, Interpolate, Label, Link, LinkAnchorSvg, LinkBlankTargetSvg, LinkCurrentSvg, List, ListItem, ListItemGroup, ListItems, Loading, LoadingDotsSvg, LoadingIndicator, LoadingIndicatorFluid, LoadingOutline, MessageBox, Meter, Nav, NaviDebug, NumberSpin, Paragraph, Picker, Popover, Popup, Quantity, RadioGroup, Route, RouteTravel, RowNumberCol, RowNumberTableCell, SVGMaskOverlay, SearchSvg, Select, SelectableInput, SelectionContext, Separator, SettingsSvg, SidePanel, Slide, SlideContainer, Spin, StarSvg, SummaryMarker, Svg, Table, TableCell, Tbody, Text, TextBox, Textarea, TextareaCharCount, Thead, Time, Title, Tr, UITransition, Unit, UserSvg, ViewportLayout, Wheel, WheelGroup, WheelItem, actionRunEffect, anyMatchingRouteSignal, applySearch, arraySignalMembership, coarsePointerSignal, compareTwoJsValues, createAction, createAvailableConstraint, createRequestCanceller, createSearch, createSelectionKeyboardShortcuts, createSlot, defineNaviConfirmPopupOptions, detectHorizontalOverflow, enableDebugActions, enableDebugOnDocumentLoading, ensureDocumentStartViewTransition, filterTableSelection, formatDatetime, formatDay, formatDayRelative, formatMonth, formatNumber, formatTime, formatTimeRelative, getNowHours, getNowHoursRoundedToStep, interpolateText, isCellSelected, isColumnSelected, isRowSelected, isToday, languagesSignal, localStorageSignal, moveArrayItemByIndex, navBack, navForward, navIntegratedVia, navTo, naviI18n, openCallout, rawUrlPart, registerGlobalConstraint, reload, rerunActions, resource, route, routeAction, setBaseUrl, setPreferredLanguage, setSupportedLanguages, setupRoutes, stateSignal, stopLoad, stringifyTableSelectionValue, swapArrayItemByIndex, syncOwnedResourceToSignals, syncResourceToSignals, updateActions, useActionStatus, useArraySignalMembership, useAsyncData, useCalloutRequestClose, useCancelPrevious, useCellGridFromRows, useConstraintValidityState, useDependenciesDiff, useDisplayedLayoutEffect, useDocumentResource, useDocumentState, useDocumentUrl, useEditionController, useFocusGroup, useInputGroup, useKeyboardShortcuts, useNavState, useOrderedColumns, usePopupMode, useRouteStatus, useRunOnMount, useSearchText, useSelectableElement, useSelectionController, useSignalSync, useSlideValue, useStateArray, useTitleLevel, useUrlSearchParam, valueInLocalStorage, windowWidthSignal };
|
|
67437
|
+
export { ActionRenderer, ActiveKeyboardShortcuts, Address, Badge, BadgeCount, BadgeList, Binder, Box, Button, ButtonCopyToClipboard, Caption, CardLayout, CheckSvg, CheckboxGroup, CloseSvg, Code, Col, Colgroup, Color, ConstructionSvg, ControlGroup, DaySpin, Details, Dialog, Editable, ErrorBoundary, ErrorBoundaryContext, ExclamationSvg, EyeClosedSvg, EyeSvg, Field, FixedBar, Form, Group, Head, HeartSvg, HomeSvg, Icon, Image, Input, InputDuration, Interpolate, Label, Link, LinkAnchorSvg, LinkBlankTargetSvg, LinkCurrentSvg, List, ListItem, ListItemGroup, ListItems, Loading, LoadingDotsSvg, LoadingIndicator, LoadingIndicatorFluid, LoadingOutline, MessageBox, Meter, Nav, NaviDebug, NumberSpin, Paragraph, Picker, Popover, Popup, Quantity, RadioGroup, Route, RouteTravel, RowNumberCol, RowNumberTableCell, SVGMaskOverlay, SearchSvg, Select, SelectableInput, SelectionContext, Separator, SettingsSvg, SidePanel, Slide, SlideContainer, Spin, StarSvg, SummaryMarker, Svg, Table, TableCell, Tbody, Text, TextBox, Textarea, TextareaCharCount, Thead, Time, Title, Tr, UITransition, Unit, UserSvg, ViewportLayout, Wheel, WheelGroup, WheelItem, actionRunEffect, anyMatchingRouteSignal, applySearch, arraySignalMembership, coarsePointerSignal, compareTwoJsValues, createAction, createAvailableConstraint, createRequestCanceller, createSearch, createSelectionKeyboardShortcuts, createSlot, defineNaviConfirmPopupOptions, detectHorizontalOverflow, enableDebugActions, enableDebugOnDocumentLoading, ensureDocumentStartViewTransition, filterTableSelection, formatDatetime, formatDay, formatDayRelative, formatMonth, formatNumber, formatTime, formatTimeRelative, getNowHours, getNowHoursRoundedToStep, interpolateText, isCellSelected, isColumnSelected, isRowSelected, isToday, languagesSignal, localStorageSignal, moveArrayItemByIndex, navBack, navForward, navIntegratedVia, navTo, naviI18n, openCallout, rawUrlPart, registerGlobalConstraint, reload, rerunActions, resource, route, routeAction, setBaseUrl, setPreferredLanguage, setSupportedLanguages, setupRoutes, stateSignal, stopLoad, stringifyTableSelectionValue, swapArrayItemByIndex, syncOwnedResourceToSignals, syncResourceToSignals, triggerNaviCommand, updateActions, useActionStatus, useArraySignalMembership, useAsyncData, useCalloutRequestClose, useCancelPrevious, useCellGridFromRows, useConstraintValidityState, useDependenciesDiff, useDisplayedLayoutEffect, useDocumentResource, useDocumentState, useDocumentUrl, useEditionController, useFocusGroup, useInputGroup, useKeyboardShortcuts, useNavState, useOrderedColumns, usePopupMode, useRouteStatus, useRunOnMount, useSearchText, useSelectableElement, useSelectionController, useSignalSync, useSlideValue, useStateArray, useTitleLevel, useUrlSearchParam, valueInLocalStorage, windowWidthSignal };
|
|
66821
67438
|
//# sourceMappingURL=jsenv_navi.js.map
|