@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, 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";
@@ -10812,6 +10812,179 @@ const dropGeneratedIdKeys = (state) => {
10812
10812
  return stateWithoutGeneratedIdKeys;
10813
10813
  };
10814
10814
 
10815
+ const FRAME_TIMEOUT_MS = 100;
10816
+
10817
+ /**
10818
+ * Calls back once the browser has painted what is committed now.
10819
+ *
10820
+ * The callbacks of the next frame run before that frame paints, so the call is
10821
+ * made from a task queued from inside one — the same way preact schedules
10822
+ * useEffect. Unlike an effect, nothing runs this early: preact flushes a
10823
+ * component's pending effects as soon as that component renders again, and a
10824
+ * re-render before the paint is exactly what a caller waiting for the paint
10825
+ * has to survive. A frame not coming at all (a background tab) still answers,
10826
+ * late, through the timeout.
10827
+ *
10828
+ * @param {() => void} callback
10829
+ * @returns {() => void} cancel
10830
+ */
10831
+ const afterPaint = (callback) => {
10832
+ let called = false;
10833
+ let timeoutId;
10834
+ let frameId;
10835
+ const onFrame = () => {
10836
+ if (called) {
10837
+ return;
10838
+ }
10839
+ called = true;
10840
+ clearTimeout(timeoutId);
10841
+ cancelAnimationFrame(frameId);
10842
+ timeoutId = setTimeout(callback, 0);
10843
+ };
10844
+ timeoutId = setTimeout(onFrame, FRAME_TIMEOUT_MS);
10845
+ frameId = requestAnimationFrame(onFrame);
10846
+ return () => {
10847
+ called = true;
10848
+ clearTimeout(timeoutId);
10849
+ cancelAnimationFrame(frameId);
10850
+ };
10851
+ };
10852
+
10853
+ /**
10854
+ * Runs `fn` and commits whatever it re-renders before returning, instead of
10855
+ * letting Preact batch it into the next microtask. Layout effects of what gets
10856
+ * mounted run inside the call too, exactly as they would on any other commit.
10857
+ *
10858
+ * For the caller that has to read the DOM it just asked for — measuring an
10859
+ * element whose content it mounts in the same breath — and cannot wait a tick
10860
+ * to do it, because what comes after is a browser event still in flight
10861
+ * (preventDefault, focus placement) that no longer accepts being answered late.
10862
+ *
10863
+ * `options.debounceRendering` is Preact's own hook for deciding *when* the
10864
+ * render queue drains; swapping it for "right now" for the duration of the call
10865
+ * is exactly how preact/compat implements React's flushSync. Reserve it for the
10866
+ * case above: rendering synchronously in the middle of an event gives up the
10867
+ * batching that makes several state changes one commit.
10868
+ */
10869
+ const flushSyncRendering = (fn) => {
10870
+ const debounceRenderingPrevious = options.debounceRendering;
10871
+ options.debounceRendering = (drainRenderQueue) => {
10872
+ drainRenderQueue();
10873
+ };
10874
+ try {
10875
+ fn();
10876
+ } finally {
10877
+ options.debounceRendering = debounceRenderingPrevious;
10878
+ }
10879
+ };
10880
+
10881
+ /**
10882
+ * The content of a `<Box mount="after-paint">`: built right after the first
10883
+ * frame that shows the box, unless that frame would show it.
10884
+ *
10885
+ * The browser paints nothing until the render that builds a page has ended, and
10886
+ * what lies below the first screen weighs in that render as much as what is on
10887
+ * it. A box marked this way is left empty in that render, then built once the
10888
+ * frame is painted — the page-level twin of a list's render window (see
10889
+ * `renderBudget` in list.jsx), for a page made of sections.
10890
+ *
10891
+ * Which boxes lie below the screen is measured, not taken from the caller. The
10892
+ * caller cannot know: a taller phone, a profile with nothing in its first
10893
+ * section, and the box it marked is on screen. So each box looks where it
10894
+ * stands in the animation callbacks of its first frame — laid out, not yet
10895
+ * painted — and one the frame would show, or one above it, is built there:
10896
+ * content appearing on screen is a pop-in, and content growing ABOVE what is
10897
+ * read pushes it down. Marking too much costs a second render in that frame,
10898
+ * never a wrong picture.
10899
+ *
10900
+ * The document put back where it was read is the other frame that shows it.
10901
+ * That offset is written by navi (see scroll_restoration.js), and written into
10902
+ * a document still missing its bottom it is clamped: the page opens higher than
10903
+ * it was left, and the bottom arrives under a page that has already jumped. So
10904
+ * whoever writes an arrival's offset builds first the boxes that offset shows
10905
+ * (buildDeferredBoxesAbove), in the same task — synchronously, since what comes
10906
+ * right after it (the offset, a route travel measuring its window) reads the
10907
+ * layout.
10908
+ */
10909
+
10910
+ const deferredBoxes = new Set();
10911
+ const MountAfterPaint = ({
10912
+ boxRef,
10913
+ children
10914
+ }) => {
10915
+ const [built, setBuilt] = useState(false);
10916
+ useLayoutEffect(() => {
10917
+ let frame;
10918
+ let cancelAfterPaint;
10919
+ const stopWaiting = () => {
10920
+ deferredBoxes.delete(deferredBox);
10921
+ cancelAnimationFrame(frame);
10922
+ cancelAfterPaint();
10923
+ };
10924
+ // The first of the three that come — the offset of an arrival, the frame
10925
+ // finding the box on screen, the paint — builds it and cancels the others.
10926
+ const deferredBox = {
10927
+ getElement: () => boxRef.current,
10928
+ build: () => {
10929
+ stopWaiting();
10930
+ setBuilt(true);
10931
+ }
10932
+ };
10933
+ deferredBoxes.add(deferredBox);
10934
+ // Requested before afterPaint's own frame callback, so the box is looked at
10935
+ // before that one turns the paint into a build.
10936
+ frame = requestAnimationFrame(() => {
10937
+ const top = readTopInWindow(boxRef.current);
10938
+ if (top !== null && top < window.innerHeight) {
10939
+ deferredBox.build();
10940
+ }
10941
+ });
10942
+ cancelAfterPaint = afterPaint(deferredBox.build);
10943
+ return stopWaiting;
10944
+ }, []);
10945
+ return built ? children : null;
10946
+ };
10947
+
10948
+ /**
10949
+ * Builds, now, every box still waiting whose top is above `bottom` (in the
10950
+ * document's coordinates): what the document will show once scrolled so that
10951
+ * `bottom` is the bottom of the screen. Boxes built push those below them
10952
+ * further down, so the ones measured above `bottom` are all the ones that can
10953
+ * be shown there, and a few more at worst.
10954
+ *
10955
+ * Renders synchronously, so it must be called outside a Preact commit: a render
10956
+ * flushed from a layout effect runs inside the commit still walking the tree.
10957
+ */
10958
+ const buildDeferredBoxesAbove = bottom => {
10959
+ if (deferredBoxes.size === 0) {
10960
+ return;
10961
+ }
10962
+ const toBuild = [];
10963
+ for (const deferredBox of deferredBoxes) {
10964
+ const top = readTopInWindow(deferredBox.getElement());
10965
+ if (top !== null && top + window.scrollY < bottom) {
10966
+ toBuild.push(deferredBox);
10967
+ }
10968
+ }
10969
+ if (toBuild.length === 0) {
10970
+ return;
10971
+ }
10972
+ flushSyncRendering(() => {
10973
+ for (const deferredBox of toBuild) {
10974
+ deferredBox.build();
10975
+ }
10976
+ });
10977
+ };
10978
+
10979
+ // null for a box nothing draws (a closed popup, a hidden tab): no frame shows
10980
+ // it, wherever the document is.
10981
+ const readTopInWindow = element => {
10982
+ if (!element || element.getClientRects().length === 0) {
10983
+ return null;
10984
+ }
10985
+ return element.getBoundingClientRect().top;
10986
+ };
10987
+
10815
10988
  /**
10816
10989
  * A container has put its page on screen — or as much of it as it can.
10817
10990
  *
@@ -10867,7 +11040,9 @@ const [publishRouteRender, observeRouteRender] = createPubSub();
10867
11040
  * What is NOT covered, and cannot be from here: a page whose height depends on
10868
11041
  * something still loading. Its content is not there at the moment it is put
10869
11042
  * back, so a position beyond what has arrived is clamped as before. Only the
10870
- * page knows when it is whole.
11043
+ * page knows when it is whole. What navi itself leaves out of a first render
11044
+ * is the exception — a `<Box mount="after-paint">` — and is built before the
11045
+ * offset that shows it is written (see mount_after_paint.jsx).
10871
11046
  *
10872
11047
  * WHEN a page is arrived at is not decided here either. A document navigation
10873
11048
  * lands where its kind says (see via_history.js), and one scrollport can be
@@ -11016,14 +11191,18 @@ const installScrollRestoration = () => {
11016
11191
  // What a reload asks for, now that the browser has been told not to do it.
11017
11192
  // Once, and at the first render of a route: the position is only meaningful
11018
11193
  // once there is a page under it.
11194
+ // Written once the commit that rendered it has ended: said from inside it,
11195
+ // and what the offset shows may still have to be built (see scrollTo).
11019
11196
  const positionOnLoad = positionByUrl.get(window.location.href);
11020
11197
  if (positionOnLoad && (positionOnLoad.x || positionOnLoad.y)) {
11021
11198
  const stopListening = observeRouteRender(() => {
11022
11199
  stopListening();
11023
- if (documentHoldCount) {
11024
- return;
11025
- }
11026
- scrollTo(positionOnLoad);
11200
+ queueMicrotask(() => {
11201
+ if (documentHoldCount) {
11202
+ return;
11203
+ }
11204
+ scrollTo(positionOnLoad);
11205
+ });
11027
11206
  });
11028
11207
  }
11029
11208
  };
@@ -11130,14 +11309,25 @@ const forgetScrollersOnArrival = (url, { from } = {}) => {
11130
11309
  // last read. Where this one was read, and its top when it never was — leaving
11131
11310
  // the offset alone would seat the reader wherever the neighbour happened to
11132
11311
  // be, so here "nothing recorded" and "stay" are not the same thing.
11312
+ //
11313
+ // Called outside a Preact commit only, like scrollTo: a row of tabs measures
11314
+ // its window right after, so what the top shows is built first here rather
11315
+ // than in the frame (see mount_after_paint.jsx).
11133
11316
  const arriveAtScrollPosition = (url) => {
11134
11317
  if (restoreScrollPosition(url)) {
11135
11318
  return;
11136
11319
  }
11320
+ buildDeferredBoxesAbove(window.innerHeight);
11137
11321
  startAtTop(url);
11138
11322
  };
11139
11323
 
11324
+ // What the offset shows is built first: written into a document still missing
11325
+ // a deferred box, the offset is clamped (see mount_after_paint.jsx). That build
11326
+ // is a render of its own, so this is never called from inside a commit — a
11327
+ // caller that learns the page is there from a layout effect (observeRouteRender)
11328
+ // writes once that commit has ended.
11140
11329
  const scrollTo = ({ x, y }) => {
11330
+ buildDeferredBoxesAbove(y + window.innerHeight);
11141
11331
  window.scrollTo({ top: y, left: x, behavior: "instant" });
11142
11332
  };
11143
11333
 
@@ -14802,6 +14992,9 @@ const createOpenController = (
14802
14992
  // content is still waiting for a first open to be built. Called below,
14803
14993
  // before openEffect, so the popup measures and positions the real thing.
14804
14994
  mountContent: null,
14995
+ // Set alongside mountContent: the content asked for outside any opening,
14996
+ // by something that reads it while the popup is closed.
14997
+ buildContent: null,
14805
14998
  // The caller's own `onOpen`, set by Dialog/Popover from their props on
14806
14999
  // every render (like openEffect). Called BEFORE mountContent, so whatever
14807
15000
  // it decides — which record this dialog is opening on — is already true by
@@ -30029,6 +30222,7 @@ const PSEUDO_STATE_CHILD_PROP_SET = new Set(["tabIndex", "tabindex"]);
30029
30222
  * footer?: boolean,
30030
30223
  * body?: boolean,
30031
30224
  * sticky?: boolean,
30225
+ * mount?: "always" | "after-paint",
30032
30226
  * children?: import("ignore:preact").ComponentChildren,
30033
30227
  * [key: string]: any,
30034
30228
  * }>}
@@ -30077,6 +30271,16 @@ const PSEUDO_STATE_CHILD_PROP_SET = new Set(["tabIndex", "tabindex"]);
30077
30271
  * (see docs/interactions.md). A plain box has no wiring of its own: it does
30078
30272
  * nothing with `action` (that is a control's prop), so a click on it is
30079
30273
  * declared here like any other interaction: `interactions={{ click: fn }}`.
30274
+ * @param {"always"|"after-paint"} [mount="always"] When `children` are built.
30275
+ * `"after-paint"` leaves the box empty in the render that first mounts it and
30276
+ * builds them right after the first frame that shows the page, then keeps
30277
+ * them: for the sections of a page that lie below its first screen. navi
30278
+ * measures where the box stands before that frame, and one the frame would
30279
+ * show (or where the document is put back, on a return) is built before the
30280
+ * paint all the same. Until built, what is inside is not in the document:
30281
+ * no id there is found, no named control there is there to hear its own
30282
+ * `signal` (see docs/control_object.md). Only for content nothing reaches
30283
+ * before the first frame (see docs/scroll.md).
30080
30284
  */
30081
30285
  const Box = props => {
30082
30286
  const {
@@ -30084,6 +30288,7 @@ const Box = props => {
30084
30288
  children,
30085
30289
  separator,
30086
30290
  interactions,
30291
+ mount,
30087
30292
  ...computeProps
30088
30293
  } = props;
30089
30294
  const parentBoxFlow = useContext(BoxFlowContext);
@@ -30161,6 +30366,12 @@ const Box = props => {
30161
30366
  children: innerChildren
30162
30367
  });
30163
30368
  }
30369
+ if (mount === "after-paint") {
30370
+ innerChildren = jsx(MountAfterPaint, {
30371
+ boxRef: finalRef,
30372
+ children: innerChildren
30373
+ });
30374
+ }
30164
30375
  return jsx(TagName, {
30165
30376
  ref: finalRef,
30166
30377
  className: innerClassName,
@@ -33639,6 +33850,13 @@ const OLD_BAND_TOP_PROPERTY = "--navi-transition-old-band-top";
33639
33850
  const OLD_BAND_RIGHT_PROPERTY = "--navi-transition-old-band-right";
33640
33851
  const OLD_BAND_BOTTOM_PROPERTY = "--navi-transition-old-band-bottom";
33641
33852
  const OLD_BAND_LEFT_PROPERTY = "--navi-transition-old-band-left";
33853
+ const VIEWPORT_TOP_PROPERTY = "--navi-transition-viewport-top";
33854
+ const VIEWPORT_LEFT_PROPERTY = "--navi-transition-viewport-left";
33855
+ // Worn by the root once the group is held at the window by our numbers alone
33856
+ // (see placeTransitionWindow): the group rule of each movement is written on
33857
+ // it. Read by rules on pseudo-elements only, so wearing it restyles nothing
33858
+ // the document inherits.
33859
+ const WINDOW_PLACED_ATTRIBUTE = "data-navi-transition-window-placed";
33642
33860
  const WINDOW_PROPERTIES = [
33643
33861
  WINDOW_TOP_PROPERTY,
33644
33862
  WINDOW_LEFT_PROPERTY,
@@ -33652,6 +33870,8 @@ const WINDOW_PROPERTIES = [
33652
33870
  OLD_BAND_RIGHT_PROPERTY,
33653
33871
  OLD_BAND_BOTTOM_PROPERTY,
33654
33872
  OLD_BAND_LEFT_PROPERTY,
33873
+ VIEWPORT_TOP_PROPERTY,
33874
+ VIEWPORT_LEFT_PROPERTY,
33655
33875
  ];
33656
33876
 
33657
33877
  // Whose numbers are currently published. The window belongs to the movement
@@ -33659,6 +33879,8 @@ const WINDOW_PROPERTIES = [
33659
33879
  // after another has replaced it must not wipe numbers the new one is standing
33660
33880
  // on.
33661
33881
  let windowOwner = null;
33882
+ // The element the box is captured as, for as long as the window is held.
33883
+ let windowElement = null;
33662
33884
  // Stops re-reading the arriving state's corner, when a movement is on.
33663
33885
  let unfollowArrivingState = null;
33664
33886
 
@@ -33714,6 +33936,10 @@ const holdTransitionWindow = (
33714
33936
  const right =
33715
33937
  rectBefore.right > rectAfter.right ? rectBefore.right : rectAfter.right;
33716
33938
  windowOwner = owner;
33939
+ windowElement = element;
33940
+ // Pictures of a movement this one took over from are placed by the browser
33941
+ // until this one's own pictures exist.
33942
+ document.documentElement.removeAttribute(WINDOW_PLACED_ATTRIBUTE);
33717
33943
  setTransitionValue(WINDOW_TOP_PROPERTY, `${top}px`);
33718
33944
  setTransitionValue(WINDOW_LEFT_PROPERTY, `${left}px`);
33719
33945
  setTransitionValue(WINDOW_WIDTH_PROPERTY, `${right - left}px`);
@@ -33738,6 +33964,33 @@ const holdTransitionWindow = (
33738
33964
  });
33739
33965
  };
33740
33966
 
33967
+ /**
33968
+ * Called once the pictures exist — the view transition's `ready` — with the
33969
+ * name the box was captured under. Where the viewport's corner stands in the
33970
+ * space the pictures are drawn in is the browser's placement of the group
33971
+ * minus our reading of the same element, both taken in this one instant; the
33972
+ * group is held at the window with it from then on (see the top of the file).
33973
+ * A browser that does not hand the group's placement back leaves the group
33974
+ * where it places it, moved to the window by the arriving corner.
33975
+ */
33976
+ const placeTransitionWindow = (owner, groupName) => {
33977
+ if (owner !== windowOwner) {
33978
+ return;
33979
+ }
33980
+ const { transform } = getComputedStyle(
33981
+ document.documentElement,
33982
+ `::view-transition-group(${groupName})`,
33983
+ );
33984
+ if (!transform || transform === "none") {
33985
+ return;
33986
+ }
33987
+ const placement = new DOMMatrixReadOnly(transform);
33988
+ const rect = windowElement.getBoundingClientRect();
33989
+ setTransitionValue(VIEWPORT_TOP_PROPERTY, `${placement.m42 - rect.top}px`);
33990
+ setTransitionValue(VIEWPORT_LEFT_PROPERTY, `${placement.m41 - rect.left}px`);
33991
+ document.documentElement.setAttribute(WINDOW_PLACED_ATTRIBUTE, "");
33992
+ };
33993
+
33741
33994
  // The live layout takes the box back. A discontinuity by construction — the
33742
33995
  // window stands at the held height, the box is at its own — and an invisible
33743
33996
  // one: the page arriving is fully in place, and the strip below it that the
@@ -33747,20 +34000,22 @@ const releaseTransitionWindow = (owner) => {
33747
34000
  return;
33748
34001
  }
33749
34002
  windowOwner = null;
34003
+ windowElement = null;
33750
34004
  if (unfollowArrivingState) {
33751
34005
  unfollowArrivingState();
33752
34006
  }
34007
+ document.documentElement.removeAttribute(WINDOW_PLACED_ATTRIBUTE);
33753
34008
  for (const property of WINDOW_PROPERTIES) {
33754
34009
  removeTransitionValue(property);
33755
34010
  }
33756
34011
  };
33757
34012
 
33758
- // The arriving state's corner, re-read every frame — as often as the browser
33759
- // refreshes the group's placement from the same element (see the top of the
33760
- // file) — and with it the offset outside the box, which carries the window
33761
- // and the corner of the state being left along. Requested, never awaited: the
33762
- // first call runs inside the update callback, where a frame cannot come.
33763
- // Written only when it moved: every write restyles the pictures.
34013
+ // The arriving state's corner, re-read every frame — as often as anything can
34014
+ // move the page arriving (see the top of the file) — and with it the offset
34015
+ // outside the box, which carries the window and the corner of the state being
34016
+ // left along. Requested, never awaited: the first call runs inside the update
34017
+ // callback, where a frame cannot come. Written only when it moved: every write
34018
+ // restyles the pictures.
33764
34019
  const followArrivingState = (element, rectAtHold, heldAt) => {
33765
34020
  if (unfollowArrivingState) {
33766
34021
  unfollowArrivingState();
@@ -34265,6 +34520,13 @@ const css$12 = /* css */`@property --navi-route-transition-leave {
34265
34520
  animation-name: none;
34266
34521
  }
34267
34522
 
34523
+ &[data-navi-transition-window-placed]::view-transition-group(navi-route-transition) {
34524
+ top: calc(var(--navi-transition-window-top) + var(--navi-transition-viewport-top));
34525
+ left: calc(var(--navi-transition-window-left) +
34526
+ var(--navi-transition-viewport-left));
34527
+ transform: none;
34528
+ }
34529
+
34268
34530
  &::view-transition-old(navi-route-transition) {
34269
34531
  top: calc(var(--navi-transition-window-old-top) - var(--navi-transition-window-top));
34270
34532
  left: calc(var(--navi-transition-window-old-left) - var(--navi-transition-window-left));
@@ -35312,6 +35574,8 @@ const beginTransition = ({
35312
35574
  // the only place the silent misconfigurations show. They are all about the
35313
35575
  // same thing — a movement playing on pictures that are not the pages.
35314
35576
  const viewTransitionReady = () => {
35577
+ // Read before anything below writes to the document.
35578
+ placeTransitionWindow(transition, AREA_NAME);
35315
35579
  startTransitionFurniture(transition, {
35316
35580
  duration: transition.duration
35317
35581
  });
@@ -35823,6 +36087,13 @@ const css$11 = /* css */`.navi_route_travel {
35823
36087
  animation-duration: var(--navi-route-travel-duration, .3s);
35824
36088
  animation-name: none;
35825
36089
  }
36090
+
36091
+ &[data-navi-transition-window-placed]::view-transition-group(navi-route-travel) {
36092
+ top: calc(var(--navi-transition-window-top) + var(--navi-transition-viewport-top));
36093
+ left: calc(var(--navi-transition-window-left) +
36094
+ var(--navi-transition-viewport-left));
36095
+ transform: none;
36096
+ }
35826
36097
  }
35827
36098
 
35828
36099
  :root[data-navi-route-travel-held] {
@@ -36201,6 +36472,10 @@ const RouteTravel = ({
36201
36472
  });
36202
36473
  });
36203
36474
  travel.viewTransition = viewTransition;
36475
+ // Subscribed first, so it reads before the scrub below writes anything.
36476
+ viewTransition.ready.then(() => {
36477
+ placeTransitionWindow(travel, TRAVEL_NAME);
36478
+ }, ignoreSkipped);
36204
36479
  if (scrub) {
36205
36480
  // Said only now: the release has to have something to let go of, and the
36206
36481
  // transition did not exist a line above.
@@ -36400,16 +36675,20 @@ const RouteTravel = ({
36400
36675
  // what makes the document tall enough to hold that offset again —
36401
36676
  // and the recording stays deaf until then, over the clamp the swap
36402
36677
  // back makes on the way. Its own deafness rather than the travel's:
36403
- // the travel ends here, and this outlives it by a render.
36678
+ // the travel ends here, and this outlives it by a render. Written
36679
+ // once the commit that says so has ended, as arriveAtScrollPosition
36680
+ // asks.
36404
36681
  const resumeScrollRecording = suspendScrollRecording();
36405
36682
  const stopWatchingRender = observeRouteRender(() => {
36406
36683
  stopWatchingRender();
36407
- const {
36408
- route,
36409
- params
36410
- } = travel.fromPage;
36411
- arriveAtScrollPosition(route.buildUrl(params));
36412
- resumeScrollRecording();
36684
+ queueMicrotask(() => {
36685
+ const {
36686
+ route,
36687
+ params
36688
+ } = travel.fromPage;
36689
+ arriveAtScrollPosition(route.buildUrl(params));
36690
+ resumeScrollRecording();
36691
+ });
36413
36692
  });
36414
36693
  }
36415
36694
  releaseRendering();
@@ -39142,34 +39421,6 @@ registerNaviCommand("--navi-unselect", (source, event) => {
39142
39421
  };
39143
39422
  });
39144
39423
 
39145
- /**
39146
- * Runs `fn` and commits whatever it re-renders before returning, instead of
39147
- * letting Preact batch it into the next microtask. Layout effects of what gets
39148
- * mounted run inside the call too, exactly as they would on any other commit.
39149
- *
39150
- * For the caller that has to read the DOM it just asked for — measuring an
39151
- * element whose content it mounts in the same breath — and cannot wait a tick
39152
- * to do it, because what comes after is a browser event still in flight
39153
- * (preventDefault, focus placement) that no longer accepts being answered late.
39154
- *
39155
- * `options.debounceRendering` is Preact's own hook for deciding *when* the
39156
- * render queue drains; swapping it for "right now" for the duration of the call
39157
- * is exactly how preact/compat implements React's flushSync. Reserve it for the
39158
- * case above: rendering synchronously in the middle of an event gives up the
39159
- * batching that makes several state changes one commit.
39160
- */
39161
- const flushSyncRendering = (fn) => {
39162
- const debounceRenderingPrevious = options.debounceRendering;
39163
- options.debounceRendering = (drainRenderQueue) => {
39164
- drainRenderQueue();
39165
- };
39166
- try {
39167
- fn();
39168
- } finally {
39169
- options.debounceRendering = debounceRenderingPrevious;
39170
- }
39171
- };
39172
-
39173
39424
  /**
39174
39425
  * When a popup builds what it holds, and when it throws it away.
39175
39426
  *
@@ -39204,7 +39455,11 @@ const flushSyncRendering = (fn) => {
39204
39455
  *
39205
39456
  * "always" is for content something else depends on before any opening: a
39206
39457
  * value the popup's owner reads off its own children, fields a form around it
39207
- * collects on submit, a size measured from outside.
39458
+ * collects on submit, a size measured from outside. When the popup's owner
39459
+ * cannot tell at mount whether anything will depend on it, the content waits
39460
+ * and whatever depends on it asks for it, closed, with
39461
+ * `openController.buildContent()` (a picker's drawing reading the value its
39462
+ * popup holds, see usePickerContext).
39208
39463
  *
39209
39464
  * "idle" is "always" minus the cost on the critical render: the page appears
39210
39465
  * without the content, and the browser builds it in an idle moment after
@@ -39331,6 +39586,14 @@ const usePopupContentMount = (
39331
39586
  }
39332
39587
  }
39333
39588
  };
39589
+ // A plain render rather than a flush: the popup stays closed, and nothing is
39590
+ // about to measure it.
39591
+ openController.buildContent = () => {
39592
+ if (contentMountedRef.current) {
39593
+ return;
39594
+ }
39595
+ setContentMounted(true);
39596
+ };
39334
39597
  openController.unmountContent =
39335
39598
  mount === "while-opened"
39336
39599
  ? () => {
@@ -41456,6 +41719,65 @@ const keptOrFirstDefinedChildUIState = (children, fallbackState, kept) => {
41456
41719
  }
41457
41720
  return kept;
41458
41721
  };
41722
+ // The same claim, for an object: a key whose control is not there cannot be
41723
+ // contradicted by it — a slide not built yet (SlideContainer mount="near"), a
41724
+ // row scrolled out, a page parked. A key of `kept` stays until a control of
41725
+ // that name, there, says otherwise; a value put ON the group replaces the
41726
+ // whole object, which is how a key is dropped on purpose.
41727
+ const keptAndNamedChildUIStates = (children, fallbackState, kept) => {
41728
+ const groupValues =
41729
+ kept !== null && typeof kept === "object" && !Array.isArray(kept)
41730
+ ? { ...kept }
41731
+ : {};
41732
+ return Object.assign(
41733
+ groupValues,
41734
+ readNamedChildUIStates(children, { warnNameless: true }),
41735
+ );
41736
+ };
41737
+ // What the named controls among `children` say, one key each. A nameless
41738
+ // GROUP is a grouping, not a value: it exists to hold its children together
41739
+ // (a WheelGroup sharing navigation, a fieldset-ish cluster) without claiming a
41740
+ // key of its own, so what its controls say is written as if they were here.
41741
+ // Naming it is what turns the same group into one key — see ControlGroup's
41742
+ // `name`. Only what its CONTROLS say: such a grouping is handed the whole
41743
+ // object around it (see distributeChildUIState below) and keeps what they do
41744
+ // not answer for, so merged whole it would carry its siblings' keys back up,
41745
+ // stale.
41746
+ const readNamedChildUIStates = (children, { warnNameless }) => {
41747
+ const values = {};
41748
+ for (const child of children) {
41749
+ const { name, emptyUIState } = child;
41750
+ // A control holding nothing writes its own empty, not a hole: the key is in
41751
+ // the object either way, and what is read from it keeps the shape the
41752
+ // reader was promised (see resolveEmptyUIState).
41753
+ const uiState =
41754
+ child.uiState === undefined && emptyUIState !== undefined
41755
+ ? emptyUIState
41756
+ : child.uiState;
41757
+ if (!name) {
41758
+ if (isNamelessGrouping(child, uiState)) {
41759
+ Object.assign(
41760
+ values,
41761
+ child.keepsUnansweredKeys
41762
+ ? readNamedChildUIStates(child.getChildControllers(), {
41763
+ warnNameless: false,
41764
+ })
41765
+ : uiState,
41766
+ );
41767
+ continue;
41768
+ }
41769
+ if (warnNameless) {
41770
+ console.warn(
41771
+ "A group child is missing a name property, its state won't be included in the group state",
41772
+ child,
41773
+ );
41774
+ }
41775
+ continue;
41776
+ }
41777
+ values[name] = uiState;
41778
+ }
41779
+ return values;
41780
+ };
41459
41781
 
41460
41782
  // Default aggregate/distribute implementations keyed by controlType or stateType.
41461
41783
  // Looked up in useUIGroupStateController to fill in omitted aggregateChildStates /
@@ -41525,37 +41847,7 @@ const GROUP_DEFAULTS = {
41525
41847
  childControlFilter: (child) => {
41526
41848
  return child.controlType !== "button" && child.controlType !== "link";
41527
41849
  },
41528
- aggregateChildStates: (children) => {
41529
- const groupValues = {};
41530
- for (const child of children) {
41531
- const { name, emptyUIState } = child;
41532
- // A control holding nothing writes its own empty, not a hole: the key
41533
- // is in the object either way, and what is read from it keeps the shape
41534
- // the reader was promised (see resolveEmptyUIState).
41535
- const uiState =
41536
- child.uiState === undefined && emptyUIState !== undefined
41537
- ? emptyUIState
41538
- : child.uiState;
41539
- if (!name) {
41540
- // A nameless GROUP is a grouping, not a value: it exists to hold its
41541
- // children together (a WheelGroup sharing navigation, a fieldset-ish
41542
- // cluster) without claiming a key of its own, so what it holds is
41543
- // merged in as if its children had been written here. Naming it is
41544
- // what turns the same group into one key — see ControlGroup's `name`.
41545
- if (isNamelessGrouping(child, uiState)) {
41546
- Object.assign(groupValues, uiState);
41547
- continue;
41548
- }
41549
- console.warn(
41550
- "A group child is missing a name property, its state won't be included in the group state",
41551
- child,
41552
- );
41553
- continue;
41554
- }
41555
- groupValues[name] = uiState;
41556
- }
41557
- return groupValues;
41558
- },
41850
+ aggregateChildStates: keptAndNamedChildUIStates,
41559
41851
  distributeChildUIState: (newUIState, child) => {
41560
41852
  const childName = child.name;
41561
41853
  if (
@@ -41579,7 +41871,8 @@ const GROUP_DEFAULTS = {
41579
41871
  return childUIState;
41580
41872
  }
41581
41873
  // Merged in on the way up (see above), so on the way down it takes the
41582
- // whole object and picks out its own keys — the same value it produced.
41874
+ // whole object: its controls pick out their own keys, and it keeps the
41875
+ // rest for those of its controls that are not there yet.
41583
41876
  // Only for a child with no name of its own: the same condition the
41584
41877
  // aggregate applies before merging. A NAMED child holding an object (a
41585
41878
  // `type="object"` picker, say) answers for one key, and the object it is
@@ -42057,6 +42350,11 @@ const useUIGroupStateController = (
42057
42350
  // a picker filling its popup, a value prop) rather than worked out from
42058
42351
  // its children. What it protects is read in onChange.
42059
42352
  stateGivenFromAbove: hasValueProp || hasDefaultValueProp,
42353
+ // Its value carries keys none of its controls answers for (see
42354
+ // keptAndNamedChildUIStates): a group merging it as a nameless
42355
+ // grouping reads its controls instead of its value.
42356
+ keepsUnansweredKeys:
42357
+ resolvedAggregateChildStates === keptAndNamedChildUIStates,
42060
42358
  uiStateSignal,
42061
42359
  wantRequesterButtonState,
42062
42360
  ref,
@@ -52536,7 +52834,8 @@ const useExpandableContext = partName => {
52536
52834
  * other three, intent on the UI part (pointer entering it, focus landing in
52537
52835
  * it) builds the content ahead of the click; `"while-opened"` content is
52538
52836
  * built by a press on it only, and thrown away if that press does not
52539
- * expand it.
52837
+ * expand it. Named controls in content not built are kept by the group
52838
+ * around them (docs/control_object.md, "A control that is not there").
52540
52839
  */
52541
52840
  const Expandable = props => {
52542
52841
  import.meta.css = [css$R, "@jsenv/navi/src/control/expandable/expandable.jsx"];
@@ -59598,12 +59897,19 @@ const readArea = slideElement => slideElement.getAttribute("data-slide-area") ||
59598
59897
  * would bring alongside (neighbours on the map, along the axes
59599
59898
  * `travelByDrag` opens, the other end of a `loop` included) — and kept from
59600
59899
  * then on. For slides that are costly to build (a grid of a few hundred
59601
- * controls each): opening builds the slide shown and its neighbours rather
59900
+ * items each): opening builds the slide shown and its neighbours rather
59602
59901
  * than all of them, and a jump to a far slide builds that one and none of
59603
59902
  * those in between, which a travel never shows. The box is then as large as
59604
59903
  * the largest slide BUILT so far (see `sizing`), so it grows as a larger one
59605
59904
  * is reached: give it a size of its own when the slides differ.
59606
59905
  * `<Slide mount>` says it for one slide — a heavy one among light ones.
59906
+ * A slide not built has no controls, and a group around the container keeps
59907
+ * their keys for them: what it was handed reaches them once built. What is
59908
+ * shared between slides is written through that group (its `signal`, a
59909
+ * `--navi-update` aimed at it), never into one control's own `signal`, which
59910
+ * nobody reads while that control is not built; and nothing validates a
59911
+ * field before its slide is built. See docs/control_object.md, "A control
59912
+ * that is not there".
59607
59913
  */
59608
59914
  const SlideContainer = ({
59609
59915
  layout = "row",
@@ -67997,6 +68303,69 @@ const toPopoverAnimation = animation => {
67997
68303
  return animation;
67998
68304
  };
67999
68305
 
68306
+ const PickerContext = createContext();
68307
+
68308
+ /*
68309
+ * The picker's context, for whatever draws its value. A picker told no value
68310
+ * holds what the control in its popup holds, and builds that popup only when
68311
+ * something reads it (see the mount decision in picker_custom.jsx): what the
68312
+ * picker draws itself it knows about, a drawing given as an element is the
68313
+ * caller's, and says so here by reading. In a layout effect, so the frame that
68314
+ * first shows the drawing already shows the value.
68315
+ */
68316
+ const usePickerContext = () => {
68317
+ const context = useContext(PickerContext);
68318
+ const requestValue = context?.requestValue;
68319
+ useLayoutEffect(() => {
68320
+ requestValue?.();
68321
+ }, [requestValue]);
68322
+ return context;
68323
+ };
68324
+
68325
+ /*
68326
+ * A typed picker (`type="date"`, `type="array"`, …) draws its value with a ui
68327
+ * of navi's own, installed as the very "ui" prop a caller overrides to draw
68328
+ * that value themselves. Marking navi's own tells the two apart afterwards:
68329
+ * an empty value is greyed as a placeholder only when the drawing is navi's —
68330
+ * a caller's ui may well be their way of writing "no filter", which is an
68331
+ * answer, not a blank (see navi-placeholder in picker.jsx).
68332
+ */
68333
+ const asPickerOwnUI = PickerUI => {
68334
+ PickerUI.isPickerOwnUI = true;
68335
+ return PickerUI;
68336
+ };
68337
+ const pickerUIIsNaviOwn = ui => {
68338
+ return Boolean(ui) && typeof ui === "object" && Boolean(ui.type?.isPickerOwnUI);
68339
+ };
68340
+
68341
+ /**
68342
+ * What the picker holds, read from inside its `ui` — for a drawing given as
68343
+ * an element (`ui={<MyCard game={game} />}`), which the picker cannot hand
68344
+ * props to. `value` is the state the picker holds right now: the answer the
68345
+ * popup just closed on, before the server has said anything, and put back if
68346
+ * the action fails. `loading` is the run in flight; `interactive` is false
68347
+ * while the picker is disabled, read-only or busy.
68348
+ *
68349
+ * @returns {{ value: any, loading: boolean, interactive: boolean } | null}
68350
+ * null outside a picker.
68351
+ */
68352
+ const usePickerState = () => {
68353
+ const context = usePickerContext();
68354
+ if (!context) {
68355
+ return null;
68356
+ }
68357
+ const {
68358
+ value,
68359
+ loading,
68360
+ interactive
68361
+ } = context;
68362
+ return {
68363
+ value,
68364
+ loading,
68365
+ interactive
68366
+ };
68367
+ };
68368
+
68000
68369
  installImportMetaCssBuild(import.meta);
68001
68370
  const css$B = /* css */`.navi_picker_callout_dock > [data-picker-content] {
68002
68371
  display: none;
@@ -68224,6 +68593,12 @@ const PickerCustom = props => {
68224
68593
  const idDefault = useId();
68225
68594
  const controlId = useContext(ControlIdContext);
68226
68595
  props.id = props.id || controlId || idDefault;
68596
+ // Given no value, the picker holds what the control in its popup holds (see
68597
+ // the `mount` decision below) — and a group around it collects that from the
68598
+ // moment both exist.
68599
+ const valueInPopup = !isControlValueGivenByProps(props);
68600
+ const parentController = useContext(ParentUIStateControllerContext);
68601
+ const answersToGroup = Boolean(parentController) && !props.standalone;
68227
68602
  // Same narrow-container/maxWidth-compact heuristic Popup itself uses (see
68228
68603
  // popup_mode.jsx's own useResolvedPopupMode) — frozen for the lifetime of an opening
68229
68604
  // (computed when closed, stable while open, so a screen resize mid-session
@@ -68456,12 +68831,20 @@ const PickerCustom = props => {
68456
68831
  const requestInteraction = options => {
68457
68832
  dispatchRequestInteraction(ref.current, options);
68458
68833
  };
68834
+
68835
+ // What a drawing of the caller's calls as it reads the value (see
68836
+ // usePickerContext): the popup holding it, built while closed. Only when
68837
+ // `mount` is left to the picker — a caller who set it has said when.
68838
+ const requestValue = useStableCallback(() => {
68839
+ openController.buildContent();
68840
+ });
68459
68841
  const {
68460
68842
  onActionStart,
68461
68843
  children,
68462
68844
  uiAction: uiActionProp
68463
68845
  } = props;
68464
68846
  Object.assign(pickerProps, {
68847
+ "requestValue": valueInPopup && props.mount === undefined ? requestValue : undefined,
68465
68848
  "aria-expanded": Boolean(expanded),
68466
68849
  "onActionStart": e => {
68467
68850
  onActionStart?.(e);
@@ -68500,13 +68883,12 @@ const PickerCustom = props => {
68500
68883
  anchor: props.anchor || props.ref,
68501
68884
  openController,
68502
68885
  // A picker whose value was never given to it reads it off the control in
68503
- // its popup (see useUIFacadeStateController): the trigger shows what the
68504
- // list inside says is selected, so that list has to exist before anyone
68505
- // opens anything. Told a value — even an empty one — the picker owns it
68506
- // and pushes it down instead, leaving the popup free to build its
68507
- // content only when it is first opened (see popup_content_mount.js).
68508
- // A caller who knows better says so with the popup's own props.
68509
- mount: props.mount ?? (isControlValueGivenByProps(props) ? MOUNT_DEFAULT : "always"),
68886
+ // its popup (see useUIFacadeStateController), so that control is built as
68887
+ // soon as something reads the value: at mount when the picker sees the
68888
+ // reader, otherwise when a drawing of the caller's asks (requestValue
68889
+ // above). Told a value — even an empty one — the picker pushes it down,
68890
+ // and the popup waits for its first open (see popup_content_mount.js).
68891
+ mount: props.mount ?? (valueInPopup && isPickerValueReadAtMount(props, answersToGroup) ? "always" : MOUNT_DEFAULT),
68510
68892
  // Not on pickerProps (the trigger): commands.js's own
68511
68893
  // resolveClosestExpandable() does `el.closest("[aria-expanded]")` to
68512
68894
  // find where to dispatch navi_request_open/navi_request_close — and
@@ -68793,6 +69175,29 @@ const PickerCustom = props => {
68793
69175
  });
68794
69176
  };
68795
69177
 
69178
+ // Whether something reads a picker's value before its popup has opened, as
69179
+ // far as the picker sees from its own props: the drawing navi renders for it
69180
+ // (its own, or the typed one PickerTypeResolver installs — `ui` is still unset
69181
+ // here), one of navi's Picker.UI.*, a component handed `value`, the clear
69182
+ // cross, a form submitting it by name, a group it answers to. A headless
69183
+ // picker draws nothing, and an element `ui` is the caller's: it reads through
69184
+ // usePickerContext, which asks for the value then.
69185
+ const isPickerValueReadAtMount = (props, answersToGroup) => {
69186
+ if (props.name !== undefined || answersToGroup) {
69187
+ return true;
69188
+ }
69189
+ if (props.variant === "headless") {
69190
+ return false;
69191
+ }
69192
+ if (props.clearable) {
69193
+ return true;
69194
+ }
69195
+ const {
69196
+ ui
69197
+ } = props;
69198
+ return ui === undefined || typeof ui === "function" || pickerUIIsNaviOwn(ui);
69199
+ };
69200
+
68796
69201
  // A hold declared on something AROUND the picker (a card opened by
68797
69202
  // `openOn="longpress"`, holding this one in its drawing): the finger going
68798
69203
  // down may be the start of that hold, so this picker opens on the click — which
@@ -69308,52 +69713,6 @@ const PickerConfirmBody = ({
69308
69713
  });
69309
69714
  };
69310
69715
 
69311
- const PickerContext = createContext();
69312
-
69313
- /*
69314
- * A typed picker (`type="date"`, `type="array"`, …) draws its value with a ui
69315
- * of navi's own, installed as the very "ui" prop a caller overrides to draw
69316
- * that value themselves. Marking navi's own tells the two apart afterwards:
69317
- * an empty value is greyed as a placeholder only when the drawing is navi's —
69318
- * a caller's ui may well be their way of writing "no filter", which is an
69319
- * answer, not a blank (see navi-placeholder in picker.jsx).
69320
- */
69321
- const asPickerOwnUI = PickerUI => {
69322
- PickerUI.isPickerOwnUI = true;
69323
- return PickerUI;
69324
- };
69325
- const pickerUIIsNaviOwn = ui => {
69326
- return Boolean(ui) && typeof ui === "object" && Boolean(ui.type?.isPickerOwnUI);
69327
- };
69328
-
69329
- /**
69330
- * What the picker holds, read from inside its `ui` — for a drawing given as
69331
- * an element (`ui={<MyCard game={game} />}`), which the picker cannot hand
69332
- * props to. `value` is the state the picker holds right now: the answer the
69333
- * popup just closed on, before the server has said anything, and put back if
69334
- * the action fails. `loading` is the run in flight; `interactive` is false
69335
- * while the picker is disabled, read-only or busy.
69336
- *
69337
- * @returns {{ value: any, loading: boolean, interactive: boolean } | null}
69338
- * null outside a picker.
69339
- */
69340
- const usePickerState = () => {
69341
- const context = useContext(PickerContext);
69342
- if (!context) {
69343
- return null;
69344
- }
69345
- const {
69346
- value,
69347
- loading,
69348
- interactive
69349
- } = context;
69350
- return {
69351
- value,
69352
- loading,
69353
- interactive
69354
- };
69355
- };
69356
-
69357
69716
  const PickerNaviMinute = props => {
69358
69717
  const Next = useNextResolver();
69359
69718
  const {
@@ -69475,44 +69834,6 @@ document.addEventListener(
69475
69834
  { capture: true, passive: true },
69476
69835
  );
69477
69836
 
69478
- const FRAME_TIMEOUT_MS = 100;
69479
-
69480
- /**
69481
- * Calls back once the browser has painted what is committed now.
69482
- *
69483
- * The callbacks of the next frame run before that frame paints, so the call is
69484
- * made from a task queued from inside one — the same way preact schedules
69485
- * useEffect. Unlike an effect, nothing runs this early: preact flushes a
69486
- * component's pending effects as soon as that component renders again, and a
69487
- * re-render before the paint is exactly what a caller waiting for the paint
69488
- * has to survive. A frame not coming at all (a background tab) still answers,
69489
- * late, through the timeout.
69490
- *
69491
- * @param {() => void} callback
69492
- * @returns {() => void} cancel
69493
- */
69494
- const afterPaint = (callback) => {
69495
- let called = false;
69496
- let timeoutId;
69497
- let frameId;
69498
- const onFrame = () => {
69499
- if (called) {
69500
- return;
69501
- }
69502
- called = true;
69503
- clearTimeout(timeoutId);
69504
- cancelAnimationFrame(frameId);
69505
- timeoutId = setTimeout(callback, 0);
69506
- };
69507
- timeoutId = setTimeout(onFrame, FRAME_TIMEOUT_MS);
69508
- frameId = requestAnimationFrame(onFrame);
69509
- return () => {
69510
- called = true;
69511
- clearTimeout(timeoutId);
69512
- cancelAnimationFrame(frameId);
69513
- };
69514
- };
69515
-
69516
69837
  const LoadingDotsSvg = () => {
69517
69838
  return jsxs("svg", {
69518
69839
  viewBox: "0 0 200 200",
@@ -77763,7 +78084,7 @@ const PickerObjectUI = /*#__PURE__*/asPickerOwnUI(() => {
77763
78084
  const {
77764
78085
  value,
77765
78086
  placeholder
77766
- } = useContext(PickerContext);
78087
+ } = usePickerContext();
77767
78088
  if (uiStateHoldsNothing(value)) {
77768
78089
  if (!placeholder) {
77769
78090
  return null;
@@ -77799,7 +78120,7 @@ const PickerArrayUI = /*#__PURE__*/asPickerOwnUI(() => {
77799
78120
  value,
77800
78121
  placeholder,
77801
78122
  maxLines
77802
- } = useContext(PickerContext);
78123
+ } = usePickerContext();
77803
78124
  if (uiStateHoldsNothing(value)) {
77804
78125
  if (!placeholder) {
77805
78126
  return null;
@@ -77881,7 +78202,7 @@ const PickerColorUI = /*#__PURE__*/asPickerOwnUI(() => {
77881
78202
  const {
77882
78203
  value,
77883
78204
  placeholder
77884
- } = useContext(PickerContext);
78205
+ } = usePickerContext();
77885
78206
  if (!value) {
77886
78207
  if (!placeholder) {
77887
78208
  return jsx(Color, {});
@@ -77905,7 +78226,7 @@ const PickerDateUI = /*#__PURE__*/asPickerOwnUI(props => {
77905
78226
  const {
77906
78227
  value,
77907
78228
  placeholder
77908
- } = useContext(PickerContext);
78229
+ } = usePickerContext();
77909
78230
  if (!value) {
77910
78231
  if (!placeholder) {
77911
78232
  return jsx(Time, {
@@ -77937,7 +78258,7 @@ const PickerMonthUI = /*#__PURE__*/asPickerOwnUI(props => {
77937
78258
  const {
77938
78259
  value,
77939
78260
  placeholder
77940
- } = useContext(PickerContext);
78261
+ } = usePickerContext();
77941
78262
  if (!value) {
77942
78263
  if (!placeholder) {
77943
78264
  return jsx(Time, {
@@ -77968,7 +78289,7 @@ const PickerWeekUI = /*#__PURE__*/asPickerOwnUI(props => {
77968
78289
  const {
77969
78290
  value,
77970
78291
  placeholder
77971
- } = useContext(PickerContext);
78292
+ } = usePickerContext();
77972
78293
  if (!value) {
77973
78294
  if (!placeholder) {
77974
78295
  return jsx(Time, {
@@ -77999,7 +78320,7 @@ const PickerTimeUI = /*#__PURE__*/asPickerOwnUI(props => {
77999
78320
  const {
78000
78321
  value,
78001
78322
  placeholder
78002
- } = useContext(PickerContext);
78323
+ } = usePickerContext();
78003
78324
  if (!value) {
78004
78325
  if (!placeholder) {
78005
78326
  return jsx(Time, {
@@ -78030,7 +78351,7 @@ const PickerDurationUI = /*#__PURE__*/asPickerOwnUI(props => {
78030
78351
  const {
78031
78352
  value,
78032
78353
  placeholder
78033
- } = useContext(PickerContext);
78354
+ } = usePickerContext();
78034
78355
  if (!value) {
78035
78356
  if (!placeholder) {
78036
78357
  return jsx(Time, {
@@ -78060,7 +78381,7 @@ const PickerDatetimeUI = /*#__PURE__*/asPickerOwnUI(props => {
78060
78381
  const {
78061
78382
  value,
78062
78383
  placeholder
78063
- } = useContext(PickerContext);
78384
+ } = usePickerContext();
78064
78385
  if (!value) {
78065
78386
  if (!placeholder) {
78066
78387
  return jsx(Time, {
@@ -78089,7 +78410,7 @@ const PickerFileUI = /*#__PURE__*/asPickerOwnUI(() => {
78089
78410
  const {
78090
78411
  value,
78091
78412
  placeholder
78092
- } = useContext(PickerContext);
78413
+ } = usePickerContext();
78093
78414
  if (!value) {
78094
78415
  if (!placeholder) {
78095
78416
  return null;
@@ -78623,7 +78944,11 @@ const PickerButton = props => {
78623
78944
  // a piece of, waiting as a whole (see the `loadingOutline` doc).
78624
78945
  loadingOutline,
78625
78946
  // Where the outline runs, in px from the picker's box: negative is outside.
78626
- loadingOutlineInset = -2
78947
+ loadingOutlineInset = -2,
78948
+ // Builds the popup this picker's value is read from, for a drawing that
78949
+ // reads it (see usePickerContext). Set by PickerCustom, only while that
78950
+ // value lives in the popup.
78951
+ requestValue
78627
78952
  } = props;
78628
78953
  // A word in a sentence is never truncated — and the clamp's overflow: hidden
78629
78954
  // would cut its dotted underline, which sits on the edge of the line box
@@ -78719,7 +79044,8 @@ const PickerButton = props => {
78719
79044
  id: inputProps.id,
78720
79045
  interactive,
78721
79046
  loading,
78722
- clearConfirm
79047
+ clearConfirm,
79048
+ requestValue
78723
79049
  };
78724
79050
  return (
78725
79051
  /* Read-only crosses into everything the picker is made of: what it really
@@ -78770,7 +79096,8 @@ const PickerButton = props => {
78770
79096
  error: undefined,
78771
79097
  dayLabel: undefined,
78772
79098
  loadingOutline: undefined,
78773
- loadingOutlineInset: undefined
79099
+ loadingOutlineInset: undefined,
79100
+ requestValue: undefined
78774
79101
  // This wrapper will receive keyboard event bubbling from the picker popup content
78775
79102
  // we re-dispatch on the input (to get escape to close for instance)
78776
79103
  ,
@@ -78984,7 +79311,7 @@ const PickerClear = ({
78984
79311
  children,
78985
79312
  ...rest
78986
79313
  }) => {
78987
- const pickerContext = useContext(PickerContext);
79314
+ const pickerContext = usePickerContext();
78988
79315
  if (!pickerContext) {
78989
79316
  return null;
78990
79317
  }
@@ -79256,7 +79583,7 @@ const PickerDefaultUI = () => {
79256
79583
  const {
79257
79584
  value,
79258
79585
  placeholder
79259
- } = useContext(PickerContext);
79586
+ } = usePickerContext();
79260
79587
  if (!value) {
79261
79588
  if (!placeholder) {
79262
79589
  return null;