@jsenv/navi 0.29.424 → 0.29.426

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.
@@ -7,7 +7,7 @@ import { createPubSub, elementIsFocusable, createIterableWeakSet, dispatchIntern
7
7
  export { chainEvent, clickIsSuppressed, contrastColor, createDragGestureController, dragAfterIntent, findEvent, markDragSource, startDragTo } from "@jsenv/dom";
8
8
  import { signal, computed, effect, untracked, batch, useComputed, useSignal } from "@preact/signals";
9
9
  import { options, isValidElement, createContext, render, h, toChildArray, createRef, cloneElement, createElement, Fragment as Fragment$1 } from "preact";
10
- import { useErrorBoundary, useLayoutEffect, useContext, useMemo, useRef, useEffect, useCallback, useState, useReducer, useId } from "preact/hooks";
10
+ import { useErrorBoundary, useLayoutEffect, useContext, useState, useMemo, useRef, useEffect, useCallback, useReducer, useId } from "preact/hooks";
11
11
  import { humanizeI18n, prefixFirstAndIndentRemainingLines, setRuntimeLangSource, formatDuration, formatMonth, formatDay, resolveTimeRangePrecision, formatDatePlaceholder, toDate, getRelativeDay, formatDayRelative, formatMonthPlaceholder, formatWeekPlaceholder, formatDatetimePlaceholder, formatDatetime, toTimeOfDay, formatTimeOfDay, formatTime, formatMinuteDuration, formatSecondDuration, formatHourDuration, formatTimeRelative, formatNumber, interpolateText, installInterpolateJsx } from "@jsenv/humanize";
12
12
  export { createI18n, formatDatetime, formatDay, formatDayRelative, formatDuration, formatHourDuration, formatMinuteDuration, formatMonth, formatNumber, formatSecondDuration, formatTime, formatTimeOfDay, formatTimeRange, formatTimeRelative, interpolateText } from "@jsenv/humanize";
13
13
  import { jsxs, jsx, Fragment } from "preact/jsx-runtime";
@@ -11240,6 +11240,179 @@ const dropGeneratedIdKeys = (state) => {
11240
11240
  return stateWithoutGeneratedIdKeys;
11241
11241
  };
11242
11242
 
11243
+ const FRAME_TIMEOUT_MS = 100;
11244
+
11245
+ /**
11246
+ * Calls back once the browser has painted what is committed now.
11247
+ *
11248
+ * The callbacks of the next frame run before that frame paints, so the call is
11249
+ * made from a task queued from inside one — the same way preact schedules
11250
+ * useEffect. Unlike an effect, nothing runs this early: preact flushes a
11251
+ * component's pending effects as soon as that component renders again, and a
11252
+ * re-render before the paint is exactly what a caller waiting for the paint
11253
+ * has to survive. A frame not coming at all (a background tab) still answers,
11254
+ * late, through the timeout.
11255
+ *
11256
+ * @param {() => void} callback
11257
+ * @returns {() => void} cancel
11258
+ */
11259
+ const afterPaint = (callback) => {
11260
+ let called = false;
11261
+ let timeoutId;
11262
+ let frameId;
11263
+ const onFrame = () => {
11264
+ if (called) {
11265
+ return;
11266
+ }
11267
+ called = true;
11268
+ clearTimeout(timeoutId);
11269
+ cancelAnimationFrame(frameId);
11270
+ timeoutId = setTimeout(callback, 0);
11271
+ };
11272
+ timeoutId = setTimeout(onFrame, FRAME_TIMEOUT_MS);
11273
+ frameId = requestAnimationFrame(onFrame);
11274
+ return () => {
11275
+ called = true;
11276
+ clearTimeout(timeoutId);
11277
+ cancelAnimationFrame(frameId);
11278
+ };
11279
+ };
11280
+
11281
+ /**
11282
+ * Runs `fn` and commits whatever it re-renders before returning, instead of
11283
+ * letting Preact batch it into the next microtask. Layout effects of what gets
11284
+ * mounted run inside the call too, exactly as they would on any other commit.
11285
+ *
11286
+ * For the caller that has to read the DOM it just asked for — measuring an
11287
+ * element whose content it mounts in the same breath — and cannot wait a tick
11288
+ * to do it, because what comes after is a browser event still in flight
11289
+ * (preventDefault, focus placement) that no longer accepts being answered late.
11290
+ *
11291
+ * `options.debounceRendering` is Preact's own hook for deciding *when* the
11292
+ * render queue drains; swapping it for "right now" for the duration of the call
11293
+ * is exactly how preact/compat implements React's flushSync. Reserve it for the
11294
+ * case above: rendering synchronously in the middle of an event gives up the
11295
+ * batching that makes several state changes one commit.
11296
+ */
11297
+ const flushSyncRendering = (fn) => {
11298
+ const debounceRenderingPrevious = options.debounceRendering;
11299
+ options.debounceRendering = (drainRenderQueue) => {
11300
+ drainRenderQueue();
11301
+ };
11302
+ try {
11303
+ fn();
11304
+ } finally {
11305
+ options.debounceRendering = debounceRenderingPrevious;
11306
+ }
11307
+ };
11308
+
11309
+ /**
11310
+ * The content of a `<Box mount="after-paint">`: built right after the first
11311
+ * frame that shows the box, unless that frame would show it.
11312
+ *
11313
+ * The browser paints nothing until the render that builds a page has ended, and
11314
+ * what lies below the first screen weighs in that render as much as what is on
11315
+ * it. A box marked this way is left empty in that render, then built once the
11316
+ * frame is painted — the page-level twin of a list's render window (see
11317
+ * `renderBudget` in list.jsx), for a page made of sections.
11318
+ *
11319
+ * Which boxes lie below the screen is measured, not taken from the caller. The
11320
+ * caller cannot know: a taller phone, a profile with nothing in its first
11321
+ * section, and the box it marked is on screen. So each box looks where it
11322
+ * stands in the animation callbacks of its first frame — laid out, not yet
11323
+ * painted — and one the frame would show, or one above it, is built there:
11324
+ * content appearing on screen is a pop-in, and content growing ABOVE what is
11325
+ * read pushes it down. Marking too much costs a second render in that frame,
11326
+ * never a wrong picture.
11327
+ *
11328
+ * The document put back where it was read is the other frame that shows it.
11329
+ * That offset is written by navi (see scroll_restoration.js), and written into
11330
+ * a document still missing its bottom it is clamped: the page opens higher than
11331
+ * it was left, and the bottom arrives under a page that has already jumped. So
11332
+ * whoever writes an arrival's offset builds first the boxes that offset shows
11333
+ * (buildDeferredBoxesAbove), in the same task — synchronously, since what comes
11334
+ * right after it (the offset, a route travel measuring its window) reads the
11335
+ * layout.
11336
+ */
11337
+
11338
+ const deferredBoxes = new Set();
11339
+ const MountAfterPaint = ({
11340
+ boxRef,
11341
+ children
11342
+ }) => {
11343
+ const [built, setBuilt] = useState(false);
11344
+ useLayoutEffect(() => {
11345
+ let frame;
11346
+ let cancelAfterPaint;
11347
+ const stopWaiting = () => {
11348
+ deferredBoxes.delete(deferredBox);
11349
+ cancelAnimationFrame(frame);
11350
+ cancelAfterPaint();
11351
+ };
11352
+ // The first of the three that come — the offset of an arrival, the frame
11353
+ // finding the box on screen, the paint — builds it and cancels the others.
11354
+ const deferredBox = {
11355
+ getElement: () => boxRef.current,
11356
+ build: () => {
11357
+ stopWaiting();
11358
+ setBuilt(true);
11359
+ }
11360
+ };
11361
+ deferredBoxes.add(deferredBox);
11362
+ // Requested before afterPaint's own frame callback, so the box is looked at
11363
+ // before that one turns the paint into a build.
11364
+ frame = requestAnimationFrame(() => {
11365
+ const top = readTopInWindow(boxRef.current);
11366
+ if (top !== null && top < window.innerHeight) {
11367
+ deferredBox.build();
11368
+ }
11369
+ });
11370
+ cancelAfterPaint = afterPaint(deferredBox.build);
11371
+ return stopWaiting;
11372
+ }, []);
11373
+ return built ? children : null;
11374
+ };
11375
+
11376
+ /**
11377
+ * Builds, now, every box still waiting whose top is above `bottom` (in the
11378
+ * document's coordinates): what the document will show once scrolled so that
11379
+ * `bottom` is the bottom of the screen. Boxes built push those below them
11380
+ * further down, so the ones measured above `bottom` are all the ones that can
11381
+ * be shown there, and a few more at worst.
11382
+ *
11383
+ * Renders synchronously, so it must be called outside a Preact commit: a render
11384
+ * flushed from a layout effect runs inside the commit still walking the tree.
11385
+ */
11386
+ const buildDeferredBoxesAbove = bottom => {
11387
+ if (deferredBoxes.size === 0) {
11388
+ return;
11389
+ }
11390
+ const toBuild = [];
11391
+ for (const deferredBox of deferredBoxes) {
11392
+ const top = readTopInWindow(deferredBox.getElement());
11393
+ if (top !== null && top + window.scrollY < bottom) {
11394
+ toBuild.push(deferredBox);
11395
+ }
11396
+ }
11397
+ if (toBuild.length === 0) {
11398
+ return;
11399
+ }
11400
+ flushSyncRendering(() => {
11401
+ for (const deferredBox of toBuild) {
11402
+ deferredBox.build();
11403
+ }
11404
+ });
11405
+ };
11406
+
11407
+ // null for a box nothing draws (a closed popup, a hidden tab): no frame shows
11408
+ // it, wherever the document is.
11409
+ const readTopInWindow = element => {
11410
+ if (!element || element.getClientRects().length === 0) {
11411
+ return null;
11412
+ }
11413
+ return element.getBoundingClientRect().top;
11414
+ };
11415
+
11243
11416
  /**
11244
11417
  * A container has put its page on screen — or as much of it as it can.
11245
11418
  *
@@ -11295,7 +11468,9 @@ const [publishRouteRender, observeRouteRender] = createPubSub();
11295
11468
  * What is NOT covered, and cannot be from here: a page whose height depends on
11296
11469
  * something still loading. Its content is not there at the moment it is put
11297
11470
  * back, so a position beyond what has arrived is clamped as before. Only the
11298
- * page knows when it is whole.
11471
+ * page knows when it is whole. What navi itself leaves out of a first render
11472
+ * is the exception — a `<Box mount="after-paint">` — and is built before the
11473
+ * offset that shows it is written (see mount_after_paint.jsx).
11299
11474
  *
11300
11475
  * WHEN a page is arrived at is not decided here either. A document navigation
11301
11476
  * lands where its kind says (see via_history.js), and one scrollport can be
@@ -11444,14 +11619,18 @@ const installScrollRestoration = () => {
11444
11619
  // What a reload asks for, now that the browser has been told not to do it.
11445
11620
  // Once, and at the first render of a route: the position is only meaningful
11446
11621
  // once there is a page under it.
11622
+ // Written once the commit that rendered it has ended: said from inside it,
11623
+ // and what the offset shows may still have to be built (see scrollTo).
11447
11624
  const positionOnLoad = positionByUrl.get(window.location.href);
11448
11625
  if (positionOnLoad && (positionOnLoad.x || positionOnLoad.y)) {
11449
11626
  const stopListening = observeRouteRender(() => {
11450
11627
  stopListening();
11451
- if (documentHoldCount) {
11452
- return;
11453
- }
11454
- scrollTo(positionOnLoad);
11628
+ queueMicrotask(() => {
11629
+ if (documentHoldCount) {
11630
+ return;
11631
+ }
11632
+ scrollTo(positionOnLoad);
11633
+ });
11455
11634
  });
11456
11635
  }
11457
11636
  };
@@ -11558,14 +11737,25 @@ const forgetScrollersOnArrival = (url, { from } = {}) => {
11558
11737
  // last read. Where this one was read, and its top when it never was — leaving
11559
11738
  // the offset alone would seat the reader wherever the neighbour happened to
11560
11739
  // be, so here "nothing recorded" and "stay" are not the same thing.
11740
+ //
11741
+ // Called outside a Preact commit only, like scrollTo: a row of tabs measures
11742
+ // its window right after, so what the top shows is built first here rather
11743
+ // than in the frame (see mount_after_paint.jsx).
11561
11744
  const arriveAtScrollPosition = (url) => {
11562
11745
  if (restoreScrollPosition(url)) {
11563
11746
  return;
11564
11747
  }
11748
+ buildDeferredBoxesAbove(window.innerHeight);
11565
11749
  startAtTop(url);
11566
11750
  };
11567
11751
 
11752
+ // What the offset shows is built first: written into a document still missing
11753
+ // a deferred box, the offset is clamped (see mount_after_paint.jsx). That build
11754
+ // is a render of its own, so this is never called from inside a commit — a
11755
+ // caller that learns the page is there from a layout effect (observeRouteRender)
11756
+ // writes once that commit has ended.
11568
11757
  const scrollTo = ({ x, y }) => {
11758
+ buildDeferredBoxesAbove(y + window.innerHeight);
11569
11759
  window.scrollTo({ top: y, left: x, behavior: "instant" });
11570
11760
  };
11571
11761
 
@@ -15389,6 +15579,9 @@ const createOpenController = (
15389
15579
  // content is still waiting for a first open to be built. Called below,
15390
15580
  // before openEffect, so the popup measures and positions the real thing.
15391
15581
  mountContent: null,
15582
+ // Set alongside mountContent: the content asked for outside any opening,
15583
+ // by something that reads it while the popup is closed.
15584
+ buildContent: null,
15392
15585
  // The caller's own `onOpen`, set by Dialog/Popover from their props on
15393
15586
  // every render (like openEffect). Called BEFORE mountContent, so whatever
15394
15587
  // it decides — which record this dialog is opening on — is already true by
@@ -30925,6 +31118,7 @@ const PSEUDO_STATE_CHILD_PROP_SET = new Set(["tabIndex", "tabindex"]);
30925
31118
  * footer?: boolean,
30926
31119
  * body?: boolean,
30927
31120
  * sticky?: boolean,
31121
+ * mount?: "always" | "after-paint",
30928
31122
  * children?: import("ignore:preact").ComponentChildren,
30929
31123
  * [key: string]: any,
30930
31124
  * }>}
@@ -30973,6 +31167,16 @@ const PSEUDO_STATE_CHILD_PROP_SET = new Set(["tabIndex", "tabindex"]);
30973
31167
  * (see docs/interactions.md). A plain box has no wiring of its own: it does
30974
31168
  * nothing with `action` (that is a control's prop), so a click on it is
30975
31169
  * declared here like any other interaction: `interactions={{ click: fn }}`.
31170
+ * @param {"always"|"after-paint"} [mount="always"] When `children` are built.
31171
+ * `"after-paint"` leaves the box empty in the render that first mounts it and
31172
+ * builds them right after the first frame that shows the page, then keeps
31173
+ * them: for the sections of a page that lie below its first screen. navi
31174
+ * measures where the box stands before that frame, and one the frame would
31175
+ * show (or where the document is put back, on a return) is built before the
31176
+ * paint all the same. Until built, what is inside is not in the document:
31177
+ * no id there is found, no named control there is there to hear its own
31178
+ * `signal` (see docs/control_object.md). Only for content nothing reaches
31179
+ * before the first frame (see docs/scroll.md).
30976
31180
  */
30977
31181
  const Box = props => {
30978
31182
  const {
@@ -30980,6 +31184,7 @@ const Box = props => {
30980
31184
  children,
30981
31185
  separator,
30982
31186
  interactions,
31187
+ mount,
30983
31188
  ...computeProps
30984
31189
  } = props;
30985
31190
  const parentBoxFlow = useContext(BoxFlowContext);
@@ -31057,6 +31262,12 @@ const Box = props => {
31057
31262
  children: innerChildren
31058
31263
  });
31059
31264
  }
31265
+ if (mount === "after-paint") {
31266
+ innerChildren = jsx(MountAfterPaint, {
31267
+ boxRef: finalRef,
31268
+ children: innerChildren
31269
+ });
31270
+ }
31060
31271
  return jsx(TagName, {
31061
31272
  ref: finalRef,
31062
31273
  className: innerClassName,
@@ -34627,6 +34838,13 @@ const OLD_BAND_TOP_PROPERTY = "--navi-transition-old-band-top";
34627
34838
  const OLD_BAND_RIGHT_PROPERTY = "--navi-transition-old-band-right";
34628
34839
  const OLD_BAND_BOTTOM_PROPERTY = "--navi-transition-old-band-bottom";
34629
34840
  const OLD_BAND_LEFT_PROPERTY = "--navi-transition-old-band-left";
34841
+ const VIEWPORT_TOP_PROPERTY = "--navi-transition-viewport-top";
34842
+ const VIEWPORT_LEFT_PROPERTY = "--navi-transition-viewport-left";
34843
+ // Worn by the root once the group is held at the window by our numbers alone
34844
+ // (see placeTransitionWindow): the group rule of each movement is written on
34845
+ // it. Read by rules on pseudo-elements only, so wearing it restyles nothing
34846
+ // the document inherits.
34847
+ const WINDOW_PLACED_ATTRIBUTE = "data-navi-transition-window-placed";
34630
34848
  const WINDOW_PROPERTIES = [
34631
34849
  WINDOW_TOP_PROPERTY,
34632
34850
  WINDOW_LEFT_PROPERTY,
@@ -34640,6 +34858,8 @@ const WINDOW_PROPERTIES = [
34640
34858
  OLD_BAND_RIGHT_PROPERTY,
34641
34859
  OLD_BAND_BOTTOM_PROPERTY,
34642
34860
  OLD_BAND_LEFT_PROPERTY,
34861
+ VIEWPORT_TOP_PROPERTY,
34862
+ VIEWPORT_LEFT_PROPERTY,
34643
34863
  ];
34644
34864
 
34645
34865
  // Whose numbers are currently published. The window belongs to the movement
@@ -34647,6 +34867,8 @@ const WINDOW_PROPERTIES = [
34647
34867
  // after another has replaced it must not wipe numbers the new one is standing
34648
34868
  // on.
34649
34869
  let windowOwner = null;
34870
+ // The element the box is captured as, for as long as the window is held.
34871
+ let windowElement = null;
34650
34872
  // Stops re-reading the arriving state's corner, when a movement is on.
34651
34873
  let unfollowArrivingState = null;
34652
34874
 
@@ -34702,6 +34924,10 @@ const holdTransitionWindow = (
34702
34924
  const right =
34703
34925
  rectBefore.right > rectAfter.right ? rectBefore.right : rectAfter.right;
34704
34926
  windowOwner = owner;
34927
+ windowElement = element;
34928
+ // Pictures of a movement this one took over from are placed by the browser
34929
+ // until this one's own pictures exist.
34930
+ document.documentElement.removeAttribute(WINDOW_PLACED_ATTRIBUTE);
34705
34931
  setTransitionValue(WINDOW_TOP_PROPERTY, `${top}px`);
34706
34932
  setTransitionValue(WINDOW_LEFT_PROPERTY, `${left}px`);
34707
34933
  setTransitionValue(WINDOW_WIDTH_PROPERTY, `${right - left}px`);
@@ -34726,6 +34952,33 @@ const holdTransitionWindow = (
34726
34952
  });
34727
34953
  };
34728
34954
 
34955
+ /**
34956
+ * Called once the pictures exist — the view transition's `ready` — with the
34957
+ * name the box was captured under. Where the viewport's corner stands in the
34958
+ * space the pictures are drawn in is the browser's placement of the group
34959
+ * minus our reading of the same element, both taken in this one instant; the
34960
+ * group is held at the window with it from then on (see the top of the file).
34961
+ * A browser that does not hand the group's placement back leaves the group
34962
+ * where it places it, moved to the window by the arriving corner.
34963
+ */
34964
+ const placeTransitionWindow = (owner, groupName) => {
34965
+ if (owner !== windowOwner) {
34966
+ return;
34967
+ }
34968
+ const { transform } = getComputedStyle(
34969
+ document.documentElement,
34970
+ `::view-transition-group(${groupName})`,
34971
+ );
34972
+ if (!transform || transform === "none") {
34973
+ return;
34974
+ }
34975
+ const placement = new DOMMatrixReadOnly(transform);
34976
+ const rect = windowElement.getBoundingClientRect();
34977
+ setTransitionValue(VIEWPORT_TOP_PROPERTY, `${placement.m42 - rect.top}px`);
34978
+ setTransitionValue(VIEWPORT_LEFT_PROPERTY, `${placement.m41 - rect.left}px`);
34979
+ document.documentElement.setAttribute(WINDOW_PLACED_ATTRIBUTE, "");
34980
+ };
34981
+
34729
34982
  // The live layout takes the box back. A discontinuity by construction — the
34730
34983
  // window stands at the held height, the box is at its own — and an invisible
34731
34984
  // one: the page arriving is fully in place, and the strip below it that the
@@ -34735,20 +34988,22 @@ const releaseTransitionWindow = (owner) => {
34735
34988
  return;
34736
34989
  }
34737
34990
  windowOwner = null;
34991
+ windowElement = null;
34738
34992
  if (unfollowArrivingState) {
34739
34993
  unfollowArrivingState();
34740
34994
  }
34995
+ document.documentElement.removeAttribute(WINDOW_PLACED_ATTRIBUTE);
34741
34996
  for (const property of WINDOW_PROPERTIES) {
34742
34997
  removeTransitionValue(property);
34743
34998
  }
34744
34999
  };
34745
35000
 
34746
- // The arriving state's corner, re-read every frame — as often as the browser
34747
- // refreshes the group's placement from the same element (see the top of the
34748
- // file) — and with it the offset outside the box, which carries the window
34749
- // and the corner of the state being left along. Requested, never awaited: the
34750
- // first call runs inside the update callback, where a frame cannot come.
34751
- // Written only when it moved: every write restyles the pictures.
35001
+ // The arriving state's corner, re-read every frame — as often as anything can
35002
+ // move the page arriving (see the top of the file) — and with it the offset
35003
+ // outside the box, which carries the window and the corner of the state being
35004
+ // left along. Requested, never awaited: the first call runs inside the update
35005
+ // callback, where a frame cannot come. Written only when it moved: every write
35006
+ // restyles the pictures.
34752
35007
  const followArrivingState = (element, rectAtHold, heldAt) => {
34753
35008
  if (unfollowArrivingState) {
34754
35009
  unfollowArrivingState();
@@ -35253,6 +35508,13 @@ const css$12 = /* css */`@property --navi-route-transition-leave {
35253
35508
  animation-name: none;
35254
35509
  }
35255
35510
 
35511
+ &[data-navi-transition-window-placed]::view-transition-group(navi-route-transition) {
35512
+ top: calc(var(--navi-transition-window-top) + var(--navi-transition-viewport-top));
35513
+ left: calc(var(--navi-transition-window-left) +
35514
+ var(--navi-transition-viewport-left));
35515
+ transform: none;
35516
+ }
35517
+
35256
35518
  &::view-transition-old(navi-route-transition) {
35257
35519
  top: calc(var(--navi-transition-window-old-top) - var(--navi-transition-window-top));
35258
35520
  left: calc(var(--navi-transition-window-old-left) - var(--navi-transition-window-left));
@@ -36300,6 +36562,8 @@ const beginTransition = ({
36300
36562
  // the only place the silent misconfigurations show. They are all about the
36301
36563
  // same thing — a movement playing on pictures that are not the pages.
36302
36564
  const viewTransitionReady = () => {
36565
+ // Read before anything below writes to the document.
36566
+ placeTransitionWindow(transition, AREA_NAME);
36303
36567
  startTransitionFurniture(transition, {
36304
36568
  duration: transition.duration
36305
36569
  });
@@ -36811,6 +37075,13 @@ const css$11 = /* css */`.navi_route_travel {
36811
37075
  animation-duration: var(--navi-route-travel-duration, .3s);
36812
37076
  animation-name: none;
36813
37077
  }
37078
+
37079
+ &[data-navi-transition-window-placed]::view-transition-group(navi-route-travel) {
37080
+ top: calc(var(--navi-transition-window-top) + var(--navi-transition-viewport-top));
37081
+ left: calc(var(--navi-transition-window-left) +
37082
+ var(--navi-transition-viewport-left));
37083
+ transform: none;
37084
+ }
36814
37085
  }
36815
37086
 
36816
37087
  :root[data-navi-route-travel-held] {
@@ -37189,6 +37460,10 @@ const RouteTravel = ({
37189
37460
  });
37190
37461
  });
37191
37462
  travel.viewTransition = viewTransition;
37463
+ // Subscribed first, so it reads before the scrub below writes anything.
37464
+ viewTransition.ready.then(() => {
37465
+ placeTransitionWindow(travel, TRAVEL_NAME);
37466
+ }, ignoreSkipped);
37192
37467
  if (scrub) {
37193
37468
  // Said only now: the release has to have something to let go of, and the
37194
37469
  // transition did not exist a line above.
@@ -37388,16 +37663,20 @@ const RouteTravel = ({
37388
37663
  // what makes the document tall enough to hold that offset again —
37389
37664
  // and the recording stays deaf until then, over the clamp the swap
37390
37665
  // back makes on the way. Its own deafness rather than the travel's:
37391
- // the travel ends here, and this outlives it by a render.
37666
+ // the travel ends here, and this outlives it by a render. Written
37667
+ // once the commit that says so has ended, as arriveAtScrollPosition
37668
+ // asks.
37392
37669
  const resumeScrollRecording = suspendScrollRecording();
37393
37670
  const stopWatchingRender = observeRouteRender(() => {
37394
37671
  stopWatchingRender();
37395
- const {
37396
- route,
37397
- params
37398
- } = travel.fromPage;
37399
- arriveAtScrollPosition(route.buildUrl(params));
37400
- resumeScrollRecording();
37672
+ queueMicrotask(() => {
37673
+ const {
37674
+ route,
37675
+ params
37676
+ } = travel.fromPage;
37677
+ arriveAtScrollPosition(route.buildUrl(params));
37678
+ resumeScrollRecording();
37679
+ });
37401
37680
  });
37402
37681
  }
37403
37682
  releaseRendering();
@@ -40181,34 +40460,6 @@ registerNaviCommand("--navi-unselect", (source, event) => {
40181
40460
  };
40182
40461
  });
40183
40462
 
40184
- /**
40185
- * Runs `fn` and commits whatever it re-renders before returning, instead of
40186
- * letting Preact batch it into the next microtask. Layout effects of what gets
40187
- * mounted run inside the call too, exactly as they would on any other commit.
40188
- *
40189
- * For the caller that has to read the DOM it just asked for — measuring an
40190
- * element whose content it mounts in the same breath — and cannot wait a tick
40191
- * to do it, because what comes after is a browser event still in flight
40192
- * (preventDefault, focus placement) that no longer accepts being answered late.
40193
- *
40194
- * `options.debounceRendering` is Preact's own hook for deciding *when* the
40195
- * render queue drains; swapping it for "right now" for the duration of the call
40196
- * is exactly how preact/compat implements React's flushSync. Reserve it for the
40197
- * case above: rendering synchronously in the middle of an event gives up the
40198
- * batching that makes several state changes one commit.
40199
- */
40200
- const flushSyncRendering = (fn) => {
40201
- const debounceRenderingPrevious = options.debounceRendering;
40202
- options.debounceRendering = (drainRenderQueue) => {
40203
- drainRenderQueue();
40204
- };
40205
- try {
40206
- fn();
40207
- } finally {
40208
- options.debounceRendering = debounceRenderingPrevious;
40209
- }
40210
- };
40211
-
40212
40463
  /**
40213
40464
  * When a popup builds what it holds, and when it throws it away.
40214
40465
  *
@@ -40243,7 +40494,11 @@ const flushSyncRendering = (fn) => {
40243
40494
  *
40244
40495
  * "always" is for content something else depends on before any opening: a
40245
40496
  * value the popup's owner reads off its own children, fields a form around it
40246
- * collects on submit, a size measured from outside.
40497
+ * collects on submit, a size measured from outside. When the popup's owner
40498
+ * cannot tell at mount whether anything will depend on it, the content waits
40499
+ * and whatever depends on it asks for it, closed, with
40500
+ * `openController.buildContent()` (a picker's drawing reading the value its
40501
+ * popup holds, see usePickerContext).
40247
40502
  *
40248
40503
  * "idle" is "always" minus the cost on the critical render: the page appears
40249
40504
  * without the content, and the browser builds it in an idle moment after
@@ -40370,6 +40625,14 @@ const usePopupContentMount = (
40370
40625
  }
40371
40626
  }
40372
40627
  };
40628
+ // A plain render rather than a flush: the popup stays closed, and nothing is
40629
+ // about to measure it.
40630
+ openController.buildContent = () => {
40631
+ if (contentMountedRef.current) {
40632
+ return;
40633
+ }
40634
+ setContentMounted(true);
40635
+ };
40373
40636
  openController.unmountContent =
40374
40637
  mount === "while-opened"
40375
40638
  ? () => {
@@ -42519,6 +42782,65 @@ const keptOrFirstDefinedChildUIState = (children, fallbackState, kept) => {
42519
42782
  }
42520
42783
  return kept;
42521
42784
  };
42785
+ // The same claim, for an object: a key whose control is not there cannot be
42786
+ // contradicted by it — a slide not built yet (SlideContainer mount="near"), a
42787
+ // row scrolled out, a page parked. A key of `kept` stays until a control of
42788
+ // that name, there, says otherwise; a value put ON the group replaces the
42789
+ // whole object, which is how a key is dropped on purpose.
42790
+ const keptAndNamedChildUIStates = (children, fallbackState, kept) => {
42791
+ const groupValues =
42792
+ kept !== null && typeof kept === "object" && !Array.isArray(kept)
42793
+ ? { ...kept }
42794
+ : {};
42795
+ return Object.assign(
42796
+ groupValues,
42797
+ readNamedChildUIStates(children, { warnNameless: true }),
42798
+ );
42799
+ };
42800
+ // What the named controls among `children` say, one key each. A nameless
42801
+ // GROUP is a grouping, not a value: it exists to hold its children together
42802
+ // (a WheelGroup sharing navigation, a fieldset-ish cluster) without claiming a
42803
+ // key of its own, so what its controls say is written as if they were here.
42804
+ // Naming it is what turns the same group into one key — see ControlGroup's
42805
+ // `name`. Only what its CONTROLS say: such a grouping is handed the whole
42806
+ // object around it (see distributeChildUIState below) and keeps what they do
42807
+ // not answer for, so merged whole it would carry its siblings' keys back up,
42808
+ // stale.
42809
+ const readNamedChildUIStates = (children, { warnNameless }) => {
42810
+ const values = {};
42811
+ for (const child of children) {
42812
+ const { name, emptyUIState } = child;
42813
+ // A control holding nothing writes its own empty, not a hole: the key is in
42814
+ // the object either way, and what is read from it keeps the shape the
42815
+ // reader was promised (see resolveEmptyUIState).
42816
+ const uiState =
42817
+ child.uiState === undefined && emptyUIState !== undefined
42818
+ ? emptyUIState
42819
+ : child.uiState;
42820
+ if (!name) {
42821
+ if (isNamelessGrouping(child, uiState)) {
42822
+ Object.assign(
42823
+ values,
42824
+ child.keepsUnansweredKeys
42825
+ ? readNamedChildUIStates(child.getChildControllers(), {
42826
+ warnNameless: false,
42827
+ })
42828
+ : uiState,
42829
+ );
42830
+ continue;
42831
+ }
42832
+ if (warnNameless) {
42833
+ console.warn(
42834
+ "A group child is missing a name property, its state won't be included in the group state",
42835
+ child,
42836
+ );
42837
+ }
42838
+ continue;
42839
+ }
42840
+ values[name] = uiState;
42841
+ }
42842
+ return values;
42843
+ };
42522
42844
 
42523
42845
  // Default aggregate/distribute implementations keyed by controlType or stateType.
42524
42846
  // Looked up in useUIGroupStateController to fill in omitted aggregateChildStates /
@@ -42588,37 +42910,7 @@ const GROUP_DEFAULTS = {
42588
42910
  childControlFilter: (child) => {
42589
42911
  return child.controlType !== "button" && child.controlType !== "link";
42590
42912
  },
42591
- aggregateChildStates: (children) => {
42592
- const groupValues = {};
42593
- for (const child of children) {
42594
- const { name, emptyUIState } = child;
42595
- // A control holding nothing writes its own empty, not a hole: the key
42596
- // is in the object either way, and what is read from it keeps the shape
42597
- // the reader was promised (see resolveEmptyUIState).
42598
- const uiState =
42599
- child.uiState === undefined && emptyUIState !== undefined
42600
- ? emptyUIState
42601
- : child.uiState;
42602
- if (!name) {
42603
- // A nameless GROUP is a grouping, not a value: it exists to hold its
42604
- // children together (a WheelGroup sharing navigation, a fieldset-ish
42605
- // cluster) without claiming a key of its own, so what it holds is
42606
- // merged in as if its children had been written here. Naming it is
42607
- // what turns the same group into one key — see ControlGroup's `name`.
42608
- if (isNamelessGrouping(child, uiState)) {
42609
- Object.assign(groupValues, uiState);
42610
- continue;
42611
- }
42612
- console.warn(
42613
- "A group child is missing a name property, its state won't be included in the group state",
42614
- child,
42615
- );
42616
- continue;
42617
- }
42618
- groupValues[name] = uiState;
42619
- }
42620
- return groupValues;
42621
- },
42913
+ aggregateChildStates: keptAndNamedChildUIStates,
42622
42914
  distributeChildUIState: (newUIState, child) => {
42623
42915
  const childName = child.name;
42624
42916
  if (
@@ -42642,7 +42934,8 @@ const GROUP_DEFAULTS = {
42642
42934
  return childUIState;
42643
42935
  }
42644
42936
  // Merged in on the way up (see above), so on the way down it takes the
42645
- // whole object and picks out its own keys — the same value it produced.
42937
+ // whole object: its controls pick out their own keys, and it keeps the
42938
+ // rest for those of its controls that are not there yet.
42646
42939
  // Only for a child with no name of its own: the same condition the
42647
42940
  // aggregate applies before merging. A NAMED child holding an object (a
42648
42941
  // `type="object"` picker, say) answers for one key, and the object it is
@@ -43130,6 +43423,11 @@ const useUIGroupStateController = (
43130
43423
  // a picker filling its popup, a value prop) rather than worked out from
43131
43424
  // its children. What it protects is read in onChange.
43132
43425
  stateGivenFromAbove: hasValueProp || hasDefaultValueProp,
43426
+ // Its value carries keys none of its controls answers for (see
43427
+ // keptAndNamedChildUIStates): a group merging it as a nameless
43428
+ // grouping reads its controls instead of its value.
43429
+ keepsUnansweredKeys:
43430
+ resolvedAggregateChildStates === keptAndNamedChildUIStates,
43133
43431
  uiStateSignal,
43134
43432
  wantRequesterButtonState,
43135
43433
  ref,
@@ -53731,7 +54029,8 @@ const useExpandableContext = partName => {
53731
54029
  * other three, intent on the UI part (pointer entering it, focus landing in
53732
54030
  * it) builds the content ahead of the click; `"while-opened"` content is
53733
54031
  * built by a press on it only, and thrown away if that press does not
53734
- * expand it.
54032
+ * expand it. Named controls in content not built are kept by the group
54033
+ * around them (docs/control_object.md, "A control that is not there").
53735
54034
  */
53736
54035
  const Expandable = props => {
53737
54036
  import.meta.css = [css$R, "@jsenv/navi/src/control/expandable/expandable.jsx"];
@@ -60822,12 +61121,19 @@ const readArea = slideElement => slideElement.getAttribute("data-slide-area") ||
60822
61121
  * would bring alongside (neighbours on the map, along the axes
60823
61122
  * `travelByDrag` opens, the other end of a `loop` included) — and kept from
60824
61123
  * then on. For slides that are costly to build (a grid of a few hundred
60825
- * controls each): opening builds the slide shown and its neighbours rather
61124
+ * items each): opening builds the slide shown and its neighbours rather
60826
61125
  * than all of them, and a jump to a far slide builds that one and none of
60827
61126
  * those in between, which a travel never shows. The box is then as large as
60828
61127
  * the largest slide BUILT so far (see `sizing`), so it grows as a larger one
60829
61128
  * is reached: give it a size of its own when the slides differ.
60830
61129
  * `<Slide mount>` says it for one slide — a heavy one among light ones.
61130
+ * A slide not built has no controls, and a group around the container keeps
61131
+ * their keys for them: what it was handed reaches them once built. What is
61132
+ * shared between slides is written through that group (its `signal`, a
61133
+ * `--navi-update` aimed at it), never into one control's own `signal`, which
61134
+ * nobody reads while that control is not built; and nothing validates a
61135
+ * field before its slide is built. See docs/control_object.md, "A control
61136
+ * that is not there".
60831
61137
  */
60832
61138
  const SlideContainer = ({
60833
61139
  layout = "row",
@@ -69366,6 +69672,69 @@ const toPopoverAnimation = animation => {
69366
69672
  return animation;
69367
69673
  };
69368
69674
 
69675
+ const PickerContext = createContext();
69676
+
69677
+ /*
69678
+ * The picker's context, for whatever draws its value. A picker told no value
69679
+ * holds what the control in its popup holds, and builds that popup only when
69680
+ * something reads it (see the mount decision in picker_custom.jsx): what the
69681
+ * picker draws itself it knows about, a drawing given as an element is the
69682
+ * caller's, and says so here by reading. In a layout effect, so the frame that
69683
+ * first shows the drawing already shows the value.
69684
+ */
69685
+ const usePickerContext = () => {
69686
+ const context = useContext(PickerContext);
69687
+ const requestValue = context?.requestValue;
69688
+ useLayoutEffect(() => {
69689
+ requestValue?.();
69690
+ }, [requestValue]);
69691
+ return context;
69692
+ };
69693
+
69694
+ /*
69695
+ * A typed picker (`type="date"`, `type="array"`, …) draws its value with a ui
69696
+ * of navi's own, installed as the very "ui" prop a caller overrides to draw
69697
+ * that value themselves. Marking navi's own tells the two apart afterwards:
69698
+ * an empty value is greyed as a placeholder only when the drawing is navi's —
69699
+ * a caller's ui may well be their way of writing "no filter", which is an
69700
+ * answer, not a blank (see navi-placeholder in picker.jsx).
69701
+ */
69702
+ const asPickerOwnUI = PickerUI => {
69703
+ PickerUI.isPickerOwnUI = true;
69704
+ return PickerUI;
69705
+ };
69706
+ const pickerUIIsNaviOwn = ui => {
69707
+ return Boolean(ui) && typeof ui === "object" && Boolean(ui.type?.isPickerOwnUI);
69708
+ };
69709
+
69710
+ /**
69711
+ * What the picker holds, read from inside its `ui` — for a drawing given as
69712
+ * an element (`ui={<MyCard game={game} />}`), which the picker cannot hand
69713
+ * props to. `value` is the state the picker holds right now: the answer the
69714
+ * popup just closed on, before the server has said anything, and put back if
69715
+ * the action fails. `loading` is the run in flight; `interactive` is false
69716
+ * while the picker is disabled, read-only or busy.
69717
+ *
69718
+ * @returns {{ value: any, loading: boolean, interactive: boolean } | null}
69719
+ * null outside a picker.
69720
+ */
69721
+ const usePickerState = () => {
69722
+ const context = usePickerContext();
69723
+ if (!context) {
69724
+ return null;
69725
+ }
69726
+ const {
69727
+ value,
69728
+ loading,
69729
+ interactive
69730
+ } = context;
69731
+ return {
69732
+ value,
69733
+ loading,
69734
+ interactive
69735
+ };
69736
+ };
69737
+
69369
69738
  installImportMetaCssBuild(import.meta);
69370
69739
  const css$B = /* css */`.navi_picker_callout_dock > [data-picker-content] {
69371
69740
  display: none;
@@ -69593,6 +69962,12 @@ const PickerCustom = props => {
69593
69962
  const idDefault = useId();
69594
69963
  const controlId = useContext(ControlIdContext);
69595
69964
  props.id = props.id || controlId || idDefault;
69965
+ // Given no value, the picker holds what the control in its popup holds (see
69966
+ // the `mount` decision below) — and a group around it collects that from the
69967
+ // moment both exist.
69968
+ const valueInPopup = !isControlValueGivenByProps(props);
69969
+ const parentController = useContext(ParentUIStateControllerContext);
69970
+ const answersToGroup = Boolean(parentController) && !props.standalone;
69596
69971
  // Same narrow-container/maxWidth-compact heuristic Popup itself uses (see
69597
69972
  // popup_mode.jsx's own useResolvedPopupMode) — frozen for the lifetime of an opening
69598
69973
  // (computed when closed, stable while open, so a screen resize mid-session
@@ -69838,12 +70213,20 @@ const PickerCustom = props => {
69838
70213
  const requestInteraction = options => {
69839
70214
  dispatchRequestInteraction(ref.current, options);
69840
70215
  };
70216
+
70217
+ // What a drawing of the caller's calls as it reads the value (see
70218
+ // usePickerContext): the popup holding it, built while closed. Only when
70219
+ // `mount` is left to the picker — a caller who set it has said when.
70220
+ const requestValue = useStableCallback(() => {
70221
+ openController.buildContent();
70222
+ });
69841
70223
  const {
69842
70224
  onActionStart,
69843
70225
  children,
69844
70226
  uiAction: uiActionProp
69845
70227
  } = props;
69846
70228
  Object.assign(pickerProps, {
70229
+ "requestValue": valueInPopup && props.mount === undefined ? requestValue : undefined,
69847
70230
  "aria-expanded": Boolean(expanded),
69848
70231
  "onActionStart": e => {
69849
70232
  onActionStart?.(e);
@@ -69882,13 +70265,12 @@ const PickerCustom = props => {
69882
70265
  anchor: props.anchor || props.ref,
69883
70266
  openController,
69884
70267
  // A picker whose value was never given to it reads it off the control in
69885
- // its popup (see useUIFacadeStateController): the trigger shows what the
69886
- // list inside says is selected, so that list has to exist before anyone
69887
- // opens anything. Told a value — even an empty one — the picker owns it
69888
- // and pushes it down instead, leaving the popup free to build its
69889
- // content only when it is first opened (see popup_content_mount.js).
69890
- // A caller who knows better says so with the popup's own props.
69891
- mount: props.mount ?? (isControlValueGivenByProps(props) ? MOUNT_DEFAULT : "always"),
70268
+ // its popup (see useUIFacadeStateController), so that control is built as
70269
+ // soon as something reads the value: at mount when the picker sees the
70270
+ // reader, otherwise when a drawing of the caller's asks (requestValue
70271
+ // above). Told a value — even an empty one — the picker pushes it down,
70272
+ // and the popup waits for its first open (see popup_content_mount.js).
70273
+ mount: props.mount ?? (valueInPopup && isPickerValueReadAtMount(props, answersToGroup) ? "always" : MOUNT_DEFAULT),
69892
70274
  // Not on pickerProps (the trigger): commands.js's own
69893
70275
  // resolveClosestExpandable() does `el.closest("[aria-expanded]")` to
69894
70276
  // find where to dispatch navi_request_open/navi_request_close — and
@@ -70176,6 +70558,29 @@ const PickerCustom = props => {
70176
70558
  });
70177
70559
  };
70178
70560
 
70561
+ // Whether something reads a picker's value before its popup has opened, as
70562
+ // far as the picker sees from its own props: the drawing navi renders for it
70563
+ // (its own, or the typed one PickerTypeResolver installs — `ui` is still unset
70564
+ // here), one of navi's Picker.UI.*, a component handed `value`, the clear
70565
+ // cross, a form submitting it by name, a group it answers to. A headless
70566
+ // picker draws nothing, and an element `ui` is the caller's: it reads through
70567
+ // usePickerContext, which asks for the value then.
70568
+ const isPickerValueReadAtMount = (props, answersToGroup) => {
70569
+ if (props.name !== undefined || answersToGroup) {
70570
+ return true;
70571
+ }
70572
+ if (props.variant === "headless") {
70573
+ return false;
70574
+ }
70575
+ if (props.clearable) {
70576
+ return true;
70577
+ }
70578
+ const {
70579
+ ui
70580
+ } = props;
70581
+ return ui === undefined || typeof ui === "function" || pickerUIIsNaviOwn(ui);
70582
+ };
70583
+
70179
70584
  // A hold declared on something AROUND the picker (a card opened by
70180
70585
  // `openOn="longpress"`, holding this one in its drawing): the finger going
70181
70586
  // down may be the start of that hold, so this picker opens on the click — which
@@ -70691,52 +71096,6 @@ const PickerConfirmBody = ({
70691
71096
  });
70692
71097
  };
70693
71098
 
70694
- const PickerContext = createContext();
70695
-
70696
- /*
70697
- * A typed picker (`type="date"`, `type="array"`, …) draws its value with a ui
70698
- * of navi's own, installed as the very "ui" prop a caller overrides to draw
70699
- * that value themselves. Marking navi's own tells the two apart afterwards:
70700
- * an empty value is greyed as a placeholder only when the drawing is navi's —
70701
- * a caller's ui may well be their way of writing "no filter", which is an
70702
- * answer, not a blank (see navi-placeholder in picker.jsx).
70703
- */
70704
- const asPickerOwnUI = PickerUI => {
70705
- PickerUI.isPickerOwnUI = true;
70706
- return PickerUI;
70707
- };
70708
- const pickerUIIsNaviOwn = ui => {
70709
- return Boolean(ui) && typeof ui === "object" && Boolean(ui.type?.isPickerOwnUI);
70710
- };
70711
-
70712
- /**
70713
- * What the picker holds, read from inside its `ui` — for a drawing given as
70714
- * an element (`ui={<MyCard game={game} />}`), which the picker cannot hand
70715
- * props to. `value` is the state the picker holds right now: the answer the
70716
- * popup just closed on, before the server has said anything, and put back if
70717
- * the action fails. `loading` is the run in flight; `interactive` is false
70718
- * while the picker is disabled, read-only or busy.
70719
- *
70720
- * @returns {{ value: any, loading: boolean, interactive: boolean } | null}
70721
- * null outside a picker.
70722
- */
70723
- const usePickerState = () => {
70724
- const context = useContext(PickerContext);
70725
- if (!context) {
70726
- return null;
70727
- }
70728
- const {
70729
- value,
70730
- loading,
70731
- interactive
70732
- } = context;
70733
- return {
70734
- value,
70735
- loading,
70736
- interactive
70737
- };
70738
- };
70739
-
70740
71099
  const PickerNaviMinute = props => {
70741
71100
  const Next = useNextResolver();
70742
71101
  const {
@@ -70858,44 +71217,6 @@ document.addEventListener(
70858
71217
  { capture: true, passive: true },
70859
71218
  );
70860
71219
 
70861
- const FRAME_TIMEOUT_MS = 100;
70862
-
70863
- /**
70864
- * Calls back once the browser has painted what is committed now.
70865
- *
70866
- * The callbacks of the next frame run before that frame paints, so the call is
70867
- * made from a task queued from inside one — the same way preact schedules
70868
- * useEffect. Unlike an effect, nothing runs this early: preact flushes a
70869
- * component's pending effects as soon as that component renders again, and a
70870
- * re-render before the paint is exactly what a caller waiting for the paint
70871
- * has to survive. A frame not coming at all (a background tab) still answers,
70872
- * late, through the timeout.
70873
- *
70874
- * @param {() => void} callback
70875
- * @returns {() => void} cancel
70876
- */
70877
- const afterPaint = (callback) => {
70878
- let called = false;
70879
- let timeoutId;
70880
- let frameId;
70881
- const onFrame = () => {
70882
- if (called) {
70883
- return;
70884
- }
70885
- called = true;
70886
- clearTimeout(timeoutId);
70887
- cancelAnimationFrame(frameId);
70888
- timeoutId = setTimeout(callback, 0);
70889
- };
70890
- timeoutId = setTimeout(onFrame, FRAME_TIMEOUT_MS);
70891
- frameId = requestAnimationFrame(onFrame);
70892
- return () => {
70893
- called = true;
70894
- clearTimeout(timeoutId);
70895
- cancelAnimationFrame(frameId);
70896
- };
70897
- };
70898
-
70899
71220
  const LoadingDotsSvg = () => {
70900
71221
  return jsxs("svg", {
70901
71222
  viewBox: "0 0 200 200",
@@ -79253,7 +79574,7 @@ const PickerObjectUI = /*#__PURE__*/asPickerOwnUI(() => {
79253
79574
  const {
79254
79575
  value,
79255
79576
  placeholder
79256
- } = useContext(PickerContext);
79577
+ } = usePickerContext();
79257
79578
  if (uiStateHoldsNothing(value)) {
79258
79579
  if (!placeholder) {
79259
79580
  return null;
@@ -79289,7 +79610,7 @@ const PickerArrayUI = /*#__PURE__*/asPickerOwnUI(() => {
79289
79610
  value,
79290
79611
  placeholder,
79291
79612
  maxLines
79292
- } = useContext(PickerContext);
79613
+ } = usePickerContext();
79293
79614
  if (uiStateHoldsNothing(value)) {
79294
79615
  if (!placeholder) {
79295
79616
  return null;
@@ -79371,7 +79692,7 @@ const PickerColorUI = /*#__PURE__*/asPickerOwnUI(() => {
79371
79692
  const {
79372
79693
  value,
79373
79694
  placeholder
79374
- } = useContext(PickerContext);
79695
+ } = usePickerContext();
79375
79696
  if (!value) {
79376
79697
  if (!placeholder) {
79377
79698
  return jsx(Color, {});
@@ -79395,7 +79716,7 @@ const PickerDateUI = /*#__PURE__*/asPickerOwnUI(props => {
79395
79716
  const {
79396
79717
  value,
79397
79718
  placeholder
79398
- } = useContext(PickerContext);
79719
+ } = usePickerContext();
79399
79720
  if (!value) {
79400
79721
  if (!placeholder) {
79401
79722
  return jsx(Time, {
@@ -79427,7 +79748,7 @@ const PickerMonthUI = /*#__PURE__*/asPickerOwnUI(props => {
79427
79748
  const {
79428
79749
  value,
79429
79750
  placeholder
79430
- } = useContext(PickerContext);
79751
+ } = usePickerContext();
79431
79752
  if (!value) {
79432
79753
  if (!placeholder) {
79433
79754
  return jsx(Time, {
@@ -79458,7 +79779,7 @@ const PickerWeekUI = /*#__PURE__*/asPickerOwnUI(props => {
79458
79779
  const {
79459
79780
  value,
79460
79781
  placeholder
79461
- } = useContext(PickerContext);
79782
+ } = usePickerContext();
79462
79783
  if (!value) {
79463
79784
  if (!placeholder) {
79464
79785
  return jsx(Time, {
@@ -79489,7 +79810,7 @@ const PickerTimeUI = /*#__PURE__*/asPickerOwnUI(props => {
79489
79810
  const {
79490
79811
  value,
79491
79812
  placeholder
79492
- } = useContext(PickerContext);
79813
+ } = usePickerContext();
79493
79814
  if (!value) {
79494
79815
  if (!placeholder) {
79495
79816
  return jsx(Time, {
@@ -79520,7 +79841,7 @@ const PickerDurationUI = /*#__PURE__*/asPickerOwnUI(props => {
79520
79841
  const {
79521
79842
  value,
79522
79843
  placeholder
79523
- } = useContext(PickerContext);
79844
+ } = usePickerContext();
79524
79845
  if (!value) {
79525
79846
  if (!placeholder) {
79526
79847
  return jsx(Time, {
@@ -79550,7 +79871,7 @@ const PickerDatetimeUI = /*#__PURE__*/asPickerOwnUI(props => {
79550
79871
  const {
79551
79872
  value,
79552
79873
  placeholder
79553
- } = useContext(PickerContext);
79874
+ } = usePickerContext();
79554
79875
  if (!value) {
79555
79876
  if (!placeholder) {
79556
79877
  return jsx(Time, {
@@ -79579,7 +79900,7 @@ const PickerFileUI = /*#__PURE__*/asPickerOwnUI(() => {
79579
79900
  const {
79580
79901
  value,
79581
79902
  placeholder
79582
- } = useContext(PickerContext);
79903
+ } = usePickerContext();
79583
79904
  if (!value) {
79584
79905
  if (!placeholder) {
79585
79906
  return null;
@@ -80115,7 +80436,11 @@ const PickerButton = props => {
80115
80436
  // a piece of, waiting as a whole (see the `loadingOutline` doc).
80116
80437
  loadingOutline,
80117
80438
  // Where the outline runs, in px from the picker's box: negative is outside.
80118
- loadingOutlineInset = -2
80439
+ loadingOutlineInset = -2,
80440
+ // Builds the popup this picker's value is read from, for a drawing that
80441
+ // reads it (see usePickerContext). Set by PickerCustom, only while that
80442
+ // value lives in the popup.
80443
+ requestValue
80119
80444
  } = props;
80120
80445
  // A word in a sentence is never truncated — and the clamp's overflow: hidden
80121
80446
  // would cut its dotted underline, which sits on the edge of the line box
@@ -80213,7 +80538,8 @@ const PickerButton = props => {
80213
80538
  id: inputProps.id,
80214
80539
  interactive,
80215
80540
  loading,
80216
- clearConfirm
80541
+ clearConfirm,
80542
+ requestValue
80217
80543
  };
80218
80544
  return (
80219
80545
  /* Read-only crosses into everything the picker is made of: what it really
@@ -80264,7 +80590,8 @@ const PickerButton = props => {
80264
80590
  error: undefined,
80265
80591
  dayLabel: undefined,
80266
80592
  loadingOutline: undefined,
80267
- loadingOutlineInset: undefined
80593
+ loadingOutlineInset: undefined,
80594
+ requestValue: undefined
80268
80595
  // This wrapper will receive keyboard event bubbling from the picker popup content
80269
80596
  // we re-dispatch on the input (to get escape to close for instance)
80270
80597
  ,
@@ -80478,7 +80805,7 @@ const PickerClear = ({
80478
80805
  children,
80479
80806
  ...rest
80480
80807
  }) => {
80481
- const pickerContext = useContext(PickerContext);
80808
+ const pickerContext = usePickerContext();
80482
80809
  if (!pickerContext) {
80483
80810
  warnOnClearOutsidePicker();
80484
80811
  return null;
@@ -80762,7 +81089,7 @@ const PickerDefaultUI = () => {
80762
81089
  const {
80763
81090
  value,
80764
81091
  placeholder
80765
- } = useContext(PickerContext);
81092
+ } = usePickerContext();
80766
81093
  if (!value) {
80767
81094
  if (!placeholder) {
80768
81095
  return null;
@@ -81336,10 +81663,13 @@ const PickerFirstResolver = props => {
81336
81663
  * @param {string} [cancelTestId]
81337
81664
  * @param {"always"|"idle"|"from-first-open"|"while-opened"} [mount] The popup's own
81338
81665
  * (see Popup): when the children are built and thrown away. Left out, a
81339
- * picker told no value builds them right away (`"always"`) — it reads its
81340
- * value off the control they hold — and one told a value waits for the first
81341
- * open. A `type="confirm"` picker never builds them early: its popup is a
81342
- * question, there is nothing to read.
81666
+ * picker told no value holds what the control they hold holds, and builds
81667
+ * them as soon as that value is read: right away (`"always"`) when navi draws
81668
+ * it (no `ui`, a `Picker.UI.*`, a component `ui`), when there is a clear
81669
+ * cross, or when a form or group around the picker collects it; when an
81670
+ * element `ui` calls `usePickerState()`. Read by nothing, it waits for the
81671
+ * first open, like a picker told a value. A `type="confirm"` picker never
81672
+ * builds them early: its popup is a question, there is nothing to read.
81343
81673
  */
81344
81674
  // Every picker type is resolved into one of these before it gets here (see
81345
81675
  // PickerTypeResolver and resolveInputProps): a native input type, or navi_js