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