@jsenv/navi 0.29.459 → 0.29.460
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 +324 -86
- package/dist/dev/jsenv_navi.js.map +16 -17
- package/dist/jsenv_navi.js +324 -86
- package/dist/jsenv_navi.js.map +16 -17
- package/docs/actions.md +15 -0
- package/docs/mobile_touch.md +56 -0
- package/docs/resource.md +21 -0
- package/package.json +1 -1
package/dist/dev/jsenv_navi.js
CHANGED
|
@@ -1927,6 +1927,27 @@ const markErrorAsTakenByRender = (error) => {
|
|
|
1927
1927
|
}
|
|
1928
1928
|
};
|
|
1929
1929
|
|
|
1930
|
+
/**
|
|
1931
|
+
* A prerun's failure is owed to no screen: nothing on screen asked for that
|
|
1932
|
+
* run, so the component that owns the read asks again rather than show a
|
|
1933
|
+
* failure from before it existed (see `run` in use_async_data.jsx). Taken
|
|
1934
|
+
* once, so the run asking again fails as its own even when it fails with the
|
|
1935
|
+
* same error object. Held by the error, it lasts as long as the failure stays
|
|
1936
|
+
* on the action: a later run replaces it, whether or not an owner ever came.
|
|
1937
|
+
*/
|
|
1938
|
+
const prerunFailureSet = new WeakSet();
|
|
1939
|
+
const markErrorAsPrerunFailure = (error) => {
|
|
1940
|
+
if (error && typeof error === "object") {
|
|
1941
|
+
prerunFailureSet.add(error);
|
|
1942
|
+
}
|
|
1943
|
+
};
|
|
1944
|
+
const takePrerunFailure = (error) => {
|
|
1945
|
+
if (!error || typeof error !== "object") {
|
|
1946
|
+
return false;
|
|
1947
|
+
}
|
|
1948
|
+
return prerunFailureSet.delete(error);
|
|
1949
|
+
};
|
|
1950
|
+
|
|
1930
1951
|
/**
|
|
1931
1952
|
* When the answer "nobody took it" is final.
|
|
1932
1953
|
*
|
|
@@ -4352,6 +4373,9 @@ const createAction = (callback, rootOptions = {}) => {
|
|
|
4352
4373
|
console.log(`"${action}": failed (error: ${error})`);
|
|
4353
4374
|
}
|
|
4354
4375
|
error.action = action;
|
|
4376
|
+
if (isPrerun) {
|
|
4377
|
+
markErrorAsPrerunFailure(error);
|
|
4378
|
+
}
|
|
4355
4379
|
batch(() => {
|
|
4356
4380
|
errorSignal.value = error;
|
|
4357
4381
|
runningStateSignal.value = FAILED;
|
|
@@ -4928,6 +4952,25 @@ const runUnwatched = (startRun) => {
|
|
|
4928
4952
|
return result;
|
|
4929
4953
|
};
|
|
4930
4954
|
|
|
4955
|
+
/**
|
|
4956
|
+
* Starts a prerun navi has nobody to await. Its failure is not reported:
|
|
4957
|
+
* nothing on screen asked for it. It stays on the action, marked as a
|
|
4958
|
+
* prerun's (see markErrorAsPrerunFailure), where whatever owns the read asks
|
|
4959
|
+
* again.
|
|
4960
|
+
*/
|
|
4961
|
+
const prerunUnwatched = (startPrerun) => {
|
|
4962
|
+
let result;
|
|
4963
|
+
try {
|
|
4964
|
+
result = startPrerun();
|
|
4965
|
+
} catch {
|
|
4966
|
+
return undefined;
|
|
4967
|
+
}
|
|
4968
|
+
if (result && typeof result.catch === "function") {
|
|
4969
|
+
result.catch(() => {});
|
|
4970
|
+
}
|
|
4971
|
+
return result;
|
|
4972
|
+
};
|
|
4973
|
+
|
|
4931
4974
|
/**
|
|
4932
4975
|
* Reactively runs an action whenever the params derived from signals change.
|
|
4933
4976
|
*
|
|
@@ -26470,10 +26513,24 @@ const resolveRerunOn = (rerunOn, inheritedRerunOn = defaultRerunOn) => {
|
|
|
26470
26513
|
};
|
|
26471
26514
|
};
|
|
26472
26515
|
|
|
26516
|
+
// What a `dependencies` entry names are callbacks, not verbs (`rerunOn` names
|
|
26517
|
+
// verbs, where `PATCH` covers `PATCH_MANY`): one callback of a resource can be
|
|
26518
|
+
// the write that creates (a move into a new item) while its sibling only edits.
|
|
26519
|
+
const WRITE_CALLBACK_NAMES = [
|
|
26520
|
+
"POST",
|
|
26521
|
+
"POST_MANY",
|
|
26522
|
+
"PUT",
|
|
26523
|
+
"PUT_MANY",
|
|
26524
|
+
"PATCH",
|
|
26525
|
+
"PATCH_MANY",
|
|
26526
|
+
"DELETE",
|
|
26527
|
+
"DELETE_MANY",
|
|
26528
|
+
];
|
|
26529
|
+
|
|
26473
26530
|
// This handles ALL resource lifecycle logic (rerun/reset) across all resources
|
|
26474
26531
|
const createResourceLifecycleManager = () => {
|
|
26475
26532
|
const registeredResources = new Map(); // Map<resourceInstance, lifecycleConfig>
|
|
26476
|
-
const resourceDependencies = new Map(); // Map<resourceInstance, Set<
|
|
26533
|
+
const resourceDependencies = new Map(); // Map<resourceInstance, Map<dependentResource, Set<writeCallbackName>>> — user-configured
|
|
26477
26534
|
const scopedManyParents = new Map(); // Map<childResource, Set<{resource, propertyName}>> — auto from scopedMany
|
|
26478
26535
|
|
|
26479
26536
|
const registerResource = (resourceScope, config) => {
|
|
@@ -26492,15 +26549,37 @@ const createResourceLifecycleManager = () => {
|
|
|
26492
26549
|
rangeReaderSet: new Set(),
|
|
26493
26550
|
});
|
|
26494
26551
|
|
|
26495
|
-
|
|
26496
|
-
|
|
26497
|
-
|
|
26498
|
-
|
|
26499
|
-
|
|
26500
|
-
|
|
26501
|
-
|
|
26552
|
+
for (const dependency of dependencies) {
|
|
26553
|
+
const [dependencyScope, writeNames] = resolveDependency(
|
|
26554
|
+
dependency,
|
|
26555
|
+
resourceScope,
|
|
26556
|
+
);
|
|
26557
|
+
let dependentMap = resourceDependencies.get(dependencyScope);
|
|
26558
|
+
if (!dependentMap) {
|
|
26559
|
+
dependentMap = new Map();
|
|
26560
|
+
resourceDependencies.set(dependencyScope, dependentMap);
|
|
26502
26561
|
}
|
|
26562
|
+
let writeNameSet = dependentMap.get(resourceScope);
|
|
26563
|
+
if (!writeNameSet) {
|
|
26564
|
+
writeNameSet = new Set();
|
|
26565
|
+
dependentMap.set(resourceScope, writeNameSet);
|
|
26566
|
+
}
|
|
26567
|
+
for (const writeName of writeNames) {
|
|
26568
|
+
writeNameSet.add(writeName);
|
|
26569
|
+
}
|
|
26570
|
+
}
|
|
26571
|
+
};
|
|
26572
|
+
// Whether `resourceScope` listed the facade that wrote, with this write
|
|
26573
|
+
// among the ones that count.
|
|
26574
|
+
const isWriteHeardBy = (resourceScope, triggeringAction, triggerScope) => {
|
|
26575
|
+
const writeNameSet = resourceDependencies
|
|
26576
|
+
.get(triggerScope)
|
|
26577
|
+
?.get(resourceScope);
|
|
26578
|
+
if (!writeNameSet) {
|
|
26579
|
+
return false;
|
|
26503
26580
|
}
|
|
26581
|
+
const { verb, isMany } = triggeringAction.meta;
|
|
26582
|
+
return writeNameSet.has(isMany ? `${verb}_MANY` : verb);
|
|
26504
26583
|
};
|
|
26505
26584
|
// Only the reader the resource exposes is registered; the ones `bindParams`
|
|
26506
26585
|
// makes share its signal (see resource_range_reader.js).
|
|
@@ -26542,17 +26621,18 @@ const createResourceLifecycleManager = () => {
|
|
|
26542
26621
|
triggerVerb === "PATCH") &&
|
|
26543
26622
|
config.uniqueKeys.length > 0;
|
|
26544
26623
|
|
|
26545
|
-
const
|
|
26546
|
-
|
|
26547
|
-
|
|
26548
|
-
|
|
26624
|
+
const isHeardDependency = isWriteHeardBy(
|
|
26625
|
+
resourceScope,
|
|
26626
|
+
triggeringAction,
|
|
26627
|
+
triggerResourceScope,
|
|
26628
|
+
);
|
|
26549
26629
|
|
|
26550
26630
|
if (
|
|
26551
26631
|
!shouldRerunGetMany &&
|
|
26552
26632
|
!shouldRerunGet &&
|
|
26553
26633
|
triggerVerb !== "DELETE" &&
|
|
26554
26634
|
!hasUniqueKeyAutorerun &&
|
|
26555
|
-
!
|
|
26635
|
+
!isHeardDependency
|
|
26556
26636
|
) {
|
|
26557
26637
|
continue;
|
|
26558
26638
|
}
|
|
@@ -26673,21 +26753,10 @@ const createResourceLifecycleManager = () => {
|
|
|
26673
26753
|
}
|
|
26674
26754
|
|
|
26675
26755
|
// Cross-resource dependency effects: rerun dependent GET / GET_MANY
|
|
26676
|
-
//
|
|
26677
|
-
//
|
|
26678
|
-
// (POST, PUT, PATCH, DELETE) on the dependency should trigger a rerun.
|
|
26756
|
+
// after a write the dependency counts — every write for a resource
|
|
26757
|
+
// listed bare, the callbacks named in `on` otherwise.
|
|
26679
26758
|
{
|
|
26680
|
-
if (
|
|
26681
|
-
triggerResourceScope &&
|
|
26682
|
-
resourceDependencies
|
|
26683
|
-
.get(triggerResourceScope)
|
|
26684
|
-
?.has(resourceScope) &&
|
|
26685
|
-
(triggerVerb === "POST" ||
|
|
26686
|
-
triggerVerb === "PUT" ||
|
|
26687
|
-
triggerVerb === "PATCH" ||
|
|
26688
|
-
triggerVerb === "DELETE") &&
|
|
26689
|
-
candidateVerb === "GET"
|
|
26690
|
-
) {
|
|
26759
|
+
if (isHeardDependency && candidateVerb === "GET") {
|
|
26691
26760
|
actionsToRerun.add(actionCandidate);
|
|
26692
26761
|
reasonSet.add("dependency autorerun");
|
|
26693
26762
|
continue;
|
|
@@ -26764,9 +26833,10 @@ const createResourceLifecycleManager = () => {
|
|
|
26764
26833
|
continue;
|
|
26765
26834
|
}
|
|
26766
26835
|
const isSameResource = resourceScope === triggerResourceScope;
|
|
26767
|
-
const isDependent =
|
|
26768
|
-
|
|
26769
|
-
|
|
26836
|
+
const isDependent = isWriteHeardBy(
|
|
26837
|
+
resourceScope,
|
|
26838
|
+
triggeringAction,
|
|
26839
|
+
triggerResourceScope,
|
|
26770
26840
|
);
|
|
26771
26841
|
if (!isSameResource && !isDependent) {
|
|
26772
26842
|
continue;
|
|
@@ -26831,6 +26901,35 @@ const shouldRerunAfter = (rerunConfig, verb) => {
|
|
|
26831
26901
|
}
|
|
26832
26902
|
return false;
|
|
26833
26903
|
};
|
|
26904
|
+
// A `dependencies` entry: a resource (every write counts), or
|
|
26905
|
+
// `{ resource, on }` naming the write callbacks that do. A name that cannot
|
|
26906
|
+
// fire throws here: a dependency that never fires leaves a read on its old
|
|
26907
|
+
// value with nothing to say so.
|
|
26908
|
+
const resolveDependency = (dependency, dependentScope) => {
|
|
26909
|
+
if (!Object.hasOwn(dependency, "resource")) {
|
|
26910
|
+
return [dependency, WRITE_CALLBACK_NAMES];
|
|
26911
|
+
}
|
|
26912
|
+
const { resource: dependencyScope, on } = dependency;
|
|
26913
|
+
const where = `resource(${dependentScope.name}) dependency on ${dependencyScope.name}`;
|
|
26914
|
+
if (!Array.isArray(on) || on.length === 0) {
|
|
26915
|
+
throw new Error(
|
|
26916
|
+
`${where}: "on" lists the writes that count, e.g. on: ["POST", "DELETE"] — list the resource itself for every write`,
|
|
26917
|
+
);
|
|
26918
|
+
}
|
|
26919
|
+
for (const writeName of on) {
|
|
26920
|
+
if (!WRITE_CALLBACK_NAMES.includes(writeName)) {
|
|
26921
|
+
throw new Error(
|
|
26922
|
+
`${where}: "${writeName}" is not a write, "on" takes ${WRITE_CALLBACK_NAMES.join(", ")}`,
|
|
26923
|
+
);
|
|
26924
|
+
}
|
|
26925
|
+
if (!dependencyScope[writeName]) {
|
|
26926
|
+
throw new Error(
|
|
26927
|
+
`${where}: ${dependencyScope.name} has no ${writeName}, so it would never rerun ${dependentScope.name}`,
|
|
26928
|
+
);
|
|
26929
|
+
}
|
|
26930
|
+
}
|
|
26931
|
+
return [dependencyScope, on];
|
|
26932
|
+
};
|
|
26834
26933
|
const isParamSubset = (parentParams, childParams) => {
|
|
26835
26934
|
if (!parentParams || !childParams) {
|
|
26836
26935
|
return false;
|
|
@@ -27579,6 +27678,10 @@ const debug$2 = (...args) => {
|
|
|
27579
27678
|
* @param {Object} restCallbacks - `{ idKey, uniqueKeys, rerunOn, dependencies, GET, GET_MANY, GET_RANGE, POST, POST_MANY, PUT, PUT_MANY, PATCH, PATCH_MANY, DELETE, DELETE_MANY }`
|
|
27580
27679
|
* @param {string} [restCallbacks.idKey] - primary key property, defaults to `"id"`
|
|
27581
27680
|
* @param {string[]} [restCallbacks.uniqueKeys] - alternate keys the store can find an item by (e.g. `"username"`)
|
|
27681
|
+
* @param {Array<Object | { resource: Object, on: string[] }>} [restCallbacks.dependencies] -
|
|
27682
|
+
* resources whose writes rerun this one's reads: one listed bare counts every
|
|
27683
|
+
* write, `{ resource, on: ["PATCH_MANY", "DELETE"] }` only the callbacks it
|
|
27684
|
+
* names (`PATCH` is not `PATCH_MANY`)
|
|
27582
27685
|
* @param {{ signal: import("@preact/signals").Signal, when?: () => boolean }} [restCallbacks.persist] -
|
|
27583
27686
|
* mirrors the rows `GET` lands with into `signal` — a `stateSignal` with
|
|
27584
27687
|
* `persists: true` and `type: "object"` keeps them across a reload, whose
|
|
@@ -29669,6 +29772,7 @@ const useAsyncData = (promiseOrAction, {
|
|
|
29669
29772
|
loading = "delegate",
|
|
29670
29773
|
error = "delegate",
|
|
29671
29774
|
run,
|
|
29775
|
+
prerun,
|
|
29672
29776
|
onLoad
|
|
29673
29777
|
} = {}) => {
|
|
29674
29778
|
// The request this component owns, as a function: one action per instance,
|
|
@@ -29694,6 +29798,7 @@ const useAsyncData = (promiseOrAction, {
|
|
|
29694
29798
|
loadingEffect: loading,
|
|
29695
29799
|
errorEffect: error,
|
|
29696
29800
|
run,
|
|
29801
|
+
prerun,
|
|
29697
29802
|
onLoad
|
|
29698
29803
|
});
|
|
29699
29804
|
}
|
|
@@ -29725,8 +29830,14 @@ const useActionAsyncData = (action, {
|
|
|
29725
29830
|
loadingEffect,
|
|
29726
29831
|
errorEffect,
|
|
29727
29832
|
run,
|
|
29833
|
+
prerun,
|
|
29728
29834
|
onLoad
|
|
29729
29835
|
}) => {
|
|
29836
|
+
if (prerun) {
|
|
29837
|
+
// A head start draws none of what it starts, so it has no wait to
|
|
29838
|
+
// delegate.
|
|
29839
|
+
loadingEffect = "use";
|
|
29840
|
+
}
|
|
29730
29841
|
const loadingRef = useContext(LoadingContext);
|
|
29731
29842
|
// The boundary is where a suspension lands, so only the delegating path needs
|
|
29732
29843
|
// one: `loading: true` draws the wait itself and never suspends.
|
|
@@ -29742,13 +29853,18 @@ const useActionAsyncData = (action, {
|
|
|
29742
29853
|
// no-op — which is what makes starting it while rendering sound. From there
|
|
29743
29854
|
// the wait is the ordinary one: suspended into <Loading>, or drawn by the
|
|
29744
29855
|
// component under `loading: true`, exactly as for data someone else ran.
|
|
29745
|
-
if (run && action.
|
|
29856
|
+
if (run && action.paramsSignal.peek() !== undefined && runIsOwed(action)) {
|
|
29746
29857
|
// Nothing waits on this run: the failure is held by the action, and this
|
|
29747
29858
|
// hook is what reads it back.
|
|
29748
29859
|
runUnwatched(() => action.run({
|
|
29749
29860
|
reason: "useAsyncData({ run: true })"
|
|
29750
29861
|
}));
|
|
29751
29862
|
}
|
|
29863
|
+
if (prerun && action.runningStateSignal.peek() === IDLE && action.paramsSignal.peek() !== undefined) {
|
|
29864
|
+
prerunUnwatched(() => action.prerun({
|
|
29865
|
+
reason: "useAsyncData({ prerun: true })"
|
|
29866
|
+
}));
|
|
29867
|
+
}
|
|
29752
29868
|
|
|
29753
29869
|
// Use peek() instead of .value to avoid subscribing this component to the signal.
|
|
29754
29870
|
// Reading .value would make Preact re-render the component reactively when the state
|
|
@@ -29857,6 +29973,11 @@ const useActionAsyncData = (action, {
|
|
|
29857
29973
|
return [action.dataSignal.peek(), false, undefined];
|
|
29858
29974
|
}
|
|
29859
29975
|
if (runningState === FAILED) {
|
|
29976
|
+
if (prerun) {
|
|
29977
|
+
// Neither shown nor reported from a head start: the failure is the
|
|
29978
|
+
// owner's to ask again (see runIsOwed).
|
|
29979
|
+
return [action.dataSignal.peek(), false, undefined];
|
|
29980
|
+
}
|
|
29860
29981
|
if (dismissedActionWeakSet.has(action)) {
|
|
29861
29982
|
// Dismissing removes the error and nothing else: what is handed back is
|
|
29862
29983
|
// the action's state without it — the data it holds, and no run in
|
|
@@ -29962,6 +30083,21 @@ const useActionAsyncData = (action, {
|
|
|
29962
30083
|
throw pendingPromise;
|
|
29963
30084
|
};
|
|
29964
30085
|
|
|
30086
|
+
// What `run: true` starts from: an action nobody ran, or one a prerun failed.
|
|
30087
|
+
// Any other failure stays — a render retrying it would loop while the network
|
|
30088
|
+
// is down — but nothing on screen asked for a prerun, so its failure is asked
|
|
30089
|
+
// again, once: the mark is taken here, and the run started fails as its own.
|
|
30090
|
+
const runIsOwed = action => {
|
|
30091
|
+
const runningState = action.runningStateSignal.peek();
|
|
30092
|
+
if (runningState === IDLE) {
|
|
30093
|
+
return true;
|
|
30094
|
+
}
|
|
30095
|
+
if (runningState === FAILED) {
|
|
30096
|
+
return takePrerunFailure(action.errorSignal.peek());
|
|
30097
|
+
}
|
|
30098
|
+
return false;
|
|
30099
|
+
};
|
|
30100
|
+
|
|
29965
30101
|
// What a screen does with the data once, when it becomes known (see onLoad in
|
|
29966
30102
|
// the JSDoc above). Kept apart because the two questions it answers are not the
|
|
29967
30103
|
// ones the hook around it answers: WHEN — a layout effect, so a form taking its
|
|
@@ -39597,15 +39733,7 @@ const prefetchAsIf = (action, readParams, values) => {
|
|
|
39597
39733
|
// the arrival focuses the link, or is the one that asked), and a reset
|
|
39598
39734
|
// landing after that would pull the answer from under the page reading it.
|
|
39599
39735
|
const prerunAhead = (instance) => {
|
|
39600
|
-
|
|
39601
|
-
try {
|
|
39602
|
-
result = instance.prerun({ reason: "intent" });
|
|
39603
|
-
} catch {
|
|
39604
|
-
return;
|
|
39605
|
-
}
|
|
39606
|
-
if (result && typeof result.catch === "function") {
|
|
39607
|
-
result.catch(() => {});
|
|
39608
|
-
}
|
|
39736
|
+
prerunUnwatched(() => instance.prerun({ reason: "intent" }));
|
|
39609
39737
|
};
|
|
39610
39738
|
|
|
39611
39739
|
// I delibrately prefer the term "any" and avoid "some" so dev are not tempted to think
|
|
@@ -41693,7 +41821,7 @@ registerNaviCommand("--navi-open", (source, event, { anchor, value } = {}) => {
|
|
|
41693
41821
|
* The target is found the way --navi-open finds it, but silently: nothing
|
|
41694
41822
|
* runs here, and a source with no target says so at the release.
|
|
41695
41823
|
*/
|
|
41696
|
-
const announceOpeningPress = (source, command) => {
|
|
41824
|
+
const announceOpeningPress = (source, command, pointerdownEvent) => {
|
|
41697
41825
|
if (command !== "--navi-open" && command !== "--navi-toggle") {
|
|
41698
41826
|
return;
|
|
41699
41827
|
}
|
|
@@ -41704,7 +41832,7 @@ const announceOpeningPress = (source, command) => {
|
|
|
41704
41832
|
if (!target) {
|
|
41705
41833
|
return;
|
|
41706
41834
|
}
|
|
41707
|
-
dispatchCustomEvent(target, "navi_open_press");
|
|
41835
|
+
dispatchCustomEvent(target, "navi_open_press", { event: pointerdownEvent });
|
|
41708
41836
|
};
|
|
41709
41837
|
// "--navi-close:all" closes every expandable above the source, nearest first —
|
|
41710
41838
|
// a link leaving from a badge shown over a sheet leaves both. A surface that
|
|
@@ -41977,11 +42105,18 @@ registerNaviCommand("--navi-unselect", (source, event) => {
|
|
|
41977
42105
|
* where a delay is felt hardest — while the ~100-300ms between hovering a
|
|
41978
42106
|
* trigger and pressing it are free. The warming render starts at the frame
|
|
41979
42107
|
* after the one that follows the intent: nothing here needs the content in the
|
|
41980
|
-
* DOM before the click, only before the open that follows it, and
|
|
41981
|
-
*
|
|
41982
|
-
*
|
|
41983
|
-
*
|
|
41984
|
-
*
|
|
42108
|
+
* DOM before the click, only before the open that follows it, and a build
|
|
42109
|
+
* started at once would hold back the frame that shows the hover or the focus.
|
|
42110
|
+
*
|
|
42111
|
+
* Under a finger, `pointerover` is not an intent yet: it arrives with the
|
|
42112
|
+
* `pointerdown` of every touch, the ones starting a scroll included, and at
|
|
42113
|
+
* reading speed the browser decides a touch is a scroll about 35ms in — a
|
|
42114
|
+
* build started then holds that scroll start back. So a touch warms once it has been held
|
|
42115
|
+
* TOUCH_WARM_HOLD_MS without the browser taking it (`pointercancel`) or the
|
|
42116
|
+
* finger lifting; a quicker tap builds at the open, like a click with no hover
|
|
42117
|
+
* before it. Counted in animation frames, never a timeout: browsers hold timers
|
|
42118
|
+
* back while a finger is down, and a timeout started by the press fires at its
|
|
42119
|
+
* release, on top of the click.
|
|
41985
42120
|
*
|
|
41986
42121
|
* The anchor only says where the popup is placed, so intent inside it is read
|
|
41987
42122
|
* off the control it is aimed at, the nearest one around the target. A control
|
|
@@ -42005,8 +42140,11 @@ registerNaviCommand("--navi-unselect", (source, event) => {
|
|
|
42005
42140
|
* anchor: it says where the popup is placed, not what opens it — a card
|
|
42006
42141
|
* opened by a hold is pressed all day by taps meant for what it holds.
|
|
42007
42142
|
*
|
|
42008
|
-
* The press builds the content,
|
|
42009
|
-
*
|
|
42143
|
+
* The press builds the content when it starts, a touch once it has been held
|
|
42144
|
+
* as above: the announcement carries the pointerdown that made it. A press
|
|
42145
|
+
* released before its build leaves the building to the open its click asks
|
|
42146
|
+
* for. The press ending without an open throws the content away — its click
|
|
42147
|
+
* reaching the document with the popup still closed, no
|
|
42010
42148
|
* click coming after the release, the browser taking the gesture
|
|
42011
42149
|
* (`pointercancel`), or the next press starting, before any of its own
|
|
42012
42150
|
* handlers run: one press at a time holds a content. A keyboard opening builds
|
|
@@ -42050,6 +42188,12 @@ const cancelIdle = (id) =>
|
|
|
42050
42188
|
? cancelIdleCallback(id)
|
|
42051
42189
|
: clearTimeout(id);
|
|
42052
42190
|
|
|
42191
|
+
// How long a finger rests on an anchor before it warms it: past the ~35ms at
|
|
42192
|
+
// which the browser takes a touch moving at reading speed for a scroll, short
|
|
42193
|
+
// of the ~100ms a tap is held, so a deliberate tap is warmed before its
|
|
42194
|
+
// release.
|
|
42195
|
+
const TOUCH_WARM_HOLD_MS = 80;
|
|
42196
|
+
|
|
42053
42197
|
const usePopupContentMount = (
|
|
42054
42198
|
openController,
|
|
42055
42199
|
ref,
|
|
@@ -42140,14 +42284,35 @@ const usePopupContentMount = (
|
|
|
42140
42284
|
return undefined;
|
|
42141
42285
|
}
|
|
42142
42286
|
let cancelWarm = null;
|
|
42287
|
+
let warmIsTouchHold = false;
|
|
42143
42288
|
const warm = () => {
|
|
42144
42289
|
if (cancelWarm) {
|
|
42145
42290
|
return;
|
|
42146
42291
|
}
|
|
42292
|
+
warmIsTouchHold = false;
|
|
42147
42293
|
cancelWarm = requestFrameAfterNext(() => {
|
|
42148
42294
|
setContentMounted(true);
|
|
42149
42295
|
});
|
|
42150
42296
|
};
|
|
42297
|
+
const warmAfterTouchHold = () => {
|
|
42298
|
+
if (cancelWarm) {
|
|
42299
|
+
return;
|
|
42300
|
+
}
|
|
42301
|
+
warmIsTouchHold = true;
|
|
42302
|
+
cancelWarm = requestFrameAfterDuration(TOUCH_WARM_HOLD_MS, () => {
|
|
42303
|
+
setContentMounted(true);
|
|
42304
|
+
});
|
|
42305
|
+
};
|
|
42306
|
+
// A touch leaves the anchor when the finger lifts or the browser takes the
|
|
42307
|
+
// touch: either way, before the hold said anything.
|
|
42308
|
+
const onPointerLeave = () => {
|
|
42309
|
+
if (!warmIsTouchHold) {
|
|
42310
|
+
return;
|
|
42311
|
+
}
|
|
42312
|
+
cancelWarm();
|
|
42313
|
+
cancelWarm = null;
|
|
42314
|
+
warmIsTouchHold = false;
|
|
42315
|
+
};
|
|
42151
42316
|
// The anchor itself when it is a control (a Popover anchored on the button
|
|
42152
42317
|
// opening it), or the one the popup is written in (a picker's trigger).
|
|
42153
42318
|
const isOwnControl = (control) =>
|
|
@@ -42167,6 +42332,10 @@ const usePopupContentMount = (
|
|
|
42167
42332
|
return;
|
|
42168
42333
|
}
|
|
42169
42334
|
}
|
|
42335
|
+
if (pointeroverEvent.pointerType === "touch") {
|
|
42336
|
+
warmAfterTouchHold();
|
|
42337
|
+
return;
|
|
42338
|
+
}
|
|
42170
42339
|
warm();
|
|
42171
42340
|
};
|
|
42172
42341
|
const onFocusIn = (focusinEvent) => {
|
|
@@ -42177,10 +42346,12 @@ const usePopupContentMount = (
|
|
|
42177
42346
|
warm();
|
|
42178
42347
|
};
|
|
42179
42348
|
anchorElement.addEventListener("pointerover", onPointerOver);
|
|
42349
|
+
anchorElement.addEventListener("pointerleave", onPointerLeave);
|
|
42180
42350
|
anchorElement.addEventListener("focusin", onFocusIn);
|
|
42181
42351
|
return () => {
|
|
42182
42352
|
cancelWarm?.();
|
|
42183
42353
|
anchorElement.removeEventListener("pointerover", onPointerOver);
|
|
42354
|
+
anchorElement.removeEventListener("pointerleave", onPointerLeave);
|
|
42184
42355
|
anchorElement.removeEventListener("focusin", onFocusIn);
|
|
42185
42356
|
};
|
|
42186
42357
|
}, [contentMounted, anchor, mount]);
|
|
@@ -42190,12 +42361,14 @@ const usePopupContentMount = (
|
|
|
42190
42361
|
const stopPressWarmRef = useRef(null);
|
|
42191
42362
|
openController.onOpeningPress =
|
|
42192
42363
|
mount === "while-opened"
|
|
42193
|
-
? () => {
|
|
42364
|
+
? (openPressEvent) => {
|
|
42194
42365
|
if (contentMountedRef.current || openController.onOpen) {
|
|
42195
42366
|
return;
|
|
42196
42367
|
}
|
|
42197
|
-
const cancelBuild =
|
|
42198
|
-
|
|
42368
|
+
const cancelBuild = requestBuildOnPress(openPressEvent, {
|
|
42369
|
+
onBuild: () => {
|
|
42370
|
+
setContentMounted(true);
|
|
42371
|
+
},
|
|
42199
42372
|
});
|
|
42200
42373
|
const stopWatching = watchPressEnd(() => {
|
|
42201
42374
|
stopPressWarmRef.current = null;
|
|
@@ -42257,6 +42430,64 @@ const requestFrameAfterNext = (callback) => {
|
|
|
42257
42430
|
};
|
|
42258
42431
|
};
|
|
42259
42432
|
|
|
42433
|
+
// What an opening press builds, it builds at the frame after the next one, so
|
|
42434
|
+
// the pressed trigger is painted first — under a finger, once the touch has
|
|
42435
|
+
// been held TOUCH_WARM_HOLD_MS (see the top comment). Released or taken by the
|
|
42436
|
+
// browser before then, the press builds nothing: the open its click asks for
|
|
42437
|
+
// builds, when one comes. Called from the press's own pointerdown, through its
|
|
42438
|
+
// navi_open_press: the capture listeners below only hear what follows.
|
|
42439
|
+
const requestBuildOnPress = (openPressEvent, { onBuild, onGiveUp }) => {
|
|
42440
|
+
const pointerdownEvent = openPressEvent.detail.event;
|
|
42441
|
+
const cancelFrame =
|
|
42442
|
+
pointerdownEvent.pointerType === "touch"
|
|
42443
|
+
? requestFrameAfterDuration(TOUCH_WARM_HOLD_MS, () => {
|
|
42444
|
+
stopListening();
|
|
42445
|
+
onBuild();
|
|
42446
|
+
})
|
|
42447
|
+
: requestFrameAfterNext(() => {
|
|
42448
|
+
stopListening();
|
|
42449
|
+
onBuild();
|
|
42450
|
+
});
|
|
42451
|
+
const onPressEnd = (pointerEvent) => {
|
|
42452
|
+
if (pointerEvent.pointerId !== pointerdownEvent.pointerId) {
|
|
42453
|
+
return;
|
|
42454
|
+
}
|
|
42455
|
+
cancel();
|
|
42456
|
+
onGiveUp?.();
|
|
42457
|
+
};
|
|
42458
|
+
const stopListening = () => {
|
|
42459
|
+
document.removeEventListener("pointerup", onPressEnd, { capture: true });
|
|
42460
|
+
document.removeEventListener("pointercancel", onPressEnd, {
|
|
42461
|
+
capture: true,
|
|
42462
|
+
});
|
|
42463
|
+
};
|
|
42464
|
+
const cancel = () => {
|
|
42465
|
+
cancelFrame();
|
|
42466
|
+
stopListening();
|
|
42467
|
+
};
|
|
42468
|
+
document.addEventListener("pointerup", onPressEnd, { capture: true });
|
|
42469
|
+
document.addEventListener("pointercancel", onPressEnd, { capture: true });
|
|
42470
|
+
return cancel;
|
|
42471
|
+
};
|
|
42472
|
+
|
|
42473
|
+
// The first frame at least `ms` after the call, read off the frames' own
|
|
42474
|
+
// timestamps: a 120Hz screen waits as long as a 60Hz one.
|
|
42475
|
+
const requestFrameAfterDuration = (ms, callback) => {
|
|
42476
|
+
const start = performance.now();
|
|
42477
|
+
let frame;
|
|
42478
|
+
const onFrame = (timestamp) => {
|
|
42479
|
+
if (timestamp - start >= ms) {
|
|
42480
|
+
callback();
|
|
42481
|
+
return;
|
|
42482
|
+
}
|
|
42483
|
+
frame = requestAnimationFrame(onFrame);
|
|
42484
|
+
};
|
|
42485
|
+
frame = requestAnimationFrame(onFrame);
|
|
42486
|
+
return () => {
|
|
42487
|
+
cancelAnimationFrame(frame);
|
|
42488
|
+
};
|
|
42489
|
+
};
|
|
42490
|
+
|
|
42260
42491
|
// How long the click of a released press may take to arrive. Nothing tells a
|
|
42261
42492
|
// page that a release brings no click — a press that became a drag has its
|
|
42262
42493
|
// click swallowed (suppressClickAfterGesture in @jsenv/dom) — so past this the
|
|
@@ -51441,7 +51672,7 @@ const usePreloadOpeningOnPress = (ref, command, prefetch = true) => {
|
|
|
51441
51672
|
if (pointerdownEvent.button !== 0) {
|
|
51442
51673
|
return;
|
|
51443
51674
|
}
|
|
51444
|
-
announceOpeningPress(element, command);
|
|
51675
|
+
announceOpeningPress(element, command, pointerdownEvent);
|
|
51445
51676
|
};
|
|
51446
51677
|
element.addEventListener("pointerdown", onPointerDown);
|
|
51447
51678
|
return () => {
|
|
@@ -55630,7 +55861,9 @@ const Expandable = props => {
|
|
|
55630
55861
|
// its own (see announceOpeningPress in commands.js) — unless aimed at a
|
|
55631
55862
|
// control inside the UI part, whose click does not toggle (see onUIClick).
|
|
55632
55863
|
if (pointerdownEvent.button === 0 && !openController.opened && !isAimedAtControlInsideUI(pointerdownEvent.target)) {
|
|
55633
|
-
dispatchCustomEvent(rootRef.current, "navi_open_press"
|
|
55864
|
+
dispatchCustomEvent(rootRef.current, "navi_open_press", {
|
|
55865
|
+
event: pointerdownEvent
|
|
55866
|
+
});
|
|
55634
55867
|
}
|
|
55635
55868
|
};
|
|
55636
55869
|
|
|
@@ -56051,7 +56284,7 @@ const Expandable = props => {
|
|
|
56051
56284
|
,
|
|
56052
56285
|
onnavi_open_press: e => {
|
|
56053
56286
|
rest.onnavi_open_press?.(e);
|
|
56054
|
-
openController.onOpeningPress?.();
|
|
56287
|
+
openController.onOpeningPress?.(e);
|
|
56055
56288
|
},
|
|
56056
56289
|
onnavi_request_close: e => {
|
|
56057
56290
|
rest.onnavi_request_close?.(e);
|
|
@@ -56131,8 +56364,8 @@ const ExpandableUI = ({
|
|
|
56131
56364
|
onnavi_request_open: e => {
|
|
56132
56365
|
openController.open(e);
|
|
56133
56366
|
},
|
|
56134
|
-
onnavi_open_press:
|
|
56135
|
-
openController.onOpeningPress?.();
|
|
56367
|
+
onnavi_open_press: e => {
|
|
56368
|
+
openController.onOpeningPress?.(e);
|
|
56136
56369
|
},
|
|
56137
56370
|
onnavi_request_close: e => {
|
|
56138
56371
|
openController.requestClose(e, {
|
|
@@ -68279,10 +68512,10 @@ const DialogStandIn = props => {
|
|
|
68279
68512
|
}
|
|
68280
68513
|
// eslint-disable-next-line react/no-unknown-property
|
|
68281
68514
|
,
|
|
68282
|
-
onnavi_open_press:
|
|
68515
|
+
onnavi_open_press: e => {
|
|
68283
68516
|
// What the opening will read is asked for at once, as the dialog
|
|
68284
|
-
// proper does (see open_controller.js); the dialog itself
|
|
68285
|
-
//
|
|
68517
|
+
// proper does (see open_controller.js); the dialog itself once the
|
|
68518
|
+
// press is taken for one (see requestBuildOnPress) — the click that
|
|
68286
68519
|
// follows opens it built, or builds it itself when it comes sooner.
|
|
68287
68520
|
if (signal) {
|
|
68288
68521
|
preloadState(signal, readOpenValue(value));
|
|
@@ -68290,9 +68523,14 @@ const DialogStandIn = props => {
|
|
|
68290
68523
|
if (warmRef.current) {
|
|
68291
68524
|
return;
|
|
68292
68525
|
}
|
|
68293
|
-
warmRef.current =
|
|
68294
|
-
|
|
68295
|
-
|
|
68526
|
+
warmRef.current = requestBuildOnPress(e, {
|
|
68527
|
+
onBuild: () => {
|
|
68528
|
+
warmRef.current = null;
|
|
68529
|
+
build("press");
|
|
68530
|
+
},
|
|
68531
|
+
onGiveUp: () => {
|
|
68532
|
+
warmRef.current = null;
|
|
68533
|
+
}
|
|
68296
68534
|
});
|
|
68297
68535
|
}
|
|
68298
68536
|
});
|
|
@@ -68373,8 +68611,8 @@ const UncontrolledDialog = props => {
|
|
|
68373
68611
|
// The press announcing that request (see announceOpeningPress in
|
|
68374
68612
|
// commands.js), heard where the request is.
|
|
68375
68613
|
,
|
|
68376
|
-
onnavi_open_press:
|
|
68377
|
-
openController.onOpeningPress?.();
|
|
68614
|
+
onnavi_open_press: e => {
|
|
68615
|
+
openController.onOpeningPress?.(e);
|
|
68378
68616
|
},
|
|
68379
68617
|
onnavi_request_close: e => {
|
|
68380
68618
|
const closing = openController.requestClose(e, {
|
|
@@ -70261,8 +70499,8 @@ const UncontrolledPopover = props => {
|
|
|
70261
70499
|
// The press announcing that request (see announceOpeningPress in
|
|
70262
70500
|
// commands.js), heard where the request is.
|
|
70263
70501
|
,
|
|
70264
|
-
onnavi_open_press:
|
|
70265
|
-
openController.onOpeningPress?.();
|
|
70502
|
+
onnavi_open_press: e => {
|
|
70503
|
+
openController.onOpeningPress?.(e);
|
|
70266
70504
|
},
|
|
70267
70505
|
onnavi_request_close: e => {
|
|
70268
70506
|
const closing = openController.requestClose(e, {
|
|
@@ -71674,15 +71912,15 @@ const css$B = /* css */`.navi_picker_callout_dock > [data-picker-content] {
|
|
|
71674
71912
|
display: flex;
|
|
71675
71913
|
}
|
|
71676
71914
|
|
|
71915
|
+
&:has( > .navi_list_container:not([data-scroller], [data-overflow-visible])) {
|
|
71916
|
+
overflow: visible;
|
|
71917
|
+
}
|
|
71918
|
+
|
|
71677
71919
|
& .navi_list_container {
|
|
71678
71920
|
--list-border-radius: max(0px,
|
|
71679
71921
|
var(--popover-border-radius) - var(--popover-border-width));
|
|
71680
71922
|
overscroll-behavior: none;
|
|
71681
71923
|
width: 100%;
|
|
71682
|
-
|
|
71683
|
-
&:not([data-overflow-visible]) {
|
|
71684
|
-
overflow: auto;
|
|
71685
|
-
}
|
|
71686
71924
|
}
|
|
71687
71925
|
}
|
|
71688
71926
|
|
|
@@ -71701,15 +71939,15 @@ const css$B = /* css */`.navi_picker_callout_dock > [data-picker-content] {
|
|
|
71701
71939
|
flex-direction: column;
|
|
71702
71940
|
}
|
|
71703
71941
|
|
|
71942
|
+
&:has( > .navi_list_container:not([data-scroller], [data-overflow-visible])) {
|
|
71943
|
+
overflow: visible;
|
|
71944
|
+
}
|
|
71945
|
+
|
|
71704
71946
|
& .navi_list_container {
|
|
71705
71947
|
--list-border-radius: max(0px,
|
|
71706
71948
|
var(--dialog-border-radius) - var(--dialog-border-width));
|
|
71707
71949
|
overscroll-behavior: none;
|
|
71708
71950
|
width: 100%;
|
|
71709
|
-
|
|
71710
|
-
&:not([data-overflow-visible]) {
|
|
71711
|
-
overflow: auto;
|
|
71712
|
-
}
|
|
71713
71951
|
}
|
|
71714
71952
|
}
|
|
71715
71953
|
`;
|
|
@@ -72168,8 +72406,8 @@ const PickerCustom = props => {
|
|
|
72168
72406
|
},
|
|
72169
72407
|
// And the press announcing that request (see announceOpeningPress in
|
|
72170
72408
|
// commands.js), for the popup's content to be built on it.
|
|
72171
|
-
"onnavi_open_press":
|
|
72172
|
-
dispatchCustomEvent(popupRef.current, "navi_open_press");
|
|
72409
|
+
"onnavi_open_press": e => {
|
|
72410
|
+
dispatchCustomEvent(popupRef.current, "navi_open_press", e.detail);
|
|
72173
72411
|
},
|
|
72174
72412
|
"onnavi_request_close": e => {
|
|
72175
72413
|
const closing = dispatchCustomEvent(popupRef.current, "navi_request_close", e.detail);
|
|
@@ -72232,8 +72470,8 @@ const PickerCustom = props => {
|
|
|
72232
72470
|
// The press announcing that request (see announceOpeningPress in
|
|
72233
72471
|
// commands.js), heard where the request is — here too for the same
|
|
72234
72472
|
// reason. Nothing to gate: the announcer asked before saying it.
|
|
72235
|
-
onnavi_open_press:
|
|
72236
|
-
openController.onOpeningPress?.();
|
|
72473
|
+
onnavi_open_press: e => {
|
|
72474
|
+
openController.onOpeningPress?.(e);
|
|
72237
72475
|
},
|
|
72238
72476
|
onnavi_request_close: e => {
|
|
72239
72477
|
requestInteraction({
|
|
@@ -72386,7 +72624,9 @@ const PickerCustom = props => {
|
|
|
72386
72624
|
})) {
|
|
72387
72625
|
return;
|
|
72388
72626
|
}
|
|
72389
|
-
dispatchCustomEvent(pickerEl, "navi_open_press"
|
|
72627
|
+
dispatchCustomEvent(pickerEl, "navi_open_press", {
|
|
72628
|
+
event: pointerdownEvent
|
|
72629
|
+
});
|
|
72390
72630
|
};
|
|
72391
72631
|
}
|
|
72392
72632
|
let interactions = props.interactions;
|
|
@@ -75212,15 +75452,13 @@ const css$x = /* css */`@property --list-group-label-height {
|
|
|
75212
75452
|
flex-direction: column;
|
|
75213
75453
|
transition: opacity .2s;
|
|
75214
75454
|
display: flex;
|
|
75215
|
-
overflow: hidden;
|
|
75216
75455
|
|
|
75217
|
-
|
|
75218
|
-
|
|
75219
|
-
overflow: visible;
|
|
75456
|
+
&:not([data-borderless], [data-overflow-visible], [data-scroller]) {
|
|
75457
|
+
min-height: 0;
|
|
75220
75458
|
}
|
|
75221
75459
|
|
|
75222
|
-
&[data-
|
|
75223
|
-
|
|
75460
|
+
&[data-borderless] {
|
|
75461
|
+
--x-list-border-radius: 0px;
|
|
75224
75462
|
}
|
|
75225
75463
|
|
|
75226
75464
|
&[data-overflow-visible="x"] {
|