@jsenv/navi 0.29.173 → 0.29.175

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.
@@ -4,7 +4,7 @@
4
4
  */
5
5
  import { installImportMetaCssBuild, windowHeightSignal, windowWidthSignal, visualViewportHeightSignal, visualViewportWidthSignal, getAppHeight, getAppWidth, coarsePointerSignal, smallTouchScreenSignal } from "./jsenv_navi_side_effects.js";
6
6
  export { disableVirtualKeyboardOverlay } from "./jsenv_navi_side_effects.js";
7
- import { elementIsFocusable, createIterableWeakSet, dispatchInternalCustomEvent, dispatchCustomEvent, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, getElementSignature, createPubSub, findEvent, createValueEffect, findFocusDelegateTarget, findFocusable, scrollIntoViewThroughScrollables, allowWheelThrough, dispatchPublicCustomEvent, resolveCSSColor, ELEMENT_SIZE_CHANGE, findSelfOrAncestorFixedPosition, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, applyNewPosition, measureLongestVisualLineWidth, chainEvent, waitForPressHeld, suppressClickAfterGesture, startDragToTravel, markDragSource, startDragTo, createEventGroupLogger, getKeyboardEventDefaultAction, activeElementSignal, normalizeStyle, mergeOneStyle, getPositionedParent, normalizeStyles, createGroupTransitionController, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, watchWheelTravel, scrollRoomTowards, getScrollContainer, closestOpenableAncestor, isAncestorOpen, isDisplayedDespiteClosedAncestor, observeAncestorOpenState, getAncestorOpenType, findBefore, findAfter, resolveCSSSize, hasCSSSizeUnit, releaseWheelGesture, getScrollIntoViewScopedOffsets, wheelGestureIsTakenFrom, claimWheelGesture, scrollIntoViewScoped, initFocusGroup, isTouchDrivenEvent, stringifyStyle as stringifyStyle$1, resolveOklchLightness, contrastColor, parsePositionArea, snapToPixel, trapFocusInside, trapScrollInside, onAncestorReopen, isPressDisputedByDrag, canScroll, measureWidestChildRow, performTabNavigation, dragAfterIntent, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement } from "@jsenv/dom";
7
+ import { elementIsFocusable, createIterableWeakSet, dispatchInternalCustomEvent, dispatchCustomEvent, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, getElementSignature, createPubSub, findEvent, createValueEffect, findFocusDelegateTarget, findFocusable, scrollIntoViewThroughScrollables, allowWheelThrough, dispatchPublicCustomEvent, resolveCSSColor, ELEMENT_SIZE_CHANGE, findSelfOrAncestorFixedPosition, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, applyNewPosition, measureLongestVisualLineWidth, chainEvent, keepTouchRefusable, waitForPressHeld, suppressClickAfterGesture, startDragToTravel, markDragSource, startDragTo, createEventGroupLogger, getKeyboardEventDefaultAction, activeElementSignal, normalizeStyle, mergeOneStyle, getPositionedParent, normalizeStyles, createGroupTransitionController, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, watchWheelTravel, scrollRoomTowards, getScrollContainer, closestOpenableAncestor, isAncestorOpen, isDisplayedDespiteClosedAncestor, observeAncestorOpenState, getAncestorOpenType, findBefore, findAfter, resolveCSSSize, hasCSSSizeUnit, releaseWheelGesture, getScrollIntoViewScopedOffsets, wheelGestureIsTakenFrom, claimWheelGesture, scrollIntoViewScoped, initFocusGroup, isTouchDrivenEvent, stringifyStyle as stringifyStyle$1, resolveOklchLightness, contrastColor, parsePositionArea, snapToPixel, trapFocusInside, trapScrollInside, onAncestorReopen, isPressDisputedByDrag, canScroll, measureWidestChildRow, performTabNavigation, dragAfterIntent, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement } from "@jsenv/dom";
8
8
  export { chainEvent, clickIsSuppressed, contrastColor, findEvent, startDragTo } from "@jsenv/dom";
9
9
  import { signal, computed, effect, untracked, batch, useComputed, useSignal } from "@preact/signals";
10
10
  import { isValidElement, h, Fragment, createContext, render, toChildArray, options, cloneElement } from "preact";
@@ -2905,6 +2905,15 @@ const compareTwoJsValues = (
2905
2905
  if (aKeys.length !== bKeys.length) {
2906
2906
  return false;
2907
2907
  }
2908
+ // The same number of keys is not the same keys, and only a's are walked
2909
+ // below. A key b does not have reads as undefined, so { include: undefined }
2910
+ // and { search: "a" } would compare "include" (undefined on both sides),
2911
+ // never look at "search", and pass for equal.
2912
+ for (const bKey of bKeys) {
2913
+ if (!Object.hasOwn(a, bKey)) {
2914
+ return false;
2915
+ }
2916
+ }
2908
2917
  if (aKeys.length === 0 && (!isPlainObject$1(a) || !isPlainObject$1(b))) {
2909
2918
  // A Set, a Map, an element, a URL: nothing enumerable to tell two of them
2910
2919
  // apart, so they are the same one or they are not — and they are not,
@@ -8995,8 +9004,13 @@ const formatSecondDuration = (
8995
9004
  * are preserved as-is and non-numeric mid-edit values (e.g. "2a") are rendered
8996
9005
  * with their unit symbol rather than being stringified.
8997
9006
  *
8998
- * @param {{ years?: any, months?: any, weeks?: any, days?: any,
8999
- * hours?: any, minutes?: any, seconds?: any, milliseconds?: any }} duration
9007
+ * @param {string|number|{ years?: any, months?: any, weeks?: any, days?: any,
9008
+ * hours?: any, minutes?: any, seconds?: any, milliseconds?: any }} duration -
9009
+ * A string goes through {@link parseDuration} ("PT1H30M", "1h30"), a number
9010
+ * is read as seconds. Each unit is written with the value it carries: 90
9011
+ * minutes reads "90 minutes", never "1 heure 30" — the variants that
9012
+ * promote a count into bigger units are {@link formatMinuteDuration},
9013
+ * {@link formatHourDuration} and {@link formatSecondDuration}.
9000
9014
  * @param {{ lang?: string, format?: "long"|"short"|"narrow"|"compact" }} [options]
9001
9015
  *
9002
9016
  * @example
@@ -11524,6 +11538,17 @@ defineInteractionDetector({
11524
11538
  undo.push(() => {
11525
11539
  element.removeEventListener("dragstart", onDragStart);
11526
11540
  });
11541
+
11542
+ // A touch this element may take has to be refusable before the finger
11543
+ // lands, or the browser can cancel an established swipe mid-gesture by
11544
+ // scrolling the list around it along the axis touch-action leaves free —
11545
+ // same rule, same moment as the attributes above (see keepTouchRefusable).
11546
+ element.addEventListener("touchmove", keepTouchRefusable, {
11547
+ passive: false
11548
+ });
11549
+ undo.push(() => {
11550
+ element.removeEventListener("touchmove", keepTouchRefusable);
11551
+ });
11527
11552
  }
11528
11553
  if (hasLongPress) {
11529
11554
  mark(LONGPRESS_ATTRIBUTE, "");
@@ -31185,7 +31210,14 @@ const RouteTravel = ({
31185
31210
  ,
31186
31211
 
31187
31212
  "data-travel-by-drag": travelByDrag ? axis : undefined,
31188
- "data-travel-by-wheel": travelByDrag ? axis : undefined,
31213
+ "data-travel-by-wheel": travelByDrag ? axis : undefined
31214
+ // A touch this box may take has to be refusable before the finger lands,
31215
+ // or the browser can cancel an established travel mid-swipe by scrolling
31216
+ // the axis touch-action leaves it — see keepTouchRefusable for why a JSX
31217
+ // prop is enough (an element-level touchmove listener is non-passive).
31218
+ ,
31219
+
31220
+ onTouchMove: travelByDrag ? keepTouchRefusable : undefined,
31189
31221
  onPointerDown: onPointerDown,
31190
31222
  children: children
31191
31223
  });
@@ -34125,6 +34157,11 @@ const useUIStateController = (
34125
34157
 
34126
34158
  state: stateInitial,
34127
34159
  uiState: stateInitial,
34160
+ // What a bound signal last said — the change detector for writes coming
34161
+ // from the outside. Kept apart from `state` on purpose: `state` is the
34162
+ // last ACCEPTED value (the rollback target), and a signal echoing the
34163
+ // control's own unconfirmed write moves this field without moving it.
34164
+ stateFromSignalPrevious: stateInitial,
34128
34165
  uiStateSignal,
34129
34166
  // What this control holds by ITSELF — the same signal as above unless
34130
34167
  // it is a button inheriting from the control around it (see `inherit`).
@@ -34635,22 +34672,43 @@ const useUIStateController = (
34635
34672
  if (controller.hasStateProp) {
34636
34673
  controller.hasStateProp = false;
34637
34674
  controller.state = stateInitial;
34675
+ controller.stateFromSignalPrevious = stateInitial;
34638
34676
  }
34639
34677
  if (controlInfo.signal) {
34640
34678
  // The other half of a bound signal: the control follows it when
34641
- // something else writes it. Compared against `state` (what the signal
34642
- // last said), never against the ui state — otherwise typing into an
34679
+ // something else writes it. Compared against what the signal last
34680
+ // said, never against the ui state — otherwise typing into an
34643
34681
  // uncontrolled control would be undone on the next render. A write
34644
34682
  // coming from the control itself lands here with the value already in
34645
34683
  // place, and setUIState treats an unchanged state as a no-op.
34646
- const currentState = controller.state;
34647
- if (!compareTwoJsValues(stateFromSignal, currentState)) {
34648
- controller.state = stateFromSignal;
34649
- if (!optimisticWorkInFlight) {
34650
- controller.setUIState(
34651
- stateFromSignal,
34652
- new CustomEvent("state_prop_change"),
34653
- );
34684
+ if (
34685
+ !compareTwoJsValues(
34686
+ stateFromSignal,
34687
+ controller.stateFromSignalPrevious,
34688
+ )
34689
+ ) {
34690
+ controller.stateFromSignalPrevious = stateFromSignal;
34691
+ // On a control that runs an action, a signal matching the ui state
34692
+ // is the echo of the control's own write (setUIState wrote the
34693
+ // signal at the press — for a picker, at the pick, before the
34694
+ // close even runs the action). `state` is the rollback target —
34695
+ // the last value the outside accepted — and the echo must not
34696
+ // become it: resetOnError would then "restore" the very value the
34697
+ // server is about to refuse. The action settles `state` on its
34698
+ // own: success acknowledges (state = uiState), failure resets to
34699
+ // the state kept here. A control with no action has no such
34700
+ // settlement, so it keeps following the signal.
34701
+ const signalEchoesOwnWrite =
34702
+ Boolean(props.action) &&
34703
+ compareTwoJsValues(stateFromSignal, controller.uiState);
34704
+ if (!signalEchoesOwnWrite) {
34705
+ controller.state = stateFromSignal;
34706
+ if (!optimisticWorkInFlight) {
34707
+ controller.setUIState(
34708
+ stateFromSignal,
34709
+ new CustomEvent("state_prop_change"),
34710
+ );
34711
+ }
34654
34712
  }
34655
34713
  }
34656
34714
  }
@@ -37876,6 +37934,30 @@ const useInteractiveProps = (props, {
37876
37934
  resetOnError,
37877
37935
  optimistic
37878
37936
  } = props;
37937
+ // The DOM's word for the unconfirmed moment: an optimistic control paints
37938
+ // the value before the server accepts it, and stays interactive while the
37939
+ // run is out — aria-busy says "false" on purpose. This attribute is the
37940
+ // only trace of that moment, for an app that wants to dim or mark the
37941
+ // value until it is confirmed: `[data-optimistic]` on the control host and
37942
+ // its control root. Written at the run's boundaries rather than from
37943
+ // render: the bound action is a proxy following the UI state, so once the
37944
+ // state moves ahead of the running instance the proxy stops reporting the
37945
+ // run (same trap as the queue below).
37946
+ const syncOptimisticAttribute = on => {
37947
+ const el = ref.current;
37948
+ if (!el) {
37949
+ return;
37950
+ }
37951
+ const controlRoot = findControlRoot(el);
37952
+ const targets = controlRoot && controlRoot !== el ? [el, controlRoot] : [el];
37953
+ for (const target of targets) {
37954
+ if (on) {
37955
+ target.setAttribute("data-optimistic", "");
37956
+ } else {
37957
+ target.removeAttribute("data-optimistic");
37958
+ }
37959
+ }
37960
+ };
37879
37961
  Object.assign(controlHostProps, {
37880
37962
  onFocus: e => {
37881
37963
  // Transfer programmatic focus to the delegate target (navi-focus-delegate or navi-control-proxy-for)
@@ -37962,6 +38044,9 @@ const useInteractiveProps = (props, {
37962
38044
  uiStateController.pendingActionEvent = e.detail.event;
37963
38045
  uiStateController.actionInFlight = true;
37964
38046
  uiStateController.parallelGuard?.claim(uiStateController);
38047
+ if (optimistic) {
38048
+ syncOptimisticAttribute(true);
38049
+ }
37965
38050
  // The very instance this run uses, resolved now — while the UI state
37966
38051
  // still holds the value the run was made for. detail.action may be a
37967
38052
  // proxy following that state, and by the time anyone wants to abort
@@ -37979,6 +38064,7 @@ const useInteractiveProps = (props, {
37979
38064
  uiStateController.parallelGuard?.release(uiStateController);
37980
38065
  const queuedEvent = uiStateController.queuedActionAllowedEvent;
37981
38066
  if (!queuedEvent) {
38067
+ syncOptimisticAttribute(false);
37982
38068
  return;
37983
38069
  }
37984
38070
  if (outcome.error) {
@@ -37986,6 +38072,7 @@ const useInteractiveProps = (props, {
37986
38072
  // known state (resetOnError above), and what was queued was built
37987
38073
  // on top of the state that just failed.
37988
38074
  uiStateController.queuedActionAllowedEvent = null;
38075
+ syncOptimisticAttribute(false);
37989
38076
  return;
37990
38077
  }
37991
38078
  // A microtask later, not right here: this runs inside the batch()
@@ -45864,32 +45951,56 @@ const resolveAutoAnimationKind = (anchor, parsedPositionArea) => {
45864
45951
  * use_displayed_layout_effect.js).
45865
45952
  *
45866
45953
  * The `mount` prop moves that line. "closed" is two states, not one — never
45867
- * opened yet, and closed again after an opening — and the three values answer
45954
+ * opened yet, and closed again after an opening — and the four values answer
45868
45955
  * both at once:
45869
45956
  *
45870
- * | mount | before the first open | after a close |
45871
- * | ----------------- | --------------------- | ------------- |
45872
- * | "always" | mounted | mounted |
45873
- * | "from-first-open" | not mounted | mounted |
45874
- * | "while-opened" | not mounted | not mounted |
45957
+ * | mount | before the first open | after a close |
45958
+ * | ----------------- | --------------------------- | ------------- |
45959
+ * | "always" | mounted | mounted |
45960
+ * | "idle" | mounted once the page idles | mounted |
45961
+ * | "from-first-open" | not mounted | mounted |
45962
+ * | "while-opened" | not mounted | not mounted |
45875
45963
  *
45876
45964
  * "always" is for content something else depends on before any opening: a
45877
45965
  * value the popup's owner reads off its own children, fields a form around it
45878
45966
  * collects on submit, a size measured from outside.
45879
45967
  *
45968
+ * "idle" is "always" minus the cost on the critical render: the page appears
45969
+ * without the content, and the browser builds it in an idle moment after
45970
+ * load — so by the time anyone clicks, it is usually already there.
45971
+ *
45880
45972
  * "while-opened" is the opposite end: content that must be rebuilt from
45881
45973
  * scratch every time, because what it shows is read once at build time and can
45882
45974
  * change while the popup is closed — an uncontrolled field seeded from a
45883
45975
  * `defaultValue`, a form whose fresh state is its initial state.
45976
+ *
45977
+ * On top of whichever value is picked, intent on the anchor warms the content:
45978
+ * a pointer entering the popup's anchor, or focus landing in it, builds the
45979
+ * content ahead of the click that will open it. Deferring the build to the
45980
+ * opening puts its whole cost in the frame right after the click — the frame
45981
+ * where a delay is felt hardest — while the ~100-300ms between hovering a
45982
+ * trigger and pressing it are free. The warming render is asynchronous
45983
+ * (batched, not flushed): nothing here needs the content in the DOM before
45984
+ * the click, only before the open that follows it.
45884
45985
  */
45885
45986
 
45886
45987
 
45887
45988
  const MOUNT_DEFAULT = "from-first-open";
45888
45989
 
45990
+ // requestIdleCallback is missing from Safari; a timeout is close enough there.
45991
+ const requestIdle = (callback) =>
45992
+ typeof requestIdleCallback === "function"
45993
+ ? requestIdleCallback(callback)
45994
+ : setTimeout(callback, 300);
45995
+ const cancelIdle = (id) =>
45996
+ typeof cancelIdleCallback === "function"
45997
+ ? cancelIdleCallback(id)
45998
+ : clearTimeout(id);
45999
+
45889
46000
  const usePopupContentMount = (
45890
46001
  openController,
45891
46002
  ref,
45892
- { mount = MOUNT_DEFAULT },
46003
+ { mount = MOUNT_DEFAULT, anchor },
45893
46004
  ) => {
45894
46005
  const mountedAlways = mount === "always";
45895
46006
  const [contentMounted, setContentMounted] = useState(
@@ -45928,6 +46039,42 @@ const usePopupContentMount = (
45928
46039
  setContentMounted(true);
45929
46040
  }
45930
46041
  }, [mountedAlways]);
46042
+ useEffect(() => {
46043
+ if (mount !== "idle" || contentMounted) {
46044
+ return undefined;
46045
+ }
46046
+ const idleId = requestIdle(() => {
46047
+ setContentMounted(true);
46048
+ });
46049
+ return () => {
46050
+ cancelIdle(idleId);
46051
+ };
46052
+ }, [mount, contentMounted]);
46053
+ // Warm on intent (see the top comment). The anchor accepts the same shapes
46054
+ // Popover resolves at open time — a string id, a ref, an element — but is
46055
+ // resolved here at effect time: an id that matches nothing yet simply
46056
+ // doesn't warm, the open still mounts the content like it always does.
46057
+ useEffect(() => {
46058
+ if (contentMounted || !anchor) {
46059
+ return undefined;
46060
+ }
46061
+ const anchorElement =
46062
+ typeof anchor === "string"
46063
+ ? document.getElementById(anchor)
46064
+ : (anchor.current ?? anchor);
46065
+ if (!anchorElement) {
46066
+ return undefined;
46067
+ }
46068
+ const warm = () => {
46069
+ setContentMounted(true);
46070
+ };
46071
+ anchorElement.addEventListener("pointerenter", warm);
46072
+ anchorElement.addEventListener("focusin", warm);
46073
+ return () => {
46074
+ anchorElement.removeEventListener("pointerenter", warm);
46075
+ anchorElement.removeEventListener("focusin", warm);
46076
+ };
46077
+ }, [contentMounted, anchor]);
45931
46078
 
45932
46079
  return contentMounted;
45933
46080
  };
@@ -46162,7 +46309,7 @@ const useExpandableContext = partName => {
46162
46309
  * openDirection?: "down" | "up" | "right" | "left",
46163
46310
  * autoFocus?: boolean,
46164
46311
  * maxContentHeight?: string | number,
46165
- * mount?: "always" | "from-first-open" | "while-opened",
46312
+ * mount?: "always" | "idle" | "from-first-open" | "while-opened",
46166
46313
  * arrowKeyShortcuts?: boolean,
46167
46314
  * openKeyShortcut?: string,
46168
46315
  * closeKeyShortcut?: string,
@@ -46217,14 +46364,17 @@ const useExpandableContext = partName => {
46217
46364
  * @param maxContentHeight - Caps the content height; taller content scrolls
46218
46365
  * inside the expandable instead of growing it.
46219
46366
  * @param mount - When the content is built and thrown away, a popup's own
46220
- * three values and a popup's own code (see popup_content_mount.js). `"from-first-open"` (the
46367
+ * values and a popup's own code (see popup_content_mount.js). `"from-first-open"` (the
46221
46368
  * default) builds it on the first expansion and keeps it afterwards.
46222
46369
  * `"always"` builds it right away; in layout="column" it also gives the
46223
46370
  * closed expandable its content's height (the content is kept laid out at
46224
46371
  * its open width), so opening only reveals the width instead of changing the
46225
- * height too. `"while-opened"` throws the content away once the collapse
46372
+ * height too. `"idle"` builds it in a browser idle moment after load.
46373
+ * `"while-opened"` throws the content away once the collapse
46226
46374
  * settles — after the closing animation, so it still plays on real
46227
- * content — and rebuilds it from scratch on every expansion.
46375
+ * content — and rebuilds it from scratch on every expansion. Whatever the
46376
+ * value, intent on the UI part (pointer entering it, focus landing in it)
46377
+ * builds the content ahead of the click.
46228
46378
  */
46229
46379
  const Expandable = props => {
46230
46380
  import.meta.css = [css$Q, "@jsenv/navi/src/control/expandable/expandable.jsx"];
@@ -46286,8 +46436,12 @@ const Expandable = props => {
46286
46436
  // Fully open (or fully closed) and no longer moving — what releases the
46287
46437
  // clipping, see the CSS.
46288
46438
  const [settled, setSettled] = useState(true);
46289
- const contentMounted = usePopupContentMount(openController, contentContainerRef, {
46290
- mount
46439
+ const contentMounted = usePopupContentMount(openController, contentContainerRef,
46440
+ // The UI part is what the user aims at to expand — the closest thing an
46441
+ // expandable has to a popup's anchor, warming the content on intent.
46442
+ {
46443
+ mount,
46444
+ anchor: uiRef
46291
46445
  });
46292
46446
 
46293
46447
  // The pointer press that is about to toggle can blur the focused field
@@ -55270,6 +55424,15 @@ const SlideContainer = ({
55270
55424
  ,
55271
55425
 
55272
55426
  "data-travel-by-drag": dragAxes ?? undefined
55427
+ // …and said as a listener too, from the same render: a touch this box may
55428
+ // take has to be refusable before the finger lands, and being registered
55429
+ // is the whole of it — see keepTouchRefusable for why a JSX prop is
55430
+ // enough here (an element-level touchmove listener is non-passive).
55431
+ // Without it, a swipe the box has taken can be cancelled mid-gesture by
55432
+ // the browser scrolling the page along the axis touch-action leaves it.
55433
+ ,
55434
+
55435
+ onTouchMove: dragAxes ? keepTouchRefusable : undefined
55273
55436
  // The same fact for a wheel, and only for that second reason: this box
55274
55437
  // takes the push, whatever the box around it also travels on.
55275
55438
  ,
@@ -57945,15 +58108,19 @@ const css$E = /* css */`
57945
58108
  * open controller (see `open_controller.js`) for a caller that wants to
57946
58109
  * drive open/close itself instead of `open`/`defaultOpen`/`onClose` (used
57947
58110
  * by `picker_custom.jsx`).
57948
- * @param {"always"|"from-first-open"|"while-opened"} [props.mount] - When
58111
+ * @param {"always"|"idle"|"from-first-open"|"while-opened"} [props.mount] - When
57949
58112
  * `children` are built and thrown away (see popup_content_mount.js).
57950
58113
  * `"from-first-open"` (the default) builds them on the first open and keeps
57951
58114
  * them afterwards. `"always"` builds them right away, for content something
57952
58115
  * depends on while the popup is still closed: a value read off it, fields a
57953
58116
  * surrounding form collects on submit, a size measured from outside.
57954
- * `"while-opened"` throws them away once the popup has finished closing, for
57955
- * content whose fresh state is its initial state: an uncontrolled field
57956
- * seeded from a `defaultValue` that changed while the popup was closed.
58117
+ * `"idle"` builds them in a browser idle moment after load — "always" minus
58118
+ * the cost on the critical render. `"while-opened"` throws them away once
58119
+ * the popup has finished closing, for content whose fresh state is its
58120
+ * initial state: an uncontrolled field seeded from a `defaultValue` that
58121
+ * changed while the popup was closed. Whatever the value, intent on the
58122
+ * anchor (pointer entering it, focus landing in it) builds the content
58123
+ * ahead of the click.
57957
58124
  * @param {import("ignore:preact").ComponentChildren} props.children
57958
58125
  */
57959
58126
  const Dialog = props => {
@@ -58198,7 +58365,8 @@ const useDialogProps = props => {
58198
58365
  // built: what this popup opens ON is known before anything reads it.
58199
58366
  openController.onOpen = onOpen || null;
58200
58367
  const contentMounted = usePopupContentMount(openController, props.ref, {
58201
- mount
58368
+ mount,
58369
+ anchor
58202
58370
  });
58203
58371
  const children = contentMounted ? childrenProp : null;
58204
58372
  const isModal = layer === "top";
@@ -59412,15 +59580,19 @@ const css$D = /* css */`
59412
59580
  * open controller (see `open_controller.js`) for a caller that wants to
59413
59581
  * drive open/close itself instead of `open`/`defaultOpen`/`onClose` (used
59414
59582
  * by `picker_custom.jsx`/`side_panel.jsx`).
59415
- * @param {"always"|"from-first-open"|"while-opened"} [props.mount] - When
59583
+ * @param {"always"|"idle"|"from-first-open"|"while-opened"} [props.mount] - When
59416
59584
  * `children` are built and thrown away (see popup_content_mount.js).
59417
59585
  * `"from-first-open"` (the default) builds them on the first open and keeps
59418
59586
  * them afterwards. `"always"` builds them right away, for content something
59419
59587
  * depends on while the popup is still closed: a value read off it, fields a
59420
59588
  * surrounding form collects on submit, a size measured from outside.
59421
- * `"while-opened"` throws them away once the popup has finished closing, for
59422
- * content whose fresh state is its initial state: an uncontrolled field
59423
- * seeded from a `defaultValue` that changed while the popup was closed.
59589
+ * `"idle"` builds them in a browser idle moment after load — "always" minus
59590
+ * the cost on the critical render. `"while-opened"` throws them away once
59591
+ * the popup has finished closing, for content whose fresh state is its
59592
+ * initial state: an uncontrolled field seeded from a `defaultValue` that
59593
+ * changed while the popup was closed. Whatever the value, intent on the
59594
+ * anchor (pointer entering it, focus landing in it) builds the content
59595
+ * ahead of the click.
59424
59596
  * @param {import("ignore:preact").ComponentChildren} props.children
59425
59597
  */
59426
59598
  const Popover = props => {
@@ -59643,7 +59815,8 @@ const usePopoverProps = props => {
59643
59815
  // built: what this popup opens ON is known before anything reads it.
59644
59816
  openController.onOpen = onOpen || null;
59645
59817
  const contentMounted = usePopupContentMount(openController, props.ref, {
59646
- mount
59818
+ mount,
59819
+ anchor
59647
59820
  });
59648
59821
  const children = contentMounted ? childrenProp : null;
59649
59822
  const isTopLayer = layer === "top";
@@ -60583,7 +60756,7 @@ const css$C = /* css */`@layer navi {
60583
60756
  * @param {string} [props.className] - Merged with the shared
60584
60757
  * `"navi_popup"` class (see this file's own CSS) rather than replacing
60585
60758
  * it.
60586
- * @param {"always"|"from-first-open"|"while-opened"} [props.mount] - When
60759
+ * @param {"always"|"idle"|"from-first-open"|"while-opened"} [props.mount] - When
60587
60760
  * `children` are built and thrown away (see popup_content_mount.js).
60588
60761
  * `"from-first-open"` (the default) builds them on the first open and keeps
60589
60762
  * them afterwards. `"always"` builds them right away, for content something
@@ -60880,7 +61053,13 @@ const PickerCustom = props => {
60880
61053
  // What a `--navi-confirm` said inside the popup means, once the popup has
60881
61054
  // closed on it. A confirm picker is the one saying something (see
60882
61055
  // picker_confirm.jsx): its press, deferred until the question is answered.
60883
- onConfirm
61056
+ onConfirm,
61057
+ // The popup's lifecycle, the same pair Dialog and Popover take. Taken on
61058
+ // the picker and chained into its own openController below: the picker
61059
+ // hands the popup that controller, and a controlled Dialog/Popover reads
61060
+ // no onOpen/onClose of its own.
61061
+ onOpen,
61062
+ onClose
60884
61063
  } = props;
60885
61064
  // Resolve the id the same way useControlProps does (own id > Field's id > generated id)
60886
61065
  // before computing popupId below, so two Pickers without an explicit id never collide.
@@ -60914,6 +61093,8 @@ const PickerCustom = props => {
60914
61093
  delete pickerProps.defaultOpen;
60915
61094
  delete pickerProps.escapeEffect;
60916
61095
  delete pickerProps.onConfirm;
61096
+ delete pickerProps.onOpen;
61097
+ delete pickerProps.onClose;
60917
61098
  // Read below for the popup alone; on the trigger it would land on the DOM as
60918
61099
  // an unknown attribute holding a ref object.
60919
61100
  delete pickerProps.anchor;
@@ -60922,6 +61103,13 @@ const PickerCustom = props => {
60922
61103
  popupProps,
60923
61104
  actionEvent: "custom"
60924
61105
  });
61106
+ if (pickerProps.resetOnError === undefined) {
61107
+ // The picker's action fails after its popup closed on the value: nobody is
61108
+ // left mid-edit, so the value the server refused rolls back to the last
61109
+ // accepted one and the error callout says why — the same default
61110
+ // PickerNative takes.
61111
+ pickerProps.resetOnError = true;
61112
+ }
60925
61113
  // ref
60926
61114
  const popupRef = useRef(null);
60927
61115
  popupProps.ref = popupRef;
@@ -60978,6 +61166,7 @@ const PickerCustom = props => {
60978
61166
  // isUIStateHeld). Read at open, before anything inside can change it.
60979
61167
  const heldAtOpen = isUIStateHeld(getPickerInput(ref.current)?.__uiStateController__);
60980
61168
  debugPopup(openEvent, `picker opened, store value at open`, valueAtOpen, heldAtOpen ? `(held)` : `(a suggestion, not an answer yet)`);
61169
+ onOpen?.(openEvent);
60981
61170
  return {
60982
61171
  onRequestClose: requestCloseEvent => {
60983
61172
  if (requestCloseEvent.detail.isCancel) {
@@ -61060,6 +61249,9 @@ const PickerCustom = props => {
61060
61249
  if (confirmEvent && !closeEvent.detail.isCancel) {
61061
61250
  onConfirm?.(confirmEvent);
61062
61251
  }
61252
+ // Last, after the value bookkeeping above: whoever listens reads the
61253
+ // picker's value as it ends up — committed, or restored on a cancel.
61254
+ onClose?.(closeEvent);
61063
61255
  }
61064
61256
  };
61065
61257
  });
@@ -82122,7 +82314,14 @@ const SidePanel = ({
82122
82314
  ,
82123
82315
 
82124
82316
  "data-drag-travel": swipeToClose ? SWIPE_AXIS_BY_SIDE[side] : undefined,
82125
- "data-travel-by-drag": swipeToClose ? SWIPE_AXIS_BY_SIDE[side] : undefined,
82317
+ "data-travel-by-drag": swipeToClose ? SWIPE_AXIS_BY_SIDE[side] : undefined
82318
+ // A touch this panel may take has to be refusable before the finger
82319
+ // lands, or the browser can cancel the close gesture mid-swipe by
82320
+ // scrolling the panel's content — see keepTouchRefusable for why a JSX
82321
+ // prop is enough (an element-level touchmove listener is non-passive).
82322
+ ,
82323
+
82324
+ onTouchMove: swipeToClose ? keepTouchRefusable : undefined,
82126
82325
  ...rest,
82127
82326
  onPointerDown: pointerDownEvent => {
82128
82327
  rest.onPointerDown?.(pointerDownEvent);
@@ -82358,5 +82557,5 @@ const UserSvg = () => jsx("svg", {
82358
82557
  })
82359
82558
  });
82360
82559
 
82361
- export { ActionRenderer, ActiveKeyboardShortcuts, Address, Badge, BadgeCount, BadgeList, Binder, Box, Button, ButtonCopyToClipboard, CalloutStatusIcon, Caption, CardLayout, CheckSvg, CheckboxGroup, CloseSvg, Code, Col, Colgroup, Color, ConstructionSvg, ControlGroup, ControlSwap, DaySpin, Details, Dialog, Editable, ErrorBoundary, ErrorBoundaryContext, ExclamationSvg, Expandable, EyeClosedSvg, EyeSvg, Field, FixedBar, Form, Group, Head, HeartSvg, HomeSvg, Icon, Image, InfoSvg, Input, InputDuration, Interpolate, Label, Link, LinkAnchorSvg, LinkBlankTargetSvg, LinkCurrentSvg, List, ListItem, ListItemGroup, ListItems, Loading, LoadingDotsSvg, LoadingIndicator, LoadingIndicatorFluid, LoadingOutline, MessageBox, Meter, Nav, NaviDebug, NumberSpin, OfflineError, Paragraph, Picker, Popover, Popup, Quantity, RadioGroup, Route, RouteTransitionArea, RouteTravel, RowNumberCol, RowNumberTableCell, SVGMaskOverlay, SearchSvg, Select, SelectableInput, SelectionContext, Separator, SettingsSvg, SidePanel, Slide, SlideContainer, Spin, SpinGroup, SplitButton, StarSvg, Step, StepList, SummaryMarker, Svg, Table, TableCell, Tbody, Text, TextBox, Textarea, TextareaCharCount, Thead, Time, TimeRange, TimeRangeSpin, TimeRangeWheel, TimeSpin, TimeWheel, Title, Tr, UITransition, Unit, UserSvg, ViewportLayout, Wheel, WheelGroup, WheelItem, actionRunEffect, anyMatchingRouteSignal, applySearch, arraySignalMembership, canNavBackSignal, canNavForwardSignal, coarsePointerSignal, compareTwoJsValues, constraintFromValidityRule, createAction, createAvailableConstraint, createI18n, createRequestCanceller, createSearch, createSelectionKeyboardShortcuts, createSlot, defineInteractionDetector, defineRouteDefaultTransition, defineRouteTransition, detectHorizontalOverflow, enableDebugActions, enableDebugOnDocumentLoading, ensureDocumentStartViewTransition, errorIsDisplayed, filterTableSelection, formatDatetime, formatDay, formatDayRelative, formatMonth, formatNumber, formatTime, formatTimeRelative, getNowHours, getNowHoursRoundedToStep, interpolateText, isCellSelected, isColumnSelected, isOfflineError, isRowSelected, isScrolling, isToday, languagesSignal, localStorageSignal, markErrorAsDisplayedBy, moveArrayItemByIndex, moveFocusTo, navBack, navForward, navIntegratedVia, navTo, naviI18n, openCallout, rawUrlPart, registerGlobalConstraint, reload, rerunActions, resource, route, routeAction, scrollActivitySignal, setBaseUrl, setNetworkPolicy, setPreferredLanguage, setSupportedLanguages, setUrlTargetOptions, setupRoutes, smallTouchScreenSignal, stateSignal, stopLoad, stringifyTableSelectionValue, swapArrayItemByIndex, syncOwnedResourceToSignals, syncResourceToSignals, triggerNaviCommand, updateActions, useActionStatus, useArraySignalMembership, useAsyncData, useCalloutElement, useCalloutRequestClose, useCanNavBack, useCanNavForward, useCancelPrevious, useCellGridFromRows, useConstraintValidityState, useDependenciesDiff, useDisplayedLayoutEffect, useDocumentResource, useDocumentState, useDocumentUrl, useEditionController, useFocusGroup, useInputGroup, useKeyboardShortcuts, useNavState, useNetworkPolicyReason, useOrderedColumns, usePopupMode, useRouteStatus, useSearchText, useSelectableElement, useSelectionController, useSignalSync, useSlideContainer, useSlideValue, useStateArray, useTitleLevel, useTransitionCover, useUrlSearchParam, useUrlTargetId, valueInLocalStorage, windowWidthSignal };
82560
+ export { ActionRenderer, ActiveKeyboardShortcuts, Address, Badge, BadgeCount, BadgeList, Binder, Box, Button, ButtonCopyToClipboard, CalloutStatusIcon, Caption, CardLayout, CheckSvg, CheckboxGroup, CloseSvg, Code, Col, Colgroup, Color, ConstructionSvg, ControlGroup, ControlSwap, DaySpin, Details, Dialog, Editable, ErrorBoundary, ErrorBoundaryContext, ExclamationSvg, Expandable, EyeClosedSvg, EyeSvg, Field, FixedBar, Form, Group, Head, HeartSvg, HomeSvg, Icon, Image, InfoSvg, Input, InputDuration, Interpolate, Label, Link, LinkAnchorSvg, LinkBlankTargetSvg, LinkCurrentSvg, List, ListItem, ListItemGroup, ListItems, Loading, LoadingDotsSvg, LoadingIndicator, LoadingIndicatorFluid, LoadingOutline, MessageBox, Meter, Nav, NaviDebug, NumberSpin, OfflineError, Paragraph, Picker, Popover, Popup, Quantity, RadioGroup, Route, RouteTransitionArea, RouteTravel, RowNumberCol, RowNumberTableCell, SVGMaskOverlay, SearchSvg, Select, SelectableInput, SelectionContext, Separator, SettingsSvg, SidePanel, Slide, SlideContainer, Spin, SpinGroup, SplitButton, StarSvg, Step, StepList, SummaryMarker, Svg, Table, TableCell, Tbody, Text, TextBox, Textarea, TextareaCharCount, Thead, Time, TimeRange, TimeRangeSpin, TimeRangeWheel, TimeSpin, TimeWheel, Title, Tr, UITransition, Unit, UserSvg, ViewportLayout, Wheel, WheelGroup, WheelItem, actionRunEffect, anyMatchingRouteSignal, applySearch, arraySignalMembership, canNavBackSignal, canNavForwardSignal, coarsePointerSignal, compareTwoJsValues, constraintFromValidityRule, createAction, createAvailableConstraint, createI18n, createRequestCanceller, createSearch, createSelectionKeyboardShortcuts, createSlot, defineInteractionDetector, defineRouteDefaultTransition, defineRouteTransition, detectHorizontalOverflow, enableDebugActions, enableDebugOnDocumentLoading, ensureDocumentStartViewTransition, errorIsDisplayed, filterTableSelection, formatDatetime, formatDay, formatDayRelative, formatDuration, formatHourDuration, formatMinuteDuration, formatMonth, formatNumber, formatSecondDuration, formatTime, formatTimeRelative, getNowHours, getNowHoursRoundedToStep, interpolateText, isCellSelected, isColumnSelected, isOfflineError, isRowSelected, isScrolling, isToday, languagesSignal, localStorageSignal, markErrorAsDisplayedBy, moveArrayItemByIndex, moveFocusTo, navBack, navForward, navIntegratedVia, navTo, naviI18n, openCallout, rawUrlPart, registerGlobalConstraint, reload, rerunActions, resource, route, routeAction, scrollActivitySignal, setBaseUrl, setNetworkPolicy, setPreferredLanguage, setSupportedLanguages, setUrlTargetOptions, setupRoutes, smallTouchScreenSignal, stateSignal, stopLoad, stringifyTableSelectionValue, swapArrayItemByIndex, syncOwnedResourceToSignals, syncResourceToSignals, triggerNaviCommand, updateActions, useActionStatus, useArraySignalMembership, useAsyncData, useCalloutElement, useCalloutRequestClose, useCanNavBack, useCanNavForward, useCancelPrevious, useCellGridFromRows, useConstraintValidityState, useDependenciesDiff, useDisplayedLayoutEffect, useDocumentResource, useDocumentState, useDocumentUrl, useEditionController, useFocusGroup, useInputGroup, useKeyboardShortcuts, useNavState, useNetworkPolicyReason, useOrderedColumns, usePopupMode, useRouteStatus, useSearchText, useSelectableElement, useSelectionController, useSignalSync, useSlideContainer, useSlideValue, useStateArray, useTitleLevel, useTransitionCover, useUrlSearchParam, useUrlTargetId, valueInLocalStorage, windowWidthSignal };
82362
82561
  //# sourceMappingURL=jsenv_navi.js.map