@jsenv/navi 0.29.321 → 0.29.323
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 +238 -102
- package/dist/dev/jsenv_navi.js.map +9 -9
- package/dist/jsenv_navi.js +234 -102
- package/dist/jsenv_navi.js.map +9 -9
- package/docs/popup_open.md +13 -0
- package/docs/scroll.md +6 -4
- package/package.json +1 -1
package/dist/dev/jsenv_navi.js
CHANGED
|
@@ -34689,19 +34689,27 @@ const writeInSignal = (signal, value, { history }) => {
|
|
|
34689
34689
|
// is open (`?seat=<gameId>` over a list of cards), and closed is the signal
|
|
34690
34690
|
// holding none of their values — `undefined`, which a state signal reads as
|
|
34691
34691
|
// its default.
|
|
34692
|
+
//
|
|
34693
|
+
// A popup without a `value` reads anything the signal holds — save `false`,
|
|
34694
|
+
// `null` and `undefined` — as open: a single sheet bound to `?open=<id>` is
|
|
34695
|
+
// open on whichever id the address names, and the id is the popup's content
|
|
34696
|
+
// to read, not its to write. Opening writes `true` only into a signal that
|
|
34697
|
+
// reads closed; closing writes `false` where the signal held `true`, so a
|
|
34698
|
+
// boolean signal keeps its boolean, and `undefined` for anything else.
|
|
34692
34699
|
const writeOpenedInSignal = (signal, opened, event, popupValue) => {
|
|
34693
|
-
|
|
34700
|
+
const signalValue = signal.peek();
|
|
34701
|
+
if (readOpened(signalValue, popupValue) === opened) {
|
|
34694
34702
|
// The signal already says so, meaning this open/close IS what it asked
|
|
34695
34703
|
// for: a back press that took the popup out of the url, the application
|
|
34696
34704
|
// writing it. Nothing to write back — and nothing to go back to either,
|
|
34697
34705
|
// since the navigation navBack would undo is the one that asked for this.
|
|
34698
34706
|
return;
|
|
34699
34707
|
}
|
|
34700
|
-
const closedValue = popupValue === undefined ? false : undefined;
|
|
34701
34708
|
if (opened) {
|
|
34702
34709
|
signal.value = popupValue === undefined ? true : popupValue;
|
|
34703
34710
|
return;
|
|
34704
34711
|
}
|
|
34712
|
+
const closedValue = readClosedValue(signalValue);
|
|
34705
34713
|
if (
|
|
34706
34714
|
signal.options?.getHistory?.() === "push" &&
|
|
34707
34715
|
// Nothing of this document behind: the popup was opened by the url itself
|
|
@@ -34722,10 +34730,15 @@ const writeOpenedInSignal = (signal, opened, event, popupValue) => {
|
|
|
34722
34730
|
};
|
|
34723
34731
|
const readOpened = (signalValue, popupValue) => {
|
|
34724
34732
|
if (popupValue === undefined) {
|
|
34725
|
-
return
|
|
34733
|
+
return (
|
|
34734
|
+
signalValue !== undefined && signalValue !== null && signalValue !== false
|
|
34735
|
+
);
|
|
34726
34736
|
}
|
|
34727
34737
|
return signalValue === popupValue;
|
|
34728
34738
|
};
|
|
34739
|
+
const readClosedValue = (openSignalValue) => {
|
|
34740
|
+
return openSignalValue === true ? false : undefined;
|
|
34741
|
+
};
|
|
34729
34742
|
|
|
34730
34743
|
/**
|
|
34731
34744
|
* Keeps an open controller in sync with where the caller says the popup should
|
|
@@ -34780,6 +34793,13 @@ const useOpenPropsEffectOnOpenController = (
|
|
|
34780
34793
|
: signal
|
|
34781
34794
|
? readOpened(signal.value, value)
|
|
34782
34795
|
: props.open;
|
|
34796
|
+
// What the signal holds while it reads open — `true`, the popup's `value`,
|
|
34797
|
+
// or the id a single sheet is open on. Put back when a close is refused,
|
|
34798
|
+
// since by then the signal has been written closed.
|
|
34799
|
+
const openSignalValueRef = useRef(undefined);
|
|
34800
|
+
if (signal && open) {
|
|
34801
|
+
openSignalValueRef.current = signal.value;
|
|
34802
|
+
}
|
|
34783
34803
|
// Assigned on every render, like openEffect, so it always closes over the
|
|
34784
34804
|
// latest prop: a popup that opens or closes on its own (Escape, backdrop, a
|
|
34785
34805
|
// --navi-close command) writes what happened where the caller keeps it, so
|
|
@@ -34860,10 +34880,11 @@ const useOpenPropsEffectOnOpenController = (
|
|
|
34860
34880
|
// request already reads in the signal, and with a `value` what the
|
|
34861
34881
|
// signal holds may be another popup's, which the write would erase.
|
|
34862
34882
|
if (signal && openController.opened !== open) {
|
|
34863
|
-
const opened = openController.opened;
|
|
34864
34883
|
writeInSignal(
|
|
34865
34884
|
signal,
|
|
34866
|
-
|
|
34885
|
+
openController.opened
|
|
34886
|
+
? openSignalValueRef.current
|
|
34887
|
+
: readClosedValue(signal.peek()),
|
|
34867
34888
|
{ history: "replace" },
|
|
34868
34889
|
);
|
|
34869
34890
|
}
|
|
@@ -35883,6 +35904,8 @@ const createDisplayedEvent = (ancestor, becauseAncestorOpened) => {
|
|
|
35883
35904
|
* @returns {Function} triggerAutofocus — can be called manually with a synthetic
|
|
35884
35905
|
* event to re-run the focus logic outside of the layout-effect lifecycle.
|
|
35885
35906
|
*/
|
|
35907
|
+
const NO_ELEMENT_REF = { current: null };
|
|
35908
|
+
|
|
35886
35909
|
const useAutoFocus = (
|
|
35887
35910
|
focusableElementRef,
|
|
35888
35911
|
autoFocus,
|
|
@@ -36016,8 +36039,15 @@ const useAutoFocus = (
|
|
|
36016
36039
|
};
|
|
36017
36040
|
};
|
|
36018
36041
|
|
|
36042
|
+
// Nothing to place when the control asks for no focus ("restore" is given
|
|
36043
|
+
// focus by a transfer, never by this effect): the displayed-effect is then
|
|
36044
|
+
// handed no element at all, so it never asks the layout whether one is on
|
|
36045
|
+
// screen — a question every control in a list would otherwise ask at
|
|
36046
|
+
// mount, for an answer this hook would not use. Read at mount, like the
|
|
36047
|
+
// deps below say.
|
|
36048
|
+
const claimsFocus = Boolean(autoFocus) && autoFocus !== "restore";
|
|
36019
36049
|
useDisplayedLayoutEffect(
|
|
36020
|
-
focusableElementRef,
|
|
36050
|
+
claimsFocus ? focusableElementRef : NO_ELEMENT_REF,
|
|
36021
36051
|
(el, e) => {
|
|
36022
36052
|
return triggerAutofocus(e);
|
|
36023
36053
|
},
|
|
@@ -37401,6 +37431,46 @@ const acknowledgeOwnAction = (controller) => {
|
|
|
37401
37431
|
controller.acknowledgeUIState();
|
|
37402
37432
|
};
|
|
37403
37433
|
|
|
37434
|
+
// What a group of checkboxes or radios is worth is a claim about values, and
|
|
37435
|
+
// a value whose box is not there cannot be contradicted by that box: a row
|
|
37436
|
+
// scrolled out of a virtualized list, a filtered-out option, a whole page being
|
|
37437
|
+
// parked while a route action reruns — its children leave one at a time, and
|
|
37438
|
+
// each partial reading would otherwise be taken for the answer and written
|
|
37439
|
+
// into whatever the group is bound to (an array signal emptied down to `[]`
|
|
37440
|
+
// by its own unmount). `kept` is what the group holds as it is asked; a value
|
|
37441
|
+
// in it stays until a DRAWN child carrying that value says otherwise.
|
|
37442
|
+
//
|
|
37443
|
+
// Read off the group rather than remembered here on purpose: a value put ON
|
|
37444
|
+
// the group (a `value` prop, a signal, a reopened popup) replaces the whole
|
|
37445
|
+
// selection, and a private memory would go on holding the rows it could not
|
|
37446
|
+
// see being unselected.
|
|
37447
|
+
const keptAndCheckedChildUIStates = (children, fallbackState, kept) => {
|
|
37448
|
+
const drawnValues = new Set(children.map((child) => child.props.value));
|
|
37449
|
+
const values = Array.isArray(kept)
|
|
37450
|
+
? kept.filter((value) => !drawnValues.has(value))
|
|
37451
|
+
: [];
|
|
37452
|
+
for (const child of children) {
|
|
37453
|
+
if (child.uiState !== undefined) {
|
|
37454
|
+
values.push(child.uiState);
|
|
37455
|
+
}
|
|
37456
|
+
}
|
|
37457
|
+
return values.length === 0 ? undefined : values;
|
|
37458
|
+
};
|
|
37459
|
+
const keptOrFirstDefinedChildUIState = (children, fallbackState, kept) => {
|
|
37460
|
+
for (const child of children) {
|
|
37461
|
+
if (child.uiState !== undefined) {
|
|
37462
|
+
return child.uiState;
|
|
37463
|
+
}
|
|
37464
|
+
}
|
|
37465
|
+
// No drawn child claims it. If the child that held it IS drawn, it was really
|
|
37466
|
+
// deselected; if it is not, the group keeps what it holds.
|
|
37467
|
+
const keptIsDrawn = children.some((child) => child.props.value === kept);
|
|
37468
|
+
if (keptIsDrawn) {
|
|
37469
|
+
return undefined;
|
|
37470
|
+
}
|
|
37471
|
+
return kept;
|
|
37472
|
+
};
|
|
37473
|
+
|
|
37404
37474
|
// Default aggregate/distribute implementations keyed by controlType or stateType.
|
|
37405
37475
|
// Looked up in useUIGroupStateController to fill in omitted aggregateChildStates /
|
|
37406
37476
|
// distributeChildUIState. If neither a default nor an explicit impl is found for a
|
|
@@ -37409,7 +37479,7 @@ const GROUP_DEFAULTS = {
|
|
|
37409
37479
|
radio_group: {
|
|
37410
37480
|
childControlFilter: (child) =>
|
|
37411
37481
|
child.controlType === "input" && child.controlHostProps?.type === "radio",
|
|
37412
|
-
aggregateChildStates:
|
|
37482
|
+
aggregateChildStates: keptOrFirstDefinedChildUIState,
|
|
37413
37483
|
distributeChildUIState: (newUIState, childUIStateController) => {
|
|
37414
37484
|
const childSelected = childUIStateController.props.value === newUIState;
|
|
37415
37485
|
if (childSelected) {
|
|
@@ -37422,16 +37492,7 @@ const GROUP_DEFAULTS = {
|
|
|
37422
37492
|
childControlFilter: (child) =>
|
|
37423
37493
|
child.controlType === "input" &&
|
|
37424
37494
|
child.controlHostProps?.type === "checkbox",
|
|
37425
|
-
aggregateChildStates:
|
|
37426
|
-
const values = [];
|
|
37427
|
-
for (const child of children) {
|
|
37428
|
-
const childUIState = child.uiState;
|
|
37429
|
-
if (childUIState !== undefined) {
|
|
37430
|
-
values.push(childUIState);
|
|
37431
|
-
}
|
|
37432
|
-
}
|
|
37433
|
-
return values.length === 0 ? undefined : values;
|
|
37434
|
-
},
|
|
37495
|
+
aggregateChildStates: keptAndCheckedChildUIStates,
|
|
37435
37496
|
distributeChildUIState: (newUIState, childUIStateController) => {
|
|
37436
37497
|
const childSelected =
|
|
37437
37498
|
Array.isArray(newUIState) &&
|
|
@@ -60544,14 +60605,15 @@ const css$E = /* css */`
|
|
|
60544
60605
|
* the focus only leaves it for something that asked by name (`autoFocus` on
|
|
60545
60606
|
* that element, which outranks whatever the dialog says).
|
|
60546
60607
|
* @param {boolean} [props.open] - Controlled open state.
|
|
60547
|
-
* @param {import("@preact/signals").Signal<
|
|
60608
|
+
* @param {import("@preact/signals").Signal<any>} [props.signal] - The open
|
|
60548
60609
|
* state said the way every navi control says it: the dialog opens and closes
|
|
60549
60610
|
* to match the signal, and writes into it whenever it opens or closes on its
|
|
60550
60611
|
* own (Escape, backdrop, a --navi-close command) — one binding to both drive
|
|
60551
60612
|
* the dialog and know where it is, and the state stays where the app put it.
|
|
60552
60613
|
* Excludes `open`; `onOpen`/`onClose` still fire. A signal holding `true` at
|
|
60553
60614
|
* mount behaves like `defaultOpen`: the dialog was already open, no entrance
|
|
60554
|
-
* plays.
|
|
60615
|
+
* plays. A signal holding an id (one dialog showing whichever card
|
|
60616
|
+
* the address names) reads as open, and the dialog leaves the id alone.
|
|
60555
60617
|
* @param {any} [props.value] - What `signal` holds while THIS dialog is
|
|
60556
60618
|
* open, for several of them sharing one signal that says which is open
|
|
60557
60619
|
* (`?seat=<gameId>` over a list of cards): open while `signal.value` is this
|
|
@@ -62205,14 +62267,15 @@ const css$D = /* css */`
|
|
|
62205
62267
|
* the focus only leaves it for something that asked by name (`autoFocus` on
|
|
62206
62268
|
* that element, which outranks whatever the popover says).
|
|
62207
62269
|
* @param {boolean} [props.open] - Controlled open state.
|
|
62208
|
-
* @param {import("@preact/signals").Signal<
|
|
62270
|
+
* @param {import("@preact/signals").Signal<any>} [props.signal] - The open
|
|
62209
62271
|
* state said the way every navi control says it: the popover opens and closes
|
|
62210
62272
|
* to match the signal, and writes into it whenever it opens or closes on its
|
|
62211
62273
|
* own (Escape, light dismiss, a --navi-close command) — one binding to both
|
|
62212
62274
|
* drive the popover and know where it is, and the state stays where the app
|
|
62213
62275
|
* put it. Excludes `open`; `onOpen`/`onClose` still fire. A signal holding
|
|
62214
62276
|
* `true` at mount behaves like `defaultOpen`: the popover was already open,
|
|
62215
|
-
* no entrance plays.
|
|
62277
|
+
* no entrance plays. A signal holding an id (one popover showing whichever card
|
|
62278
|
+
* the address names) reads as open, and the popover leaves the id alone.
|
|
62216
62279
|
* @param {any} [props.value] - What `signal` holds while THIS popover is
|
|
62217
62280
|
* open, for several of them sharing one signal that says which is open
|
|
62218
62281
|
* (`?seat=<gameId>` over a list of cards): open while `signal.value` is this
|
|
@@ -65883,45 +65946,6 @@ const ListSelectable = props => {
|
|
|
65883
65946
|
focusGroupDirection,
|
|
65884
65947
|
focusGroupWrap
|
|
65885
65948
|
} = props;
|
|
65886
|
-
// `kept` is what the list holds as it is asked, which is not the same as what
|
|
65887
|
-
// its rows say: a list draws the rows it needs and no more, so the selected
|
|
65888
|
-
// one may be scrolled out of the window or filtered out of the view, and a
|
|
65889
|
-
// row that is not there cannot say it is not selected. Reading it off the
|
|
65890
|
-
// group rather than remembering it here is what makes a value put ON the list
|
|
65891
|
-
// (a `value` prop, a signal, a reopened popup) replace the whole selection —
|
|
65892
|
-
// a private memory of its own would go on holding the rows it could not see
|
|
65893
|
-
// being unselected.
|
|
65894
|
-
//
|
|
65895
|
-
// `fallbackState` is the empty of the shape the list declared below
|
|
65896
|
-
// (`stateType`): `[]` for a multiple list, nothing for a single one. Taking
|
|
65897
|
-
// it is what lets an emptied list say "empty" — `undefined` is the word for
|
|
65898
|
-
// "unset", and a bound stateSignal reads that as "nothing decided here, go
|
|
65899
|
-
// back to the default" (see docs/control_value.md), which is how a list
|
|
65900
|
-
// emptied down to its last row puts that row back on reload.
|
|
65901
|
-
const aggregateChildStates = (children, fallbackState, kept) => {
|
|
65902
|
-
if (multiple) {
|
|
65903
|
-
const drawnValues = new Set(children.map(child => child.props.value));
|
|
65904
|
-
const stillSelected = Array.isArray(kept) ? kept.filter(value => !drawnValues.has(value)) : [];
|
|
65905
|
-
for (const child of children) {
|
|
65906
|
-
if (child.uiState !== undefined) {
|
|
65907
|
-
stillSelected.push(child.uiState);
|
|
65908
|
-
}
|
|
65909
|
-
}
|
|
65910
|
-
return stillSelected.length === 0 ? fallbackState : stillSelected;
|
|
65911
|
-
}
|
|
65912
|
-
for (const child of children) {
|
|
65913
|
-
if (child.uiState !== undefined) {
|
|
65914
|
-
return child.uiState;
|
|
65915
|
-
}
|
|
65916
|
-
}
|
|
65917
|
-
// No drawn row claims it. If the row that held it IS drawn, it was really
|
|
65918
|
-
// deselected; if it is not, the list keeps what it holds.
|
|
65919
|
-
const keptIsDrawn = children.some(child => child.props.value === kept);
|
|
65920
|
-
if (keptIsDrawn) {
|
|
65921
|
-
return undefined;
|
|
65922
|
-
}
|
|
65923
|
-
return kept;
|
|
65924
|
-
};
|
|
65925
65949
|
const [listControlRootProps, listControlProps, childrenWrapperProps] = useControlgroupProps({
|
|
65926
65950
|
// The defaults of the group this IS (see CheckboxGroup / RadioGroup):
|
|
65927
65951
|
// what a selectable list shows is a claim about what was accepted, so
|
|
@@ -65933,9 +65957,15 @@ const ListSelectable = props => {
|
|
|
65933
65957
|
resetOnError: true,
|
|
65934
65958
|
...props
|
|
65935
65959
|
}, {
|
|
65960
|
+
// A list draws the rows it needs and no more, so a selected row may be
|
|
65961
|
+
// scrolled out of the window or filtered out of the view: the default
|
|
65962
|
+
// aggregate of these two groups keeps a value whose row is not drawn
|
|
65963
|
+
// (see GROUP_DEFAULTS in ui_state_controller.js). `stateType` is what
|
|
65964
|
+
// an emptied multiple list says — `[]`, not `undefined`, which a bound
|
|
65965
|
+
// stateSignal reads as "nothing decided here, go back to the default"
|
|
65966
|
+
// (see docs/control_value.md).
|
|
65936
65967
|
stateType: multiple ? "array" : "",
|
|
65937
|
-
controlType: multiple ? "checkbox_group" : "radio_group"
|
|
65938
|
-
aggregateChildStates
|
|
65968
|
+
controlType: multiple ? "checkbox_group" : "radio_group"
|
|
65939
65969
|
});
|
|
65940
65970
|
const uiGroupStateController = getUIStateControllerById(listControlProps.id);
|
|
65941
65971
|
useFocusGroup(ref, {
|
|
@@ -67137,6 +67167,7 @@ const ListUI = props => {
|
|
|
67137
67167
|
ref,
|
|
67138
67168
|
tracker,
|
|
67139
67169
|
renderBudget,
|
|
67170
|
+
renderBudgetSteady: renderBudgetAfterPaint,
|
|
67140
67171
|
virtualItemSize,
|
|
67141
67172
|
virtual,
|
|
67142
67173
|
scrolled,
|
|
@@ -67464,6 +67495,7 @@ const useListScrollSync = ({
|
|
|
67464
67495
|
ref,
|
|
67465
67496
|
tracker,
|
|
67466
67497
|
renderBudget,
|
|
67498
|
+
renderBudgetSteady,
|
|
67467
67499
|
virtualItemSize,
|
|
67468
67500
|
virtual,
|
|
67469
67501
|
scrolled,
|
|
@@ -68217,31 +68249,36 @@ const useListScrollSync = ({
|
|
|
68217
68249
|
}
|
|
68218
68250
|
});
|
|
68219
68251
|
|
|
68220
|
-
//
|
|
68221
|
-
|
|
68222
|
-
|
|
68223
|
-
|
|
68224
|
-
|
|
68252
|
+
// The window the visible band asks for, decided from geometry: the band's
|
|
68253
|
+
// two edges in row units, from where the scroller's viewport cuts the list
|
|
68254
|
+
// and the row size. What has to be drawn is everything between the edges,
|
|
68255
|
+
// plus what the scroll is about to bring — so the window is judged on the
|
|
68256
|
+
// rows it keeps AHEAD of the band, in the direction the user goes, and moves
|
|
68257
|
+
// once those fall under half a screen. Re-framed, it puts three quarters of
|
|
68258
|
+
// its spare rows ahead and one quarter behind: a window centred on the band
|
|
68259
|
+
// is due to move again as soon as the band has crossed the few rows it kept
|
|
68260
|
+
// ahead, and each move is rows drawn while the user waits for them.
|
|
68261
|
+
// Without a row size there is no geometry to reason on; the row under a
|
|
68262
|
+
// probe stands in for the band, framed the old way.
|
|
68263
|
+
const scrollDirectionRef = useRef(1);
|
|
68264
|
+
const windowSlidRef = useRef(false);
|
|
68265
|
+
const budgetWarnedRef = useRef(false);
|
|
68266
|
+
const evaluateWindow = reason => {
|
|
68267
|
+
const total = virtual.totalSignal.peek();
|
|
68268
|
+
if (total <= renderBudget) {
|
|
68269
|
+
return;
|
|
68225
68270
|
}
|
|
68226
68271
|
const scrollerEl = getScroller();
|
|
68227
68272
|
const listEl = getListEl();
|
|
68228
|
-
|
|
68229
|
-
|
|
68230
|
-
|
|
68231
|
-
|
|
68232
|
-
|
|
68233
|
-
|
|
68234
|
-
|
|
68235
|
-
|
|
68236
|
-
|
|
68237
|
-
reportPosition();
|
|
68238
|
-
return;
|
|
68239
|
-
}
|
|
68240
|
-
reportPosition();
|
|
68241
|
-
const total = virtual.totalSignal.peek();
|
|
68242
|
-
if (total <= renderBudget) {
|
|
68243
|
-
return;
|
|
68244
|
-
}
|
|
68273
|
+
if (!scrollerEl || !listEl) {
|
|
68274
|
+
return;
|
|
68275
|
+
}
|
|
68276
|
+
const {
|
|
68277
|
+
start,
|
|
68278
|
+
end
|
|
68279
|
+
} = renderWindowRef.current;
|
|
68280
|
+
const virtualItemSize = virtualItemSizeSignal.peek();
|
|
68281
|
+
if (virtualItemSize === 0) {
|
|
68245
68282
|
const scrollInfo = getScrollInfo({
|
|
68246
68283
|
scrollValues: {
|
|
68247
68284
|
left: scrollerEl.scrollLeft,
|
|
@@ -68258,17 +68295,8 @@ const useListScrollSync = ({
|
|
|
68258
68295
|
return;
|
|
68259
68296
|
}
|
|
68260
68297
|
const {
|
|
68261
|
-
index
|
|
68262
|
-
reason
|
|
68298
|
+
index
|
|
68263
68299
|
} = scrollInfo;
|
|
68264
|
-
// Recentering on every row crossed would rebuild the whole window a few
|
|
68265
|
-
// times a second, and a window rebuilt is every row of it rendered
|
|
68266
|
-
// again. It only moves once what is on screen comes near one of its
|
|
68267
|
-
// edges — until then, it already holds what has to be drawn.
|
|
68268
|
-
const {
|
|
68269
|
-
start,
|
|
68270
|
-
end
|
|
68271
|
-
} = renderWindowRef.current;
|
|
68272
68300
|
const margin = Math.floor(renderBudget / 4);
|
|
68273
68301
|
const farFromStart = index - start >= margin || start === 0;
|
|
68274
68302
|
const farFromEnd = end - index > margin || end === total;
|
|
@@ -68276,12 +68304,117 @@ const useListScrollSync = ({
|
|
|
68276
68304
|
return;
|
|
68277
68305
|
}
|
|
68278
68306
|
const half = Math.floor(renderBudget / 2);
|
|
68279
|
-
let newStart =
|
|
68280
|
-
let newEnd =
|
|
68281
|
-
if (newEnd
|
|
68282
|
-
|
|
68307
|
+
let newStart = index - half < 0 ? 0 : index - half;
|
|
68308
|
+
let newEnd = newStart + renderBudget;
|
|
68309
|
+
if (newEnd > total) {
|
|
68310
|
+
newEnd = total;
|
|
68311
|
+
newStart = total - renderBudget < 0 ? 0 : total - renderBudget;
|
|
68312
|
+
}
|
|
68313
|
+
windowSlidRef.current = true;
|
|
68314
|
+
updateRenderWindow(newStart, newEnd, `${reason}: ${scrollInfo.reason}`);
|
|
68315
|
+
return;
|
|
68316
|
+
}
|
|
68317
|
+
const viewportRect = getScrollerViewportRect(scrollerEl);
|
|
68318
|
+
const listRect = listEl.getBoundingClientRect();
|
|
68319
|
+
const viewportFrom = horizontal ? viewportRect.left : viewportRect.top;
|
|
68320
|
+
const viewportTo = horizontal ? viewportRect.right : viewportRect.bottom;
|
|
68321
|
+
const listFrom = horizontal ? listRect.left : listRect.top;
|
|
68322
|
+
let bandStart = Math.floor((viewportFrom - listFrom) / virtualItemSize);
|
|
68323
|
+
// Exclusive, like the window's end.
|
|
68324
|
+
let bandEnd = Math.ceil((viewportTo - listFrom) / virtualItemSize);
|
|
68325
|
+
if (bandStart < 0) {
|
|
68326
|
+
bandStart = 0;
|
|
68327
|
+
}
|
|
68328
|
+
if (bandEnd > total) {
|
|
68329
|
+
bandEnd = total;
|
|
68330
|
+
}
|
|
68331
|
+
if (bandEnd <= bandStart) {
|
|
68332
|
+
return;
|
|
68333
|
+
}
|
|
68334
|
+
const visibleCount = bandEnd - bandStart;
|
|
68335
|
+
const spare = renderBudget - visibleCount;
|
|
68336
|
+
if (!budgetWarnedRef.current && renderBudget === renderBudgetSteady && spare < 2) {
|
|
68337
|
+
budgetWarnedRef.current = true;
|
|
68338
|
+
console.warn(`List: renderBudget=${renderBudget} draws ${renderBudget} rows and this scroller shows ${visibleCount} at once: rows will go blank as it scrolls. Give it room for a screen ahead — ${visibleCount * 2} or more.`);
|
|
68339
|
+
}
|
|
68340
|
+
const behind = spare > 0 ? Math.floor(spare / 4) : 0;
|
|
68341
|
+
const ahead = spare > 0 ? spare - behind : 0;
|
|
68342
|
+
const halfScreen = Math.ceil(visibleCount / 2);
|
|
68343
|
+
const aheadNeeded = halfScreen < ahead / 2 ? halfScreen : Math.floor(ahead / 2);
|
|
68344
|
+
const behindNeeded = halfScreen < behind / 2 ? halfScreen : Math.floor(behind / 2);
|
|
68345
|
+
const forward = scrollDirectionRef.current > 0;
|
|
68346
|
+
const rowsAheadOfBand = forward ? end - bandEnd : bandStart - start;
|
|
68347
|
+
const rowsBehindBand = forward ? bandStart - start : end - bandEnd;
|
|
68348
|
+
const atEdgeAhead = forward ? end === total : start === 0;
|
|
68349
|
+
const atEdgeBehind = forward ? start === 0 : end === total;
|
|
68350
|
+
const coversBand = start <= bandStart && end >= bandEnd;
|
|
68351
|
+
const coversAhead = atEdgeAhead || rowsAheadOfBand >= aheadNeeded;
|
|
68352
|
+
const coversBehind = atEdgeBehind || rowsBehindBand >= behindNeeded;
|
|
68353
|
+
if (coversBand && coversAhead && coversBehind) {
|
|
68354
|
+
return;
|
|
68355
|
+
}
|
|
68356
|
+
let newStart = forward ? bandStart - behind : bandEnd + behind - renderBudget;
|
|
68357
|
+
if (newStart < 0) {
|
|
68358
|
+
newStart = 0;
|
|
68359
|
+
}
|
|
68360
|
+
let newEnd = newStart + renderBudget;
|
|
68361
|
+
if (newEnd > total) {
|
|
68362
|
+
newEnd = total;
|
|
68363
|
+
newStart = total - renderBudget < 0 ? 0 : total - renderBudget;
|
|
68364
|
+
}
|
|
68365
|
+
windowSlidRef.current = true;
|
|
68366
|
+
updateRenderWindow(newStart, newEnd, `${reason}: rows ${bandStart}-${bandEnd} on screen, going ${forward ? "forward" : "backward"}`);
|
|
68367
|
+
};
|
|
68368
|
+
// A slide is judged again once its rows are laid out: the scroll had moved
|
|
68369
|
+
// on while the rows were being drawn, or the anchoring of this very commit
|
|
68370
|
+
// moved it, and a window that stops short of the screen's edge with no
|
|
68371
|
+
// scroll event to come would stay short for good. On the next frame, after
|
|
68372
|
+
// the layout this commit's anchoring settles on.
|
|
68373
|
+
useLayoutEffect(() => {
|
|
68374
|
+
if (!windowSlidRef.current) {
|
|
68375
|
+
return undefined;
|
|
68376
|
+
}
|
|
68377
|
+
windowSlidRef.current = false;
|
|
68378
|
+
const frameId = requestAnimationFrame(() => {
|
|
68379
|
+
evaluateWindow("after slide");
|
|
68380
|
+
});
|
|
68381
|
+
return () => {
|
|
68382
|
+
cancelAnimationFrame(frameId);
|
|
68383
|
+
};
|
|
68384
|
+
}, [renderWindow]);
|
|
68385
|
+
|
|
68386
|
+
// Scroll listener — slides the window as the user scrolls.
|
|
68387
|
+
useLayoutEffect(() => {
|
|
68388
|
+
const listContainerEl = ref.current;
|
|
68389
|
+
if (!listContainerEl) {
|
|
68390
|
+
return undefined;
|
|
68391
|
+
}
|
|
68392
|
+
const scrollerEl = getScroller();
|
|
68393
|
+
const onScroll = () => {
|
|
68394
|
+
// Which way the user is going, read before the position is stored: what
|
|
68395
|
+
// the window keeps ahead is ahead of this.
|
|
68396
|
+
const previousScroll = currentScrollRef.current;
|
|
68397
|
+
if (previousScroll) {
|
|
68398
|
+
const delta = horizontal ? scrollerEl.scrollLeft - previousScroll.left : scrollerEl.scrollTop - previousScroll.top;
|
|
68399
|
+
if (delta > 0) {
|
|
68400
|
+
scrollDirectionRef.current = 1;
|
|
68401
|
+
} else if (delta < 0) {
|
|
68402
|
+
scrollDirectionRef.current = -1;
|
|
68403
|
+
}
|
|
68283
68404
|
}
|
|
68284
|
-
|
|
68405
|
+
updateCurrentScroll();
|
|
68406
|
+
// Where the user is now is where things must be held from now on.
|
|
68407
|
+
anchorRef.current = null;
|
|
68408
|
+
if (scrolledByListRef.current) {
|
|
68409
|
+
// The window stays where it is — the position it would be re-derived
|
|
68410
|
+
// from was chosen to keep the rows still — but where the list is has
|
|
68411
|
+
// genuinely changed, and whoever keeps that position must hear it.
|
|
68412
|
+
scrolledByListRef.current = false;
|
|
68413
|
+
reportPosition();
|
|
68414
|
+
return;
|
|
68415
|
+
}
|
|
68416
|
+
reportPosition();
|
|
68417
|
+
evaluateWindow("scroll");
|
|
68285
68418
|
};
|
|
68286
68419
|
// A page-level scroller does not emit "scroll" on the element itself
|
|
68287
68420
|
// (document.scrollingElement); the document does.
|
|
@@ -71273,8 +71406,11 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
|
|
|
71273
71406
|
* window, which slides as the user scrolls while fillers hold the room of
|
|
71274
71407
|
* the rows outside it. Rows declared one by one as `<List.Item>` children
|
|
71275
71408
|
* are all drawn, whatever this says — a list with more than a few dozen rows
|
|
71276
|
-
* gives them to a run (see docs/scroll.md, "Many rows").
|
|
71277
|
-
*
|
|
71409
|
+
* gives them to a run (see docs/scroll.md, "Many rows"). The window keeps
|
|
71410
|
+
* most of its spare rows ahead of the scroll, and moves once those fall
|
|
71411
|
+
* under half a screen; it has to be larger than what the scroller shows at
|
|
71412
|
+
* once, with room for that lookahead — the list warns below 30, and when a
|
|
71413
|
+
* budget leaves fewer than two rows beyond the screen.
|
|
71278
71414
|
*
|
|
71279
71415
|
* `{ initial, after }` for a list drawn in the click that opens it (a popup):
|
|
71280
71416
|
* `initial` rows in the commit the browser paints first — about what a phone
|
|
@@ -85887,7 +86023,7 @@ const css = /* css */`.navi_side_panel {
|
|
|
85887
86023
|
* @param {object} props
|
|
85888
86024
|
* @param {boolean} [props.open] - Controlled open state, forwarded as-is to
|
|
85889
86025
|
* `Popup`'s own `open`.
|
|
85890
|
-
* @param {import("@preact/signals").Signal<
|
|
86026
|
+
* @param {import("@preact/signals").Signal<any>} [props.signal] - The open
|
|
85891
86027
|
* state said the way every navi control says it: the panel opens and closes
|
|
85892
86028
|
* to match the signal, and writes into it whenever it opens or closes on its
|
|
85893
86029
|
* own (Escape, swipe, a --navi-close command) — one binding to both drive
|