@jsenv/navi 0.29.507 → 0.29.509
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/dist/dev/jsenv_navi.js +119 -78
- package/dist/dev/jsenv_navi.js.map +5 -5
- package/dist/jsenv_navi.js +119 -78
- package/dist/jsenv_navi.js.map +5 -5
- package/docs/navigation.md +9 -6
- package/package.json +1 -1
package/dist/jsenv_navi.js
CHANGED
|
@@ -8553,7 +8553,7 @@ const route = (pattern, options = {}) => {
|
|
|
8553
8553
|
};
|
|
8554
8554
|
route.redirectTo = (
|
|
8555
8555
|
params,
|
|
8556
|
-
{ callReason, history = "replace", debounceUrl } = {},
|
|
8556
|
+
{ callReason, history = "replace", debounceUrl, isSignalChange } = {},
|
|
8557
8557
|
) => {
|
|
8558
8558
|
if (!integration) {
|
|
8559
8559
|
return Promise.resolve();
|
|
@@ -8563,6 +8563,7 @@ const route = (pattern, options = {}) => {
|
|
|
8563
8563
|
replace: history !== "push",
|
|
8564
8564
|
debounce: debounceUrl,
|
|
8565
8565
|
callReason,
|
|
8566
|
+
fromSignal: isSignalChange,
|
|
8566
8567
|
});
|
|
8567
8568
|
};
|
|
8568
8569
|
route.replaceParams = (
|
|
@@ -8577,52 +8578,18 @@ const route = (pattern, options = {}) => {
|
|
|
8577
8578
|
return null;
|
|
8578
8579
|
}
|
|
8579
8580
|
|
|
8580
|
-
|
|
8581
|
-
let mostSpecificRoute = route;
|
|
8582
|
-
const routePrivateProperties = getRoutePrivateProperties(route);
|
|
8583
|
-
let maxDepth = routePrivateProperties.routePattern.depth;
|
|
8584
|
-
for (const routeCandidate of routeSet) {
|
|
8585
|
-
if (routeCandidate === route) {
|
|
8586
|
-
continue;
|
|
8587
|
-
}
|
|
8588
|
-
if (!routeCandidate.matching) {
|
|
8589
|
-
continue;
|
|
8590
|
-
}
|
|
8591
|
-
const matchingRoute = routeCandidate;
|
|
8592
|
-
const matchingRoutePrivateProperties =
|
|
8593
|
-
getRoutePrivateProperties(matchingRoute);
|
|
8594
|
-
const depth = matchingRoutePrivateProperties.routePattern.depth;
|
|
8595
|
-
if (depth > maxDepth) {
|
|
8596
|
-
maxDepth = depth;
|
|
8597
|
-
mostSpecificRoute = matchingRoute;
|
|
8598
|
-
}
|
|
8599
|
-
}
|
|
8581
|
+
const mostSpecificRoute = findMostSpecificMatchingRoute(route, routeSet);
|
|
8600
8582
|
const isMostSpecificRoute = mostSpecificRoute === route;
|
|
8601
8583
|
|
|
8602
|
-
// If we found a more specific route, delegate to it; otherwise handle it ourselves
|
|
8584
|
+
// If we found a more specific route, delegate to it; otherwise handle it ourselves.
|
|
8585
|
+
// A signal change reaches this only when the more specific route does not
|
|
8586
|
+
// carry the param (see writeValueInUrl): left to it, the change would be dropped.
|
|
8603
8587
|
if (!isMostSpecificRoute) {
|
|
8604
|
-
// For signal-originated calls, only skip delegation if the more specific route
|
|
8605
|
-
// has its own signal connection for the same params — meaning its own effect will
|
|
8606
|
-
// fire and handle the redirect. If it doesn't have the connection, the signal
|
|
8607
|
-
// change would be silently dropped, so we must delegate anyway.
|
|
8608
|
-
if (isSignalChange) {
|
|
8609
|
-
const mostSpecificRoutePrivateProperties =
|
|
8610
|
-
getRoutePrivateProperties(mostSpecificRoute);
|
|
8611
|
-
const { pathConnectionMap, queryConnectionMap } =
|
|
8612
|
-
mostSpecificRoutePrivateProperties.routePattern;
|
|
8613
|
-
const willHandleItself = Object.keys(newParams).every(
|
|
8614
|
-
(paramName) =>
|
|
8615
|
-
pathConnectionMap.has(paramName) ||
|
|
8616
|
-
queryConnectionMap.has(paramName),
|
|
8617
|
-
);
|
|
8618
|
-
if (willHandleItself) {
|
|
8619
|
-
return null;
|
|
8620
|
-
}
|
|
8621
|
-
}
|
|
8622
8588
|
return mostSpecificRoute.redirectTo(newParams, {
|
|
8623
8589
|
callReason: `replaceParams delegation from ${route} to ${mostSpecificRoute} (original reason: ${callReason})`,
|
|
8624
8590
|
history,
|
|
8625
8591
|
debounceUrl,
|
|
8592
|
+
isSignalChange,
|
|
8626
8593
|
});
|
|
8627
8594
|
}
|
|
8628
8595
|
|
|
@@ -8641,6 +8608,7 @@ const route = (pattern, options = {}) => {
|
|
|
8641
8608
|
replace: history !== "push",
|
|
8642
8609
|
debounce: debounceUrl,
|
|
8643
8610
|
callReason,
|
|
8611
|
+
fromSignal: isSignalChange,
|
|
8644
8612
|
});
|
|
8645
8613
|
};
|
|
8646
8614
|
route.matchesParams = (providedParams) => {
|
|
@@ -8703,7 +8671,7 @@ const route = (pattern, options = {}) => {
|
|
|
8703
8671
|
});
|
|
8704
8672
|
// Signal -> URL sync: When signal changes, update URL to reflect meaningful state
|
|
8705
8673
|
// Only sync non-default values to keep URLs clean (static fallbacks stay invisible)
|
|
8706
|
-
registerSetup(() => {
|
|
8674
|
+
registerSetup(({ routeSet }) => {
|
|
8707
8675
|
const cleanupSignalUrlEffectSet = new Set();
|
|
8708
8676
|
// important: read connections at setup time so it includes query connections
|
|
8709
8677
|
// inherited from ancestor patterns
|
|
@@ -8742,6 +8710,24 @@ const route = (pattern, options = {}) => {
|
|
|
8742
8710
|
// Route not matching, no URL sync needed
|
|
8743
8711
|
return;
|
|
8744
8712
|
}
|
|
8713
|
+
// A param declared on an ancestor is carried by every route below it,
|
|
8714
|
+
// each with its own connection: all of them hear the write, the most
|
|
8715
|
+
// specific one makes it. The others step aside before the history is
|
|
8716
|
+
// read — a stateSignalGroup's push goes to the first write reading it.
|
|
8717
|
+
const mostSpecificRoute = findMostSpecificMatchingRoute(
|
|
8718
|
+
route,
|
|
8719
|
+
routeSet,
|
|
8720
|
+
);
|
|
8721
|
+
if (mostSpecificRoute !== route) {
|
|
8722
|
+
const { pathConnectionMap, queryConnectionMap } =
|
|
8723
|
+
getRoutePrivateProperties(mostSpecificRoute).routePattern;
|
|
8724
|
+
if (
|
|
8725
|
+
pathConnectionMap.has(paramName) ||
|
|
8726
|
+
queryConnectionMap.has(paramName)
|
|
8727
|
+
) {
|
|
8728
|
+
return;
|
|
8729
|
+
}
|
|
8730
|
+
}
|
|
8745
8731
|
if (urlParamValue === undefined) {
|
|
8746
8732
|
// No URL parameter exists - check if signal has meaningful value to add
|
|
8747
8733
|
if (connection.isDefaultValue(value)) {
|
|
@@ -9515,6 +9501,27 @@ const urlWithoutSearchAndHash = (url) => {
|
|
|
9515
9501
|
return `${urlObject.origin}${urlObject.pathname}`;
|
|
9516
9502
|
};
|
|
9517
9503
|
|
|
9504
|
+
// Deeper is more specific: among the routes matching the address, the one a
|
|
9505
|
+
// params write lands on.
|
|
9506
|
+
const findMostSpecificMatchingRoute = (route, routeSet) => {
|
|
9507
|
+
let mostSpecificRoute = route;
|
|
9508
|
+
let maxDepth = getRoutePrivateProperties(route).routePattern.depth;
|
|
9509
|
+
for (const routeCandidate of routeSet) {
|
|
9510
|
+
if (routeCandidate === route) {
|
|
9511
|
+
continue;
|
|
9512
|
+
}
|
|
9513
|
+
if (!routeCandidate.matching) {
|
|
9514
|
+
continue;
|
|
9515
|
+
}
|
|
9516
|
+
const depth = getRoutePrivateProperties(routeCandidate).routePattern.depth;
|
|
9517
|
+
if (depth > maxDepth) {
|
|
9518
|
+
maxDepth = depth;
|
|
9519
|
+
mostSpecificRoute = routeCandidate;
|
|
9520
|
+
}
|
|
9521
|
+
}
|
|
9522
|
+
return mostSpecificRoute;
|
|
9523
|
+
};
|
|
9524
|
+
|
|
9518
9525
|
// Every route action, with whether it asks something of the address at the
|
|
9519
9526
|
// moment: its route matches and its params getter returns something.
|
|
9520
9527
|
const routeActionEntriesSignal = signal([]);
|
|
@@ -13890,11 +13897,15 @@ const setupBrowserIntegrationViaHistory = ({
|
|
|
13890
13897
|
// browser answers — a before/after cycle here would be about a navigation
|
|
13891
13898
|
// that never happens. A push with something of its own for its entry — a
|
|
13892
13899
|
// state, what the press handed over (see carryByLinkNavigation) — is a
|
|
13893
|
-
// new entry, never one standing there already.
|
|
13900
|
+
// new entry, never one standing there already. So is a push a signal write
|
|
13901
|
+
// makes: the writes after it in the same move (a layer, then what is open
|
|
13902
|
+
// inside it) build on its address at once, and a traversal would only land
|
|
13903
|
+
// after they went to the entry being left.
|
|
13894
13904
|
if (
|
|
13895
13905
|
options.navigationType === "push" &&
|
|
13896
13906
|
options.state === undefined &&
|
|
13897
13907
|
!options.pressWrites &&
|
|
13908
|
+
!options.fromSignal &&
|
|
13898
13909
|
url !== readAddress()
|
|
13899
13910
|
) {
|
|
13900
13911
|
const delta = adjacentEntryDelta(url);
|
|
@@ -14317,12 +14328,23 @@ const setupBrowserIntegrationViaHistory = ({
|
|
|
14317
14328
|
|
|
14318
14329
|
const navTo = async (
|
|
14319
14330
|
url,
|
|
14320
|
-
{
|
|
14331
|
+
{
|
|
14332
|
+
replace,
|
|
14333
|
+
state,
|
|
14334
|
+
routeTransition,
|
|
14335
|
+
debounce,
|
|
14336
|
+
asReturn,
|
|
14337
|
+
open,
|
|
14338
|
+
fromSignal,
|
|
14339
|
+
} = {},
|
|
14321
14340
|
) => {
|
|
14322
14341
|
const routingOptions = {
|
|
14323
14342
|
reason: `navTo called with "${url}"`,
|
|
14324
14343
|
navigationType: replace ? "replace" : "push",
|
|
14325
14344
|
state,
|
|
14345
|
+
// Made by a signal written into the url (see route.js writeValueInUrl):
|
|
14346
|
+
// such a push is never turned into a traversal (see handleRoutingTask).
|
|
14347
|
+
fromSignal,
|
|
14326
14348
|
// A push that lands where its address was last left (see
|
|
14327
14349
|
// link_return.js).
|
|
14328
14350
|
asReturn,
|
|
@@ -55951,6 +55973,10 @@ const css$$ = /* css */`@layer navi {
|
|
|
55951
55973
|
--link-color: unset;
|
|
55952
55974
|
--link-text-decoration: none;
|
|
55953
55975
|
}
|
|
55976
|
+
|
|
55977
|
+
.navi_link[data-color-inherit] {
|
|
55978
|
+
--link-color-visited: var(--navi-link-color-visited, var(--link-color));
|
|
55979
|
+
}
|
|
55954
55980
|
}
|
|
55955
55981
|
|
|
55956
55982
|
.navi_link {
|
|
@@ -56767,6 +56793,7 @@ const LinkPlain = props => {
|
|
|
56767
56793
|
// Visual
|
|
56768
56794
|
,
|
|
56769
56795
|
"data-variant": variant,
|
|
56796
|
+
"data-color-inherit": props.color === "inherit" ? "" : undefined,
|
|
56770
56797
|
"data-current-effect-bold": currentEffectBoldTechnique,
|
|
56771
56798
|
"data-current-effect-shadow": currentEffectShadow ? "" : undefined,
|
|
56772
56799
|
"data-current-indicator-position": currentIndicatorPosition,
|
|
@@ -68838,7 +68865,7 @@ const SlideContainer = ({
|
|
|
68838
68865
|
// A transfer waiting for a travel that never happened — a controlled
|
|
68839
68866
|
// `current` the caller chose not to move: dropped, the focus has no
|
|
68840
68867
|
// business going anywhere. The one for a travel that DID happen stays,
|
|
68841
|
-
// and lands
|
|
68868
|
+
// and lands once the slide it is for is final (see landFocusHandOver).
|
|
68842
68869
|
if (focusHandOverRef.current && focusHandOverRef.current.area !== readArea(currentElement)) {
|
|
68843
68870
|
focusHandOverRef.current = null;
|
|
68844
68871
|
}
|
|
@@ -68992,6 +69019,11 @@ const SlideContainer = ({
|
|
|
68992
69019
|
requestAnimationFrame(rollBack);
|
|
68993
69020
|
}
|
|
68994
69021
|
}
|
|
69022
|
+
// The slide a transfer waits for is current (any other one was dropped
|
|
69023
|
+
// above): it lands once the renders this commit sets off are over.
|
|
69024
|
+
if (focusHandOverRef.current) {
|
|
69025
|
+
landFocusHandOver();
|
|
69026
|
+
}
|
|
68995
69027
|
// The keyboard is on a slide about to go out of reach and nobody has moved
|
|
68996
69028
|
// it: inert would drop it on the floor (document.body), so it is handed to
|
|
68997
69029
|
// the slide on screen instead. No event to read — this travel was asked for
|
|
@@ -69009,9 +69041,10 @@ const SlideContainer = ({
|
|
|
69009
69041
|
// Out of reach LAST, once the keyboard has already moved on: a browser
|
|
69010
69042
|
// takes the focus off an element the moment it becomes inert, and on its
|
|
69011
69043
|
// own schedule — doing this first would let that undo the landing above and
|
|
69012
|
-
// leave the focus on nothing.
|
|
69013
|
-
|
|
69014
|
-
|
|
69044
|
+
// leave the focus on nothing. A transfer still on its way does it itself,
|
|
69045
|
+
// once landed.
|
|
69046
|
+
if (!focusHandOverRef.current) {
|
|
69047
|
+
markOutOfReach(currentElement);
|
|
69015
69048
|
}
|
|
69016
69049
|
// What is on screen now, for the travel after this one to depart from.
|
|
69017
69050
|
// While a travel is playing the stage is the one that knows (it holds the
|
|
@@ -69362,14 +69395,7 @@ const SlideContainer = ({
|
|
|
69362
69395
|
// dialog is OPEN, not when it was asked to open). `prepareFocusTransfer` has
|
|
69363
69396
|
// to read the interaction — which element a mousedown landed on, whether the
|
|
69364
69397
|
// modality is the keyboard — while the event is still being dispatched. The
|
|
69365
|
-
// landing waits for the slide to
|
|
69366
|
-
// which is later than one would think: a travel hands the slide a value
|
|
69367
|
-
// (useSlideValue) and that value reaches it through context, so Preact
|
|
69368
|
-
// re-renders the slide in a flush of its own — the container's own layout
|
|
69369
|
-
// effect runs BEFORE its children have seen the value. A screen keyed by it,
|
|
69370
|
-
// a form whose uncontrolled fields must start again from a new prefill,
|
|
69371
|
-
// replaces its whole subtree there. Landing before that took the focus to a
|
|
69372
|
-
// node about to be thrown away, and the focus went to document.body with it.
|
|
69398
|
+
// landing waits for the slide's DOM to be final (see landFocusHandOver).
|
|
69373
69399
|
const handOverFocus = (slideElement, focusTransfer, event) => {
|
|
69374
69400
|
focusTransfer.transferFocus(event, slideElement);
|
|
69375
69401
|
// Asked of the SLIDE, not of this box: the box contains itself, so asking
|
|
@@ -69394,21 +69420,46 @@ const SlideContainer = ({
|
|
|
69394
69420
|
}
|
|
69395
69421
|
};
|
|
69396
69422
|
|
|
69397
|
-
//
|
|
69398
|
-
//
|
|
69399
|
-
//
|
|
69400
|
-
// and
|
|
69401
|
-
|
|
69423
|
+
// The transfer prepared in goToArea lands once the slide it is for is the
|
|
69424
|
+
// current one AND everything the travel set off has rendered — which is
|
|
69425
|
+
// later than this box's own commit. A travel hands the slide a value
|
|
69426
|
+
// (useSlideValue), and what reads it below a component that skipped its own
|
|
69427
|
+
// render is re-rendered by Preact in a commit of its own, after this one. A
|
|
69428
|
+
// screen keyed by it, a form whose uncontrolled fields must start again from
|
|
69429
|
+
// a new prefill, replaces its whole subtree there: a focus landed before it
|
|
69430
|
+
// goes to document.body with the node. Nor can the slide say when it is done
|
|
69431
|
+
// from its own layout effect: rendered in the same pass as this box, its
|
|
69432
|
+
// effects run before this box has made it current.
|
|
69433
|
+
// Preact drains its render queue in one go, the renders it cascades into
|
|
69434
|
+
// included, so a microtask queued from this commit runs after all of them.
|
|
69435
|
+
// Until then the slide being left stays within reach (see markOutOfReach):
|
|
69436
|
+
// inert would take the keyboard off it first, and drop it on document.body.
|
|
69437
|
+
const landFocusHandOver = () => {
|
|
69402
69438
|
const focusHandOver = focusHandOverRef.current;
|
|
69403
|
-
|
|
69404
|
-
|
|
69405
|
-
|
|
69406
|
-
|
|
69407
|
-
|
|
69408
|
-
|
|
69439
|
+
queueMicrotask(() => {
|
|
69440
|
+
// Already landed by an earlier commit of this cascade, given up on, or
|
|
69441
|
+
// replaced by the transfer of a newer travel.
|
|
69442
|
+
if (focusHandOverRef.current !== focusHandOver || !trackRef.current) {
|
|
69443
|
+
return;
|
|
69444
|
+
}
|
|
69445
|
+
focusHandOverRef.current = null;
|
|
69446
|
+
const currentElement = readMap().slideElements.find(slideElement => slideElement.hasAttribute("data-current"));
|
|
69447
|
+
if (!currentElement) {
|
|
69448
|
+
return;
|
|
69449
|
+
}
|
|
69450
|
+
if (readArea(currentElement) === focusHandOver.area) {
|
|
69451
|
+
handOverFocus(currentElement, focusHandOver.focusTransfer, focusHandOver.event);
|
|
69452
|
+
}
|
|
69453
|
+
markOutOfReach(currentElement);
|
|
69454
|
+
});
|
|
69455
|
+
};
|
|
69456
|
+
|
|
69457
|
+
// Every slide but the one on screen out of reach — of the keyboard and of
|
|
69458
|
+
// assistive technology alike.
|
|
69459
|
+
const markOutOfReach = currentElement => {
|
|
69460
|
+
for (const slideElement of readMap().slideElements) {
|
|
69461
|
+
slideElement.toggleAttribute("inert", slideElement !== currentElement);
|
|
69409
69462
|
}
|
|
69410
|
-
focusHandOverRef.current = null;
|
|
69411
|
-
handOverFocus(slideElement, focusHandOver.focusTransfer, focusHandOver.event);
|
|
69412
69463
|
};
|
|
69413
69464
|
|
|
69414
69465
|
/**
|
|
@@ -70596,7 +70647,6 @@ const SlideContainer = ({
|
|
|
70596
70647
|
answeredAreas,
|
|
70597
70648
|
done,
|
|
70598
70649
|
valueByArea,
|
|
70599
|
-
settleFocus,
|
|
70600
70650
|
// What a slide reads to know whether its content is rendered yet
|
|
70601
70651
|
// (see Slide's `mount`).
|
|
70602
70652
|
mount,
|
|
@@ -70680,15 +70730,6 @@ const Slide = ({
|
|
|
70680
70730
|
}), [nextIsLocked, preventNavPrevious]);
|
|
70681
70731
|
// What the travel into this slide carried, if anything (see useSlideValue).
|
|
70682
70732
|
const slideValue = container?.valueByArea?.[slideArea];
|
|
70683
|
-
// This slide's DOM is settled — said after EVERY commit, no dependency
|
|
70684
|
-
// array, because it is a fact about the render that just happened, not a
|
|
70685
|
-
// reaction to a particular prop. The container is the one holding a focus
|
|
70686
|
-
// transfer waiting for it (see settleFocus): a travel's value reaches this
|
|
70687
|
-
// slide through context one flush after the container moved, and what it
|
|
70688
|
-
// shows (a form keyed by a prefill) is only final here.
|
|
70689
|
-
useLayoutEffect(() => {
|
|
70690
|
-
container?.settleFocus?.(slideArea);
|
|
70691
|
-
});
|
|
70692
70733
|
return jsx(SlideContext.Provider, {
|
|
70693
70734
|
value: locks,
|
|
70694
70735
|
children: jsx(SlideValueContext.Provider, {
|