@jsenv/navi 0.29.417 → 0.29.419
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/README.md +1 -1
- package/dist/dev/jsenv_navi.js +347 -83
- package/dist/dev/jsenv_navi.js.map +12 -12
- package/dist/jsenv_navi.js +345 -81
- package/dist/jsenv_navi.js.map +12 -12
- package/docs/AI_INSTRUCTIONS.md +177 -258
- package/docs/actions.md +164 -251
- package/docs/autofocus.md +66 -83
- package/docs/control_object.md +117 -214
- package/docs/control_value.md +121 -185
- package/docs/create_and_edit.md +159 -254
- package/docs/css_architecture.md +196 -260
- package/docs/data_states.md +41 -44
- package/docs/dialog_shape.md +91 -134
- package/docs/drag_interactions.md +291 -454
- package/docs/drag_to_travel.md +152 -748
- package/docs/dynamic_import.md +119 -136
- package/docs/error_handling.md +75 -127
- package/docs/field_validation.md +49 -89
- package/docs/form_changed.md +61 -78
- package/docs/group.md +141 -0
- package/docs/i18n.md +31 -76
- package/docs/interactions.md +170 -326
- package/docs/list_action.md +36 -51
- package/docs/list_refresh.md +73 -115
- package/docs/mobile_touch.md +56 -62
- package/docs/navigation.md +377 -723
- package/docs/network_policy.md +61 -75
- package/docs/pan_zoom.md +62 -103
- package/docs/popup_backdrop.md +128 -168
- package/docs/popup_lift.md +179 -268
- package/docs/popup_open.md +380 -651
- package/docs/resource.md +228 -408
- package/docs/route_transitions.md +333 -473
- package/docs/safe_area.md +102 -89
- package/docs/scroll.md +175 -317
- package/docs/state_binding.md +40 -84
- package/docs/testid.md +18 -23
- package/docs/typography.md +111 -141
- package/docs/view_transitions.md +106 -104
- package/docs/z_index.md +61 -109
- package/package.json +2 -2
- package/docs/badge_list.md +0 -83
- package/docs/control_group.md +0 -168
- package/docs/mobile_layout_pitfalls.md +0 -145
package/dist/jsenv_navi.js
CHANGED
|
@@ -10826,6 +10826,28 @@ const suspendScrollRecording = () => {
|
|
|
10826
10826
|
};
|
|
10827
10827
|
};
|
|
10828
10828
|
|
|
10829
|
+
// A list scrolling the document that opens on a row puts the document where
|
|
10830
|
+
// that row is, by measuring it (see placeWhereHeld in list.jsx). The offset kept
|
|
10831
|
+
// for the url is pixels of the page as it was drawn — and a list draws a window
|
|
10832
|
+
// of its rows and holds the room of the others with fillers of an estimated
|
|
10833
|
+
// height, so the same pixels now fall on other rows. Put back after the list
|
|
10834
|
+
// has placed itself, they would undo it, in the very picture a route transition
|
|
10835
|
+
// takes. So the list holds the document while it is placing itself, and the
|
|
10836
|
+
// url's offset is not put back meanwhile. Counted: two lists can share one
|
|
10837
|
+
// document.
|
|
10838
|
+
let documentHoldCount = 0;
|
|
10839
|
+
const holdDocumentScroll = () => {
|
|
10840
|
+
documentHoldCount++;
|
|
10841
|
+
let released = false;
|
|
10842
|
+
return () => {
|
|
10843
|
+
if (released) {
|
|
10844
|
+
return;
|
|
10845
|
+
}
|
|
10846
|
+
released = true;
|
|
10847
|
+
documentHoldCount--;
|
|
10848
|
+
};
|
|
10849
|
+
};
|
|
10850
|
+
|
|
10829
10851
|
let installed = false;
|
|
10830
10852
|
const installScrollRestoration = () => {
|
|
10831
10853
|
if (installed) {
|
|
@@ -10860,6 +10882,9 @@ const installScrollRestoration = () => {
|
|
|
10860
10882
|
if (positionOnLoad && (positionOnLoad.x || positionOnLoad.y)) {
|
|
10861
10883
|
const stopListening = observeRouteRender(() => {
|
|
10862
10884
|
stopListening();
|
|
10885
|
+
if (documentHoldCount) {
|
|
10886
|
+
return;
|
|
10887
|
+
}
|
|
10863
10888
|
scrollTo(positionOnLoad);
|
|
10864
10889
|
});
|
|
10865
10890
|
}
|
|
@@ -10874,6 +10899,11 @@ const restoreScrollPosition = (url) => {
|
|
|
10874
10899
|
if (!position) {
|
|
10875
10900
|
return false;
|
|
10876
10901
|
}
|
|
10902
|
+
if (documentHoldCount) {
|
|
10903
|
+
// There is a place to go back to, and the list holding the document is
|
|
10904
|
+
// putting it back (see holdDocumentScroll).
|
|
10905
|
+
return true;
|
|
10906
|
+
}
|
|
10877
10907
|
scrollTo(position);
|
|
10878
10908
|
return true;
|
|
10879
10909
|
};
|
|
@@ -14446,7 +14476,11 @@ const createOpenController = (
|
|
|
14446
14476
|
const applyChange = () => {
|
|
14447
14477
|
// Recorded with the change itself: what the DOM shows is what a render
|
|
14448
14478
|
// landing between the ask and the picture must draw (see `openedInDom`).
|
|
14449
|
-
|
|
14479
|
+
// An opening records it inside the change, once its content is built
|
|
14480
|
+
// (see open() below).
|
|
14481
|
+
if (!opened) {
|
|
14482
|
+
controller.openedInDom = false;
|
|
14483
|
+
}
|
|
14450
14484
|
change();
|
|
14451
14485
|
};
|
|
14452
14486
|
const { transitionChange } = controller;
|
|
@@ -14726,15 +14760,17 @@ const createOpenController = (
|
|
|
14726
14760
|
controller.mountContent?.();
|
|
14727
14761
|
// Only now — after the content has been built, before openEffect
|
|
14728
14762
|
// shows it. Dialog/Popover recompute aria-expanded and navi-hidden
|
|
14729
|
-
// from
|
|
14763
|
+
// from `openedInDom` on every render, and mountContent above renders
|
|
14730
14764
|
// synchronously: flipping it any earlier commits an already-open DOM
|
|
14731
14765
|
// (aria-expanded "true", navi-hidden gone) before openEffect has run
|
|
14732
14766
|
// a single statement, so the "closed" frame it pins to transition
|
|
14733
14767
|
// from is in fact the open one and the entrance animation has
|
|
14734
|
-
// nothing to play
|
|
14735
|
-
//
|
|
14736
|
-
//
|
|
14737
|
-
// popup_content_mount.js and
|
|
14768
|
+
// nothing to play — on every first opening that builds its content.
|
|
14769
|
+
// It also gives the content it just built the opening it is
|
|
14770
|
+
// documented to observe — mounted while the popup reads as closed,
|
|
14771
|
+
// told it opened right after (see popup_content_mount.js and
|
|
14772
|
+
// use_displayed_layout_effect.js).
|
|
14773
|
+
controller.openedInDom = true;
|
|
14738
14774
|
controller.opened = true;
|
|
14739
14775
|
// Which press it opened during, so the release of that press is not
|
|
14740
14776
|
// read as somebody dismissing it (see openedDuringThisPress).
|
|
@@ -15125,7 +15161,14 @@ const useOpenPropsEffectOnOpenController = (
|
|
|
15125
15161
|
lastRunOpenRef.current = open;
|
|
15126
15162
|
|
|
15127
15163
|
if (isFirstRun) {
|
|
15128
|
-
|
|
15164
|
+
// "interaction" is said even over what the caller holds reading open: a
|
|
15165
|
+
// popup built by the request that opens it — a dialog standing in until
|
|
15166
|
+
// its signal said open, see DialogStandIn in dialog.jsx — mounts BECAUSE
|
|
15167
|
+
// of that opening.
|
|
15168
|
+
const mountOpenReason =
|
|
15169
|
+
open && defaultOpen === "interaction"
|
|
15170
|
+
? "interaction"
|
|
15171
|
+
: open || defaultOpen;
|
|
15129
15172
|
if (mountOpenReason) {
|
|
15130
15173
|
// Whether this popup being open is something that just happened, or
|
|
15131
15174
|
// something that was already true when the page appeared. "interaction"
|
|
@@ -27774,6 +27817,18 @@ const useAsyncData = (promiseOrAction, {
|
|
|
27774
27817
|
|
|
27775
27818
|
// ─── useAction ────────────────────────────────────────────────────────────────
|
|
27776
27819
|
|
|
27820
|
+
// subscribe() calls back at once with the value the signal holds; what these
|
|
27821
|
+
// subscriptions are for is what changes after.
|
|
27822
|
+
const subscribeToChanges = (signal, onChange) => {
|
|
27823
|
+
let initial = true;
|
|
27824
|
+
return signal.subscribe(value => {
|
|
27825
|
+
if (initial) {
|
|
27826
|
+
initial = false;
|
|
27827
|
+
return;
|
|
27828
|
+
}
|
|
27829
|
+
onChange(value);
|
|
27830
|
+
});
|
|
27831
|
+
};
|
|
27777
27832
|
const LoadingContext = createContext(null);
|
|
27778
27833
|
const actionPendingPromiseWeakMap = new WeakMap();
|
|
27779
27834
|
const dismissedActionWeakSet = new WeakSet();
|
|
@@ -27818,6 +27873,15 @@ const useActionAsyncData = (action, {
|
|
|
27818
27873
|
const runningState = action.runningStateSignal.peek();
|
|
27819
27874
|
const [, setTick] = useState(0);
|
|
27820
27875
|
const routePage = useContext(RoutePageContext);
|
|
27876
|
+
// What this render read of the action, for the subscriptions below to tell
|
|
27877
|
+
// a change that came between the render and them from none at all.
|
|
27878
|
+
const readRef = useRef(null);
|
|
27879
|
+
readRef.current = {
|
|
27880
|
+
runningState,
|
|
27881
|
+
data: action.dataSignal.peek(),
|
|
27882
|
+
params: action.paramsSignal.peek(),
|
|
27883
|
+
settling: action.paramsSettlingSignal.peek()
|
|
27884
|
+
};
|
|
27821
27885
|
useEffect(() => {
|
|
27822
27886
|
let stopOwingRender = null;
|
|
27823
27887
|
const rerender = () => {
|
|
@@ -27845,7 +27909,12 @@ const useActionAsyncData = (action, {
|
|
|
27845
27909
|
}
|
|
27846
27910
|
setTick(n => n + 1);
|
|
27847
27911
|
};
|
|
27848
|
-
|
|
27912
|
+
// A run under way is a new try: an error dismissed before it is not the
|
|
27913
|
+
// one it may end on.
|
|
27914
|
+
if (action.runningStateSignal.peek() === RUNNING) {
|
|
27915
|
+
dismissedActionWeakSet.delete(action);
|
|
27916
|
+
}
|
|
27917
|
+
const unsubscribeFromRunningState = subscribeToChanges(action.runningStateSignal, state => {
|
|
27849
27918
|
if (state === RUNNING) {
|
|
27850
27919
|
dismissedActionWeakSet.delete(action);
|
|
27851
27920
|
}
|
|
@@ -27856,38 +27925,24 @@ const useActionAsyncData = (action, {
|
|
|
27856
27925
|
// (a PUT upserting an item that a GET_MANY list already holds) changes the
|
|
27857
27926
|
// data while this action stays COMPLETED. Subscribing here re-renders
|
|
27858
27927
|
// through the same controlled path as the run state, instead of `.value`.
|
|
27859
|
-
|
|
27860
|
-
const unsubscribeFromData = action.dataSignal.subscribe(() => {
|
|
27861
|
-
if (dataNotificationIsInitial) {
|
|
27862
|
-
// subscribe() calls back synchronously with the current value
|
|
27863
|
-
dataNotificationIsInitial = false;
|
|
27864
|
-
return;
|
|
27865
|
-
}
|
|
27866
|
-
rerender();
|
|
27867
|
-
});
|
|
27928
|
+
const unsubscribeFromData = subscribeToChanges(action.dataSignal, rerender);
|
|
27868
27929
|
// The params say WHICH question this is, and this hook reads them: to know
|
|
27869
27930
|
// there is nothing to ask for, and to start the run it owns. A binding
|
|
27870
27931
|
// retargeting from no question to a question — a filter chosen, a first
|
|
27871
27932
|
// character typed — is announced by nothing above: a fresh target is IDLE
|
|
27872
27933
|
// holding no data, the very state the hook already sees.
|
|
27873
|
-
|
|
27874
|
-
const unsubscribeFromParams = action.paramsSignal.subscribe(() => {
|
|
27875
|
-
if (paramsNotificationIsInitial) {
|
|
27876
|
-
paramsNotificationIsInitial = false;
|
|
27877
|
-
return;
|
|
27878
|
-
}
|
|
27879
|
-
rerender();
|
|
27880
|
-
});
|
|
27934
|
+
const unsubscribeFromParams = subscribeToChanges(action.paramsSignal, rerender);
|
|
27881
27935
|
// A debounced binding waits before it retargets, so nothing above changes
|
|
27882
27936
|
// while the delay runs — but what is on screen is already out of date.
|
|
27883
|
-
|
|
27884
|
-
|
|
27885
|
-
|
|
27886
|
-
|
|
27887
|
-
|
|
27888
|
-
|
|
27937
|
+
const unsubscribeFromParamsSettling = subscribeToChanges(action.paramsSettlingSignal, rerender);
|
|
27938
|
+
// Subscribed once the browser has painted, so the action may have moved
|
|
27939
|
+
// since the render: a run that very render started may be over already,
|
|
27940
|
+
// and nothing would say so again. What the render read, against what is
|
|
27941
|
+
// there now — and nothing moved, nothing to draw again.
|
|
27942
|
+
const read = readRef.current;
|
|
27943
|
+
if (action.runningStateSignal.peek() !== read.runningState || action.dataSignal.peek() !== read.data || action.paramsSignal.peek() !== read.params || action.paramsSettlingSignal.peek() !== read.settling) {
|
|
27889
27944
|
rerender();
|
|
27890
|
-
}
|
|
27945
|
+
}
|
|
27891
27946
|
return () => {
|
|
27892
27947
|
unsubscribeFromRunningState();
|
|
27893
27948
|
unsubscribeFromData();
|
|
@@ -39315,6 +39370,17 @@ const useDisplayedLayoutEffect = (ref, callback, deps) => {
|
|
|
39315
39370
|
decisionDeferredToRef.current = popupMounting;
|
|
39316
39371
|
return;
|
|
39317
39372
|
}
|
|
39373
|
+
if (
|
|
39374
|
+
(el.tagName === "DIALOG" || el.hasAttribute("popover")) &&
|
|
39375
|
+
!isAncestorOpen(el)
|
|
39376
|
+
) {
|
|
39377
|
+
// Itself a closed <dialog> or [popover]: display:none by the UA's own
|
|
39378
|
+
// rules, which the markup says without asking the layout — asked, every
|
|
39379
|
+
// closed popup mounting would force a style recalculation, and a list
|
|
39380
|
+
// whose rows each hold a few mounts hundreds of them. The observer
|
|
39381
|
+
// below fires as for any element the ancestor hides.
|
|
39382
|
+
return;
|
|
39383
|
+
}
|
|
39318
39384
|
if (!isDisplayedDespiteClosedAncestor(el, ancestor)) {
|
|
39319
39385
|
// Ancestor is closed and took this element off screen with it — skip
|
|
39320
39386
|
// now; the observeAncestorOpenState call below will fire once it
|
|
@@ -51417,9 +51483,9 @@ const FixedBar = ({
|
|
|
51417
51483
|
* A page that overflows horizontally is a bug wherever it happens, and on
|
|
51418
51484
|
* Chrome Android it is a catastrophic one: the layout viewport inflates to the
|
|
51419
51485
|
* content and `position: fixed` centering goes with it (see
|
|
51420
|
-
* docs/
|
|
51421
|
-
* `overflow-x: clip`, and it works — at the price of making the
|
|
51422
|
-
* invisible: nothing sticks out anymore, so nothing says a fixed width, a
|
|
51486
|
+
* docs/safe_area.md#a-document-wider-than-the-screen). The remedy there is a
|
|
51487
|
+
* wrapper in `overflow-x: clip`, and it works — at the price of making the
|
|
51488
|
+
* cause invisible: nothing sticks out anymore, so nothing says a fixed width, a
|
|
51423
51489
|
* `min-width` or an unbreakable string is still oversized. This puts the
|
|
51424
51490
|
* signal back, without giving up the net.
|
|
51425
51491
|
*
|
|
@@ -64349,7 +64415,9 @@ const css$E = /* css */`
|
|
|
64349
64415
|
* @param {"always"|"idle"|"from-first-open"|"while-opened"} [props.mount] - When
|
|
64350
64416
|
* `children` are built and thrown away (see popup_content_mount.js).
|
|
64351
64417
|
* `"from-first-open"` (the default) builds them on the first open and keeps
|
|
64352
|
-
* them afterwards
|
|
64418
|
+
* them afterwards — and the dialog itself is built then too: until its first
|
|
64419
|
+
* request (an open, a press on what opens it, its signal saying open) it is
|
|
64420
|
+
* a bare `<dialog>` holding its id (see docs/popup_open.md). `"always"` builds them right away, for content something
|
|
64353
64421
|
* depends on while the popup is still closed: a value read off it, fields a
|
|
64354
64422
|
* surrounding form collects on submit, a size measured from outside.
|
|
64355
64423
|
* `"idle"` builds them in a browser idle moment after load — "always" minus
|
|
@@ -64370,11 +64438,122 @@ const Dialog = props => {
|
|
|
64370
64438
|
...props
|
|
64371
64439
|
});
|
|
64372
64440
|
}
|
|
64373
|
-
return jsx(
|
|
64441
|
+
return jsx(DialogStandIn, {
|
|
64374
64442
|
...props
|
|
64375
64443
|
});
|
|
64376
64444
|
};
|
|
64377
64445
|
|
|
64446
|
+
// A dialog nobody has asked for yet stands in as its bare <dialog>. That
|
|
64447
|
+
// element is all that reaches a closed dialog before its first opening: a
|
|
64448
|
+
// command finds it by id (`commandfor`) and asks it to open, a press on what
|
|
64449
|
+
// opens it announces itself on it (navi_open_press), --navi-toggle reads its
|
|
64450
|
+
// aria-expanded. The dialog proper — its open controller, its box, its focus
|
|
64451
|
+
// and pseudo-class wiring — is built at the first request: an open, a press
|
|
64452
|
+
// about to open it, its signal or its `open` saying open. A list whose rows
|
|
64453
|
+
// each hold a few dialogs mounts hundreds of them, and would pay for every one
|
|
64454
|
+
// at every mount, for openings that mostly never come. Built once, the dialog
|
|
64455
|
+
// stays: the next opening finds it there.
|
|
64456
|
+
const DialogStandIn = props => {
|
|
64457
|
+
const {
|
|
64458
|
+
ref,
|
|
64459
|
+
id,
|
|
64460
|
+
signal,
|
|
64461
|
+
value,
|
|
64462
|
+
open
|
|
64463
|
+
} = props;
|
|
64464
|
+
const ownRef = useRef();
|
|
64465
|
+
const dialogRef = ref || ownRef;
|
|
64466
|
+
// Why the dialog proper exists, once it does; never undone.
|
|
64467
|
+
const builtRef = useRef(null);
|
|
64468
|
+
const mountedRef = useRef(false);
|
|
64469
|
+
const isMount = !mountedRef.current;
|
|
64470
|
+
mountedRef.current = true;
|
|
64471
|
+
// Its own reading of the signal: the dialog of one card among many, told
|
|
64472
|
+
// apart by `value`, follows whether IT is the one open rather than every
|
|
64473
|
+
// write the others cause.
|
|
64474
|
+
const openedSignal = useMemo(() => signal ? computed(() => readOpened(signal.value, value)) : null, [signal, value]);
|
|
64475
|
+
const openedBySignal = openedSignal ? openedSignal.value : false;
|
|
64476
|
+
if (builtRef.current === null) {
|
|
64477
|
+
if (isMount && (needsDialogFromMount(props) || open || openedBySignal)) {
|
|
64478
|
+
builtRef.current = "mount";
|
|
64479
|
+
} else if (open || openedBySignal) {
|
|
64480
|
+
// Asked open while this stood in: the opening is happening now, and it
|
|
64481
|
+
// plays like any other (see defaultOpen "interaction").
|
|
64482
|
+
builtRef.current = "interaction";
|
|
64483
|
+
}
|
|
64484
|
+
}
|
|
64485
|
+
const [, setBuildCount] = useState(0);
|
|
64486
|
+
const warmRef = useRef(null);
|
|
64487
|
+
const build = reason => {
|
|
64488
|
+
if (builtRef.current !== null) {
|
|
64489
|
+
return;
|
|
64490
|
+
}
|
|
64491
|
+
builtRef.current = reason;
|
|
64492
|
+
flushSyncRendering(() => {
|
|
64493
|
+
setBuildCount(count => count + 1);
|
|
64494
|
+
});
|
|
64495
|
+
};
|
|
64496
|
+
const builtReason = builtRef.current;
|
|
64497
|
+
if (builtReason !== null) {
|
|
64498
|
+
return jsx(UncontrolledDialog, {
|
|
64499
|
+
...props,
|
|
64500
|
+
ref: dialogRef,
|
|
64501
|
+
defaultOpen: builtReason === "interaction" ? "interaction" : props.defaultOpen
|
|
64502
|
+
});
|
|
64503
|
+
}
|
|
64504
|
+
return jsx("dialog", {
|
|
64505
|
+
ref: dialogRef,
|
|
64506
|
+
id: id,
|
|
64507
|
+
className: "navi_dialog"
|
|
64508
|
+
// eslint-disable-next-line react/no-unknown-property
|
|
64509
|
+
,
|
|
64510
|
+
"navi-hidden": ""
|
|
64511
|
+
// eslint-disable-next-line react/no-unknown-property
|
|
64512
|
+
,
|
|
64513
|
+
"navi-out-of-flow": "",
|
|
64514
|
+
"aria-expanded": "false",
|
|
64515
|
+
"data-testid": props["data-testid"]
|
|
64516
|
+
// eslint-disable-next-line react/no-unknown-property
|
|
64517
|
+
,
|
|
64518
|
+
onnavi_request_open: e => {
|
|
64519
|
+
// Built in this same event, and asked again: the opening is the one
|
|
64520
|
+
// the dialog proper would have had — its source, its anchor, its value.
|
|
64521
|
+
build("request");
|
|
64522
|
+
const opened = dispatchCustomEvent(dialogRef.current, "navi_request_open", e.detail);
|
|
64523
|
+
if (!opened) {
|
|
64524
|
+
e.preventDefault();
|
|
64525
|
+
}
|
|
64526
|
+
}
|
|
64527
|
+
// eslint-disable-next-line react/no-unknown-property
|
|
64528
|
+
,
|
|
64529
|
+
onnavi_open_press: () => {
|
|
64530
|
+
// What the opening will read is asked for at once, as the dialog
|
|
64531
|
+
// proper does (see open_controller.js); the dialog itself two frames
|
|
64532
|
+
// later, so the pressed trigger is painted first — the click that
|
|
64533
|
+
// follows opens it built, or builds it itself when it comes sooner.
|
|
64534
|
+
if (signal) {
|
|
64535
|
+
preloadState(signal, readOpenValue(value));
|
|
64536
|
+
}
|
|
64537
|
+
if (warmRef.current) {
|
|
64538
|
+
return;
|
|
64539
|
+
}
|
|
64540
|
+
warmRef.current = requestFrameAfterNext(() => {
|
|
64541
|
+
warmRef.current = null;
|
|
64542
|
+
build("press");
|
|
64543
|
+
});
|
|
64544
|
+
}
|
|
64545
|
+
});
|
|
64546
|
+
};
|
|
64547
|
+
// What needs the dialog proper from the start: an opening at mount, content
|
|
64548
|
+
// built ahead of any opening, a history entry to read, an anchor whose hover
|
|
64549
|
+
// or focus builds the content ahead of the click.
|
|
64550
|
+
const needsDialogFromMount = props => {
|
|
64551
|
+
if (props.defaultOpen || props.navState || props.anchor !== undefined) {
|
|
64552
|
+
return true;
|
|
64553
|
+
}
|
|
64554
|
+
return props.mount !== undefined && props.mount !== "from-first-open";
|
|
64555
|
+
};
|
|
64556
|
+
|
|
64378
64557
|
// No openController passed: this Dialog is used declaratively (e.g. driven
|
|
64379
64558
|
// by --navi-toggle/--navi-open/--navi-close commands, the `open` prop, or
|
|
64380
64559
|
// `defaultOpen`) rather than owned by a parent component.
|
|
@@ -71243,9 +71422,17 @@ const ListUI = props => {
|
|
|
71243
71422
|
// why not an effect), and the floor of 30 is its: it protects the scrolling
|
|
71244
71423
|
// window, not a picture nobody has seen yet.
|
|
71245
71424
|
const {
|
|
71246
|
-
initial:
|
|
71425
|
+
initial: initialRenderBudgetProp,
|
|
71247
71426
|
after: renderBudgetAfterPaint
|
|
71248
71427
|
} = resolveRenderBudget(renderBudgetProp);
|
|
71428
|
+
// Opening on a position that says how many rows were on screen from its row
|
|
71429
|
+
// on (the way back to where the list was, see reportPosition): that is what
|
|
71430
|
+
// the first picture shows, whatever `initial` guessed — with the row above
|
|
71431
|
+
// when it stood partly in view. The rest waits for the paint like any
|
|
71432
|
+
// `initial`: a page coming back in a route transition builds it inside the
|
|
71433
|
+
// update callback, and every row below the fold delays the movement.
|
|
71434
|
+
const openingPosition = scrolled ?? defaultScrolled;
|
|
71435
|
+
const initialRenderBudget = openingPosition && typeof openingPosition === "object" && typeof openingPosition.visibleCount === "number" ? openingPosition.visibleCount + (openingPosition.offset > 0 ? 1 : 0) : initialRenderBudgetProp;
|
|
71249
71436
|
let renderBudget = renderBudgetAfterPaint;
|
|
71250
71437
|
if (renderBudget < 30 && !renderBudgetSkipCheck) {
|
|
71251
71438
|
console.warn(`List: renderBudget=${renderBudget} is too low. A renderBudget below 30 is not supported: on large screens or when the list grows, items outside the window would appear as blank space instead of rendered content. Use a value of at least 30, or omit the prop to use the default (${RENDER_BUDGET_DEFAULT}).`);
|
|
@@ -71708,11 +71895,23 @@ const useListScrollSync = ({
|
|
|
71708
71895
|
horizontal
|
|
71709
71896
|
});
|
|
71710
71897
|
};
|
|
71898
|
+
|
|
71899
|
+
// How many rows the window keeps above the row the list is held at. Around it
|
|
71900
|
+
// for the scrolling to come, from the first paint on; before, the window is
|
|
71901
|
+
// the first picture's, and a picture of the list opening on a row shows that
|
|
71902
|
+
// row and what is below it — with the one above when a remembered position
|
|
71903
|
+
// says it stood partly in view.
|
|
71904
|
+
const rowsAboveOpening = (openAt, windowSize) => {
|
|
71905
|
+
if (renderBudget !== renderBudgetSteady) {
|
|
71906
|
+
return openAt && typeof openAt === "object" && openAt.offset > 0 ? 1 : 0;
|
|
71907
|
+
}
|
|
71908
|
+
return Math.floor(windowSize / 2);
|
|
71909
|
+
};
|
|
71711
71910
|
const [renderWindow, setRenderWindow] = useState(() => {
|
|
71712
71911
|
// Opening somewhere else than the beginning starts by framing there: the
|
|
71713
71912
|
// rows the list will draw are the rows it will ask for.
|
|
71714
71913
|
const openAt = scrolled ?? defaultScrolled;
|
|
71715
|
-
const start = typeof openAt === "number" ? openAt -
|
|
71914
|
+
const start = typeof openAt === "number" ? openAt - rowsAboveOpening(openAt, renderBudget) : 0;
|
|
71716
71915
|
const startClamped = start < 0 ? 0 : start;
|
|
71717
71916
|
return {
|
|
71718
71917
|
start: startClamped,
|
|
@@ -71785,8 +71984,12 @@ const useListScrollSync = ({
|
|
|
71785
71984
|
// Held somewhere it has not reached yet: what the window frames right now
|
|
71786
71985
|
// is not what it will frame, so nothing should be fetched for it.
|
|
71787
71986
|
listRows.holdPending = scrolledWanted !== "start" && scrolledWanted !== undefined;
|
|
71987
|
+
// Zero until the runs have rendered — they count their rows as they do,
|
|
71988
|
+
// after the list — which is not a collection fitting in the window: it is
|
|
71989
|
+
// one whose end is not known yet, and the window then frames the row the
|
|
71990
|
+
// list opens on, already in the first commit.
|
|
71788
71991
|
const total = listRows.totalSignal.peek();
|
|
71789
|
-
if (total <= renderBudget) {
|
|
71992
|
+
if (total > 0 && total <= renderBudget) {
|
|
71790
71993
|
// The whole collection is what the list draws: wherever in it the list is
|
|
71791
71994
|
// held, the window is already its place. Nowhere to move to means nothing
|
|
71792
71995
|
// to wait for — a hold left standing here is a list that never asks for
|
|
@@ -71794,21 +71997,21 @@ const useListScrollSync = ({
|
|
|
71794
71997
|
listRows.holdPending = false;
|
|
71795
71998
|
return;
|
|
71796
71999
|
}
|
|
71797
|
-
const
|
|
72000
|
+
const above = rowsAboveOpening(scrolledWanted, renderBudget);
|
|
71798
72001
|
let wantedStart = null;
|
|
71799
72002
|
if (scrolledWanted === "end") {
|
|
71800
72003
|
wantedStart = total - renderBudget;
|
|
71801
72004
|
} else if (typeof scrolledWanted === "number") {
|
|
71802
|
-
wantedStart = scrolledWanted -
|
|
72005
|
+
wantedStart = scrolledWanted - above;
|
|
71803
72006
|
} else if (scrolledWanted && scrolledWanted.id !== undefined) {
|
|
71804
72007
|
const rowIndex = listRows.locateRow(scrolledWanted.id);
|
|
71805
72008
|
if (rowIndex !== null) {
|
|
71806
|
-
wantedStart = rowIndex -
|
|
72009
|
+
wantedStart = rowIndex - above;
|
|
71807
72010
|
} else if (typeof scrolledWanted.index === "number") {
|
|
71808
72011
|
// The row has not come back yet, but where it stood is known: near
|
|
71809
72012
|
// enough to frame, and to put the scrollbar roughly where it will end
|
|
71810
72013
|
// up rather than at the top.
|
|
71811
|
-
wantedStart = scrolledWanted.index -
|
|
72014
|
+
wantedStart = scrolledWanted.index - above;
|
|
71812
72015
|
}
|
|
71813
72016
|
}
|
|
71814
72017
|
if (wantedStart === null) {
|
|
@@ -71817,12 +72020,12 @@ const useListScrollSync = ({
|
|
|
71817
72020
|
// back — the run asks for it by name (see useRequestMissing).
|
|
71818
72021
|
return;
|
|
71819
72022
|
}
|
|
72023
|
+
if (total > 0 && wantedStart + renderBudget > total) {
|
|
72024
|
+
wantedStart = total - renderBudget;
|
|
72025
|
+
}
|
|
71820
72026
|
if (wantedStart < 0) {
|
|
71821
72027
|
wantedStart = 0;
|
|
71822
72028
|
}
|
|
71823
|
-
if (wantedStart + renderBudget > total) {
|
|
71824
|
-
wantedStart = total - renderBudget;
|
|
71825
|
-
}
|
|
71826
72029
|
const {
|
|
71827
72030
|
start,
|
|
71828
72031
|
end
|
|
@@ -72157,8 +72360,8 @@ const useListScrollSync = ({
|
|
|
72157
72360
|
end
|
|
72158
72361
|
} = renderWindowRef.current;
|
|
72159
72362
|
if (rowIndex < start || rowIndex >= end) {
|
|
72160
|
-
const
|
|
72161
|
-
const wantedStart = rowIndex -
|
|
72363
|
+
const above = rowsAboveOpening(scrolledWanted, end - start);
|
|
72364
|
+
const wantedStart = rowIndex - above < 0 ? 0 : rowIndex - above;
|
|
72162
72365
|
updateRenderWindow(wantedStart, wantedStart + (end - start), `opening on row ${scrolledWanted.id}`);
|
|
72163
72366
|
return;
|
|
72164
72367
|
}
|
|
@@ -72204,6 +72407,15 @@ const useListScrollSync = ({
|
|
|
72204
72407
|
}
|
|
72205
72408
|
};
|
|
72206
72409
|
useLayoutEffect(placeWhereHeld);
|
|
72410
|
+
// Held on a row of a list scrolling the document: where the document goes is
|
|
72411
|
+
// this list's to say, measured on the row, not the url's offset in pixels
|
|
72412
|
+
// (see holdDocumentScroll).
|
|
72413
|
+
useLayoutEffect(() => {
|
|
72414
|
+
if (!heldSomewhere || getScroller() !== document.scrollingElement) {
|
|
72415
|
+
return undefined;
|
|
72416
|
+
}
|
|
72417
|
+
return holdDocumentScroll();
|
|
72418
|
+
}, [heldSomewhere, scrollerElResolved]);
|
|
72207
72419
|
// What to do when the list's own geometry moves under it, kept fresh for the
|
|
72208
72420
|
// observer below (which is installed once).
|
|
72209
72421
|
const onGeometryChangeRef = useRef(null);
|
|
@@ -72246,31 +72458,40 @@ const useListScrollSync = ({
|
|
|
72246
72458
|
// Where the list was at the last thing that moved it. Kept whether anyone
|
|
72247
72459
|
// asked for it or not: it is what a resize needs to put things back.
|
|
72248
72460
|
const positionRef = useRef(null);
|
|
72461
|
+
const captureListAnchor = countVisible => captureScrollAnchor({
|
|
72462
|
+
scrollerEl: getScroller(),
|
|
72463
|
+
listEl: getListEl(),
|
|
72464
|
+
items: listRows.visibleItemsSignal.peek(),
|
|
72465
|
+
horizontal,
|
|
72466
|
+
countVisible
|
|
72467
|
+
});
|
|
72468
|
+
// Said from where the row lands on its own, and with `visibleCount`: the
|
|
72469
|
+
// rows on screen from that one on, which is what a list given this position
|
|
72470
|
+
// back draws first (see ListUI).
|
|
72471
|
+
const toScrolledPosition = position => {
|
|
72472
|
+
const rowEl = findRowElement(getListEl(), position.id);
|
|
72473
|
+
return {
|
|
72474
|
+
id: position.id,
|
|
72475
|
+
index: position.index,
|
|
72476
|
+
offset: position.offset - getRowScrollInset(getScroller(), rowEl, horizontal),
|
|
72477
|
+
visibleCount: position.visibleCount
|
|
72478
|
+
};
|
|
72479
|
+
};
|
|
72249
72480
|
const reportPosition = () => {
|
|
72250
72481
|
if (!ref.current) {
|
|
72251
72482
|
return;
|
|
72252
72483
|
}
|
|
72253
|
-
const
|
|
72254
|
-
|
|
72255
|
-
|
|
72256
|
-
items: listRows.visibleItemsSignal.peek(),
|
|
72257
|
-
horizontal
|
|
72258
|
-
});
|
|
72484
|
+
const remember = rememberScrollRef.current;
|
|
72485
|
+
const onScrolledChange = onScrolledChangeRef.current;
|
|
72486
|
+
const position = captureListAnchor(remember || Boolean(onScrolledChange));
|
|
72259
72487
|
if (!position) {
|
|
72260
72488
|
return;
|
|
72261
72489
|
}
|
|
72262
72490
|
positionRef.current = position;
|
|
72263
|
-
const remember = rememberScrollRef.current;
|
|
72264
|
-
const onScrolledChange = onScrolledChangeRef.current;
|
|
72265
72491
|
if (!remember && !onScrolledChange) {
|
|
72266
72492
|
return;
|
|
72267
72493
|
}
|
|
72268
|
-
const
|
|
72269
|
-
const scrolledNow = {
|
|
72270
|
-
id: position.id,
|
|
72271
|
-
index: position.index,
|
|
72272
|
-
offset: position.offset - getRowScrollInset(getScroller(), rowEl, horizontal)
|
|
72273
|
-
};
|
|
72494
|
+
const scrolledNow = toScrolledPosition(position);
|
|
72274
72495
|
if (remember) {
|
|
72275
72496
|
rememberScrollerPosition(listId, scrolledNow);
|
|
72276
72497
|
}
|
|
@@ -72278,6 +72499,20 @@ const useListScrollSync = ({
|
|
|
72278
72499
|
onScrolledChange(scrolledNow);
|
|
72279
72500
|
}
|
|
72280
72501
|
};
|
|
72502
|
+
// Nothing scrolled, yet what is on screen changed: the list has just been
|
|
72503
|
+
// laid out, its rows have arrived, one of them grew. A list read where it
|
|
72504
|
+
// opened would otherwise come back with nothing kept — opening again as a
|
|
72505
|
+
// fresh arrival does, all of its window drawn in the first commit instead of
|
|
72506
|
+
// the rows that were on screen. Kept, and told to nobody: nobody scrolled.
|
|
72507
|
+
const rememberPosition = () => {
|
|
72508
|
+
if (!rememberScrollRef.current || !ref.current) {
|
|
72509
|
+
return;
|
|
72510
|
+
}
|
|
72511
|
+
const position = captureListAnchor(true);
|
|
72512
|
+
if (position) {
|
|
72513
|
+
rememberScrollerPosition(listId, toScrolledPosition(position));
|
|
72514
|
+
}
|
|
72515
|
+
};
|
|
72281
72516
|
// Leaving: with its page, or alone (see forgetScrollerUnlessPageLeft).
|
|
72282
72517
|
useLayoutEffect(() => {
|
|
72283
72518
|
return () => {
|
|
@@ -72303,6 +72538,7 @@ const useListScrollSync = ({
|
|
|
72303
72538
|
// list which has outgrown a bounded ancestor stops holding on to the
|
|
72304
72539
|
// page.
|
|
72305
72540
|
resolveScroller();
|
|
72541
|
+
rememberPosition();
|
|
72306
72542
|
// Two things resize here, and they call for opposite answers. The LIST
|
|
72307
72543
|
// growing is its own content settling: only a list holding itself
|
|
72308
72544
|
// somewhere cares (the end it aims at has moved), and a list the user is
|
|
@@ -72995,12 +73231,15 @@ const getRowName = rowEl => rowEl.getAttribute("navi-list-item-real");
|
|
|
72995
73231
|
// the list is rebuilt around it. Read off the rows' own boxes, not by
|
|
72996
73232
|
// hit-testing the screen: a scrolling list takes its rows out of hit-testing
|
|
72997
73233
|
// (see the navi-scrolling rule in the css above), and the scroll event is
|
|
72998
|
-
// precisely when this is asked.
|
|
73234
|
+
// precisely when this is asked. `countVisible` adds how many rows are on
|
|
73235
|
+
// screen from that one on: what a list coming back there has to draw before
|
|
73236
|
+
// anything else (see the first paint in ListUI).
|
|
72999
73237
|
const captureScrollAnchor = ({
|
|
73000
73238
|
scrollerEl,
|
|
73001
73239
|
listEl,
|
|
73002
73240
|
items,
|
|
73003
|
-
horizontal
|
|
73241
|
+
horizontal,
|
|
73242
|
+
countVisible
|
|
73004
73243
|
}) => {
|
|
73005
73244
|
if (!scrollerEl || !listEl) {
|
|
73006
73245
|
return null;
|
|
@@ -73036,6 +73275,9 @@ const captureScrollAnchor = ({
|
|
|
73036
73275
|
offset
|
|
73037
73276
|
};
|
|
73038
73277
|
if (offset >= 0) {
|
|
73278
|
+
if (countVisible) {
|
|
73279
|
+
anchor.visibleCount = countRowsStartingBefore(rowEls, i, range.to, horizontal);
|
|
73280
|
+
}
|
|
73039
73281
|
return anchor;
|
|
73040
73282
|
}
|
|
73041
73283
|
// The row under the top of the view starts above it. Good enough to hold
|
|
@@ -73044,10 +73286,26 @@ const captureScrollAnchor = ({
|
|
|
73044
73286
|
// top" reads, and can be drawn. Keep looking; this one is the fallback.
|
|
73045
73287
|
if (!fallbackAnchor) {
|
|
73046
73288
|
fallbackAnchor = anchor;
|
|
73289
|
+
if (countVisible) {
|
|
73290
|
+
anchor.visibleCount = countRowsStartingBefore(rowEls, i, range.to, horizontal);
|
|
73291
|
+
}
|
|
73047
73292
|
}
|
|
73048
73293
|
}
|
|
73049
73294
|
return fallbackAnchor;
|
|
73050
73295
|
};
|
|
73296
|
+
// How many of the rows from `fromIndex` on start before `to` along the scroll
|
|
73297
|
+
// axis — the ones on screen, when `to` is where the view ends.
|
|
73298
|
+
const countRowsStartingBefore = (rowEls, fromIndex, to, horizontal) => {
|
|
73299
|
+
let index = fromIndex;
|
|
73300
|
+
while (index < rowEls.length) {
|
|
73301
|
+
const rect = rowEls[index].getBoundingClientRect();
|
|
73302
|
+
if ((horizontal ? rect.left : rect.top) >= to) {
|
|
73303
|
+
break;
|
|
73304
|
+
}
|
|
73305
|
+
index++;
|
|
73306
|
+
}
|
|
73307
|
+
return index - fromIndex;
|
|
73308
|
+
};
|
|
73051
73309
|
// The part of the list that is on screen, along the scrolling axis. Both edges
|
|
73052
73310
|
// matter: the scroller may be larger than the list (scroller="parent") as well
|
|
73053
73311
|
// as smaller (the list scrolls inside its own box).
|
|
@@ -75403,7 +75661,7 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
|
|
|
75403
75661
|
* are skeletons, the others hold their room. `0` says the list is already
|
|
75404
75662
|
* known to be empty: the empty `fallback` shows right away rather than an
|
|
75405
75663
|
* empty frame.
|
|
75406
|
-
* @param {"start"|"end"|number|{id: string, offset?: number}} [props.defaultScrolled="start"]
|
|
75664
|
+
* @param {"start"|"end"|number|{id: string, index?: number, offset?: number, visibleCount?: number}} [props.defaultScrolled="start"]
|
|
75407
75665
|
* Where the list opens, after which the user owns the scroll — unless it is
|
|
75408
75666
|
* being come back to (see `scrollResetOnNavigation`). `"end"` is a
|
|
75409
75667
|
* thread read backwards — the last rows are the ones to show, and the ones
|
|
@@ -75415,8 +75673,10 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
|
|
|
75415
75673
|
* the screen it was saved on. `offset: 0` is where a `scrollIntoView()` puts
|
|
75416
75674
|
* it — in front of the fixed bar the scroller gives room for, below the
|
|
75417
75675
|
* sticky header and the group label the row lives under — so nothing of that
|
|
75418
|
-
* room has to be restated as a number by whoever asks.
|
|
75419
|
-
*
|
|
75676
|
+
* room has to be restated as a number by whoever asks. The `index` and
|
|
75677
|
+
* `visibleCount` it also hands out say where to aim before the row is found,
|
|
75678
|
+
* and how many rows to draw before the first paint (see `renderBudget`).
|
|
75679
|
+
* @param {"start"|"end"|number|{id: string, index?: number, offset?: number, visibleCount?: number}} [props.scrolled]
|
|
75420
75680
|
* The same, but held: the list goes back there every time this changes, even
|
|
75421
75681
|
* after the user has scrolled — the caller owns where the list is (see
|
|
75422
75682
|
* `defaultScrolled` for the uncontrolled form, and `open`/`defaultOpen`
|
|
@@ -75427,12 +75687,12 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
|
|
|
75427
75687
|
* In every form the list holds itself there while it is still finding out
|
|
75428
75688
|
* how many rows there are and how tall one is, and lets go the moment the
|
|
75429
75689
|
* user reaches for the list.
|
|
75430
|
-
* @param {(scrolled: {id: string, index: number, offset: number}) => void} [props.onScrolledChange]
|
|
75690
|
+
* @param {(scrolled: {id: string, index: number, offset: number, visibleCount: number}) => void} [props.onScrolledChange]
|
|
75431
75691
|
* Where the list is, as the user scrolls: the row at the top of the view and
|
|
75432
75692
|
* how far below the place a row lands on its own (see `defaultScrolled`) it
|
|
75433
|
-
* starts. Keep it
|
|
75434
|
-
* later through `scrolled`/`defaultScrolled` — an index
|
|
75435
|
-
* rows get inserted while a list is being read.
|
|
75693
|
+
* starts, and how many rows are on screen from that one on. Keep it whole to
|
|
75694
|
+
* come back to it later through `scrolled`/`defaultScrolled` — an index alone
|
|
75695
|
+
* would not do, since rows get inserted while a list is being read.
|
|
75436
75696
|
* @param {number|{initial: number, after: number}} [props.renderBudget=100]
|
|
75437
75697
|
* How many rows of a `<List.Items>` run are in the DOM at once: the render
|
|
75438
75698
|
* window, which slides as the user scrolls while fillers hold the room of
|
|
@@ -75444,11 +75704,15 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
|
|
|
75444
75704
|
* once, with room for that lookahead — the list warns below 30, and when a
|
|
75445
75705
|
* budget leaves fewer than two rows beyond the screen.
|
|
75446
75706
|
*
|
|
75447
|
-
* `{ initial, after }` for a list
|
|
75448
|
-
*
|
|
75449
|
-
*
|
|
75450
|
-
*
|
|
75451
|
-
*
|
|
75707
|
+
* `{ initial, after }` for a list whose first picture is taken as it is
|
|
75708
|
+
* built — drawn in the click that opens a popup, or in the update callback
|
|
75709
|
+
* of a route transition bringing its page back: `initial` rows in the commit
|
|
75710
|
+
* the browser paints first — about what a phone screen shows — counted from
|
|
75711
|
+
* the row the list opens on, and `after` from the paint on, around it, where
|
|
75712
|
+
* the floor of 30 applies. Opening on a position that says how many rows were
|
|
75713
|
+
* on screen (`visibleCount`, see `onScrolledChange`) draws those first,
|
|
75714
|
+
* whatever `initial` says. The runs ask their source for `after` rows from
|
|
75715
|
+
* the start, so the first picture costs no second request.
|
|
75452
75716
|
* @param {number} [props.virtualItemSize]
|
|
75453
75717
|
* The size of one row along the scroll axis, in px, when every row has the
|
|
75454
75718
|
* same: what the fillers are sized with and what a scroll position is
|
|
@@ -75918,10 +76182,10 @@ const useBadgeRegistry = (children, enabled) => {
|
|
|
75918
76182
|
* list renders nothing at all. In a <Picker> this is the only placeholder
|
|
75919
76183
|
* the user gets — a picker given a `ui` does not draw its own — so pass the
|
|
75920
76184
|
* placeholder text: `fallback="Select skills…"`. Plain text is the right
|
|
75921
|
-
* choice there: it reads at the picker's size, in the
|
|
75922
|
-
*
|
|
75923
|
-
* transparent <Badge> matches the slot to the pixel instead, at the
|
|
75924
|
-
* badge-sized text. See docs/
|
|
76185
|
+
* choice there: it reads at the picker's size, in the normal text color (a
|
|
76186
|
+
* caller's `ui` is never greyed as a placeholder), and the box stays the same
|
|
76187
|
+
* height. A transparent <Badge> matches the slot to the pixel instead, at the
|
|
76188
|
+
* price of badge-sized text. See docs/typography.md#badgelist.
|
|
75925
76189
|
* @param {boolean} [shrinkWrap]
|
|
75926
76190
|
* Narrows the list down to its widest row so the last row isn't ragged.
|
|
75927
76191
|
* Defaults to true inside a <Picker> — the trigger draws a border around the
|