@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/dev/jsenv_navi.js
CHANGED
|
@@ -8918,7 +8918,7 @@ const route = (pattern, options = {}) => {
|
|
|
8918
8918
|
};
|
|
8919
8919
|
route.redirectTo = (
|
|
8920
8920
|
params,
|
|
8921
|
-
{ callReason, history = "replace", debounceUrl } = {},
|
|
8921
|
+
{ callReason, history = "replace", debounceUrl, isSignalChange } = {},
|
|
8922
8922
|
) => {
|
|
8923
8923
|
if (!integration) {
|
|
8924
8924
|
{
|
|
@@ -8933,6 +8933,7 @@ const route = (pattern, options = {}) => {
|
|
|
8933
8933
|
replace: history !== "push",
|
|
8934
8934
|
debounce: debounceUrl,
|
|
8935
8935
|
callReason,
|
|
8936
|
+
fromSignal: isSignalChange,
|
|
8936
8937
|
});
|
|
8937
8938
|
};
|
|
8938
8939
|
route.replaceParams = (
|
|
@@ -8947,52 +8948,18 @@ const route = (pattern, options = {}) => {
|
|
|
8947
8948
|
return null;
|
|
8948
8949
|
}
|
|
8949
8950
|
|
|
8950
|
-
|
|
8951
|
-
let mostSpecificRoute = route;
|
|
8952
|
-
const routePrivateProperties = getRoutePrivateProperties(route);
|
|
8953
|
-
let maxDepth = routePrivateProperties.routePattern.depth;
|
|
8954
|
-
for (const routeCandidate of routeSet) {
|
|
8955
|
-
if (routeCandidate === route) {
|
|
8956
|
-
continue;
|
|
8957
|
-
}
|
|
8958
|
-
if (!routeCandidate.matching) {
|
|
8959
|
-
continue;
|
|
8960
|
-
}
|
|
8961
|
-
const matchingRoute = routeCandidate;
|
|
8962
|
-
const matchingRoutePrivateProperties =
|
|
8963
|
-
getRoutePrivateProperties(matchingRoute);
|
|
8964
|
-
const depth = matchingRoutePrivateProperties.routePattern.depth;
|
|
8965
|
-
if (depth > maxDepth) {
|
|
8966
|
-
maxDepth = depth;
|
|
8967
|
-
mostSpecificRoute = matchingRoute;
|
|
8968
|
-
}
|
|
8969
|
-
}
|
|
8951
|
+
const mostSpecificRoute = findMostSpecificMatchingRoute(route, routeSet);
|
|
8970
8952
|
const isMostSpecificRoute = mostSpecificRoute === route;
|
|
8971
8953
|
|
|
8972
|
-
// If we found a more specific route, delegate to it; otherwise handle it ourselves
|
|
8954
|
+
// If we found a more specific route, delegate to it; otherwise handle it ourselves.
|
|
8955
|
+
// A signal change reaches this only when the more specific route does not
|
|
8956
|
+
// carry the param (see writeValueInUrl): left to it, the change would be dropped.
|
|
8973
8957
|
if (!isMostSpecificRoute) {
|
|
8974
|
-
// For signal-originated calls, only skip delegation if the more specific route
|
|
8975
|
-
// has its own signal connection for the same params — meaning its own effect will
|
|
8976
|
-
// fire and handle the redirect. If it doesn't have the connection, the signal
|
|
8977
|
-
// change would be silently dropped, so we must delegate anyway.
|
|
8978
|
-
if (isSignalChange) {
|
|
8979
|
-
const mostSpecificRoutePrivateProperties =
|
|
8980
|
-
getRoutePrivateProperties(mostSpecificRoute);
|
|
8981
|
-
const { pathConnectionMap, queryConnectionMap } =
|
|
8982
|
-
mostSpecificRoutePrivateProperties.routePattern;
|
|
8983
|
-
const willHandleItself = Object.keys(newParams).every(
|
|
8984
|
-
(paramName) =>
|
|
8985
|
-
pathConnectionMap.has(paramName) ||
|
|
8986
|
-
queryConnectionMap.has(paramName),
|
|
8987
|
-
);
|
|
8988
|
-
if (willHandleItself) {
|
|
8989
|
-
return null;
|
|
8990
|
-
}
|
|
8991
|
-
}
|
|
8992
8958
|
return mostSpecificRoute.redirectTo(newParams, {
|
|
8993
8959
|
callReason: `replaceParams delegation from ${route} to ${mostSpecificRoute} (original reason: ${callReason})`,
|
|
8994
8960
|
history,
|
|
8995
8961
|
debounceUrl,
|
|
8962
|
+
isSignalChange,
|
|
8996
8963
|
});
|
|
8997
8964
|
}
|
|
8998
8965
|
|
|
@@ -9016,6 +8983,7 @@ const route = (pattern, options = {}) => {
|
|
|
9016
8983
|
replace: history !== "push",
|
|
9017
8984
|
debounce: debounceUrl,
|
|
9018
8985
|
callReason,
|
|
8986
|
+
fromSignal: isSignalChange,
|
|
9019
8987
|
});
|
|
9020
8988
|
};
|
|
9021
8989
|
route.matchesParams = (providedParams) => {
|
|
@@ -9078,7 +9046,7 @@ const route = (pattern, options = {}) => {
|
|
|
9078
9046
|
});
|
|
9079
9047
|
// Signal -> URL sync: When signal changes, update URL to reflect meaningful state
|
|
9080
9048
|
// Only sync non-default values to keep URLs clean (static fallbacks stay invisible)
|
|
9081
|
-
registerSetup(() => {
|
|
9049
|
+
registerSetup(({ routeSet }) => {
|
|
9082
9050
|
const cleanupSignalUrlEffectSet = new Set();
|
|
9083
9051
|
// important: read connections at setup time so it includes query connections
|
|
9084
9052
|
// inherited from ancestor patterns
|
|
@@ -9117,6 +9085,24 @@ const route = (pattern, options = {}) => {
|
|
|
9117
9085
|
// Route not matching, no URL sync needed
|
|
9118
9086
|
return;
|
|
9119
9087
|
}
|
|
9088
|
+
// A param declared on an ancestor is carried by every route below it,
|
|
9089
|
+
// each with its own connection: all of them hear the write, the most
|
|
9090
|
+
// specific one makes it. The others step aside before the history is
|
|
9091
|
+
// read — a stateSignalGroup's push goes to the first write reading it.
|
|
9092
|
+
const mostSpecificRoute = findMostSpecificMatchingRoute(
|
|
9093
|
+
route,
|
|
9094
|
+
routeSet,
|
|
9095
|
+
);
|
|
9096
|
+
if (mostSpecificRoute !== route) {
|
|
9097
|
+
const { pathConnectionMap, queryConnectionMap } =
|
|
9098
|
+
getRoutePrivateProperties(mostSpecificRoute).routePattern;
|
|
9099
|
+
if (
|
|
9100
|
+
pathConnectionMap.has(paramName) ||
|
|
9101
|
+
queryConnectionMap.has(paramName)
|
|
9102
|
+
) {
|
|
9103
|
+
return;
|
|
9104
|
+
}
|
|
9105
|
+
}
|
|
9120
9106
|
if (urlParamValue === undefined) {
|
|
9121
9107
|
// No URL parameter exists - check if signal has meaningful value to add
|
|
9122
9108
|
if (connection.isDefaultValue(value)) {
|
|
@@ -9909,6 +9895,27 @@ const urlWithoutSearchAndHash = (url) => {
|
|
|
9909
9895
|
return `${urlObject.origin}${urlObject.pathname}`;
|
|
9910
9896
|
};
|
|
9911
9897
|
|
|
9898
|
+
// Deeper is more specific: among the routes matching the address, the one a
|
|
9899
|
+
// params write lands on.
|
|
9900
|
+
const findMostSpecificMatchingRoute = (route, routeSet) => {
|
|
9901
|
+
let mostSpecificRoute = route;
|
|
9902
|
+
let maxDepth = getRoutePrivateProperties(route).routePattern.depth;
|
|
9903
|
+
for (const routeCandidate of routeSet) {
|
|
9904
|
+
if (routeCandidate === route) {
|
|
9905
|
+
continue;
|
|
9906
|
+
}
|
|
9907
|
+
if (!routeCandidate.matching) {
|
|
9908
|
+
continue;
|
|
9909
|
+
}
|
|
9910
|
+
const depth = getRoutePrivateProperties(routeCandidate).routePattern.depth;
|
|
9911
|
+
if (depth > maxDepth) {
|
|
9912
|
+
maxDepth = depth;
|
|
9913
|
+
mostSpecificRoute = routeCandidate;
|
|
9914
|
+
}
|
|
9915
|
+
}
|
|
9916
|
+
return mostSpecificRoute;
|
|
9917
|
+
};
|
|
9918
|
+
|
|
9912
9919
|
// Every route action, with whether it asks something of the address at the
|
|
9913
9920
|
// moment: its route matches and its params getter returns something.
|
|
9914
9921
|
const routeActionEntriesSignal = signal([]);
|
|
@@ -14496,11 +14503,15 @@ const setupBrowserIntegrationViaHistory = ({
|
|
|
14496
14503
|
// browser answers — a before/after cycle here would be about a navigation
|
|
14497
14504
|
// that never happens. A push with something of its own for its entry — a
|
|
14498
14505
|
// state, what the press handed over (see carryByLinkNavigation) — is a
|
|
14499
|
-
// new entry, never one standing there already.
|
|
14506
|
+
// new entry, never one standing there already. So is a push a signal write
|
|
14507
|
+
// makes: the writes after it in the same move (a layer, then what is open
|
|
14508
|
+
// inside it) build on its address at once, and a traversal would only land
|
|
14509
|
+
// after they went to the entry being left.
|
|
14500
14510
|
if (
|
|
14501
14511
|
options.navigationType === "push" &&
|
|
14502
14512
|
options.state === undefined &&
|
|
14503
14513
|
!options.pressWrites &&
|
|
14514
|
+
!options.fromSignal &&
|
|
14504
14515
|
url !== readAddress()
|
|
14505
14516
|
) {
|
|
14506
14517
|
const delta = adjacentEntryDelta(url);
|
|
@@ -14923,12 +14934,23 @@ const setupBrowserIntegrationViaHistory = ({
|
|
|
14923
14934
|
|
|
14924
14935
|
const navTo = async (
|
|
14925
14936
|
url,
|
|
14926
|
-
{
|
|
14937
|
+
{
|
|
14938
|
+
replace,
|
|
14939
|
+
state,
|
|
14940
|
+
routeTransition,
|
|
14941
|
+
debounce,
|
|
14942
|
+
asReturn,
|
|
14943
|
+
open,
|
|
14944
|
+
fromSignal,
|
|
14945
|
+
} = {},
|
|
14927
14946
|
) => {
|
|
14928
14947
|
const routingOptions = {
|
|
14929
14948
|
reason: `navTo called with "${url}"`,
|
|
14930
14949
|
navigationType: replace ? "replace" : "push",
|
|
14931
14950
|
state,
|
|
14951
|
+
// Made by a signal written into the url (see route.js writeValueInUrl):
|
|
14952
|
+
// such a push is never turned into a traversal (see handleRoutingTask).
|
|
14953
|
+
fromSignal,
|
|
14932
14954
|
// A push that lands where its address was last left (see
|
|
14933
14955
|
// link_return.js).
|
|
14934
14956
|
asReturn,
|
|
@@ -57481,6 +57503,10 @@ const css$$ = /* css */`@layer navi {
|
|
|
57481
57503
|
--link-color: unset;
|
|
57482
57504
|
--link-text-decoration: none;
|
|
57483
57505
|
}
|
|
57506
|
+
|
|
57507
|
+
.navi_link[data-color-inherit] {
|
|
57508
|
+
--link-color-visited: var(--navi-link-color-visited, var(--link-color));
|
|
57509
|
+
}
|
|
57484
57510
|
}
|
|
57485
57511
|
|
|
57486
57512
|
.navi_link {
|
|
@@ -58313,6 +58339,7 @@ const LinkPlain = props => {
|
|
|
58313
58339
|
// Visual
|
|
58314
58340
|
,
|
|
58315
58341
|
"data-variant": variant,
|
|
58342
|
+
"data-color-inherit": props.color === "inherit" ? "" : undefined,
|
|
58316
58343
|
"data-current-effect-bold": currentEffectBoldTechnique,
|
|
58317
58344
|
"data-current-effect-shadow": currentEffectShadow ? "" : undefined,
|
|
58318
58345
|
"data-current-indicator-position": currentIndicatorPosition,
|
|
@@ -70445,7 +70472,7 @@ const SlideContainer = ({
|
|
|
70445
70472
|
// A transfer waiting for a travel that never happened — a controlled
|
|
70446
70473
|
// `current` the caller chose not to move: dropped, the focus has no
|
|
70447
70474
|
// business going anywhere. The one for a travel that DID happen stays,
|
|
70448
|
-
// and lands
|
|
70475
|
+
// and lands once the slide it is for is final (see landFocusHandOver).
|
|
70449
70476
|
if (focusHandOverRef.current && focusHandOverRef.current.area !== readArea(currentElement)) {
|
|
70450
70477
|
focusHandOverRef.current = null;
|
|
70451
70478
|
}
|
|
@@ -70599,6 +70626,11 @@ const SlideContainer = ({
|
|
|
70599
70626
|
requestAnimationFrame(rollBack);
|
|
70600
70627
|
}
|
|
70601
70628
|
}
|
|
70629
|
+
// The slide a transfer waits for is current (any other one was dropped
|
|
70630
|
+
// above): it lands once the renders this commit sets off are over.
|
|
70631
|
+
if (focusHandOverRef.current) {
|
|
70632
|
+
landFocusHandOver();
|
|
70633
|
+
}
|
|
70602
70634
|
// The keyboard is on a slide about to go out of reach and nobody has moved
|
|
70603
70635
|
// it: inert would drop it on the floor (document.body), so it is handed to
|
|
70604
70636
|
// the slide on screen instead. No event to read — this travel was asked for
|
|
@@ -70616,9 +70648,10 @@ const SlideContainer = ({
|
|
|
70616
70648
|
// Out of reach LAST, once the keyboard has already moved on: a browser
|
|
70617
70649
|
// takes the focus off an element the moment it becomes inert, and on its
|
|
70618
70650
|
// own schedule — doing this first would let that undo the landing above and
|
|
70619
|
-
// leave the focus on nothing.
|
|
70620
|
-
|
|
70621
|
-
|
|
70651
|
+
// leave the focus on nothing. A transfer still on its way does it itself,
|
|
70652
|
+
// once landed.
|
|
70653
|
+
if (!focusHandOverRef.current) {
|
|
70654
|
+
markOutOfReach(currentElement);
|
|
70622
70655
|
}
|
|
70623
70656
|
// What is on screen now, for the travel after this one to depart from.
|
|
70624
70657
|
// While a travel is playing the stage is the one that knows (it holds the
|
|
@@ -70969,14 +71002,7 @@ const SlideContainer = ({
|
|
|
70969
71002
|
// dialog is OPEN, not when it was asked to open). `prepareFocusTransfer` has
|
|
70970
71003
|
// to read the interaction — which element a mousedown landed on, whether the
|
|
70971
71004
|
// modality is the keyboard — while the event is still being dispatched. The
|
|
70972
|
-
// landing waits for the slide to
|
|
70973
|
-
// which is later than one would think: a travel hands the slide a value
|
|
70974
|
-
// (useSlideValue) and that value reaches it through context, so Preact
|
|
70975
|
-
// re-renders the slide in a flush of its own — the container's own layout
|
|
70976
|
-
// effect runs BEFORE its children have seen the value. A screen keyed by it,
|
|
70977
|
-
// a form whose uncontrolled fields must start again from a new prefill,
|
|
70978
|
-
// replaces its whole subtree there. Landing before that took the focus to a
|
|
70979
|
-
// node about to be thrown away, and the focus went to document.body with it.
|
|
71005
|
+
// landing waits for the slide's DOM to be final (see landFocusHandOver).
|
|
70980
71006
|
const handOverFocus = (slideElement, focusTransfer, event) => {
|
|
70981
71007
|
focusTransfer.transferFocus(event, slideElement);
|
|
70982
71008
|
// Asked of the SLIDE, not of this box: the box contains itself, so asking
|
|
@@ -71001,21 +71027,46 @@ const SlideContainer = ({
|
|
|
71001
71027
|
}
|
|
71002
71028
|
};
|
|
71003
71029
|
|
|
71004
|
-
//
|
|
71005
|
-
//
|
|
71006
|
-
//
|
|
71007
|
-
// and
|
|
71008
|
-
|
|
71030
|
+
// The transfer prepared in goToArea lands once the slide it is for is the
|
|
71031
|
+
// current one AND everything the travel set off has rendered — which is
|
|
71032
|
+
// later than this box's own commit. A travel hands the slide a value
|
|
71033
|
+
// (useSlideValue), and what reads it below a component that skipped its own
|
|
71034
|
+
// render is re-rendered by Preact in a commit of its own, after this one. A
|
|
71035
|
+
// screen keyed by it, a form whose uncontrolled fields must start again from
|
|
71036
|
+
// a new prefill, replaces its whole subtree there: a focus landed before it
|
|
71037
|
+
// goes to document.body with the node. Nor can the slide say when it is done
|
|
71038
|
+
// from its own layout effect: rendered in the same pass as this box, its
|
|
71039
|
+
// effects run before this box has made it current.
|
|
71040
|
+
// Preact drains its render queue in one go, the renders it cascades into
|
|
71041
|
+
// included, so a microtask queued from this commit runs after all of them.
|
|
71042
|
+
// Until then the slide being left stays within reach (see markOutOfReach):
|
|
71043
|
+
// inert would take the keyboard off it first, and drop it on document.body.
|
|
71044
|
+
const landFocusHandOver = () => {
|
|
71009
71045
|
const focusHandOver = focusHandOverRef.current;
|
|
71010
|
-
|
|
71011
|
-
|
|
71012
|
-
|
|
71013
|
-
|
|
71014
|
-
|
|
71015
|
-
|
|
71046
|
+
queueMicrotask(() => {
|
|
71047
|
+
// Already landed by an earlier commit of this cascade, given up on, or
|
|
71048
|
+
// replaced by the transfer of a newer travel.
|
|
71049
|
+
if (focusHandOverRef.current !== focusHandOver || !trackRef.current) {
|
|
71050
|
+
return;
|
|
71051
|
+
}
|
|
71052
|
+
focusHandOverRef.current = null;
|
|
71053
|
+
const currentElement = readMap().slideElements.find(slideElement => slideElement.hasAttribute("data-current"));
|
|
71054
|
+
if (!currentElement) {
|
|
71055
|
+
return;
|
|
71056
|
+
}
|
|
71057
|
+
if (readArea(currentElement) === focusHandOver.area) {
|
|
71058
|
+
handOverFocus(currentElement, focusHandOver.focusTransfer, focusHandOver.event);
|
|
71059
|
+
}
|
|
71060
|
+
markOutOfReach(currentElement);
|
|
71061
|
+
});
|
|
71062
|
+
};
|
|
71063
|
+
|
|
71064
|
+
// Every slide but the one on screen out of reach — of the keyboard and of
|
|
71065
|
+
// assistive technology alike.
|
|
71066
|
+
const markOutOfReach = currentElement => {
|
|
71067
|
+
for (const slideElement of readMap().slideElements) {
|
|
71068
|
+
slideElement.toggleAttribute("inert", slideElement !== currentElement);
|
|
71016
71069
|
}
|
|
71017
|
-
focusHandOverRef.current = null;
|
|
71018
|
-
handOverFocus(slideElement, focusHandOver.focusTransfer, focusHandOver.event);
|
|
71019
71070
|
};
|
|
71020
71071
|
|
|
71021
71072
|
/**
|
|
@@ -72203,7 +72254,6 @@ const SlideContainer = ({
|
|
|
72203
72254
|
answeredAreas,
|
|
72204
72255
|
done,
|
|
72205
72256
|
valueByArea,
|
|
72206
|
-
settleFocus,
|
|
72207
72257
|
// What a slide reads to know whether its content is rendered yet
|
|
72208
72258
|
// (see Slide's `mount`).
|
|
72209
72259
|
mount,
|
|
@@ -72307,15 +72357,6 @@ const Slide = ({
|
|
|
72307
72357
|
}), [nextIsLocked, preventNavPrevious]);
|
|
72308
72358
|
// What the travel into this slide carried, if anything (see useSlideValue).
|
|
72309
72359
|
const slideValue = container?.valueByArea?.[slideArea];
|
|
72310
|
-
// This slide's DOM is settled — said after EVERY commit, no dependency
|
|
72311
|
-
// array, because it is a fact about the render that just happened, not a
|
|
72312
|
-
// reaction to a particular prop. The container is the one holding a focus
|
|
72313
|
-
// transfer waiting for it (see settleFocus): a travel's value reaches this
|
|
72314
|
-
// slide through context one flush after the container moved, and what it
|
|
72315
|
-
// shows (a form keyed by a prefill) is only final here.
|
|
72316
|
-
useLayoutEffect(() => {
|
|
72317
|
-
container?.settleFocus?.(slideArea);
|
|
72318
|
-
});
|
|
72319
72360
|
return jsx(SlideContext.Provider, {
|
|
72320
72361
|
value: locks,
|
|
72321
72362
|
children: jsx(SlideValueContext.Provider, {
|