@jsenv/navi 0.29.157 → 0.29.159
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/jsenv_navi.js +520 -185
- package/dist/jsenv_navi.js.map +78 -51
- package/dist/jsenv_navi_side_effects.js +1 -1
- package/dist/jsenv_navi_side_effects.js.map +1 -1
- package/docs/AI_INSTRUCTIONS.md +11 -3
- package/docs/actions.md +65 -3
- package/docs/control_object.md +13 -6
- package/docs/error_handling.md +39 -7
- package/docs/form_changed.md +24 -8
- package/docs/interactions.md +32 -10
- package/docs/popup_open.md +1 -1
- package/docs/resource.md +32 -12
- package/package.json +2 -2
package/dist/jsenv_navi.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { installImportMetaCssBuild, windowHeightSignal, windowWidthSignal, visualViewportHeightSignal, visualViewportWidthSignal, getAppHeight, getAppWidth, coarsePointerSignal, smallTouchScreenSignal } from "./jsenv_navi_side_effects.js";
|
|
6
6
|
export { disableVirtualKeyboardOverlay } from "./jsenv_navi_side_effects.js";
|
|
7
|
-
import { elementIsFocusable, createIterableWeakSet, dispatchInternalCustomEvent, dispatchCustomEvent, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, getElementSignature, createPubSub, findEvent, createValueEffect, findFocusDelegateTarget, findFocusable, allowWheelThrough, dispatchPublicCustomEvent, resolveCSSColor, ELEMENT_SIZE_CHANGE, findSelfOrAncestorFixedPosition, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, applyNewPosition, measureLongestVisualLineWidth, chainEvent, waitForPressHeld, suppressClickAfterGesture, startDragToTravel, markDragSource, startDragTo, createEventGroupLogger, getKeyboardEventDefaultAction, activeElementSignal, normalizeStyle, mergeOneStyle, getPositionedParent, normalizeStyles, createGroupTransitionController, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, watchWheelTravel, scrollRoomTowards, getScrollContainer, closestOpenableAncestor, isAncestorOpen, isDisplayedDespiteClosedAncestor, observeAncestorOpenState, getAncestorOpenType, findBefore, findAfter, resolveCSSSize, hasCSSSizeUnit,
|
|
7
|
+
import { elementIsFocusable, createIterableWeakSet, dispatchInternalCustomEvent, dispatchCustomEvent, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, getElementSignature, createPubSub, findEvent, createValueEffect, findFocusDelegateTarget, findFocusable, allowWheelThrough, dispatchPublicCustomEvent, resolveCSSColor, ELEMENT_SIZE_CHANGE, findSelfOrAncestorFixedPosition, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, applyNewPosition, measureLongestVisualLineWidth, chainEvent, waitForPressHeld, suppressClickAfterGesture, startDragToTravel, markDragSource, startDragTo, createEventGroupLogger, getKeyboardEventDefaultAction, activeElementSignal, normalizeStyle, mergeOneStyle, getPositionedParent, normalizeStyles, createGroupTransitionController, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, watchWheelTravel, scrollRoomTowards, getScrollContainer, closestOpenableAncestor, isAncestorOpen, isDisplayedDespiteClosedAncestor, observeAncestorOpenState, getAncestorOpenType, findBefore, findAfter, resolveCSSSize, hasCSSSizeUnit, releaseWheelGesture, getScrollIntoViewScopedOffsets, wheelGestureIsTakenFrom, claimWheelGesture, scrollIntoViewScoped, initFocusGroup, stringifyStyle as stringifyStyle$1, resolveOklchLightness, contrastColor, isTouchDrivenEvent, parsePositionArea, snapToPixel, trapFocusInside, trapScrollInside, onAncestorReopen, canScroll, measureWidestChildRow, performTabNavigation, dragAfterIntent, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement } from "@jsenv/dom";
|
|
8
8
|
export { chainEvent, clickIsSuppressed, contrastColor, findEvent, startDragTo } from "@jsenv/dom";
|
|
9
9
|
import { signal, computed, effect, untracked, batch, useComputed, useSignal } from "@preact/signals";
|
|
10
10
|
import { isValidElement, h, Fragment, createContext, render, toChildArray, options, cloneElement } from "preact";
|
|
@@ -3184,8 +3184,11 @@ const SYMBOL_OBJECT_SIGNAL = Symbol.for("navi_object_signal");
|
|
|
3184
3184
|
* - prerun actions may have no other reference yet, so
|
|
3185
3185
|
* prerunProtectionRegistry pins them for a few minutes.
|
|
3186
3186
|
*
|
|
3187
|
-
*
|
|
3188
|
-
*
|
|
3187
|
+
* A failing run rejects, and writes its error into errorSignal as well. The
|
|
3188
|
+
* callers that cannot take a rejection — a run started from a signal effect, a
|
|
3189
|
+
* control that already draws the failure — swallow it where they start it, and
|
|
3190
|
+
* that is where an error nobody displayed is reported once, by one rule (see
|
|
3191
|
+
* run_unwatched.js and action_error_report.js).
|
|
3189
3192
|
*
|
|
3190
3193
|
* Blast radius: this file is the substrate of routing (route_action.js,
|
|
3191
3194
|
* action_run_effect.js), resources (state/rest) and every control's `action`
|
|
@@ -3568,16 +3571,27 @@ ${lines.join("\n")}`);
|
|
|
3568
3571
|
|
|
3569
3572
|
const actionToRunPrivateProperties =
|
|
3570
3573
|
getActionPrivateProperties(actionToPrerunOrRun);
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3574
|
+
let performRunResult;
|
|
3575
|
+
try {
|
|
3576
|
+
performRunResult = actionToRunPrivateProperties.performRun({
|
|
3577
|
+
globalAbortSignal,
|
|
3578
|
+
abortSignal: effectiveSignal,
|
|
3579
|
+
reason,
|
|
3580
|
+
event,
|
|
3581
|
+
isPrerun,
|
|
3582
|
+
onComplete,
|
|
3583
|
+
onAbort,
|
|
3584
|
+
onError,
|
|
3585
|
+
});
|
|
3586
|
+
} catch (error) {
|
|
3587
|
+
// A synchronous callback that threw. Caught here so the update carries
|
|
3588
|
+
// on — the other actions of the same update have nothing to do with
|
|
3589
|
+
// this one — and given back further down to whoever asked for THIS
|
|
3590
|
+
// action, still as a throw.
|
|
3591
|
+
activationWeakSet.add(actionToPrerunOrRun);
|
|
3592
|
+
resultArray.push({ type: "sync_error", error });
|
|
3593
|
+
return;
|
|
3594
|
+
}
|
|
3581
3595
|
activationWeakSet.add(actionToPrerunOrRun);
|
|
3582
3596
|
|
|
3583
3597
|
if (performRunResult && typeof performRunResult.then === "function") {
|
|
@@ -3623,14 +3637,30 @@ ${lines.join("\n")}`);
|
|
|
3623
3637
|
requestedResult = null;
|
|
3624
3638
|
} else if (hasAsync) {
|
|
3625
3639
|
requestedResult = Promise.all(
|
|
3626
|
-
resultArray.map((item) =>
|
|
3627
|
-
item.type === "sync"
|
|
3628
|
-
|
|
3640
|
+
resultArray.map((item) => {
|
|
3641
|
+
if (item.type === "sync") {
|
|
3642
|
+
return item.result;
|
|
3643
|
+
}
|
|
3644
|
+
if (item.type === "sync_error") {
|
|
3645
|
+
return Promise.reject(item.error);
|
|
3646
|
+
}
|
|
3647
|
+
return item.promise;
|
|
3648
|
+
}),
|
|
3629
3649
|
);
|
|
3630
3650
|
} else {
|
|
3651
|
+
const itemFailed = resultArray.find((item) => item.type === "sync_error");
|
|
3652
|
+
if (itemFailed) {
|
|
3653
|
+
// Nothing of this update is left to do, so the failure can leave the way
|
|
3654
|
+
// it arrived: thrown, to the caller that asked for that action.
|
|
3655
|
+
throw itemFailed.error;
|
|
3656
|
+
}
|
|
3631
3657
|
requestedResult = resultArray.map((item) => item.result);
|
|
3632
3658
|
}
|
|
3633
3659
|
|
|
3660
|
+
// Settled rather than raced: a failure here is one outcome among several —
|
|
3661
|
+
// the other actions of the same update ran too, and "the update is over" must
|
|
3662
|
+
// not become "one of them failed". Waiting on this never rejects, which is
|
|
3663
|
+
// what lets a navigation be waited on (see via_navigation.js).
|
|
3634
3664
|
const allResult = allThenableArray.length
|
|
3635
3665
|
? Promise.allSettled(allThenableArray)
|
|
3636
3666
|
: null;
|
|
@@ -4239,20 +4269,31 @@ const createAction = (callback, rootOptions = {}) => {
|
|
|
4239
4269
|
onError?.(error, { event, action, args });
|
|
4240
4270
|
});
|
|
4241
4271
|
|
|
4242
|
-
// The error is in errorSignal; from here it is the UI's, and running
|
|
4243
|
-
// the action is not the place to decide whether the UI wants it —
|
|
4244
|
-
// that answer does not exist yet at this instant (see
|
|
4245
|
-
// action_error_report.js). So the run never throws: it settles with
|
|
4246
|
-
// the error as its value, and being displayed or not is constated
|
|
4247
|
-
// afterwards, in one place, by one rule.
|
|
4248
4272
|
if (onError) {
|
|
4249
4273
|
// Asking for the error IS taking it.
|
|
4250
4274
|
markErrorAsDisplayedBy(error, "onError");
|
|
4251
4275
|
}
|
|
4252
|
-
|
|
4276
|
+
// Handed back, not delivered: failRun decides whether this leaves as
|
|
4277
|
+
// a throw or as a rejection, because only the caller below knows
|
|
4278
|
+
// which of the two this run was. The error is in errorSignal either
|
|
4279
|
+
// way — a screen reads it there, whether or not anything was waiting.
|
|
4253
4280
|
return error;
|
|
4254
4281
|
};
|
|
4255
4282
|
|
|
4283
|
+
// A failure leaves the run the way it came: thrown when the callback
|
|
4284
|
+
// threw synchronously — code that knows this action is synchronous
|
|
4285
|
+
// writes a plain `try/catch` around the call and has to find it there —
|
|
4286
|
+
// and rejected when the run was asynchronous, since the throw below
|
|
4287
|
+
// then happens inside a `then`. An abort is neither: it settles with
|
|
4288
|
+
// its reason as its value, the way it always has.
|
|
4289
|
+
const failRun = (error) => {
|
|
4290
|
+
const errorSettled = onRunError(error);
|
|
4291
|
+
if (runningStateSignal.peek() === ABORTED) {
|
|
4292
|
+
return errorSettled;
|
|
4293
|
+
}
|
|
4294
|
+
throw errorSettled;
|
|
4295
|
+
};
|
|
4296
|
+
|
|
4256
4297
|
try {
|
|
4257
4298
|
const thenableArray = [];
|
|
4258
4299
|
const callbackResult = callback(...args);
|
|
@@ -4290,12 +4331,12 @@ const createAction = (callback, rootOptions = {}) => {
|
|
|
4290
4331
|
}
|
|
4291
4332
|
return Promise.all(thenableArray).then(() => {
|
|
4292
4333
|
if (rejected) {
|
|
4293
|
-
return
|
|
4334
|
+
return failRun(rejectedValue);
|
|
4294
4335
|
}
|
|
4295
4336
|
return onRunEnd();
|
|
4296
4337
|
});
|
|
4297
4338
|
} catch (e) {
|
|
4298
|
-
return
|
|
4339
|
+
return failRun(e);
|
|
4299
4340
|
}
|
|
4300
4341
|
};
|
|
4301
4342
|
|
|
@@ -4705,6 +4746,46 @@ const isPlainObject = (obj) => {
|
|
|
4705
4746
|
);
|
|
4706
4747
|
};
|
|
4707
4748
|
|
|
4749
|
+
/**
|
|
4750
|
+
* Starts a run navi has nobody to await.
|
|
4751
|
+
*
|
|
4752
|
+
* A failing run does not stay quiet: it throws when the callback threw
|
|
4753
|
+
* synchronously, and rejects when it was asynchronous (see failRun in
|
|
4754
|
+
* actions.js). That is what makes `ACTION(params)` behave like any other call
|
|
4755
|
+
* that can fail — and code that lets such a failure go gets the runtime's own
|
|
4756
|
+
* unhandled-error report, which is the right answer for it.
|
|
4757
|
+
*
|
|
4758
|
+
* Some runs have no such caller by construction — one started from a signal
|
|
4759
|
+
* effect because its params changed, one whose failure the control that started
|
|
4760
|
+
* it already draws, a routing whose result every caller drops. Their failure
|
|
4761
|
+
* would be an anonymous unhandled one, naming the machinery instead of what
|
|
4762
|
+
* failed. They start here, which is why the run itself is passed rather than
|
|
4763
|
+
* its result: both deliveries have to be taken.
|
|
4764
|
+
*
|
|
4765
|
+
* And taking them is what obliges this place to ask the other question: did
|
|
4766
|
+
* anything ever display this error? The answer does not exist yet at this
|
|
4767
|
+
* instant — a route action fails before the page that shows it exists — so it is
|
|
4768
|
+
* asked with a deadline, once, in action_error_report.js.
|
|
4769
|
+
*
|
|
4770
|
+
* The error is never hidden: it stays in the action's `errorSignal`, which is
|
|
4771
|
+
* where a screen reads it.
|
|
4772
|
+
*/
|
|
4773
|
+
const runUnwatched = (startRun) => {
|
|
4774
|
+
let result;
|
|
4775
|
+
try {
|
|
4776
|
+
result = startRun();
|
|
4777
|
+
} catch (error) {
|
|
4778
|
+
reportErrorIfNobodyDisplaysIt(error);
|
|
4779
|
+
return undefined;
|
|
4780
|
+
}
|
|
4781
|
+
if (result && typeof result.catch === "function") {
|
|
4782
|
+
result.catch((error) => {
|
|
4783
|
+
reportErrorIfNobodyDisplaysIt(error);
|
|
4784
|
+
});
|
|
4785
|
+
}
|
|
4786
|
+
return result;
|
|
4787
|
+
};
|
|
4788
|
+
|
|
4708
4789
|
/**
|
|
4709
4790
|
* Reactively runs an action whenever the params derived from signals change.
|
|
4710
4791
|
*
|
|
@@ -4734,12 +4815,6 @@ const isPlainObject = (obj) => {
|
|
|
4734
4815
|
// one — in dev, an error overlay thrown over a page that is already saying what
|
|
4735
4816
|
// went wrong. Nothing is lost by dropping it: the failure is held by the action
|
|
4736
4817
|
// itself, and whoever reads it (useAsyncData, <Button action>) is what shows it.
|
|
4737
|
-
const runUnwatched = (result) => {
|
|
4738
|
-
if (result && typeof result.catch === "function") {
|
|
4739
|
-
result.catch(() => {});
|
|
4740
|
-
}
|
|
4741
|
-
};
|
|
4742
|
-
|
|
4743
4818
|
const actionRunEffect = (
|
|
4744
4819
|
action,
|
|
4745
4820
|
deriveActionParamsFromSignals,
|
|
@@ -4783,7 +4858,9 @@ const actionRunEffect = (
|
|
|
4783
4858
|
// falsy params, don't run
|
|
4784
4859
|
return;
|
|
4785
4860
|
}
|
|
4786
|
-
runUnwatched(
|
|
4861
|
+
runUnwatched(() =>
|
|
4862
|
+
actionTarget.run({ reason: "truthy params first run" }),
|
|
4863
|
+
);
|
|
4787
4864
|
return;
|
|
4788
4865
|
}
|
|
4789
4866
|
|
|
@@ -4800,18 +4877,18 @@ const actionRunEffect = (
|
|
|
4800
4877
|
}
|
|
4801
4878
|
if (!actionTargetPrevious.params) {
|
|
4802
4879
|
// coming from falsy-params state: action may already be cached, avoid unnecessary rerun
|
|
4803
|
-
runUnwatched(
|
|
4880
|
+
runUnwatched(() =>
|
|
4804
4881
|
actionTarget.run({ reason: "params restored from falsy state" }),
|
|
4805
4882
|
);
|
|
4806
4883
|
} else {
|
|
4807
|
-
runUnwatched(actionTarget.rerun({ reason: "params modified" }));
|
|
4884
|
+
runUnwatched(() => actionTarget.rerun({ reason: "params modified" }));
|
|
4808
4885
|
}
|
|
4809
4886
|
}
|
|
4810
4887
|
},
|
|
4811
4888
|
...options,
|
|
4812
4889
|
});
|
|
4813
4890
|
if (actionParamsSignal.peek()) {
|
|
4814
|
-
runUnwatched(
|
|
4891
|
+
runUnwatched(() =>
|
|
4815
4892
|
actionRunnedByThisEffect.run({ reason: "initial truthy params" }),
|
|
4816
4893
|
);
|
|
4817
4894
|
}
|
|
@@ -5693,6 +5770,16 @@ const ActionRequesterContext = createContext();
|
|
|
5693
5770
|
* Whichever mode, the affordance's own handler runs from inside its own gate
|
|
5694
5771
|
* rather than from the DOM: a caller's `onClick` fires before any of this, and
|
|
5695
5772
|
* would go off from a button drawn greyed.
|
|
5773
|
+
*
|
|
5774
|
+
* These two answer about the GESTURE. Whose VALUE an element carries is a third
|
|
5775
|
+
* question, and `standalone` is where it is answered (see useUIStateController).
|
|
5776
|
+
* They are said separately because they genuinely come apart: a door that holds
|
|
5777
|
+
* no value of its own can still write into the control around it — it is
|
|
5778
|
+
* `standalone` and must still shut when that control is read-only — and an
|
|
5779
|
+
* affordance can claim a press without ever being a field. Something that is
|
|
5780
|
+
* its own on all three counts says all three (a diskette filing a name into the
|
|
5781
|
+
* reader's own address book: `selfInteractions="click"`,
|
|
5782
|
+
* `whenSelfInteractionsBlocked="ignore"`, `standalone`).
|
|
5696
5783
|
*/
|
|
5697
5784
|
|
|
5698
5785
|
|
|
@@ -10245,6 +10332,35 @@ const watchActionCompletion = (element, dispatchAction) => {
|
|
|
10245
10332
|
};
|
|
10246
10333
|
};
|
|
10247
10334
|
|
|
10335
|
+
/**
|
|
10336
|
+
* What follows the control's OWN action: it runs once that action has
|
|
10337
|
+
* succeeded, and not at all otherwise.
|
|
10338
|
+
*
|
|
10339
|
+
* The outcome arrives in one of three shapes and each decides differently:
|
|
10340
|
+
* - the gate turned the request down (`result === false`): nothing happened,
|
|
10341
|
+
* so nothing follows;
|
|
10342
|
+
* - the action is running: what follows waits for it, and an error or an abort
|
|
10343
|
+
* drops it — whatever the action left in front of the user stays;
|
|
10344
|
+
* - it is already settled, or never started at all. A control with nothing to
|
|
10345
|
+
* run leaves no outcome behind, and no outcome means nothing went wrong.
|
|
10346
|
+
*/
|
|
10347
|
+
const runWhenActionSucceeded = (completion, callback) => {
|
|
10348
|
+
if (completion.result === false) {
|
|
10349
|
+
return;
|
|
10350
|
+
}
|
|
10351
|
+
if (completion.isRunning) {
|
|
10352
|
+
completion.whenSucceeded(callback);
|
|
10353
|
+
return;
|
|
10354
|
+
}
|
|
10355
|
+
let succeeded = true;
|
|
10356
|
+
completion.whenSettled(({ error, aborted }) => {
|
|
10357
|
+
succeeded = !error && !aborted;
|
|
10358
|
+
});
|
|
10359
|
+
if (succeeded) {
|
|
10360
|
+
callback();
|
|
10361
|
+
}
|
|
10362
|
+
};
|
|
10363
|
+
|
|
10248
10364
|
const tryActionAfterInteractionAllowed = (
|
|
10249
10365
|
element,
|
|
10250
10366
|
{
|
|
@@ -12905,7 +13021,10 @@ const useExecuteAction = (
|
|
|
12905
13021
|
});
|
|
12906
13022
|
};
|
|
12907
13023
|
|
|
12908
|
-
|
|
13024
|
+
// The control is already holding the failure — the error side effect
|
|
13025
|
+
// above drew the callout, or threw it at the boundary — so nothing here
|
|
13026
|
+
// is waiting on the rejection.
|
|
13027
|
+
return runUnwatched(runAction);
|
|
12909
13028
|
},
|
|
12910
13029
|
[elementRef, errorEffect],
|
|
12911
13030
|
);
|
|
@@ -17419,15 +17538,11 @@ const useActionAsyncData = (action, {
|
|
|
17419
17538
|
// the wait is the ordinary one: suspended into <Loading>, or drawn by the
|
|
17420
17539
|
// component under `loading: true`, exactly as for data someone else ran.
|
|
17421
17540
|
if (run && action.runningStateSignal.peek() === IDLE && action.paramsSignal.peek() !== undefined) {
|
|
17422
|
-
|
|
17541
|
+
// Nothing waits on this run: the failure is held by the action, and this
|
|
17542
|
+
// hook is what reads it back.
|
|
17543
|
+
runUnwatched(() => action.run({
|
|
17423
17544
|
reason: "useAsyncData({ run: true })"
|
|
17424
|
-
});
|
|
17425
|
-
// Nobody awaits this run, and a rejection nobody awaits is an unhandled
|
|
17426
|
-
// one — in dev, an overlay over a component already saying what failed.
|
|
17427
|
-
// The failure is held by the action, and this hook is what reads it.
|
|
17428
|
-
if (runResult && typeof runResult.catch === "function") {
|
|
17429
|
-
runResult.catch(() => {});
|
|
17430
|
-
}
|
|
17545
|
+
}));
|
|
17431
17546
|
}
|
|
17432
17547
|
|
|
17433
17548
|
// Use peek() instead of .value to avoid subscribing this component to the signal.
|
|
@@ -17538,12 +17653,6 @@ const useActionAsyncData = (action, {
|
|
|
17538
17653
|
throw actionError;
|
|
17539
17654
|
}
|
|
17540
17655
|
|
|
17541
|
-
// RUNNING with loadingEffect: "use" — return stale data + loading flag, no suspend
|
|
17542
|
-
if (loadingEffect === "use" && runningState === RUNNING) {
|
|
17543
|
-
const staleData = action.dataSignal.peek();
|
|
17544
|
-
return [staleData, true, undefined];
|
|
17545
|
-
}
|
|
17546
|
-
|
|
17547
17656
|
// An action without params has nothing to run and no one to start it: this is
|
|
17548
17657
|
// where a route action lands when its params getter returns false. It is not a
|
|
17549
17658
|
// load in progress, so there is nothing to wait for — suspending here would
|
|
@@ -17553,6 +17662,17 @@ const useActionAsyncData = (action, {
|
|
|
17553
17662
|
return [action.dataSignal.peek(), false, undefined];
|
|
17554
17663
|
}
|
|
17555
17664
|
|
|
17665
|
+
// `loading: true` says the component draws its own wait, so this hook never
|
|
17666
|
+
// suspends: running, or holding params nobody has started yet — a debounced
|
|
17667
|
+
// binding that just retargeted, an aborted run, a run this very render is
|
|
17668
|
+
// about to issue. Either way what is on screen is not the current answer,
|
|
17669
|
+
// which is what `loading` says. Suspending would not draw a spinner: it swaps
|
|
17670
|
+
// the subtree for the boundary, and everything under it remounts — a dialog
|
|
17671
|
+
// open inside it is then closed, by a hook the caller told to stay out of it.
|
|
17672
|
+
if (loadingEffect === "use") {
|
|
17673
|
+
return [action.dataSignal.peek(), true, undefined];
|
|
17674
|
+
}
|
|
17675
|
+
|
|
17556
17676
|
// IDLE or RUNNING with loadingEffect: "delegate" — suspend
|
|
17557
17677
|
const reason = runningState === RUNNING ? "loading" : "idle";
|
|
17558
17678
|
loadingRef.current = {
|
|
@@ -26329,18 +26449,12 @@ const setupBrowserIntegrationViaHistory = ({
|
|
|
26329
26449
|
// something at the first announcement has a definite place to give it back.
|
|
26330
26450
|
publishBeforeRouting({ url, ...options });
|
|
26331
26451
|
try {
|
|
26332
|
-
|
|
26333
|
-
|
|
26334
|
-
|
|
26335
|
-
|
|
26336
|
-
|
|
26337
|
-
|
|
26338
|
-
// an error nobody displays (see action_error_report.js).
|
|
26339
|
-
routingResult.catch((e) => {
|
|
26340
|
-
reportErrorIfNobodyDisplaysIt(e);
|
|
26341
|
-
});
|
|
26342
|
-
}
|
|
26343
|
-
return routingResult;
|
|
26452
|
+
// Every caller below drops this value — a click handler has nothing to do
|
|
26453
|
+
// with what the routing returns — so a rejection here would become an
|
|
26454
|
+
// anonymous unhandled one, pointing at the navigation rather than at what
|
|
26455
|
+
// failed. Taken, and passed to the one place that knows what to do with
|
|
26456
|
+
// an error nobody displays (see run_unwatched.js).
|
|
26457
|
+
return runUnwatched(() => applyRoutingTask(url, options));
|
|
26344
26458
|
} finally {
|
|
26345
26459
|
publishAfterRouting({ url, ...options });
|
|
26346
26460
|
}
|
|
@@ -32755,20 +32869,21 @@ const ParentUIStateControllerContext = createContext();
|
|
|
32755
32869
|
*/
|
|
32756
32870
|
const useUIStateController = (
|
|
32757
32871
|
props,
|
|
32758
|
-
{
|
|
32759
|
-
controlInfo,
|
|
32760
|
-
syncDomState,
|
|
32761
|
-
uiActionInternal,
|
|
32762
|
-
persists,
|
|
32763
|
-
allowNameless = false,
|
|
32764
|
-
} = {},
|
|
32872
|
+
{ controlInfo, syncDomState, uiActionInternal, persists, standalone } = {},
|
|
32765
32873
|
) => {
|
|
32766
32874
|
const debugPopup = useDebugPopup();
|
|
32767
32875
|
const debugInteraction = useDebugInteraction();
|
|
32768
32876
|
const debugUIState = useDebugUIState();
|
|
32769
32877
|
const debugFocus = useDebugFocus();
|
|
32770
32878
|
|
|
32771
|
-
|
|
32879
|
+
// The whole of `standalone`: with no parent to register into, the effect
|
|
32880
|
+
// below has nobody to call, and every question a group asks about its
|
|
32881
|
+
// children is asked about someone else. Read here rather than filtered by
|
|
32882
|
+
// each group shape in turn, so a control that answers for itself is out of
|
|
32883
|
+
// the value on the way up AND out of the reset, the validation cascade and
|
|
32884
|
+
// the distribution on the way down.
|
|
32885
|
+
const parentFromContext = useContext(ParentUIStateControllerContext);
|
|
32886
|
+
const parentUIStateController = standalone ? undefined : parentFromContext;
|
|
32772
32887
|
const formContext = useContext(FormContext);
|
|
32773
32888
|
if (persists === undefined && formContext) {
|
|
32774
32889
|
persists = true;
|
|
@@ -32842,7 +32957,6 @@ const useUIStateController = (
|
|
|
32842
32957
|
parentUIStateController,
|
|
32843
32958
|
parentUiStateSignalHolder,
|
|
32844
32959
|
isProxy,
|
|
32845
|
-
allowNameless,
|
|
32846
32960
|
emptyUIState,
|
|
32847
32961
|
// Set here too, not only in `update` below: a control rendered once and
|
|
32848
32962
|
// never re-rendered would otherwise never say whether it was GIVEN a
|
|
@@ -33508,18 +33622,14 @@ const GROUP_DEFAULTS = {
|
|
|
33508
33622
|
single: {
|
|
33509
33623
|
// The same exclusions canRegisterAsFacadeChild already makes below (the
|
|
33510
33624
|
// picker façade asked the very same question: which child IS the value).
|
|
33511
|
-
// Buttons and links never hold one
|
|
33512
|
-
//
|
|
33513
|
-
// box driving some other list — not the list itself, which stays a
|
|
33625
|
+
// Buttons and links never hold one. A control *carrying* navi-list is the
|
|
33626
|
+
// search box driving some other list — not the list itself, which stays a
|
|
33514
33627
|
// perfectly good single value here (one item, or the array a multiple list
|
|
33515
33628
|
// exposes). Excluding the searcher is what leaves the list alone.
|
|
33516
33629
|
childControlFilter: (child) => {
|
|
33517
33630
|
if (child.controlType === "button" || child.controlType === "link") {
|
|
33518
33631
|
return false;
|
|
33519
33632
|
}
|
|
33520
|
-
if (child.allowNameless) {
|
|
33521
|
-
return false;
|
|
33522
|
-
}
|
|
33523
33633
|
if (child.props?.["navi-list"]) {
|
|
33524
33634
|
return false;
|
|
33525
33635
|
}
|
|
@@ -33544,7 +33654,7 @@ const GROUP_DEFAULTS = {
|
|
|
33544
33654
|
aggregateChildStates: (children) => {
|
|
33545
33655
|
const groupValues = {};
|
|
33546
33656
|
for (const child of children) {
|
|
33547
|
-
const { name,
|
|
33657
|
+
const { name, emptyUIState } = child;
|
|
33548
33658
|
// A control holding nothing writes its own empty, not a hole: the key
|
|
33549
33659
|
// is in the object either way, and what is read from it keeps the shape
|
|
33550
33660
|
// the reader was promised (see resolveEmptyUIState).
|
|
@@ -33553,13 +33663,6 @@ const GROUP_DEFAULTS = {
|
|
|
33553
33663
|
? emptyUIState
|
|
33554
33664
|
: child.uiState;
|
|
33555
33665
|
if (!name) {
|
|
33556
|
-
if (allowNameless) {
|
|
33557
|
-
// A control that says it is not a field is not one, whatever it
|
|
33558
|
-
// holds: a picker used as a door holds the shape its popup draws,
|
|
33559
|
-
// and merging that in would put the popup's keys in the object as
|
|
33560
|
-
// if the door had been a group.
|
|
33561
|
-
continue;
|
|
33562
|
-
}
|
|
33563
33666
|
// A nameless GROUP is a grouping, not a value: it exists to hold its
|
|
33564
33667
|
// children together (a WheelGroup sharing navigation, a fieldset-ish
|
|
33565
33668
|
// cluster) without claiming a key of its own, so what it holds is
|
|
@@ -33672,6 +33775,7 @@ const useUIGroupStateController = (
|
|
|
33672
33775
|
uiActionInternal,
|
|
33673
33776
|
allowCapture = false,
|
|
33674
33777
|
cascadeValidationToChildren = false,
|
|
33778
|
+
standalone,
|
|
33675
33779
|
},
|
|
33676
33780
|
) => {
|
|
33677
33781
|
const debugPopup = useDebugPopup();
|
|
@@ -33707,7 +33811,11 @@ const useUIGroupStateController = (
|
|
|
33707
33811
|
`Either use a known controlType/stateType or provide aggregateChildStates and distributeChildUIState explicitly.`,
|
|
33708
33812
|
);
|
|
33709
33813
|
}
|
|
33710
|
-
|
|
33814
|
+
// See the leaf controller's own `standalone` above: the same opt-out, and a
|
|
33815
|
+
// group taking it also takes its whole subtree out — the controls inside it
|
|
33816
|
+
// register into IT, and it registers into nobody.
|
|
33817
|
+
const parentFromContext = useContext(ParentUIStateControllerContext);
|
|
33818
|
+
const parentUIStateController = standalone ? undefined : parentFromContext;
|
|
33711
33819
|
// A bound signal seeds the group the way `defaultValue` does — uncontrolled,
|
|
33712
33820
|
// with the signal's current value as what it starts on. Write-back is handled
|
|
33713
33821
|
// by applyState's own boundSignal; the read half is below: children are
|
|
@@ -34478,7 +34586,6 @@ const EMPTY_OBJECT = {};
|
|
|
34478
34586
|
*/
|
|
34479
34587
|
const useUIFacadeStateController = (props, realUIStateController) => {
|
|
34480
34588
|
const firstChildControllerRef = useRef(null);
|
|
34481
|
-
const namelessChildSetRef = useRef(new Set());
|
|
34482
34589
|
const updatingRef = useRef(false);
|
|
34483
34590
|
const debugPopup = useDebugPopup();
|
|
34484
34591
|
const debugInteraction = useDebugInteraction();
|
|
@@ -34506,16 +34613,9 @@ const useUIFacadeStateController = (props, realUIStateController) => {
|
|
|
34506
34613
|
if (childController.isProxy) {
|
|
34507
34614
|
return false;
|
|
34508
34615
|
}
|
|
34509
|
-
if (childController.allowNameless) {
|
|
34510
|
-
// A control saying it is not a field is not the one the picker talks
|
|
34511
|
-
// to: the search box above the list, the "select all" switch beside
|
|
34512
|
-
// it. It is there to help find the answer, not to be it.
|
|
34513
|
-
namelessChildSetRef.current.add(childController);
|
|
34514
|
-
return false;
|
|
34515
|
-
}
|
|
34516
34616
|
if (childController.props["navi-list"]) {
|
|
34517
|
-
// Controls with navi-list act as
|
|
34518
|
-
// not
|
|
34617
|
+
// Controls with navi-list act as list navigators driving some other
|
|
34618
|
+
// list, not as the picker's synced child.
|
|
34519
34619
|
return false;
|
|
34520
34620
|
}
|
|
34521
34621
|
return true;
|
|
@@ -34533,10 +34633,6 @@ const useUIFacadeStateController = (props, realUIStateController) => {
|
|
|
34533
34633
|
}
|
|
34534
34634
|
const child = firstChildControllerRef.current;
|
|
34535
34635
|
if (!child) {
|
|
34536
|
-
warnPopupHasNothingButNamelessControls(
|
|
34537
|
-
props,
|
|
34538
|
-
namelessChildSetRef.current,
|
|
34539
|
-
);
|
|
34540
34636
|
return;
|
|
34541
34637
|
}
|
|
34542
34638
|
updatingRef.current = true;
|
|
@@ -34571,7 +34667,7 @@ const useUIFacadeStateController = (props, realUIStateController) => {
|
|
|
34571
34667
|
`A picker talks to ONE control: the first one receives the picker's whole value and is the only one read back, ` +
|
|
34572
34668
|
`so this one is neither filled nor collected. ` +
|
|
34573
34669
|
`A popup holding several values needs one group around them — wrap them in a <ControlGroup>, name each control inside it, and give the picker type="object". ` +
|
|
34574
|
-
`A control that
|
|
34670
|
+
`A control that answers for itself rather than for the picker — a search box, a list acting on every touch — says so with standalone and steps out of the way.`,
|
|
34575
34671
|
child,
|
|
34576
34672
|
);
|
|
34577
34673
|
} else {
|
|
@@ -34698,11 +34794,6 @@ const useUIFacadeStateController = (props, realUIStateController) => {
|
|
|
34698
34794
|
|
|
34699
34795
|
const describePicker = (props) =>
|
|
34700
34796
|
`<Picker${props.name ? ` name="${props.name}"` : ""}${props.type ? ` type="${props.type}"` : ""}>`;
|
|
34701
|
-
const warnPopupHasNothingButNamelessControls = (props, namelessChildSet) => {
|
|
34702
|
-
{
|
|
34703
|
-
return;
|
|
34704
|
-
}
|
|
34705
|
-
};
|
|
34706
34797
|
|
|
34707
34798
|
/**
|
|
34708
34799
|
* Returns true when `e` should trigger parent notification (child → parent bubbling).
|
|
@@ -34975,19 +35066,19 @@ const ControlgroupChildrenWrapper = ({
|
|
|
34975
35066
|
*/
|
|
34976
35067
|
const useControlProps = (props, {
|
|
34977
35068
|
controlType,
|
|
34978
|
-
allowNameless: allowNamelessByDefault,
|
|
34979
35069
|
persists,
|
|
34980
35070
|
uiActionInternal
|
|
34981
35071
|
}) => {
|
|
34982
35072
|
const debugUIState = useDebugUIState();
|
|
34983
35073
|
const debugAction = useDebugAction();
|
|
34984
35074
|
|
|
34985
|
-
//
|
|
34986
|
-
//
|
|
34987
|
-
//
|
|
34988
|
-
|
|
34989
|
-
|
|
34990
|
-
|
|
35075
|
+
// This control answers for itself: whatever group it sits in expects nothing
|
|
35076
|
+
// from it and gives it nothing back. See useUIStateController, which is where
|
|
35077
|
+
// the whole of it happens.
|
|
35078
|
+
const {
|
|
35079
|
+
standalone
|
|
35080
|
+
} = props;
|
|
35081
|
+
delete props.standalone;
|
|
34991
35082
|
const idDefault = useId();
|
|
34992
35083
|
const controlId = useContext(ControlIdContext);
|
|
34993
35084
|
props.id = props.id || controlId || idDefault;
|
|
@@ -35043,7 +35134,7 @@ const useControlProps = (props, {
|
|
|
35043
35134
|
const uiStateController = useUIStateController(props, {
|
|
35044
35135
|
controlInfo,
|
|
35045
35136
|
syncDomState,
|
|
35046
|
-
|
|
35137
|
+
standalone,
|
|
35047
35138
|
persists,
|
|
35048
35139
|
uiActionInternal
|
|
35049
35140
|
});
|
|
@@ -35242,28 +35333,7 @@ const useControlProps = (props, {
|
|
|
35242
35333
|
if (!deferredCommand) {
|
|
35243
35334
|
return;
|
|
35244
35335
|
}
|
|
35245
|
-
|
|
35246
|
-
// The action was turned down (a failing constraint, a gate saying
|
|
35247
|
-
// no) — nothing happened, so nothing follows.
|
|
35248
|
-
return;
|
|
35249
|
-
}
|
|
35250
|
-
if (completion.isRunning) {
|
|
35251
|
-
completion.whenSucceeded(deferredCommand);
|
|
35252
|
-
return;
|
|
35253
|
-
}
|
|
35254
|
-
// Synchronous: already settled, and how it ended still decides. An
|
|
35255
|
-
// action that never started (nothing to run) leaves no outcome, and
|
|
35256
|
-
// the command runs as it always did.
|
|
35257
|
-
let succeeded = true;
|
|
35258
|
-
completion.whenSettled(({
|
|
35259
|
-
error,
|
|
35260
|
-
aborted
|
|
35261
|
-
}) => {
|
|
35262
|
-
succeeded = !error && !aborted;
|
|
35263
|
-
});
|
|
35264
|
-
if (succeeded) {
|
|
35265
|
-
deferredCommand();
|
|
35266
|
-
}
|
|
35336
|
+
runWhenActionSucceeded(completion, deferredCommand);
|
|
35267
35337
|
};
|
|
35268
35338
|
return {
|
|
35269
35339
|
keyDown: keyDownDefault,
|
|
@@ -36024,8 +36094,10 @@ const useControlgroupProps = (props, {
|
|
|
36024
36094
|
cascadeValidationToChildren = false
|
|
36025
36095
|
}) => {
|
|
36026
36096
|
const {
|
|
36027
|
-
action
|
|
36097
|
+
action,
|
|
36098
|
+
standalone
|
|
36028
36099
|
} = props;
|
|
36100
|
+
delete props.standalone;
|
|
36029
36101
|
const uiGroupStateController = useUIGroupStateController(props, controlType, {
|
|
36030
36102
|
stateType,
|
|
36031
36103
|
childControlFilter,
|
|
@@ -36035,7 +36107,8 @@ const useControlgroupProps = (props, {
|
|
|
36035
36107
|
wantRequesterButtonState,
|
|
36036
36108
|
uiActionInternal,
|
|
36037
36109
|
allowCapture,
|
|
36038
|
-
cascadeValidationToChildren
|
|
36110
|
+
cascadeValidationToChildren,
|
|
36111
|
+
standalone
|
|
36039
36112
|
});
|
|
36040
36113
|
const [boundAction] = useActionBoundToOneParam(action, uiGroupStateController.uiStateSignal);
|
|
36041
36114
|
// Mirror single-input behaviour: a controlled value with nobody listening
|
|
@@ -40403,8 +40476,7 @@ const LinkPlain = props => {
|
|
|
40403
40476
|
selectionController
|
|
40404
40477
|
});
|
|
40405
40478
|
const [controlRootProps, controlHostProps] = useControlProps(props, {
|
|
40406
|
-
controlType: "link"
|
|
40407
|
-
allowNameless: true
|
|
40479
|
+
controlType: "link"
|
|
40408
40480
|
});
|
|
40409
40481
|
const {
|
|
40410
40482
|
basePseudoState
|
|
@@ -40836,6 +40908,10 @@ let navCount = 0;
|
|
|
40836
40908
|
// Worn by a nav of routes while a route movement between two of its tabs is
|
|
40837
40909
|
// pictured (see markIndicatorTakesPart, and the CSS below for what it decides).
|
|
40838
40910
|
const BETWEEN_TABS_ATTRIBUTE = "data-nav-between-tabs";
|
|
40911
|
+
// What one notch of a wheel that counts in LINES is worth on a row that counts
|
|
40912
|
+
// in pixels. A rough line, deliberately: nothing here needs to agree with a
|
|
40913
|
+
// paragraph, only to move the row about as far as a notch moves a page.
|
|
40914
|
+
const WHEEL_LINE_SIZE = 16;
|
|
40839
40915
|
const css$W = /* css */`@layer navi {
|
|
40840
40916
|
.navi_nav {
|
|
40841
40917
|
--nav-border: none;
|
|
@@ -40919,6 +40995,14 @@ const css$W = /* css */`@layer navi {
|
|
|
40919
40995
|
justify-content: stretch;
|
|
40920
40996
|
display: flex;
|
|
40921
40997
|
|
|
40998
|
+
&[data-scrollable] {
|
|
40999
|
+
max-width: 100%;
|
|
41000
|
+
}
|
|
41001
|
+
|
|
41002
|
+
&[data-scrollable][data-vertical] {
|
|
41003
|
+
max-height: 100%;
|
|
41004
|
+
}
|
|
41005
|
+
|
|
40922
41006
|
& .navi_link {
|
|
40923
41007
|
user-select: none;
|
|
40924
41008
|
--x-nav-child-border-radius: calc(var(--nav-border-radius) - var(--nav-padding));
|
|
@@ -41087,6 +41171,39 @@ const Nav = ({
|
|
|
41087
41171
|
const slideContainerElementRef = useRef(null);
|
|
41088
41172
|
const indicatorPosition = slideContainer ? positionOfCurrentIndicator(currentIndicator, vertical) : null;
|
|
41089
41173
|
|
|
41174
|
+
// Which tab of this row is the one being shown: the slide the container says
|
|
41175
|
+
// it is on, or, for a row of routes, the link the browser is on.
|
|
41176
|
+
const getCurrentTabElement = () => {
|
|
41177
|
+
const navElement = navRef.current;
|
|
41178
|
+
if (!navElement) {
|
|
41179
|
+
return null;
|
|
41180
|
+
}
|
|
41181
|
+
const containerElement = slideContainerElementRef.current;
|
|
41182
|
+
if (!containerElement) {
|
|
41183
|
+
return navElement.querySelector("[data-href-current]");
|
|
41184
|
+
}
|
|
41185
|
+
const currentArea = containerElement.getAttribute(SLIDE_CURRENT_ATTRIBUTE);
|
|
41186
|
+
if (currentArea === null) {
|
|
41187
|
+
return null;
|
|
41188
|
+
}
|
|
41189
|
+
return navElement.querySelector(`[data-slide-target="${CSS.escape(currentArea)}"]`);
|
|
41190
|
+
};
|
|
41191
|
+
// …and which one the picture leans on while it is not on the current one:
|
|
41192
|
+
// the slide a finger is pulling in, the slide an animation is leaving. There
|
|
41193
|
+
// is one only while a travel is playing.
|
|
41194
|
+
const getTowardTabElement = () => {
|
|
41195
|
+
const navElement = navRef.current;
|
|
41196
|
+
const containerElement = slideContainerElementRef.current;
|
|
41197
|
+
if (!navElement || !containerElement) {
|
|
41198
|
+
return null;
|
|
41199
|
+
}
|
|
41200
|
+
const towardArea = containerElement.getAttribute(SLIDE_TOWARD_ATTRIBUTE);
|
|
41201
|
+
if (towardArea === null) {
|
|
41202
|
+
return null;
|
|
41203
|
+
}
|
|
41204
|
+
return navElement.querySelector(`[data-slide-target="${CSS.escape(towardArea)}"]`);
|
|
41205
|
+
};
|
|
41206
|
+
|
|
41090
41207
|
// Where the trait is and where it is headed, as four numbers of pixels the
|
|
41091
41208
|
// CSS above interpolates between (see the .navi_nav_indicator rules). Written
|
|
41092
41209
|
// by hand rather than rendered: it is read off the row as it stands, and the
|
|
@@ -41100,8 +41217,7 @@ const Nav = ({
|
|
|
41100
41217
|
}
|
|
41101
41218
|
const tabElements = Array.from(navElement.querySelectorAll("[data-slide-target]"));
|
|
41102
41219
|
const areaOf = tabElement => tabElement.getAttribute("data-slide-target");
|
|
41103
|
-
const
|
|
41104
|
-
const currentIndex = tabElements.findIndex(tabElement => areaOf(tabElement) === currentArea);
|
|
41220
|
+
const currentIndex = tabElements.indexOf(getCurrentTabElement());
|
|
41105
41221
|
if (currentIndex === -1) {
|
|
41106
41222
|
// On a slide no tab in this row names: there is no tab to sit under.
|
|
41107
41223
|
navElement.removeAttribute("data-nav-indicator-measured");
|
|
@@ -41143,6 +41259,226 @@ const Nav = ({
|
|
|
41143
41259
|
// about the row as it is now.
|
|
41144
41260
|
const paintIndicatorGeometryRef = useRef(null);
|
|
41145
41261
|
paintIndicatorGeometryRef.current = paintIndicatorGeometry;
|
|
41262
|
+
|
|
41263
|
+
// A row of tabs wider than the box holding it scrolls (the caller asks for
|
|
41264
|
+
// that with overflowX) — and then saying which tab one is on is not enough,
|
|
41265
|
+
// that tab must be somewhere one can see. The trait needs nothing here: it
|
|
41266
|
+
// is placed with offsetLeft, inside the row, so it rides along.
|
|
41267
|
+
// The ROW is what gets scrolled, never the tab: element.scrollIntoView()
|
|
41268
|
+
// walks every scrolling ancestor it finds, and a tab row lives in sheets
|
|
41269
|
+
// that sit on pages that scroll — bringing a tab into sight must move the
|
|
41270
|
+
// row and nothing else.
|
|
41271
|
+
const tabInSightRef = useRef(null);
|
|
41272
|
+
const keepCurrentTabInSight = () => {
|
|
41273
|
+
const navElement = navRef.current;
|
|
41274
|
+
const currentTabElement = getCurrentTabElement();
|
|
41275
|
+
// Brought into sight once, and then left where it is: the row scrolls, so
|
|
41276
|
+
// the user may have pushed it elsewhere since to read the other tabs, and
|
|
41277
|
+
// a render that changed none of this has no business taking that back.
|
|
41278
|
+
if (currentTabElement === tabInSightRef.current) {
|
|
41279
|
+
return;
|
|
41280
|
+
}
|
|
41281
|
+
tabInSightRef.current = currentTabElement;
|
|
41282
|
+
if (!currentTabElement) {
|
|
41283
|
+
return;
|
|
41284
|
+
}
|
|
41285
|
+
scrollIntoViewScoped(currentTabElement, {
|
|
41286
|
+
container: navElement
|
|
41287
|
+
});
|
|
41288
|
+
};
|
|
41289
|
+
|
|
41290
|
+
// …and while a travel is playing the row gets there AT THE PACE OF THE
|
|
41291
|
+
// TRAVEL, off the very number the trait is drawn from: the container
|
|
41292
|
+
// publishes how far between the two slides the picture stands
|
|
41293
|
+
// (--slide-travel-progress, written on this row — see followerElements), and
|
|
41294
|
+
// the row is scrolled the same fraction of the way between the two tabs. The
|
|
41295
|
+
// slides gliding while the tabs jump at the end is one movement told twice.
|
|
41296
|
+
//
|
|
41297
|
+
// Read frame by frame, because that number is nobody's news: under a finger
|
|
41298
|
+
// the container writes it per frame with no announcement (a write is not a
|
|
41299
|
+
// change), and for a travel that was asked for it is the browser that
|
|
41300
|
+
// animates it. So the row samples it, exactly as the trait does — except that
|
|
41301
|
+
// a scroll offset is not a thing CSS can be handed.
|
|
41302
|
+
const travelScrollRef = useRef(null);
|
|
41303
|
+
const syncTabScroll = ({
|
|
41304
|
+
afterAFrame
|
|
41305
|
+
} = {}) => {
|
|
41306
|
+
const navElement = navRef.current;
|
|
41307
|
+
if (!navElement) {
|
|
41308
|
+
return;
|
|
41309
|
+
}
|
|
41310
|
+
const currentTabElement = getCurrentTabElement();
|
|
41311
|
+
const towardTabElement = getTowardTabElement();
|
|
41312
|
+
if (!currentTabElement || !towardTabElement) {
|
|
41313
|
+
travelScrollRef.current = null;
|
|
41314
|
+
// Nothing is travelling — but a travel that was ASKED for is announced a
|
|
41315
|
+
// breath after the slide it lands on, so a row that lands the moment the
|
|
41316
|
+
// current tab changes lands ahead of the movement it should have ridden,
|
|
41317
|
+
// and the slides then glide towards a row already there. Giving it a
|
|
41318
|
+
// frame costs nothing: the slides have not moved either.
|
|
41319
|
+
// Only for a row of slides, and only once — a row of routes has no travel
|
|
41320
|
+
// to ride, and the second look is the one that lands.
|
|
41321
|
+
if (slideContainer && !afterAFrame && tabInSightRef.current && tabInSightRef.current !== currentTabElement) {
|
|
41322
|
+
scheduleTabScrollSync({
|
|
41323
|
+
nextFrame: true
|
|
41324
|
+
});
|
|
41325
|
+
return;
|
|
41326
|
+
}
|
|
41327
|
+
// Nowhere between: the row lands where it belongs.
|
|
41328
|
+
keepCurrentTabInSight();
|
|
41329
|
+
return;
|
|
41330
|
+
}
|
|
41331
|
+
let travel = travelScrollRef.current;
|
|
41332
|
+
if (!travel || travel.from !== currentTabElement || travel.toward !== towardTabElement) {
|
|
41333
|
+
// The two ends, measured once for this travel: measuring them again on a
|
|
41334
|
+
// row that has since moved would be measuring from the answer.
|
|
41335
|
+
const fromOffsets = getScrollIntoViewScopedOffsets(currentTabElement, {
|
|
41336
|
+
container: navElement
|
|
41337
|
+
});
|
|
41338
|
+
const towardOffsets = getScrollIntoViewScopedOffsets(towardTabElement, {
|
|
41339
|
+
container: navElement
|
|
41340
|
+
});
|
|
41341
|
+
// Signed the way the container counts: +1 when the picture leans on a
|
|
41342
|
+
// slide sitting BEFORE the current one, -1 when it sits after — the same
|
|
41343
|
+
// reading paintIndicatorGeometry does of the same number.
|
|
41344
|
+
const sign = currentTabElement.compareDocumentPosition(towardTabElement) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1;
|
|
41345
|
+
travel = {
|
|
41346
|
+
from: currentTabElement,
|
|
41347
|
+
toward: towardTabElement,
|
|
41348
|
+
left: fromOffsets.left,
|
|
41349
|
+
top: fromOffsets.top,
|
|
41350
|
+
leftDelta: (towardOffsets.left - fromOffsets.left) * sign,
|
|
41351
|
+
topDelta: (towardOffsets.top - fromOffsets.top) * sign
|
|
41352
|
+
};
|
|
41353
|
+
travelScrollRef.current = travel;
|
|
41354
|
+
}
|
|
41355
|
+
const progress = parseFloat(getComputedStyle(navElement).getPropertyValue("--slide-travel-progress")) || 0;
|
|
41356
|
+
navElement.scrollTo({
|
|
41357
|
+
left: travel.left + progress * travel.leftDelta,
|
|
41358
|
+
top: travel.top + progress * travel.topDelta,
|
|
41359
|
+
// The row is being driven frame by frame: a caller who asked for smooth
|
|
41360
|
+
// scrolling asked it of a scroll that lands, not of one that is held.
|
|
41361
|
+
behavior: "instant"
|
|
41362
|
+
});
|
|
41363
|
+
scheduleTabScrollSync({
|
|
41364
|
+
nextFrame: true
|
|
41365
|
+
});
|
|
41366
|
+
};
|
|
41367
|
+
const syncTabScrollRef = useRef(null);
|
|
41368
|
+
syncTabScrollRef.current = syncTabScroll;
|
|
41369
|
+
|
|
41370
|
+
// When to look: on the next frame while a travel is being followed, and
|
|
41371
|
+
// otherwise at the end of the current task. A MICROTASK and not a frame,
|
|
41372
|
+
// because the two are told apart in the same breath — a travel that was
|
|
41373
|
+
// asked for announces the slide it lands on before it announces the one it
|
|
41374
|
+
// is leaving, so a row settling straight away would jump to the destination
|
|
41375
|
+
// and only then be asked to travel there. And it is still before the paint,
|
|
41376
|
+
// which a frame is not: a row that opens on a far tab must be scrolled
|
|
41377
|
+
// already the first time it is seen.
|
|
41378
|
+
const syncScheduledRef = useRef(false);
|
|
41379
|
+
const syncFrameRef = useRef(0);
|
|
41380
|
+
const scheduleTabScrollSync = ({
|
|
41381
|
+
nextFrame
|
|
41382
|
+
} = {}) => {
|
|
41383
|
+
if (syncScheduledRef.current) {
|
|
41384
|
+
return;
|
|
41385
|
+
}
|
|
41386
|
+
syncScheduledRef.current = true;
|
|
41387
|
+
const run = () => {
|
|
41388
|
+
syncScheduledRef.current = false;
|
|
41389
|
+
syncFrameRef.current = 0;
|
|
41390
|
+
syncTabScrollRef.current({
|
|
41391
|
+
afterAFrame: nextFrame
|
|
41392
|
+
});
|
|
41393
|
+
};
|
|
41394
|
+
if (nextFrame) {
|
|
41395
|
+
syncFrameRef.current = requestAnimationFrame(run);
|
|
41396
|
+
} else {
|
|
41397
|
+
queueMicrotask(run);
|
|
41398
|
+
}
|
|
41399
|
+
};
|
|
41400
|
+
const scheduleTabScrollSyncRef = useRef(null);
|
|
41401
|
+
scheduleTabScrollSyncRef.current = scheduleTabScrollSync;
|
|
41402
|
+
useLayoutEffect(() => {
|
|
41403
|
+
return () => {
|
|
41404
|
+
cancelAnimationFrame(syncFrameRef.current);
|
|
41405
|
+
};
|
|
41406
|
+
}, []);
|
|
41407
|
+
|
|
41408
|
+
// The row watches its own size and reads itself again when it changes — a
|
|
41409
|
+
// badge count, a font that just arrived, a window resized: what was written
|
|
41410
|
+
// is pixels, where the trait sits and how far the row is scrolled, and
|
|
41411
|
+
// pixels measured on a row of another width are wrong. A row that had no
|
|
41412
|
+
// width at all is the same story with a sharper edge: inside a closed
|
|
41413
|
+
// <dialog> everything measures 0, and the current tab does not change when
|
|
41414
|
+
// the dialog opens, so nothing else would ever look again.
|
|
41415
|
+
// A scroll takes no size from anything, so this stays quiet while the row is
|
|
41416
|
+
// being pushed around by hand.
|
|
41417
|
+
useLayoutEffect(() => {
|
|
41418
|
+
const sizeObserver = new ResizeObserver(() => {
|
|
41419
|
+
paintIndicatorGeometryRef.current();
|
|
41420
|
+
// What was brought into sight was brought there for another row.
|
|
41421
|
+
tabInSightRef.current = null;
|
|
41422
|
+
scheduleTabScrollSyncRef.current();
|
|
41423
|
+
});
|
|
41424
|
+
sizeObserver.observe(navRef.current);
|
|
41425
|
+
return () => {
|
|
41426
|
+
sizeObserver.disconnect();
|
|
41427
|
+
};
|
|
41428
|
+
}, []);
|
|
41429
|
+
|
|
41430
|
+
// A row that scrolls sideways, and a mouse that only turns one way. A
|
|
41431
|
+
// vertical wheel is left to the vertical axis, which a row of tabs does not
|
|
41432
|
+
// have: the push goes past the row to the page, and the tabs stay where they
|
|
41433
|
+
// were — a row a trackpad can push around and a mouse cannot reach at all.
|
|
41434
|
+
// Up and further along are the same wish here, so the push moves the row.
|
|
41435
|
+
// Only for a row that scrolls on ONE axis: one that scrolls the way the
|
|
41436
|
+
// wheel turns already has the answer it wants, and taking the push would be
|
|
41437
|
+
// answering it twice.
|
|
41438
|
+
useLayoutEffect(() => {
|
|
41439
|
+
const navElement = navRef.current;
|
|
41440
|
+
const onWheel = wheelEvent => {
|
|
41441
|
+
// The burst is already somebody else's — a box travelling under the
|
|
41442
|
+
// pointer, a wheel picker — and stays theirs until it dies out, wherever
|
|
41443
|
+
// the pointer has drifted since (see wheel_gesture.js).
|
|
41444
|
+
if (wheelGestureIsTakenFrom(navElement)) {
|
|
41445
|
+
return;
|
|
41446
|
+
}
|
|
41447
|
+
const {
|
|
41448
|
+
deltaX,
|
|
41449
|
+
deltaY
|
|
41450
|
+
} = wheelEvent;
|
|
41451
|
+
if (Math.abs(deltaY) <= Math.abs(deltaX)) {
|
|
41452
|
+
// Sideways already: the browser knows where to put it.
|
|
41453
|
+
return;
|
|
41454
|
+
}
|
|
41455
|
+
const scrollableWidth = navElement.scrollWidth - navElement.clientWidth;
|
|
41456
|
+
const scrollableHeight = navElement.scrollHeight - navElement.clientHeight;
|
|
41457
|
+
if (scrollableWidth <= 0 || scrollableHeight > 0) {
|
|
41458
|
+
return;
|
|
41459
|
+
}
|
|
41460
|
+
// Not every wheel speaks in pixels: some count lines, some count pages.
|
|
41461
|
+
const notch = wheelEvent.deltaMode === 1 ? WHEEL_LINE_SIZE : wheelEvent.deltaMode === 2 ? navElement.clientWidth : 1;
|
|
41462
|
+
const scrollLeftBefore = navElement.scrollLeft;
|
|
41463
|
+
const scrollLeftWanted = Math.max(0, Math.min(scrollableWidth, scrollLeftBefore + deltaY * notch));
|
|
41464
|
+
if (scrollLeftWanted === scrollLeftBefore) {
|
|
41465
|
+
// The row is at its end and the hand is still pushing: what is left
|
|
41466
|
+
// over is the page's, exactly as it would have been without any of
|
|
41467
|
+
// this.
|
|
41468
|
+
return;
|
|
41469
|
+
}
|
|
41470
|
+
wheelEvent.preventDefault();
|
|
41471
|
+
claimWheelGesture(navElement);
|
|
41472
|
+
navElement.scrollLeft = scrollLeftWanted;
|
|
41473
|
+
};
|
|
41474
|
+
navElement.addEventListener("wheel", onWheel, {
|
|
41475
|
+
passive: false
|
|
41476
|
+
});
|
|
41477
|
+
return () => {
|
|
41478
|
+
navElement.removeEventListener("wheel", onWheel);
|
|
41479
|
+
releaseWheelGesture(navElement);
|
|
41480
|
+
};
|
|
41481
|
+
}, []);
|
|
41146
41482
|
useLayoutEffect(() => {
|
|
41147
41483
|
if (!slideContainer) {
|
|
41148
41484
|
return undefined;
|
|
@@ -41156,6 +41492,7 @@ const Nav = ({
|
|
|
41156
41492
|
const readContainer = () => {
|
|
41157
41493
|
setCurrentSlideArea(containerElement.getAttribute(SLIDE_CURRENT_ATTRIBUTE) ?? undefined);
|
|
41158
41494
|
paintIndicatorGeometryRef.current();
|
|
41495
|
+
scheduleTabScrollSyncRef.current();
|
|
41159
41496
|
};
|
|
41160
41497
|
readContainer();
|
|
41161
41498
|
// Where one is and what the picture leans on are written on the container,
|
|
@@ -41165,16 +41502,8 @@ const Nav = ({
|
|
|
41165
41502
|
// because a write is not a change — the attribute the trait follows is
|
|
41166
41503
|
// written on every frame of a gesture with the same value in it.
|
|
41167
41504
|
containerElement.addEventListener(SLIDE_STATE_EVENT, readContainer);
|
|
41168
|
-
// A row whose tabs changed width — a badge count, a font that just
|
|
41169
|
-
// arrived, a window resized — is measured again: what was written is
|
|
41170
|
-
// pixels, and pixels go stale.
|
|
41171
|
-
const sizeObserver = new ResizeObserver(() => {
|
|
41172
|
-
paintIndicatorGeometryRef.current();
|
|
41173
|
-
});
|
|
41174
|
-
sizeObserver.observe(navRef.current);
|
|
41175
41505
|
return () => {
|
|
41176
41506
|
containerElement.removeEventListener(SLIDE_STATE_EVENT, readContainer);
|
|
41177
|
-
sizeObserver.disconnect();
|
|
41178
41507
|
slideContainerElementRef.current = null;
|
|
41179
41508
|
};
|
|
41180
41509
|
}, [slideContainer]);
|
|
@@ -41183,6 +41512,7 @@ const Nav = ({
|
|
|
41183
41512
|
// and no observer above watches this row's own children.
|
|
41184
41513
|
useLayoutEffect(() => {
|
|
41185
41514
|
paintIndicatorGeometry();
|
|
41515
|
+
scheduleTabScrollSync();
|
|
41186
41516
|
});
|
|
41187
41517
|
|
|
41188
41518
|
// Whether the bar keeps its name for the route movement about to be pictured
|
|
@@ -43850,7 +44180,7 @@ const Expandable = props => {
|
|
|
43850
44180
|
}
|
|
43851
44181
|
if (hasAction) {
|
|
43852
44182
|
if (nextOpen) {
|
|
43853
|
-
effectiveAction.run();
|
|
44183
|
+
runUnwatched(() => effectiveAction.run());
|
|
43854
44184
|
} else {
|
|
43855
44185
|
effectiveAction.abort();
|
|
43856
44186
|
}
|
|
@@ -44001,7 +44331,7 @@ const Expandable = props => {
|
|
|
44001
44331
|
// Mounted already open: the content is visible, its data is due.
|
|
44002
44332
|
useEffect(() => {
|
|
44003
44333
|
if (openedRef.current && hasAction) {
|
|
44004
|
-
effectiveAction.run();
|
|
44334
|
+
runUnwatched(() => effectiveAction.run());
|
|
44005
44335
|
}
|
|
44006
44336
|
}, []);
|
|
44007
44337
|
const onRootKeyDown = keyboardEvent => {
|
|
@@ -44817,8 +45147,7 @@ const ButtonUI = props => {
|
|
|
44817
45147
|
loadingOutline = true
|
|
44818
45148
|
} = props;
|
|
44819
45149
|
const [buttonControlRootProps, buttonControlHostProps, controlChildrenWrapperProps] = useControlProps(props, {
|
|
44820
|
-
controlType: "button"
|
|
44821
|
-
allowNameless: true
|
|
45150
|
+
controlType: "button"
|
|
44822
45151
|
});
|
|
44823
45152
|
const {
|
|
44824
45153
|
basePseudoState,
|
|
@@ -45213,6 +45542,7 @@ const css$O = /* css */`.navi_control_swap {
|
|
|
45213
45542
|
|
|
45214
45543
|
& > .navi_control_swap_slot {
|
|
45215
45544
|
flex: 0 0 100cqw;
|
|
45545
|
+
min-width: 0;
|
|
45216
45546
|
display: flex;
|
|
45217
45547
|
|
|
45218
45548
|
& > * {
|
|
@@ -49882,30 +50212,18 @@ const Form = props => {
|
|
|
49882
50212
|
// is a different component: same group, no <form> element and none of the
|
|
49883
50213
|
// browser machinery that comes with it.
|
|
49884
50214
|
const isNested = Boolean(useContext(FormContext));
|
|
49885
|
-
|
|
50215
|
+
return isNested ? jsx(FormNested, {
|
|
49886
50216
|
...props
|
|
49887
50217
|
}) : jsx(FormControl, {
|
|
49888
50218
|
...props
|
|
49889
50219
|
});
|
|
49890
|
-
if (props.standalone) {
|
|
49891
|
-
// Nothing above to register with: the group hooks read the parent from
|
|
49892
|
-
// this context, so emptying it here is the whole opt-out.
|
|
49893
|
-
return jsx(ParentUIStateControllerContext.Provider, {
|
|
49894
|
-
value: undefined,
|
|
49895
|
-
children: form
|
|
49896
|
-
});
|
|
49897
|
-
}
|
|
49898
|
-
return form;
|
|
49899
50220
|
};
|
|
49900
50221
|
|
|
49901
50222
|
// What both forms are made of: one group, one context for what is inside it.
|
|
49902
|
-
// standalone is read by Form above and never goes further — least of all to the
|
|
49903
|
-
// DOM.
|
|
49904
50223
|
const useFormGroup = props => {
|
|
49905
50224
|
const propsForGroup = {
|
|
49906
50225
|
...props
|
|
49907
50226
|
};
|
|
49908
|
-
delete propsForGroup.standalone;
|
|
49909
50227
|
delete propsForGroup.canSendWhileUnchanged;
|
|
49910
50228
|
delete propsForGroup.pristineKey;
|
|
49911
50229
|
// Not the generic control `command`, which a control triggers on its own ui
|
|
@@ -58795,13 +59113,13 @@ const PickerCustomResolver = props => {
|
|
|
58795
59113
|
if (circle) {
|
|
58796
59114
|
props.variant = "icon";
|
|
58797
59115
|
}
|
|
58798
|
-
// A door, never a field (see `
|
|
58799
|
-
//
|
|
58800
|
-
//
|
|
58801
|
-
//
|
|
58802
|
-
//
|
|
58803
|
-
//
|
|
58804
|
-
props.
|
|
59116
|
+
// A door, never a field (see `standalone`): the form around it expects no
|
|
59117
|
+
// value from it. So the trigger answers to "button" — it is pressed to read
|
|
59118
|
+
// something, never typed into — the same thing a confirm picker says
|
|
59119
|
+
// through `picksNothing` (see picker.jsx). Said as the role here rather
|
|
59120
|
+
// than through that prop: `picksNothing` also takes the right slot away,
|
|
59121
|
+
// and the status icon a callout trigger draws sits in it.
|
|
59122
|
+
props.standalone = true;
|
|
58805
59123
|
if (props.role === undefined) {
|
|
58806
59124
|
props.role = "button";
|
|
58807
59125
|
}
|
|
@@ -59643,7 +59961,8 @@ installImportMetaCssBuild(import.meta);/**
|
|
|
59643
59961
|
*
|
|
59644
59962
|
* Yes is said with `--navi-confirm` (see commands.js). The popup closes on it,
|
|
59645
59963
|
* and only then does the picker do what it was standing in for — run its
|
|
59646
|
-
* `action`,
|
|
59964
|
+
* `action`, trigger its `command`, or both, the command following the action
|
|
59965
|
+
* and only if it succeeded — so the work runs on the trigger, where
|
|
59647
59966
|
* the loading state and the error callout are drawn, and where the user is
|
|
59648
59967
|
* looking again. Anything else that closes the popup (the cancel button,
|
|
59649
59968
|
* Escape, a click outside) is no.
|
|
@@ -59705,21 +60024,31 @@ const PickerConfirmResolver = props => {
|
|
|
59705
60024
|
// trigger.
|
|
59706
60025
|
const onConfirm = confirmEvent => {
|
|
59707
60026
|
const inputEl = getPickerInput(ref.current);
|
|
59708
|
-
|
|
59709
|
-
|
|
59710
|
-
|
|
59711
|
-
|
|
59712
|
-
|
|
60027
|
+
const runCommand = () => {
|
|
60028
|
+
triggerNaviCommand(inputEl, command, confirmEvent);
|
|
60029
|
+
};
|
|
60030
|
+
if (action === undefined) {
|
|
60031
|
+
if (command) {
|
|
60032
|
+
runCommand();
|
|
60033
|
+
}
|
|
59713
60034
|
return;
|
|
59714
60035
|
}
|
|
60036
|
+
// Both props on one trigger means "do this, then go there": the action runs
|
|
60037
|
+
// and the command follows it, only if it worked — a delete that fails must
|
|
60038
|
+
// not send the user to the list it did not leave. Same rule a `<Button
|
|
60039
|
+
// action command>` has (see onButtonInteractionAllowed in control_hooks).
|
|
60040
|
+
const completion = watchActionCompletion(findControlHost(inputEl) || inputEl, () => dispatchRequestAction(inputEl, {
|
|
60041
|
+
event: confirmEvent,
|
|
60042
|
+
name: "confirm"
|
|
60043
|
+
}));
|
|
59715
60044
|
if (command) {
|
|
59716
|
-
|
|
60045
|
+
runWhenActionSucceeded(completion, runCommand);
|
|
59717
60046
|
}
|
|
59718
60047
|
};
|
|
59719
60048
|
return jsx(Next, {
|
|
59720
60049
|
...props,
|
|
59721
60050
|
type: "navi_js",
|
|
59722
|
-
|
|
60051
|
+
standalone: true
|
|
59723
60052
|
// No chevron, no clear cross, whatever variant the trigger is drawn in:
|
|
59724
60053
|
// both announce a value, and a question holds none.
|
|
59725
60054
|
,
|
|
@@ -62119,6 +62448,7 @@ const ListFirstResolver = props => {
|
|
|
62119
62448
|
* deselectable?: boolean,
|
|
62120
62449
|
* maxLength?: number,
|
|
62121
62450
|
* maxLengthGuard?: number,
|
|
62451
|
+
* standalone?: boolean,
|
|
62122
62452
|
* action?: (value: any) => void,
|
|
62123
62453
|
* uiAction?: (value: any) => void,
|
|
62124
62454
|
* popover?: boolean,
|
|
@@ -62294,6 +62624,12 @@ const ListFirstResolver = props => {
|
|
|
62294
62624
|
* taking — and `uiAction` is not called. The selected ones stay takeable
|
|
62295
62625
|
* back, so a selection that arrived too long can always be brought back
|
|
62296
62626
|
* under the limit. Implies `maxLength` for validity.
|
|
62627
|
+
* @param {boolean} [props.standalone]
|
|
62628
|
+
* This list answers for itself: it does not register with the control group
|
|
62629
|
+
* or picker around it, so its selection stays out of that value and nothing
|
|
62630
|
+
* coming down — a distributed value, a reset — reaches it. What a popup whose
|
|
62631
|
+
* one answer is spread over several lists says, so that none of them is taken
|
|
62632
|
+
* for the answer itself.
|
|
62297
62633
|
*/
|
|
62298
62634
|
const List = createComponentResolver([ListFirstResolver, ListSelectableResolver, ListUI]);
|
|
62299
62635
|
const ListContent = ({
|
|
@@ -66060,8 +66396,7 @@ const BadgeButtonUI = props => {
|
|
|
66060
66396
|
const defaultRef = useRef();
|
|
66061
66397
|
props.ref = props.ref || defaultRef;
|
|
66062
66398
|
const [buttonRootProps, buttonHostProps] = useControlProps(props, {
|
|
66063
|
-
controlType: "button"
|
|
66064
|
-
allowNameless: true
|
|
66399
|
+
controlType: "button"
|
|
66065
66400
|
});
|
|
66066
66401
|
return jsx(Text, {
|
|
66067
66402
|
className: "navi_badge_button",
|
|
@@ -70725,7 +71060,7 @@ const SplitButton = props => {
|
|
|
70725
71060
|
}), jsx(Picker, {
|
|
70726
71061
|
id: menuId,
|
|
70727
71062
|
variant: "headless",
|
|
70728
|
-
|
|
71063
|
+
standalone: true,
|
|
70729
71064
|
anchor: rootRef,
|
|
70730
71065
|
...popupProps,
|
|
70731
71066
|
readOnly: readOnly,
|