@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.
@@ -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<dependentResources>> — user-configured
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
- // Register dependencies
26496
- if (dependencies.length > 0) {
26497
- for (const dependency of dependencies) {
26498
- if (!resourceDependencies.has(dependency)) {
26499
- resourceDependencies.set(dependency, new Set());
26500
- }
26501
- resourceDependencies.get(dependency).add(resourceScope);
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 isKnownDependency =
26546
- triggerResourceScope !== null &&
26547
- triggerResourceScope !== undefined &&
26548
- resourceDependencies.get(triggerResourceScope)?.has(resourceScope);
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
- !isKnownDependency
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
- // Fires on any mutating verb — user-configured dependencies express
26677
- // "this resource depends on another resource's data", so any mutation
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 = Boolean(
26768
- triggerResourceScope &&
26769
- resourceDependencies.get(triggerResourceScope)?.has(resourceScope),
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.runningStateSignal.peek() === IDLE && action.paramsSignal.peek() !== undefined) {
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
- let result;
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 on a touch
41981
- * screen `pointerover` arrives together with the `pointerdown` — a build
41982
- * started there would hold back the frame that shows the press. Two animation
41983
- * frames, never a timeout: browsers hold timers back while a finger is down,
41984
- * and a timeout started by the press fires at its release, on top of the click.
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, and the press ending without an open throws
42009
- * it away — its click reaching the document with the popup still closed, no
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 = requestFrameAfterNext(() => {
42198
- setContentMounted(true);
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 two frames
68285
- // later, so the pressed trigger is painted first — the click that
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 = requestFrameAfterNext(() => {
68294
- warmRef.current = null;
68295
- build("press");
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
- &[data-borderless] {
75218
- --x-list-border-radius: 0px;
75219
- overflow: visible;
75456
+ &:not([data-borderless], [data-overflow-visible], [data-scroller]) {
75457
+ min-height: 0;
75220
75458
  }
75221
75459
 
75222
- &[data-overflow-visible="both"] {
75223
- overflow: visible;
75460
+ &[data-borderless] {
75461
+ --x-list-border-radius: 0px;
75224
75462
  }
75225
75463
 
75226
75464
  &[data-overflow-visible="x"] {