@jsenv/navi 0.29.414 → 0.29.415
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/dev/jsenv_navi.js +177 -50
- package/dist/dev/jsenv_navi.js.map +13 -11
- package/dist/jsenv_navi.js +170 -47
- package/dist/jsenv_navi.js.map +13 -11
- package/docs/autofocus.md +19 -0
- package/docs/route_transitions.md +17 -6
- package/package.json +1 -1
package/dist/dev/jsenv_navi.js
CHANGED
|
@@ -14697,6 +14697,31 @@ const moveFocusTo = (
|
|
|
14697
14697
|
});
|
|
14698
14698
|
};
|
|
14699
14699
|
|
|
14700
|
+
/**
|
|
14701
|
+
* Gives the focus back to `target` once the commit a close is part of is over:
|
|
14702
|
+
* for a close caused by an unmount (UNMOUNT_EVENT_TYPE in
|
|
14703
|
+
* layout/open_controller.js), which runs from a cleanup while the tree leaving
|
|
14704
|
+
* with it is still in the document. Only its end tells a target leaving with
|
|
14705
|
+
* it — disconnected by then, and skipped — from one staying, and a focused
|
|
14706
|
+
* element that leaves makes the removal of its whole subtree restyle it.
|
|
14707
|
+
*
|
|
14708
|
+
* And only into a void: the close took the focus with it, so something holding
|
|
14709
|
+
* the focus by then — a page arriving that focused a field of its own — took it
|
|
14710
|
+
* after, and keeps it.
|
|
14711
|
+
*/
|
|
14712
|
+
const giveFocusBackAfterCommit = (target, giveFocus) => {
|
|
14713
|
+
queueMicrotask(() => {
|
|
14714
|
+
if (!target || !target.isConnected) {
|
|
14715
|
+
return;
|
|
14716
|
+
}
|
|
14717
|
+
const focusedNow = document.activeElement;
|
|
14718
|
+
if (focusedNow && focusedNow !== document.body) {
|
|
14719
|
+
return;
|
|
14720
|
+
}
|
|
14721
|
+
giveFocus();
|
|
14722
|
+
});
|
|
14723
|
+
};
|
|
14724
|
+
|
|
14700
14725
|
const focusTransferTarget = (target, focusVisible) => {
|
|
14701
14726
|
moveFocusTo(target, { focusVisible });
|
|
14702
14727
|
if (target.hasAttribute("navi-autofocus-select")) {
|
|
@@ -15251,6 +15276,12 @@ const createOpenController = (
|
|
|
15251
15276
|
closeEvent,
|
|
15252
15277
|
`closed by focusout -> let focus go away`,
|
|
15253
15278
|
);
|
|
15279
|
+
} else if (findEvent(closeEvent, UNMOUNT_EVENT_TYPE)) {
|
|
15280
|
+
// Left the tree: what held the focus before the opening may be
|
|
15281
|
+
// leaving with it (see giveFocusBackAfterCommit).
|
|
15282
|
+
giveFocusBackAfterCommit(focusTransfer.focusedElement, () => {
|
|
15283
|
+
focusTransfer.restoreFocus(closeEvent);
|
|
15284
|
+
});
|
|
15254
15285
|
} else {
|
|
15255
15286
|
// Only the mousedown, deliberately: a popup with no backdrop is
|
|
15256
15287
|
// closed by a `pointerdown` that belongs to the page (see
|
|
@@ -18119,14 +18150,8 @@ const createCalloutManager = (
|
|
|
18119
18150
|
};
|
|
18120
18151
|
if (findEvent(closeEvent, UNMOUNT_EVENT_TYPE)) {
|
|
18121
18152
|
// Closed by a popup leaving the tree: the control may be leaving
|
|
18122
|
-
//
|
|
18123
|
-
|
|
18124
|
-
// restyles the whole subtree.
|
|
18125
|
-
queueMicrotask(() => {
|
|
18126
|
-
if (focusTarget.isConnected) {
|
|
18127
|
-
giveFocusBack();
|
|
18128
|
-
}
|
|
18129
|
-
});
|
|
18153
|
+
// with it (see giveFocusBackAfterCommit).
|
|
18154
|
+
giveFocusBackAfterCommit(focusTarget, giveFocusBack);
|
|
18130
18155
|
} else {
|
|
18131
18156
|
// Given before the callout leaves the document: removed while it
|
|
18132
18157
|
// holds the focus, it sends a focusout with nowhere to go up
|
|
@@ -32251,7 +32276,11 @@ const useContentKeepingLeavingPages = branch => {
|
|
|
32251
32276
|
// mounted with, or it would be mounted again.
|
|
32252
32277
|
key = shown.key;
|
|
32253
32278
|
page = shown.page;
|
|
32254
|
-
} else if (shown && leavingPagesHold) {
|
|
32279
|
+
} else if (shown && leavingPagesHold && !document.querySelector(":modal")) {
|
|
32280
|
+
// Never while a modal dialog is open: it keeps everything but itself out
|
|
32281
|
+
// of reach — the page arriving could not even take the focus — for as long
|
|
32282
|
+
// as it is open, and one in a page kept while leaving stays open for the
|
|
32283
|
+
// whole movement. The page is then taken down at once, as without one.
|
|
32255
32284
|
leavingRef.current.push({
|
|
32256
32285
|
vnode: shown.vnode,
|
|
32257
32286
|
page: shown.page,
|
|
@@ -32298,10 +32327,17 @@ const useContentKeepingLeavingPages = branch => {
|
|
|
32298
32327
|
}
|
|
32299
32328
|
entry.hidden = true;
|
|
32300
32329
|
entry.page.leavingSignal.value = true;
|
|
32330
|
+
const focusedElement = document.activeElement;
|
|
32301
32331
|
let node = start.nextSibling;
|
|
32302
32332
|
while (node && node !== end) {
|
|
32303
32333
|
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
32304
32334
|
node.setAttribute(LEAVING_ATTRIBUTE, "");
|
|
32335
|
+
// What the keyboard held in it goes where the page's removal would
|
|
32336
|
+
// have sent it: a hidden element keeps the focus, and would go on
|
|
32337
|
+
// receiving the keys pressed during the movement.
|
|
32338
|
+
if (node.contains(focusedElement)) {
|
|
32339
|
+
focusedElement.blur();
|
|
32340
|
+
}
|
|
32305
32341
|
}
|
|
32306
32342
|
node = node.nextSibling;
|
|
32307
32343
|
}
|
|
@@ -45138,8 +45174,12 @@ const useInteractiveProps = (props, {
|
|
|
45138
45174
|
}
|
|
45139
45175
|
// Read by READONLY_CONSTRAINT to say how many runs are already out, and by
|
|
45140
45176
|
// the action handlers below, which are where a run joins and leaves the
|
|
45141
|
-
// count (a labelled block away, hence through the controller).
|
|
45142
|
-
|
|
45177
|
+
// count (a labelled block away, hence through the controller). None for a
|
|
45178
|
+
// control without an action of its own, by the same rule as being held
|
|
45179
|
+
// back: what it runs (the noop every such control has, a link's
|
|
45180
|
+
// navigation) is nothing the guard counts, and would hold a slot for its
|
|
45181
|
+
// length.
|
|
45182
|
+
uiStateController.parallelGuard = props.action ? parallelGuard : null;
|
|
45143
45183
|
if (heldByParallelGuard) {
|
|
45144
45184
|
controlHostProps["data-readonly-reason"] = "parallel-guard";
|
|
45145
45185
|
}
|
|
@@ -45414,7 +45454,11 @@ const useInteractiveProps = (props, {
|
|
|
45414
45454
|
// carried by every navi_action_* event of that run).
|
|
45415
45455
|
uiStateController.pendingActionEvent = e.detail.event;
|
|
45416
45456
|
uiStateController.actionInFlight = true;
|
|
45417
|
-
|
|
45457
|
+
// Given back to the guard it was counted in, whatever the control
|
|
45458
|
+
// holds by then: one that lost its action mid-run has no guard left
|
|
45459
|
+
// to name, and the slot would stay taken.
|
|
45460
|
+
const countedIn = uiStateController.parallelGuard;
|
|
45461
|
+
countedIn?.claim(uiStateController);
|
|
45418
45462
|
if (optimistic) {
|
|
45419
45463
|
syncOptimisticAttribute(true);
|
|
45420
45464
|
}
|
|
@@ -45432,7 +45476,7 @@ const useInteractiveProps = (props, {
|
|
|
45432
45476
|
e.detail.addSideEffect((outcome, outcomeEvent) => {
|
|
45433
45477
|
uiStateController.actionInFlight = false;
|
|
45434
45478
|
uiStateController.runningActionSignal.value = null;
|
|
45435
|
-
|
|
45479
|
+
countedIn?.release(uiStateController);
|
|
45436
45480
|
// The outcome callbacks are told from here, the run's side effect,
|
|
45437
45481
|
// and not from the onnavi_action_* handlers below: the run can
|
|
45438
45482
|
// outlive this control (its own answer, written to a store the
|
|
@@ -48965,6 +49009,90 @@ const BinderItemContext = createContext(null);
|
|
|
48965
49009
|
*/
|
|
48966
49010
|
const NavContext = createContext(null);
|
|
48967
49011
|
|
|
49012
|
+
/**
|
|
49013
|
+
* Whether the page one is on is `route` with `params` — what a link or a button
|
|
49014
|
+
* pointing at a route shows as current.
|
|
49015
|
+
*
|
|
49016
|
+
* Read through a computed of that one answer, so the component reading it
|
|
49017
|
+
* re-renders when the answer flips, and not whenever the route's status moves:
|
|
49018
|
+
* a page arriving on the route with other params — one profile opening, under a
|
|
49019
|
+
* list of links to a hundred other profiles — starts the route matching and
|
|
49020
|
+
* changes its params, and leaves every one of those links exactly as it was.
|
|
49021
|
+
*
|
|
49022
|
+
* `except`: routes whose matching vetoes the answer — a route named there is
|
|
49023
|
+
* somewhere else, whatever the rest of the reading says. `also`: routes whose
|
|
49024
|
+
* matching grants it. Both take a route or a list of routes.
|
|
49025
|
+
*/
|
|
49026
|
+
const useRouteCurrent = (route, params, { except, also } = {}) => {
|
|
49027
|
+
const exceptRoutes = toRouteList(except);
|
|
49028
|
+
const alsoRoutes = toRouteList(also);
|
|
49029
|
+
const paramsKey = getParamsCacheKey(params);
|
|
49030
|
+
const exceptKey = routeListKey(exceptRoutes);
|
|
49031
|
+
const alsoKey = routeListKey(alsoRoutes);
|
|
49032
|
+
const currentSignal = useMemo(() => {
|
|
49033
|
+
if (paramsKey === null) {
|
|
49034
|
+
return null;
|
|
49035
|
+
}
|
|
49036
|
+
return computed(() =>
|
|
49037
|
+
readRouteCurrent(route, params, exceptRoutes, alsoRoutes),
|
|
49038
|
+
);
|
|
49039
|
+
}, [route, paramsKey, exceptKey, alsoKey]);
|
|
49040
|
+
if (!currentSignal) {
|
|
49041
|
+
// Params no key can be made of (a date, an object): read on the spot, the
|
|
49042
|
+
// component then re-renders with the status of every route it reads.
|
|
49043
|
+
return readRouteCurrent(route, params, exceptRoutes, alsoRoutes);
|
|
49044
|
+
}
|
|
49045
|
+
return currentSignal.value;
|
|
49046
|
+
};
|
|
49047
|
+
|
|
49048
|
+
// Every signal is read even once the answer is known: reading is what
|
|
49049
|
+
// subscribes, and a route skipped today is the one whose change has to be heard
|
|
49050
|
+
// tomorrow.
|
|
49051
|
+
const readRouteCurrent = (route, params, exceptRoutes, alsoRoutes) => {
|
|
49052
|
+
const matching = route.matchingSignal.value;
|
|
49053
|
+
const paramsMatching = route.matchesParams(params);
|
|
49054
|
+
let someExceptMatching = false;
|
|
49055
|
+
for (const exceptRoute of exceptRoutes) {
|
|
49056
|
+
if (exceptRoute.matchingSignal.value) {
|
|
49057
|
+
someExceptMatching = true;
|
|
49058
|
+
}
|
|
49059
|
+
}
|
|
49060
|
+
let someAlsoMatching = false;
|
|
49061
|
+
for (const alsoRoute of alsoRoutes) {
|
|
49062
|
+
if (alsoRoute.matchingSignal.value) {
|
|
49063
|
+
someAlsoMatching = true;
|
|
49064
|
+
}
|
|
49065
|
+
}
|
|
49066
|
+
if (someExceptMatching) {
|
|
49067
|
+
return false;
|
|
49068
|
+
}
|
|
49069
|
+
return (matching && paramsMatching) || someAlsoMatching;
|
|
49070
|
+
};
|
|
49071
|
+
|
|
49072
|
+
const toRouteList = (routes) => {
|
|
49073
|
+
if (!routes) {
|
|
49074
|
+
return [];
|
|
49075
|
+
}
|
|
49076
|
+
return Array.isArray(routes) ? routes : [routes];
|
|
49077
|
+
};
|
|
49078
|
+
|
|
49079
|
+
// A list of routes as a key, since an inline `currentAlso={[A, B]}` is a new
|
|
49080
|
+
// array at every render of the parent.
|
|
49081
|
+
const routeIdByRoute = new WeakMap();
|
|
49082
|
+
let routeIdCount = 0;
|
|
49083
|
+
const routeListKey = (routes) => {
|
|
49084
|
+
let key = "";
|
|
49085
|
+
for (const route of routes) {
|
|
49086
|
+
let id = routeIdByRoute.get(route);
|
|
49087
|
+
if (id === undefined) {
|
|
49088
|
+
id = ++routeIdCount;
|
|
49089
|
+
routeIdByRoute.set(route, id);
|
|
49090
|
+
}
|
|
49091
|
+
key += `${id},`;
|
|
49092
|
+
}
|
|
49093
|
+
return key;
|
|
49094
|
+
};
|
|
49095
|
+
|
|
48968
49096
|
/*
|
|
48969
49097
|
* Custom hook to apply semi-transparent color when an element should be dimmed.
|
|
48970
49098
|
*
|
|
@@ -49514,15 +49642,17 @@ const LinkWithRoute = ({
|
|
|
49514
49642
|
assertRoute(route);
|
|
49515
49643
|
}
|
|
49516
49644
|
const url = route.buildUrl(routeParams);
|
|
49517
|
-
|
|
49518
|
-
|
|
49519
|
-
|
|
49520
|
-
|
|
49521
|
-
|
|
49522
|
-
|
|
49523
|
-
|
|
49524
|
-
|
|
49525
|
-
|
|
49645
|
+
{
|
|
49646
|
+
for (const amendingRoute of [currentExcept, currentAlso].flat()) {
|
|
49647
|
+
if (amendingRoute) {
|
|
49648
|
+
assertRoute(amendingRoute);
|
|
49649
|
+
}
|
|
49650
|
+
}
|
|
49651
|
+
}
|
|
49652
|
+
const linkMatching = useRouteCurrent(route, routeParams, {
|
|
49653
|
+
except: currentExcept,
|
|
49654
|
+
also: currentAlso
|
|
49655
|
+
});
|
|
49526
49656
|
const innerCurrent = current || linkMatching;
|
|
49527
49657
|
return jsx(Link, {
|
|
49528
49658
|
href: url,
|
|
@@ -49531,25 +49661,6 @@ const LinkWithRoute = ({
|
|
|
49531
49661
|
children: children || route.buildRelativeUrl(routeParams)
|
|
49532
49662
|
});
|
|
49533
49663
|
};
|
|
49534
|
-
// A route, or a list of them, read as one answer: does any of them match.
|
|
49535
|
-
// Reading matchingSignal during the render is what subscribes the component to
|
|
49536
|
-
// it, so every route is read even once one has answered yes — stopping early
|
|
49537
|
-
// would leave the component deaf to the ones it skipped.
|
|
49538
|
-
const useSomeRouteMatching = routes => {
|
|
49539
|
-
if (!routes) {
|
|
49540
|
-
return false;
|
|
49541
|
-
}
|
|
49542
|
-
let someMatching = false;
|
|
49543
|
-
for (const route of Array.isArray(routes) ? routes : [routes]) {
|
|
49544
|
-
{
|
|
49545
|
-
assertRoute(route);
|
|
49546
|
-
}
|
|
49547
|
-
if (route.matchingSignal.value) {
|
|
49548
|
-
someMatching = true;
|
|
49549
|
-
}
|
|
49550
|
-
}
|
|
49551
|
-
return someMatching;
|
|
49552
|
-
};
|
|
49553
49664
|
const LinkPlain = props => {
|
|
49554
49665
|
const defaultRef = useRef();
|
|
49555
49666
|
props.ref = props.ref || defaultRef;
|
|
@@ -54636,11 +54747,7 @@ const ButtonWithRoute = props => {
|
|
|
54636
54747
|
assertRoute(route);
|
|
54637
54748
|
}
|
|
54638
54749
|
const url = route.buildUrl(routeParams);
|
|
54639
|
-
const
|
|
54640
|
-
matching
|
|
54641
|
-
} = useRouteStatus(route);
|
|
54642
|
-
const paramsAreMatching = route.matchesParams(routeParams);
|
|
54643
|
-
const linkMatching = matching && paramsAreMatching;
|
|
54750
|
+
const linkMatching = useRouteCurrent(route, routeParams);
|
|
54644
54751
|
|
|
54645
54752
|
// Merged into whatever the caller already holds: a button can be forced into
|
|
54646
54753
|
// a state for a demo and still learn its own current-ness from its route.
|
|
@@ -66073,10 +66180,24 @@ const useDialogProps = props => {
|
|
|
66073
66180
|
// whichever show call ran, not just when a stray authored display
|
|
66074
66181
|
// property is actually present — harmless the rest of the time.
|
|
66075
66182
|
dialogEl.setAttribute("navi-hidden", "");
|
|
66076
|
-
|
|
66077
|
-
|
|
66183
|
+
const hideNatively = () => {
|
|
66184
|
+
if (isTopLayerPopover) {
|
|
66185
|
+
dialogEl.hidePopover();
|
|
66186
|
+
} else {
|
|
66187
|
+
dialogEl.close();
|
|
66188
|
+
}
|
|
66189
|
+
};
|
|
66190
|
+
if (findEvent(closeEvent, UNMOUNT_EVENT_TYPE)) {
|
|
66191
|
+
// Leaving the tree: hidden natively once the commit is over. Its
|
|
66192
|
+
// removal takes it out of the top layer and hands the focus to
|
|
66193
|
+
// nobody, where closing it now hands the focus to whatever held it
|
|
66194
|
+
// before the opening — often a trigger leaving in the same commit
|
|
66195
|
+
// (see giveFocusBackAfterCommit). What is left to close then is its
|
|
66196
|
+
// state: a <dialog> removed while open keeps its `open` attribute, and
|
|
66197
|
+
// a subtree <Suspense> parks and puts back would show it again.
|
|
66198
|
+
queueMicrotask(hideNatively);
|
|
66078
66199
|
} else {
|
|
66079
|
-
|
|
66200
|
+
hideNatively();
|
|
66080
66201
|
}
|
|
66081
66202
|
// Held at the size it has right now, for the whole way out. cleanup()
|
|
66082
66203
|
// below already stops the JS repositioning, but the size is CSS-driven
|
|
@@ -67781,7 +67902,13 @@ const usePopoverProps = props => {
|
|
|
67781
67902
|
// reliably sufficient once a consumer's own CSS also sets display.
|
|
67782
67903
|
popoverEl.setAttribute("navi-hidden", "");
|
|
67783
67904
|
if (isTopLayer) {
|
|
67784
|
-
|
|
67905
|
+
// Left to its removal when the popover leaves the tree: that hides it
|
|
67906
|
+
// and hands the focus to nobody, where hidePopover() hands the focus
|
|
67907
|
+
// to whatever held it before the opening — often a trigger leaving in
|
|
67908
|
+
// the same commit (see giveFocusBackAfterCommit).
|
|
67909
|
+
if (!findEvent(closeEvent, UNMOUNT_EVENT_TYPE)) {
|
|
67910
|
+
popoverEl.hidePopover();
|
|
67911
|
+
}
|
|
67785
67912
|
} else {
|
|
67786
67913
|
openLocalPopoverCount = Math.max(0, openLocalPopoverCount - 1);
|
|
67787
67914
|
}
|