@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.
@@ -1895,6 +1895,27 @@ const markErrorAsTakenByRender = (error) => {
1895
1895
  }
1896
1896
  };
1897
1897
 
1898
+ /**
1899
+ * A prerun's failure is owed to no screen: nothing on screen asked for that
1900
+ * run, so the component that owns the read asks again rather than show a
1901
+ * failure from before it existed (see `run` in use_async_data.jsx). Taken
1902
+ * once, so the run asking again fails as its own even when it fails with the
1903
+ * same error object. Held by the error, it lasts as long as the failure stays
1904
+ * on the action: a later run replaces it, whether or not an owner ever came.
1905
+ */
1906
+ const prerunFailureSet = new WeakSet();
1907
+ const markErrorAsPrerunFailure = (error) => {
1908
+ if (error && typeof error === "object") {
1909
+ prerunFailureSet.add(error);
1910
+ }
1911
+ };
1912
+ const takePrerunFailure = (error) => {
1913
+ if (!error || typeof error !== "object") {
1914
+ return false;
1915
+ }
1916
+ return prerunFailureSet.delete(error);
1917
+ };
1918
+
1898
1919
  /**
1899
1920
  * When the answer "nobody took it" is final.
1900
1921
  *
@@ -4194,6 +4215,9 @@ const createAction = (callback, rootOptions = {}) => {
4194
4215
  return error;
4195
4216
  }
4196
4217
  error.action = action;
4218
+ if (isPrerun) {
4219
+ markErrorAsPrerunFailure(error);
4220
+ }
4197
4221
  batch(() => {
4198
4222
  errorSignal.value = error;
4199
4223
  runningStateSignal.value = FAILED;
@@ -4767,6 +4791,25 @@ const runUnwatched = (startRun) => {
4767
4791
  return result;
4768
4792
  };
4769
4793
 
4794
+ /**
4795
+ * Starts a prerun navi has nobody to await. Its failure is not reported:
4796
+ * nothing on screen asked for it. It stays on the action, marked as a
4797
+ * prerun's (see markErrorAsPrerunFailure), where whatever owns the read asks
4798
+ * again.
4799
+ */
4800
+ const prerunUnwatched = (startPrerun) => {
4801
+ let result;
4802
+ try {
4803
+ result = startPrerun();
4804
+ } catch {
4805
+ return undefined;
4806
+ }
4807
+ if (result && typeof result.catch === "function") {
4808
+ result.catch(() => {});
4809
+ }
4810
+ return result;
4811
+ };
4812
+
4770
4813
  /**
4771
4814
  * Reactively runs an action whenever the params derived from signals change.
4772
4815
  *
@@ -25573,10 +25616,24 @@ const resolveRerunOn = (rerunOn, inheritedRerunOn = defaultRerunOn) => {
25573
25616
  };
25574
25617
  };
25575
25618
 
25619
+ // What a `dependencies` entry names are callbacks, not verbs (`rerunOn` names
25620
+ // verbs, where `PATCH` covers `PATCH_MANY`): one callback of a resource can be
25621
+ // the write that creates (a move into a new item) while its sibling only edits.
25622
+ const WRITE_CALLBACK_NAMES = [
25623
+ "POST",
25624
+ "POST_MANY",
25625
+ "PUT",
25626
+ "PUT_MANY",
25627
+ "PATCH",
25628
+ "PATCH_MANY",
25629
+ "DELETE",
25630
+ "DELETE_MANY",
25631
+ ];
25632
+
25576
25633
  // This handles ALL resource lifecycle logic (rerun/reset) across all resources
25577
25634
  const createResourceLifecycleManager = () => {
25578
25635
  const registeredResources = new Map(); // Map<resourceInstance, lifecycleConfig>
25579
- const resourceDependencies = new Map(); // Map<resourceInstance, Set<dependentResources>> — user-configured
25636
+ const resourceDependencies = new Map(); // Map<resourceInstance, Map<dependentResource, Set<writeCallbackName>>> — user-configured
25580
25637
  const scopedManyParents = new Map(); // Map<childResource, Set<{resource, propertyName}>> — auto from scopedMany
25581
25638
 
25582
25639
  const registerResource = (resourceScope, config) => {
@@ -25595,15 +25652,37 @@ const createResourceLifecycleManager = () => {
25595
25652
  rangeReaderSet: new Set(),
25596
25653
  });
25597
25654
 
25598
- // Register dependencies
25599
- if (dependencies.length > 0) {
25600
- for (const dependency of dependencies) {
25601
- if (!resourceDependencies.has(dependency)) {
25602
- resourceDependencies.set(dependency, new Set());
25603
- }
25604
- resourceDependencies.get(dependency).add(resourceScope);
25655
+ for (const dependency of dependencies) {
25656
+ const [dependencyScope, writeNames] = resolveDependency(
25657
+ dependency,
25658
+ resourceScope,
25659
+ );
25660
+ let dependentMap = resourceDependencies.get(dependencyScope);
25661
+ if (!dependentMap) {
25662
+ dependentMap = new Map();
25663
+ resourceDependencies.set(dependencyScope, dependentMap);
25605
25664
  }
25665
+ let writeNameSet = dependentMap.get(resourceScope);
25666
+ if (!writeNameSet) {
25667
+ writeNameSet = new Set();
25668
+ dependentMap.set(resourceScope, writeNameSet);
25669
+ }
25670
+ for (const writeName of writeNames) {
25671
+ writeNameSet.add(writeName);
25672
+ }
25673
+ }
25674
+ };
25675
+ // Whether `resourceScope` listed the facade that wrote, with this write
25676
+ // among the ones that count.
25677
+ const isWriteHeardBy = (resourceScope, triggeringAction, triggerScope) => {
25678
+ const writeNameSet = resourceDependencies
25679
+ .get(triggerScope)
25680
+ ?.get(resourceScope);
25681
+ if (!writeNameSet) {
25682
+ return false;
25606
25683
  }
25684
+ const { verb, isMany } = triggeringAction.meta;
25685
+ return writeNameSet.has(isMany ? `${verb}_MANY` : verb);
25607
25686
  };
25608
25687
  // Only the reader the resource exposes is registered; the ones `bindParams`
25609
25688
  // makes share its signal (see resource_range_reader.js).
@@ -25645,17 +25724,18 @@ const createResourceLifecycleManager = () => {
25645
25724
  triggerVerb === "PATCH") &&
25646
25725
  config.uniqueKeys.length > 0;
25647
25726
 
25648
- const isKnownDependency =
25649
- triggerResourceScope !== null &&
25650
- triggerResourceScope !== undefined &&
25651
- resourceDependencies.get(triggerResourceScope)?.has(resourceScope);
25727
+ const isHeardDependency = isWriteHeardBy(
25728
+ resourceScope,
25729
+ triggeringAction,
25730
+ triggerResourceScope,
25731
+ );
25652
25732
 
25653
25733
  if (
25654
25734
  !shouldRerunGetMany &&
25655
25735
  !shouldRerunGet &&
25656
25736
  triggerVerb !== "DELETE" &&
25657
25737
  !hasUniqueKeyAutorerun &&
25658
- !isKnownDependency
25738
+ !isHeardDependency
25659
25739
  ) {
25660
25740
  continue;
25661
25741
  }
@@ -25776,21 +25856,10 @@ const createResourceLifecycleManager = () => {
25776
25856
  }
25777
25857
 
25778
25858
  // Cross-resource dependency effects: rerun dependent GET / GET_MANY
25779
- // Fires on any mutating verb — user-configured dependencies express
25780
- // "this resource depends on another resource's data", so any mutation
25781
- // (POST, PUT, PATCH, DELETE) on the dependency should trigger a rerun.
25859
+ // after a write the dependency counts — every write for a resource
25860
+ // listed bare, the callbacks named in `on` otherwise.
25782
25861
  {
25783
- if (
25784
- triggerResourceScope &&
25785
- resourceDependencies
25786
- .get(triggerResourceScope)
25787
- ?.has(resourceScope) &&
25788
- (triggerVerb === "POST" ||
25789
- triggerVerb === "PUT" ||
25790
- triggerVerb === "PATCH" ||
25791
- triggerVerb === "DELETE") &&
25792
- candidateVerb === "GET"
25793
- ) {
25862
+ if (isHeardDependency && candidateVerb === "GET") {
25794
25863
  actionsToRerun.add(actionCandidate);
25795
25864
  reasonSet.add("dependency autorerun");
25796
25865
  continue;
@@ -25867,9 +25936,10 @@ const createResourceLifecycleManager = () => {
25867
25936
  continue;
25868
25937
  }
25869
25938
  const isSameResource = resourceScope === triggerResourceScope;
25870
- const isDependent = Boolean(
25871
- triggerResourceScope &&
25872
- resourceDependencies.get(triggerResourceScope)?.has(resourceScope),
25939
+ const isDependent = isWriteHeardBy(
25940
+ resourceScope,
25941
+ triggeringAction,
25942
+ triggerResourceScope,
25873
25943
  );
25874
25944
  if (!isSameResource && !isDependent) {
25875
25945
  continue;
@@ -25934,6 +26004,35 @@ const shouldRerunAfter = (rerunConfig, verb) => {
25934
26004
  }
25935
26005
  return false;
25936
26006
  };
26007
+ // A `dependencies` entry: a resource (every write counts), or
26008
+ // `{ resource, on }` naming the write callbacks that do. A name that cannot
26009
+ // fire throws here: a dependency that never fires leaves a read on its old
26010
+ // value with nothing to say so.
26011
+ const resolveDependency = (dependency, dependentScope) => {
26012
+ if (!Object.hasOwn(dependency, "resource")) {
26013
+ return [dependency, WRITE_CALLBACK_NAMES];
26014
+ }
26015
+ const { resource: dependencyScope, on } = dependency;
26016
+ const where = `resource(${dependentScope.name}) dependency on ${dependencyScope.name}`;
26017
+ if (!Array.isArray(on) || on.length === 0) {
26018
+ throw new Error(
26019
+ `${where}: "on" lists the writes that count, e.g. on: ["POST", "DELETE"] — list the resource itself for every write`,
26020
+ );
26021
+ }
26022
+ for (const writeName of on) {
26023
+ if (!WRITE_CALLBACK_NAMES.includes(writeName)) {
26024
+ throw new Error(
26025
+ `${where}: "${writeName}" is not a write, "on" takes ${WRITE_CALLBACK_NAMES.join(", ")}`,
26026
+ );
26027
+ }
26028
+ if (!dependencyScope[writeName]) {
26029
+ throw new Error(
26030
+ `${where}: ${dependencyScope.name} has no ${writeName}, so it would never rerun ${dependentScope.name}`,
26031
+ );
26032
+ }
26033
+ }
26034
+ return [dependencyScope, on];
26035
+ };
25937
26036
  const isParamSubset = (parentParams, childParams) => {
25938
26037
  if (!parentParams || !childParams) {
25939
26038
  return false;
@@ -26682,6 +26781,10 @@ const debug$2 = (...args) => {
26682
26781
  * @param {Object} restCallbacks - `{ idKey, uniqueKeys, rerunOn, dependencies, GET, GET_MANY, GET_RANGE, POST, POST_MANY, PUT, PUT_MANY, PATCH, PATCH_MANY, DELETE, DELETE_MANY }`
26683
26782
  * @param {string} [restCallbacks.idKey] - primary key property, defaults to `"id"`
26684
26783
  * @param {string[]} [restCallbacks.uniqueKeys] - alternate keys the store can find an item by (e.g. `"username"`)
26784
+ * @param {Array<Object | { resource: Object, on: string[] }>} [restCallbacks.dependencies] -
26785
+ * resources whose writes rerun this one's reads: one listed bare counts every
26786
+ * write, `{ resource, on: ["PATCH_MANY", "DELETE"] }` only the callbacks it
26787
+ * names (`PATCH` is not `PATCH_MANY`)
26685
26788
  * @param {{ signal: import("@preact/signals").Signal, when?: () => boolean }} [restCallbacks.persist] -
26686
26789
  * mirrors the rows `GET` lands with into `signal` — a `stateSignal` with
26687
26790
  * `persists: true` and `type: "object"` keeps them across a reload, whose
@@ -28772,6 +28875,7 @@ const useAsyncData = (promiseOrAction, {
28772
28875
  loading = "delegate",
28773
28876
  error = "delegate",
28774
28877
  run,
28878
+ prerun,
28775
28879
  onLoad
28776
28880
  } = {}) => {
28777
28881
  // The request this component owns, as a function: one action per instance,
@@ -28797,6 +28901,7 @@ const useAsyncData = (promiseOrAction, {
28797
28901
  loadingEffect: loading,
28798
28902
  errorEffect: error,
28799
28903
  run,
28904
+ prerun,
28800
28905
  onLoad
28801
28906
  });
28802
28907
  }
@@ -28828,8 +28933,14 @@ const useActionAsyncData = (action, {
28828
28933
  loadingEffect,
28829
28934
  errorEffect,
28830
28935
  run,
28936
+ prerun,
28831
28937
  onLoad
28832
28938
  }) => {
28939
+ if (prerun) {
28940
+ // A head start draws none of what it starts, so it has no wait to
28941
+ // delegate.
28942
+ loadingEffect = "use";
28943
+ }
28833
28944
  const loadingRef = useContext(LoadingContext);
28834
28945
  // The boundary is where a suspension lands, so only the delegating path needs
28835
28946
  // one: `loading: true` draws the wait itself and never suspends.
@@ -28845,13 +28956,18 @@ const useActionAsyncData = (action, {
28845
28956
  // no-op — which is what makes starting it while rendering sound. From there
28846
28957
  // the wait is the ordinary one: suspended into <Loading>, or drawn by the
28847
28958
  // component under `loading: true`, exactly as for data someone else ran.
28848
- if (run && action.runningStateSignal.peek() === IDLE && action.paramsSignal.peek() !== undefined) {
28959
+ if (run && action.paramsSignal.peek() !== undefined && runIsOwed(action)) {
28849
28960
  // Nothing waits on this run: the failure is held by the action, and this
28850
28961
  // hook is what reads it back.
28851
28962
  runUnwatched(() => action.run({
28852
28963
  reason: "useAsyncData({ run: true })"
28853
28964
  }));
28854
28965
  }
28966
+ if (prerun && action.runningStateSignal.peek() === IDLE && action.paramsSignal.peek() !== undefined) {
28967
+ prerunUnwatched(() => action.prerun({
28968
+ reason: "useAsyncData({ prerun: true })"
28969
+ }));
28970
+ }
28855
28971
 
28856
28972
  // Use peek() instead of .value to avoid subscribing this component to the signal.
28857
28973
  // Reading .value would make Preact re-render the component reactively when the state
@@ -28960,6 +29076,11 @@ const useActionAsyncData = (action, {
28960
29076
  return [action.dataSignal.peek(), false, undefined];
28961
29077
  }
28962
29078
  if (runningState === FAILED) {
29079
+ if (prerun) {
29080
+ // Neither shown nor reported from a head start: the failure is the
29081
+ // owner's to ask again (see runIsOwed).
29082
+ return [action.dataSignal.peek(), false, undefined];
29083
+ }
28963
29084
  if (dismissedActionWeakSet.has(action)) {
28964
29085
  // Dismissing removes the error and nothing else: what is handed back is
28965
29086
  // the action's state without it — the data it holds, and no run in
@@ -29065,6 +29186,21 @@ const useActionAsyncData = (action, {
29065
29186
  throw pendingPromise;
29066
29187
  };
29067
29188
 
29189
+ // What `run: true` starts from: an action nobody ran, or one a prerun failed.
29190
+ // Any other failure stays — a render retrying it would loop while the network
29191
+ // is down — but nothing on screen asked for a prerun, so its failure is asked
29192
+ // again, once: the mark is taken here, and the run started fails as its own.
29193
+ const runIsOwed = action => {
29194
+ const runningState = action.runningStateSignal.peek();
29195
+ if (runningState === IDLE) {
29196
+ return true;
29197
+ }
29198
+ if (runningState === FAILED) {
29199
+ return takePrerunFailure(action.errorSignal.peek());
29200
+ }
29201
+ return false;
29202
+ };
29203
+
29068
29204
  // What a screen does with the data once, when it becomes known (see onLoad in
29069
29205
  // the JSDoc above). Kept apart because the two questions it answers are not the
29070
29206
  // ones the hook around it answers: WHEN — a layout effect, so a form taking its
@@ -38601,15 +38737,7 @@ const prefetchAsIf = (action, readParams, values) => {
38601
38737
  // the arrival focuses the link, or is the one that asked), and a reset
38602
38738
  // landing after that would pull the answer from under the page reading it.
38603
38739
  const prerunAhead = (instance) => {
38604
- let result;
38605
- try {
38606
- result = instance.prerun({ reason: "intent" });
38607
- } catch {
38608
- return;
38609
- }
38610
- if (result && typeof result.catch === "function") {
38611
- result.catch(() => {});
38612
- }
38740
+ prerunUnwatched(() => instance.prerun({ reason: "intent" }));
38613
38741
  };
38614
38742
 
38615
38743
  // I delibrately prefer the term "any" and avoid "some" so dev are not tempted to think
@@ -40646,7 +40774,7 @@ registerNaviCommand("--navi-open", (source, event, { anchor, value } = {}) => {
40646
40774
  * The target is found the way --navi-open finds it, but silently: nothing
40647
40775
  * runs here, and a source with no target says so at the release.
40648
40776
  */
40649
- const announceOpeningPress = (source, command) => {
40777
+ const announceOpeningPress = (source, command, pointerdownEvent) => {
40650
40778
  if (command !== "--navi-open" && command !== "--navi-toggle") {
40651
40779
  return;
40652
40780
  }
@@ -40657,7 +40785,7 @@ const announceOpeningPress = (source, command) => {
40657
40785
  if (!target) {
40658
40786
  return;
40659
40787
  }
40660
- dispatchCustomEvent(target, "navi_open_press");
40788
+ dispatchCustomEvent(target, "navi_open_press", { event: pointerdownEvent });
40661
40789
  };
40662
40790
  // "--navi-close:all" closes every expandable above the source, nearest first —
40663
40791
  // a link leaving from a badge shown over a sheet leaves both. A surface that
@@ -40930,11 +41058,18 @@ registerNaviCommand("--navi-unselect", (source, event) => {
40930
41058
  * where a delay is felt hardest — while the ~100-300ms between hovering a
40931
41059
  * trigger and pressing it are free. The warming render starts at the frame
40932
41060
  * after the one that follows the intent: nothing here needs the content in the
40933
- * DOM before the click, only before the open that follows it, and on a touch
40934
- * screen `pointerover` arrives together with the `pointerdown` — a build
40935
- * started there would hold back the frame that shows the press. Two animation
40936
- * frames, never a timeout: browsers hold timers back while a finger is down,
40937
- * and a timeout started by the press fires at its release, on top of the click.
41061
+ * DOM before the click, only before the open that follows it, and a build
41062
+ * started at once would hold back the frame that shows the hover or the focus.
41063
+ *
41064
+ * Under a finger, `pointerover` is not an intent yet: it arrives with the
41065
+ * `pointerdown` of every touch, the ones starting a scroll included, and at
41066
+ * reading speed the browser decides a touch is a scroll about 35ms in — a
41067
+ * build started then holds that scroll start back. So a touch warms once it has been held
41068
+ * TOUCH_WARM_HOLD_MS without the browser taking it (`pointercancel`) or the
41069
+ * finger lifting; a quicker tap builds at the open, like a click with no hover
41070
+ * before it. Counted in animation frames, never a timeout: browsers hold timers
41071
+ * back while a finger is down, and a timeout started by the press fires at its
41072
+ * release, on top of the click.
40938
41073
  *
40939
41074
  * The anchor only says where the popup is placed, so intent inside it is read
40940
41075
  * off the control it is aimed at, the nearest one around the target. A control
@@ -40958,8 +41093,11 @@ registerNaviCommand("--navi-unselect", (source, event) => {
40958
41093
  * anchor: it says where the popup is placed, not what opens it — a card
40959
41094
  * opened by a hold is pressed all day by taps meant for what it holds.
40960
41095
  *
40961
- * The press builds the content, and the press ending without an open throws
40962
- * it away — its click reaching the document with the popup still closed, no
41096
+ * The press builds the content when it starts, a touch once it has been held
41097
+ * as above: the announcement carries the pointerdown that made it. A press
41098
+ * released before its build leaves the building to the open its click asks
41099
+ * for. The press ending without an open throws the content away — its click
41100
+ * reaching the document with the popup still closed, no
40963
41101
  * click coming after the release, the browser taking the gesture
40964
41102
  * (`pointercancel`), or the next press starting, before any of its own
40965
41103
  * handlers run: one press at a time holds a content. A keyboard opening builds
@@ -41003,6 +41141,12 @@ const cancelIdle = (id) =>
41003
41141
  ? cancelIdleCallback(id)
41004
41142
  : clearTimeout(id);
41005
41143
 
41144
+ // How long a finger rests on an anchor before it warms it: past the ~35ms at
41145
+ // which the browser takes a touch moving at reading speed for a scroll, short
41146
+ // of the ~100ms a tap is held, so a deliberate tap is warmed before its
41147
+ // release.
41148
+ const TOUCH_WARM_HOLD_MS = 80;
41149
+
41006
41150
  const usePopupContentMount = (
41007
41151
  openController,
41008
41152
  ref,
@@ -41093,14 +41237,35 @@ const usePopupContentMount = (
41093
41237
  return undefined;
41094
41238
  }
41095
41239
  let cancelWarm = null;
41240
+ let warmIsTouchHold = false;
41096
41241
  const warm = () => {
41097
41242
  if (cancelWarm) {
41098
41243
  return;
41099
41244
  }
41245
+ warmIsTouchHold = false;
41100
41246
  cancelWarm = requestFrameAfterNext(() => {
41101
41247
  setContentMounted(true);
41102
41248
  });
41103
41249
  };
41250
+ const warmAfterTouchHold = () => {
41251
+ if (cancelWarm) {
41252
+ return;
41253
+ }
41254
+ warmIsTouchHold = true;
41255
+ cancelWarm = requestFrameAfterDuration(TOUCH_WARM_HOLD_MS, () => {
41256
+ setContentMounted(true);
41257
+ });
41258
+ };
41259
+ // A touch leaves the anchor when the finger lifts or the browser takes the
41260
+ // touch: either way, before the hold said anything.
41261
+ const onPointerLeave = () => {
41262
+ if (!warmIsTouchHold) {
41263
+ return;
41264
+ }
41265
+ cancelWarm();
41266
+ cancelWarm = null;
41267
+ warmIsTouchHold = false;
41268
+ };
41104
41269
  // The anchor itself when it is a control (a Popover anchored on the button
41105
41270
  // opening it), or the one the popup is written in (a picker's trigger).
41106
41271
  const isOwnControl = (control) =>
@@ -41120,6 +41285,10 @@ const usePopupContentMount = (
41120
41285
  return;
41121
41286
  }
41122
41287
  }
41288
+ if (pointeroverEvent.pointerType === "touch") {
41289
+ warmAfterTouchHold();
41290
+ return;
41291
+ }
41123
41292
  warm();
41124
41293
  };
41125
41294
  const onFocusIn = (focusinEvent) => {
@@ -41130,10 +41299,12 @@ const usePopupContentMount = (
41130
41299
  warm();
41131
41300
  };
41132
41301
  anchorElement.addEventListener("pointerover", onPointerOver);
41302
+ anchorElement.addEventListener("pointerleave", onPointerLeave);
41133
41303
  anchorElement.addEventListener("focusin", onFocusIn);
41134
41304
  return () => {
41135
41305
  cancelWarm?.();
41136
41306
  anchorElement.removeEventListener("pointerover", onPointerOver);
41307
+ anchorElement.removeEventListener("pointerleave", onPointerLeave);
41137
41308
  anchorElement.removeEventListener("focusin", onFocusIn);
41138
41309
  };
41139
41310
  }, [contentMounted, anchor, mount]);
@@ -41143,12 +41314,14 @@ const usePopupContentMount = (
41143
41314
  const stopPressWarmRef = useRef(null);
41144
41315
  openController.onOpeningPress =
41145
41316
  mount === "while-opened"
41146
- ? () => {
41317
+ ? (openPressEvent) => {
41147
41318
  if (contentMountedRef.current || openController.onOpen) {
41148
41319
  return;
41149
41320
  }
41150
- const cancelBuild = requestFrameAfterNext(() => {
41151
- setContentMounted(true);
41321
+ const cancelBuild = requestBuildOnPress(openPressEvent, {
41322
+ onBuild: () => {
41323
+ setContentMounted(true);
41324
+ },
41152
41325
  });
41153
41326
  const stopWatching = watchPressEnd(() => {
41154
41327
  stopPressWarmRef.current = null;
@@ -41210,6 +41383,64 @@ const requestFrameAfterNext = (callback) => {
41210
41383
  };
41211
41384
  };
41212
41385
 
41386
+ // What an opening press builds, it builds at the frame after the next one, so
41387
+ // the pressed trigger is painted first — under a finger, once the touch has
41388
+ // been held TOUCH_WARM_HOLD_MS (see the top comment). Released or taken by the
41389
+ // browser before then, the press builds nothing: the open its click asks for
41390
+ // builds, when one comes. Called from the press's own pointerdown, through its
41391
+ // navi_open_press: the capture listeners below only hear what follows.
41392
+ const requestBuildOnPress = (openPressEvent, { onBuild, onGiveUp }) => {
41393
+ const pointerdownEvent = openPressEvent.detail.event;
41394
+ const cancelFrame =
41395
+ pointerdownEvent.pointerType === "touch"
41396
+ ? requestFrameAfterDuration(TOUCH_WARM_HOLD_MS, () => {
41397
+ stopListening();
41398
+ onBuild();
41399
+ })
41400
+ : requestFrameAfterNext(() => {
41401
+ stopListening();
41402
+ onBuild();
41403
+ });
41404
+ const onPressEnd = (pointerEvent) => {
41405
+ if (pointerEvent.pointerId !== pointerdownEvent.pointerId) {
41406
+ return;
41407
+ }
41408
+ cancel();
41409
+ onGiveUp?.();
41410
+ };
41411
+ const stopListening = () => {
41412
+ document.removeEventListener("pointerup", onPressEnd, { capture: true });
41413
+ document.removeEventListener("pointercancel", onPressEnd, {
41414
+ capture: true,
41415
+ });
41416
+ };
41417
+ const cancel = () => {
41418
+ cancelFrame();
41419
+ stopListening();
41420
+ };
41421
+ document.addEventListener("pointerup", onPressEnd, { capture: true });
41422
+ document.addEventListener("pointercancel", onPressEnd, { capture: true });
41423
+ return cancel;
41424
+ };
41425
+
41426
+ // The first frame at least `ms` after the call, read off the frames' own
41427
+ // timestamps: a 120Hz screen waits as long as a 60Hz one.
41428
+ const requestFrameAfterDuration = (ms, callback) => {
41429
+ const start = performance.now();
41430
+ let frame;
41431
+ const onFrame = (timestamp) => {
41432
+ if (timestamp - start >= ms) {
41433
+ callback();
41434
+ return;
41435
+ }
41436
+ frame = requestAnimationFrame(onFrame);
41437
+ };
41438
+ frame = requestAnimationFrame(onFrame);
41439
+ return () => {
41440
+ cancelAnimationFrame(frame);
41441
+ };
41442
+ };
41443
+
41213
41444
  // How long the click of a released press may take to arrive. Nothing tells a
41214
41445
  // page that a release brings no click — a press that became a drag has its
41215
41446
  // click swallowed (suppressClickAfterGesture in @jsenv/dom) — so past this the
@@ -50251,7 +50482,7 @@ const usePreloadOpeningOnPress = (ref, command, prefetch = true) => {
50251
50482
  if (pointerdownEvent.button !== 0) {
50252
50483
  return;
50253
50484
  }
50254
- announceOpeningPress(element, command);
50485
+ announceOpeningPress(element, command, pointerdownEvent);
50255
50486
  };
50256
50487
  element.addEventListener("pointerdown", onPointerDown);
50257
50488
  return () => {
@@ -54427,7 +54658,9 @@ const Expandable = props => {
54427
54658
  // its own (see announceOpeningPress in commands.js) — unless aimed at a
54428
54659
  // control inside the UI part, whose click does not toggle (see onUIClick).
54429
54660
  if (pointerdownEvent.button === 0 && !openController.opened && !isAimedAtControlInsideUI(pointerdownEvent.target)) {
54430
- dispatchCustomEvent(rootRef.current, "navi_open_press");
54661
+ dispatchCustomEvent(rootRef.current, "navi_open_press", {
54662
+ event: pointerdownEvent
54663
+ });
54431
54664
  }
54432
54665
  };
54433
54666
 
@@ -54848,7 +55081,7 @@ const Expandable = props => {
54848
55081
  ,
54849
55082
  onnavi_open_press: e => {
54850
55083
  rest.onnavi_open_press?.(e);
54851
- openController.onOpeningPress?.();
55084
+ openController.onOpeningPress?.(e);
54852
55085
  },
54853
55086
  onnavi_request_close: e => {
54854
55087
  rest.onnavi_request_close?.(e);
@@ -54928,8 +55161,8 @@ const ExpandableUI = ({
54928
55161
  onnavi_request_open: e => {
54929
55162
  openController.open(e);
54930
55163
  },
54931
- onnavi_open_press: () => {
54932
- openController.onOpeningPress?.();
55164
+ onnavi_open_press: e => {
55165
+ openController.onOpeningPress?.(e);
54933
55166
  },
54934
55167
  onnavi_request_close: e => {
54935
55168
  openController.requestClose(e, {
@@ -66982,10 +67215,10 @@ const DialogStandIn = props => {
66982
67215
  }
66983
67216
  // eslint-disable-next-line react/no-unknown-property
66984
67217
  ,
66985
- onnavi_open_press: () => {
67218
+ onnavi_open_press: e => {
66986
67219
  // What the opening will read is asked for at once, as the dialog
66987
- // proper does (see open_controller.js); the dialog itself two frames
66988
- // later, so the pressed trigger is painted first — the click that
67220
+ // proper does (see open_controller.js); the dialog itself once the
67221
+ // press is taken for one (see requestBuildOnPress) — the click that
66989
67222
  // follows opens it built, or builds it itself when it comes sooner.
66990
67223
  if (signal) {
66991
67224
  preloadState(signal, readOpenValue(value));
@@ -66993,9 +67226,14 @@ const DialogStandIn = props => {
66993
67226
  if (warmRef.current) {
66994
67227
  return;
66995
67228
  }
66996
- warmRef.current = requestFrameAfterNext(() => {
66997
- warmRef.current = null;
66998
- build("press");
67229
+ warmRef.current = requestBuildOnPress(e, {
67230
+ onBuild: () => {
67231
+ warmRef.current = null;
67232
+ build("press");
67233
+ },
67234
+ onGiveUp: () => {
67235
+ warmRef.current = null;
67236
+ }
66999
67237
  });
67000
67238
  }
67001
67239
  });
@@ -67074,8 +67312,8 @@ const UncontrolledDialog = props => {
67074
67312
  // The press announcing that request (see announceOpeningPress in
67075
67313
  // commands.js), heard where the request is.
67076
67314
  ,
67077
- onnavi_open_press: () => {
67078
- openController.onOpeningPress?.();
67315
+ onnavi_open_press: e => {
67316
+ openController.onOpeningPress?.(e);
67079
67317
  },
67080
67318
  onnavi_request_close: e => {
67081
67319
  const closing = openController.requestClose(e, {
@@ -68876,8 +69114,8 @@ const UncontrolledPopover = props => {
68876
69114
  // The press announcing that request (see announceOpeningPress in
68877
69115
  // commands.js), heard where the request is.
68878
69116
  ,
68879
- onnavi_open_press: () => {
68880
- openController.onOpeningPress?.();
69117
+ onnavi_open_press: e => {
69118
+ openController.onOpeningPress?.(e);
68881
69119
  },
68882
69120
  onnavi_request_close: e => {
68883
69121
  const closing = openController.requestClose(e, {
@@ -70282,15 +70520,15 @@ const css$B = /* css */`.navi_picker_callout_dock > [data-picker-content] {
70282
70520
  display: flex;
70283
70521
  }
70284
70522
 
70523
+ &:has( > .navi_list_container:not([data-scroller], [data-overflow-visible])) {
70524
+ overflow: visible;
70525
+ }
70526
+
70285
70527
  & .navi_list_container {
70286
70528
  --list-border-radius: max(0px,
70287
70529
  var(--popover-border-radius) - var(--popover-border-width));
70288
70530
  overscroll-behavior: none;
70289
70531
  width: 100%;
70290
-
70291
- &:not([data-overflow-visible]) {
70292
- overflow: auto;
70293
- }
70294
70532
  }
70295
70533
  }
70296
70534
 
@@ -70309,15 +70547,15 @@ const css$B = /* css */`.navi_picker_callout_dock > [data-picker-content] {
70309
70547
  flex-direction: column;
70310
70548
  }
70311
70549
 
70550
+ &:has( > .navi_list_container:not([data-scroller], [data-overflow-visible])) {
70551
+ overflow: visible;
70552
+ }
70553
+
70312
70554
  & .navi_list_container {
70313
70555
  --list-border-radius: max(0px,
70314
70556
  var(--dialog-border-radius) - var(--dialog-border-width));
70315
70557
  overscroll-behavior: none;
70316
70558
  width: 100%;
70317
-
70318
- &:not([data-overflow-visible]) {
70319
- overflow: auto;
70320
- }
70321
70559
  }
70322
70560
  }
70323
70561
  `;
@@ -70763,8 +71001,8 @@ const PickerCustom = props => {
70763
71001
  },
70764
71002
  // And the press announcing that request (see announceOpeningPress in
70765
71003
  // commands.js), for the popup's content to be built on it.
70766
- "onnavi_open_press": () => {
70767
- dispatchCustomEvent(popupRef.current, "navi_open_press");
71004
+ "onnavi_open_press": e => {
71005
+ dispatchCustomEvent(popupRef.current, "navi_open_press", e.detail);
70768
71006
  },
70769
71007
  "onnavi_request_close": e => {
70770
71008
  const closing = dispatchCustomEvent(popupRef.current, "navi_request_close", e.detail);
@@ -70827,8 +71065,8 @@ const PickerCustom = props => {
70827
71065
  // The press announcing that request (see announceOpeningPress in
70828
71066
  // commands.js), heard where the request is — here too for the same
70829
71067
  // reason. Nothing to gate: the announcer asked before saying it.
70830
- onnavi_open_press: () => {
70831
- openController.onOpeningPress?.();
71068
+ onnavi_open_press: e => {
71069
+ openController.onOpeningPress?.(e);
70832
71070
  },
70833
71071
  onnavi_request_close: e => {
70834
71072
  requestInteraction({
@@ -70981,7 +71219,9 @@ const PickerCustom = props => {
70981
71219
  })) {
70982
71220
  return;
70983
71221
  }
70984
- dispatchCustomEvent(pickerEl, "navi_open_press");
71222
+ dispatchCustomEvent(pickerEl, "navi_open_press", {
71223
+ event: pointerdownEvent
71224
+ });
70985
71225
  };
70986
71226
  }
70987
71227
  let interactions = props.interactions;
@@ -73806,15 +74046,13 @@ const css$x = /* css */`@property --list-group-label-height {
73806
74046
  flex-direction: column;
73807
74047
  transition: opacity .2s;
73808
74048
  display: flex;
73809
- overflow: hidden;
73810
74049
 
73811
- &[data-borderless] {
73812
- --x-list-border-radius: 0px;
73813
- overflow: visible;
74050
+ &:not([data-borderless], [data-overflow-visible], [data-scroller]) {
74051
+ min-height: 0;
73814
74052
  }
73815
74053
 
73816
- &[data-overflow-visible="both"] {
73817
- overflow: visible;
74054
+ &[data-borderless] {
74055
+ --x-list-border-radius: 0px;
73818
74056
  }
73819
74057
 
73820
74058
  &[data-overflow-visible="x"] {