@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.
- package/dist/dev/jsenv_navi.js +522 -192
- package/dist/dev/jsenv_navi.js.map +22 -20
- package/dist/jsenv_navi.js +515 -188
- package/dist/jsenv_navi.js.map +22 -20
- package/docs/AI_INSTRUCTIONS.md +9 -5
- package/docs/control_object.md +76 -2
- package/docs/navigation.md +4 -1
- package/docs/popup_open.md +21 -13
- package/docs/scroll.md +53 -0
- package/package.json +1 -1
package/dist/jsenv_navi.js
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
11024
|
-
|
|
11025
|
-
|
|
11026
|
-
|
|
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
|
|
33759
|
-
//
|
|
33760
|
-
//
|
|
33761
|
-
//
|
|
33762
|
-
//
|
|
33763
|
-
//
|
|
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
|
-
|
|
36408
|
-
|
|
36409
|
-
|
|
36410
|
-
|
|
36411
|
-
|
|
36412
|
-
|
|
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:
|
|
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
|
|
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
|
-
*
|
|
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)
|
|
68504
|
-
//
|
|
68505
|
-
//
|
|
68506
|
-
//
|
|
68507
|
-
//
|
|
68508
|
-
|
|
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
|
-
} =
|
|
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
|
-
} =
|
|
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
|
-
} =
|
|
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
|
-
} =
|
|
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
|
-
} =
|
|
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
|
-
} =
|
|
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
|
-
} =
|
|
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
|
-
} =
|
|
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
|
-
} =
|
|
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
|
-
} =
|
|
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 =
|
|
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
|
-
} =
|
|
79586
|
+
} = usePickerContext();
|
|
79260
79587
|
if (!value) {
|
|
79261
79588
|
if (!placeholder) {
|
|
79262
79589
|
return null;
|