@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/jsenv_navi.js
CHANGED
|
@@ -33840,19 +33840,27 @@ const writeInSignal = (signal, value, { history }) => {
|
|
|
33840
33840
|
// is open (`?seat=<gameId>` over a list of cards), and closed is the signal
|
|
33841
33841
|
// holding none of their values — `undefined`, which a state signal reads as
|
|
33842
33842
|
// its default.
|
|
33843
|
+
//
|
|
33844
|
+
// A popup without a `value` reads anything the signal holds — save `false`,
|
|
33845
|
+
// `null` and `undefined` — as open: a single sheet bound to `?open=<id>` is
|
|
33846
|
+
// open on whichever id the address names, and the id is the popup's content
|
|
33847
|
+
// to read, not its to write. Opening writes `true` only into a signal that
|
|
33848
|
+
// reads closed; closing writes `false` where the signal held `true`, so a
|
|
33849
|
+
// boolean signal keeps its boolean, and `undefined` for anything else.
|
|
33843
33850
|
const writeOpenedInSignal = (signal, opened, event, popupValue) => {
|
|
33844
|
-
|
|
33851
|
+
const signalValue = signal.peek();
|
|
33852
|
+
if (readOpened(signalValue, popupValue) === opened) {
|
|
33845
33853
|
// The signal already says so, meaning this open/close IS what it asked
|
|
33846
33854
|
// for: a back press that took the popup out of the url, the application
|
|
33847
33855
|
// writing it. Nothing to write back — and nothing to go back to either,
|
|
33848
33856
|
// since the navigation navBack would undo is the one that asked for this.
|
|
33849
33857
|
return;
|
|
33850
33858
|
}
|
|
33851
|
-
const closedValue = popupValue === undefined ? false : undefined;
|
|
33852
33859
|
if (opened) {
|
|
33853
33860
|
signal.value = popupValue === undefined ? true : popupValue;
|
|
33854
33861
|
return;
|
|
33855
33862
|
}
|
|
33863
|
+
const closedValue = readClosedValue(signalValue);
|
|
33856
33864
|
if (
|
|
33857
33865
|
signal.options?.getHistory?.() === "push" &&
|
|
33858
33866
|
// Nothing of this document behind: the popup was opened by the url itself
|
|
@@ -33873,10 +33881,15 @@ const writeOpenedInSignal = (signal, opened, event, popupValue) => {
|
|
|
33873
33881
|
};
|
|
33874
33882
|
const readOpened = (signalValue, popupValue) => {
|
|
33875
33883
|
if (popupValue === undefined) {
|
|
33876
|
-
return
|
|
33884
|
+
return (
|
|
33885
|
+
signalValue !== undefined && signalValue !== null && signalValue !== false
|
|
33886
|
+
);
|
|
33877
33887
|
}
|
|
33878
33888
|
return signalValue === popupValue;
|
|
33879
33889
|
};
|
|
33890
|
+
const readClosedValue = (openSignalValue) => {
|
|
33891
|
+
return openSignalValue === true ? false : undefined;
|
|
33892
|
+
};
|
|
33880
33893
|
|
|
33881
33894
|
/**
|
|
33882
33895
|
* Keeps an open controller in sync with where the caller says the popup should
|
|
@@ -33918,6 +33931,13 @@ const useOpenPropsEffectOnOpenController = (
|
|
|
33918
33931
|
: signal
|
|
33919
33932
|
? readOpened(signal.value, value)
|
|
33920
33933
|
: props.open;
|
|
33934
|
+
// What the signal holds while it reads open — `true`, the popup's `value`,
|
|
33935
|
+
// or the id a single sheet is open on. Put back when a close is refused,
|
|
33936
|
+
// since by then the signal has been written closed.
|
|
33937
|
+
const openSignalValueRef = useRef(undefined);
|
|
33938
|
+
if (signal && open) {
|
|
33939
|
+
openSignalValueRef.current = signal.value;
|
|
33940
|
+
}
|
|
33921
33941
|
// Assigned on every render, like openEffect, so it always closes over the
|
|
33922
33942
|
// latest prop: a popup that opens or closes on its own (Escape, backdrop, a
|
|
33923
33943
|
// --navi-close command) writes what happened where the caller keeps it, so
|
|
@@ -33998,10 +34018,11 @@ const useOpenPropsEffectOnOpenController = (
|
|
|
33998
34018
|
// request already reads in the signal, and with a `value` what the
|
|
33999
34019
|
// signal holds may be another popup's, which the write would erase.
|
|
34000
34020
|
if (signal && openController.opened !== open) {
|
|
34001
|
-
const opened = openController.opened;
|
|
34002
34021
|
writeInSignal(
|
|
34003
34022
|
signal,
|
|
34004
|
-
|
|
34023
|
+
openController.opened
|
|
34024
|
+
? openSignalValueRef.current
|
|
34025
|
+
: readClosedValue(signal.peek()),
|
|
34005
34026
|
{ history: "replace" },
|
|
34006
34027
|
);
|
|
34007
34028
|
}
|
|
@@ -34983,6 +35004,8 @@ const createDisplayedEvent = (ancestor, becauseAncestorOpened) => {
|
|
|
34983
35004
|
* @returns {Function} triggerAutofocus — can be called manually with a synthetic
|
|
34984
35005
|
* event to re-run the focus logic outside of the layout-effect lifecycle.
|
|
34985
35006
|
*/
|
|
35007
|
+
const NO_ELEMENT_REF = { current: null };
|
|
35008
|
+
|
|
34986
35009
|
const useAutoFocus = (
|
|
34987
35010
|
focusableElementRef,
|
|
34988
35011
|
autoFocus,
|
|
@@ -35114,8 +35137,15 @@ const useAutoFocus = (
|
|
|
35114
35137
|
};
|
|
35115
35138
|
};
|
|
35116
35139
|
|
|
35140
|
+
// Nothing to place when the control asks for no focus ("restore" is given
|
|
35141
|
+
// focus by a transfer, never by this effect): the displayed-effect is then
|
|
35142
|
+
// handed no element at all, so it never asks the layout whether one is on
|
|
35143
|
+
// screen — a question every control in a list would otherwise ask at
|
|
35144
|
+
// mount, for an answer this hook would not use. Read at mount, like the
|
|
35145
|
+
// deps below say.
|
|
35146
|
+
const claimsFocus = Boolean(autoFocus) && autoFocus !== "restore";
|
|
35117
35147
|
useDisplayedLayoutEffect(
|
|
35118
|
-
focusableElementRef,
|
|
35148
|
+
claimsFocus ? focusableElementRef : NO_ELEMENT_REF,
|
|
35119
35149
|
(el, e) => {
|
|
35120
35150
|
return triggerAutofocus(e);
|
|
35121
35151
|
},
|
|
@@ -36477,6 +36507,46 @@ const acknowledgeOwnAction = (controller) => {
|
|
|
36477
36507
|
controller.acknowledgeUIState();
|
|
36478
36508
|
};
|
|
36479
36509
|
|
|
36510
|
+
// What a group of checkboxes or radios is worth is a claim about values, and
|
|
36511
|
+
// a value whose box is not there cannot be contradicted by that box: a row
|
|
36512
|
+
// scrolled out of a virtualized list, a filtered-out option, a whole page being
|
|
36513
|
+
// parked while a route action reruns — its children leave one at a time, and
|
|
36514
|
+
// each partial reading would otherwise be taken for the answer and written
|
|
36515
|
+
// into whatever the group is bound to (an array signal emptied down to `[]`
|
|
36516
|
+
// by its own unmount). `kept` is what the group holds as it is asked; a value
|
|
36517
|
+
// in it stays until a DRAWN child carrying that value says otherwise.
|
|
36518
|
+
//
|
|
36519
|
+
// Read off the group rather than remembered here on purpose: a value put ON
|
|
36520
|
+
// the group (a `value` prop, a signal, a reopened popup) replaces the whole
|
|
36521
|
+
// selection, and a private memory would go on holding the rows it could not
|
|
36522
|
+
// see being unselected.
|
|
36523
|
+
const keptAndCheckedChildUIStates = (children, fallbackState, kept) => {
|
|
36524
|
+
const drawnValues = new Set(children.map((child) => child.props.value));
|
|
36525
|
+
const values = Array.isArray(kept)
|
|
36526
|
+
? kept.filter((value) => !drawnValues.has(value))
|
|
36527
|
+
: [];
|
|
36528
|
+
for (const child of children) {
|
|
36529
|
+
if (child.uiState !== undefined) {
|
|
36530
|
+
values.push(child.uiState);
|
|
36531
|
+
}
|
|
36532
|
+
}
|
|
36533
|
+
return values.length === 0 ? undefined : values;
|
|
36534
|
+
};
|
|
36535
|
+
const keptOrFirstDefinedChildUIState = (children, fallbackState, kept) => {
|
|
36536
|
+
for (const child of children) {
|
|
36537
|
+
if (child.uiState !== undefined) {
|
|
36538
|
+
return child.uiState;
|
|
36539
|
+
}
|
|
36540
|
+
}
|
|
36541
|
+
// No drawn child claims it. If the child that held it IS drawn, it was really
|
|
36542
|
+
// deselected; if it is not, the group keeps what it holds.
|
|
36543
|
+
const keptIsDrawn = children.some((child) => child.props.value === kept);
|
|
36544
|
+
if (keptIsDrawn) {
|
|
36545
|
+
return undefined;
|
|
36546
|
+
}
|
|
36547
|
+
return kept;
|
|
36548
|
+
};
|
|
36549
|
+
|
|
36480
36550
|
// Default aggregate/distribute implementations keyed by controlType or stateType.
|
|
36481
36551
|
// Looked up in useUIGroupStateController to fill in omitted aggregateChildStates /
|
|
36482
36552
|
// distributeChildUIState. If neither a default nor an explicit impl is found for a
|
|
@@ -36485,7 +36555,7 @@ const GROUP_DEFAULTS = {
|
|
|
36485
36555
|
radio_group: {
|
|
36486
36556
|
childControlFilter: (child) =>
|
|
36487
36557
|
child.controlType === "input" && child.controlHostProps?.type === "radio",
|
|
36488
|
-
aggregateChildStates:
|
|
36558
|
+
aggregateChildStates: keptOrFirstDefinedChildUIState,
|
|
36489
36559
|
distributeChildUIState: (newUIState, childUIStateController) => {
|
|
36490
36560
|
const childSelected = childUIStateController.props.value === newUIState;
|
|
36491
36561
|
if (childSelected) {
|
|
@@ -36498,16 +36568,7 @@ const GROUP_DEFAULTS = {
|
|
|
36498
36568
|
childControlFilter: (child) =>
|
|
36499
36569
|
child.controlType === "input" &&
|
|
36500
36570
|
child.controlHostProps?.type === "checkbox",
|
|
36501
|
-
aggregateChildStates:
|
|
36502
|
-
const values = [];
|
|
36503
|
-
for (const child of children) {
|
|
36504
|
-
const childUIState = child.uiState;
|
|
36505
|
-
if (childUIState !== undefined) {
|
|
36506
|
-
values.push(childUIState);
|
|
36507
|
-
}
|
|
36508
|
-
}
|
|
36509
|
-
return values.length === 0 ? undefined : values;
|
|
36510
|
-
},
|
|
36571
|
+
aggregateChildStates: keptAndCheckedChildUIStates,
|
|
36511
36572
|
distributeChildUIState: (newUIState, childUIStateController) => {
|
|
36512
36573
|
const childSelected =
|
|
36513
36574
|
Array.isArray(newUIState) &&
|
|
@@ -59409,14 +59470,15 @@ const css$E = /* css */`
|
|
|
59409
59470
|
* the focus only leaves it for something that asked by name (`autoFocus` on
|
|
59410
59471
|
* that element, which outranks whatever the dialog says).
|
|
59411
59472
|
* @param {boolean} [props.open] - Controlled open state.
|
|
59412
|
-
* @param {import("@preact/signals").Signal<
|
|
59473
|
+
* @param {import("@preact/signals").Signal<any>} [props.signal] - The open
|
|
59413
59474
|
* state said the way every navi control says it: the dialog opens and closes
|
|
59414
59475
|
* to match the signal, and writes into it whenever it opens or closes on its
|
|
59415
59476
|
* own (Escape, backdrop, a --navi-close command) — one binding to both drive
|
|
59416
59477
|
* the dialog and know where it is, and the state stays where the app put it.
|
|
59417
59478
|
* Excludes `open`; `onOpen`/`onClose` still fire. A signal holding `true` at
|
|
59418
59479
|
* mount behaves like `defaultOpen`: the dialog was already open, no entrance
|
|
59419
|
-
* plays.
|
|
59480
|
+
* plays. A signal holding an id (one dialog showing whichever card
|
|
59481
|
+
* the address names) reads as open, and the dialog leaves the id alone.
|
|
59420
59482
|
* @param {any} [props.value] - What `signal` holds while THIS dialog is
|
|
59421
59483
|
* open, for several of them sharing one signal that says which is open
|
|
59422
59484
|
* (`?seat=<gameId>` over a list of cards): open while `signal.value` is this
|
|
@@ -61024,14 +61086,15 @@ const css$D = /* css */`
|
|
|
61024
61086
|
* the focus only leaves it for something that asked by name (`autoFocus` on
|
|
61025
61087
|
* that element, which outranks whatever the popover says).
|
|
61026
61088
|
* @param {boolean} [props.open] - Controlled open state.
|
|
61027
|
-
* @param {import("@preact/signals").Signal<
|
|
61089
|
+
* @param {import("@preact/signals").Signal<any>} [props.signal] - The open
|
|
61028
61090
|
* state said the way every navi control says it: the popover opens and closes
|
|
61029
61091
|
* to match the signal, and writes into it whenever it opens or closes on its
|
|
61030
61092
|
* own (Escape, light dismiss, a --navi-close command) — one binding to both
|
|
61031
61093
|
* drive the popover and know where it is, and the state stays where the app
|
|
61032
61094
|
* put it. Excludes `open`; `onOpen`/`onClose` still fire. A signal holding
|
|
61033
61095
|
* `true` at mount behaves like `defaultOpen`: the popover was already open,
|
|
61034
|
-
* no entrance plays.
|
|
61096
|
+
* no entrance plays. A signal holding an id (one popover showing whichever card
|
|
61097
|
+
* the address names) reads as open, and the popover leaves the id alone.
|
|
61035
61098
|
* @param {any} [props.value] - What `signal` holds while THIS popover is
|
|
61036
61099
|
* open, for several of them sharing one signal that says which is open
|
|
61037
61100
|
* (`?seat=<gameId>` over a list of cards): open while `signal.value` is this
|
|
@@ -64679,45 +64742,6 @@ const ListSelectable = props => {
|
|
|
64679
64742
|
focusGroupDirection,
|
|
64680
64743
|
focusGroupWrap
|
|
64681
64744
|
} = props;
|
|
64682
|
-
// `kept` is what the list holds as it is asked, which is not the same as what
|
|
64683
|
-
// its rows say: a list draws the rows it needs and no more, so the selected
|
|
64684
|
-
// one may be scrolled out of the window or filtered out of the view, and a
|
|
64685
|
-
// row that is not there cannot say it is not selected. Reading it off the
|
|
64686
|
-
// group rather than remembering it here is what makes a value put ON the list
|
|
64687
|
-
// (a `value` prop, a signal, a reopened popup) replace the whole selection —
|
|
64688
|
-
// a private memory of its own would go on holding the rows it could not see
|
|
64689
|
-
// being unselected.
|
|
64690
|
-
//
|
|
64691
|
-
// `fallbackState` is the empty of the shape the list declared below
|
|
64692
|
-
// (`stateType`): `[]` for a multiple list, nothing for a single one. Taking
|
|
64693
|
-
// it is what lets an emptied list say "empty" — `undefined` is the word for
|
|
64694
|
-
// "unset", and a bound stateSignal reads that as "nothing decided here, go
|
|
64695
|
-
// back to the default" (see docs/control_value.md), which is how a list
|
|
64696
|
-
// emptied down to its last row puts that row back on reload.
|
|
64697
|
-
const aggregateChildStates = (children, fallbackState, kept) => {
|
|
64698
|
-
if (multiple) {
|
|
64699
|
-
const drawnValues = new Set(children.map(child => child.props.value));
|
|
64700
|
-
const stillSelected = Array.isArray(kept) ? kept.filter(value => !drawnValues.has(value)) : [];
|
|
64701
|
-
for (const child of children) {
|
|
64702
|
-
if (child.uiState !== undefined) {
|
|
64703
|
-
stillSelected.push(child.uiState);
|
|
64704
|
-
}
|
|
64705
|
-
}
|
|
64706
|
-
return stillSelected.length === 0 ? fallbackState : stillSelected;
|
|
64707
|
-
}
|
|
64708
|
-
for (const child of children) {
|
|
64709
|
-
if (child.uiState !== undefined) {
|
|
64710
|
-
return child.uiState;
|
|
64711
|
-
}
|
|
64712
|
-
}
|
|
64713
|
-
// No drawn row claims it. If the row that held it IS drawn, it was really
|
|
64714
|
-
// deselected; if it is not, the list keeps what it holds.
|
|
64715
|
-
const keptIsDrawn = children.some(child => child.props.value === kept);
|
|
64716
|
-
if (keptIsDrawn) {
|
|
64717
|
-
return undefined;
|
|
64718
|
-
}
|
|
64719
|
-
return kept;
|
|
64720
|
-
};
|
|
64721
64745
|
const [listControlRootProps, listControlProps, childrenWrapperProps] = useControlgroupProps({
|
|
64722
64746
|
// The defaults of the group this IS (see CheckboxGroup / RadioGroup):
|
|
64723
64747
|
// what a selectable list shows is a claim about what was accepted, so
|
|
@@ -64729,9 +64753,15 @@ const ListSelectable = props => {
|
|
|
64729
64753
|
resetOnError: true,
|
|
64730
64754
|
...props
|
|
64731
64755
|
}, {
|
|
64756
|
+
// A list draws the rows it needs and no more, so a selected row may be
|
|
64757
|
+
// scrolled out of the window or filtered out of the view: the default
|
|
64758
|
+
// aggregate of these two groups keeps a value whose row is not drawn
|
|
64759
|
+
// (see GROUP_DEFAULTS in ui_state_controller.js). `stateType` is what
|
|
64760
|
+
// an emptied multiple list says — `[]`, not `undefined`, which a bound
|
|
64761
|
+
// stateSignal reads as "nothing decided here, go back to the default"
|
|
64762
|
+
// (see docs/control_value.md).
|
|
64732
64763
|
stateType: multiple ? "array" : "",
|
|
64733
|
-
controlType: multiple ? "checkbox_group" : "radio_group"
|
|
64734
|
-
aggregateChildStates
|
|
64764
|
+
controlType: multiple ? "checkbox_group" : "radio_group"
|
|
64735
64765
|
});
|
|
64736
64766
|
const uiGroupStateController = getUIStateControllerById(listControlProps.id);
|
|
64737
64767
|
useFocusGroup(ref, {
|
|
@@ -65933,6 +65963,7 @@ const ListUI = props => {
|
|
|
65933
65963
|
ref,
|
|
65934
65964
|
tracker,
|
|
65935
65965
|
renderBudget,
|
|
65966
|
+
renderBudgetSteady: renderBudgetAfterPaint,
|
|
65936
65967
|
virtualItemSize,
|
|
65937
65968
|
virtual,
|
|
65938
65969
|
scrolled,
|
|
@@ -66260,6 +66291,7 @@ const useListScrollSync = ({
|
|
|
66260
66291
|
ref,
|
|
66261
66292
|
tracker,
|
|
66262
66293
|
renderBudget,
|
|
66294
|
+
renderBudgetSteady,
|
|
66263
66295
|
virtualItemSize,
|
|
66264
66296
|
virtual,
|
|
66265
66297
|
scrolled,
|
|
@@ -67010,31 +67042,36 @@ const useListScrollSync = ({
|
|
|
67010
67042
|
}
|
|
67011
67043
|
});
|
|
67012
67044
|
|
|
67013
|
-
//
|
|
67014
|
-
|
|
67015
|
-
|
|
67016
|
-
|
|
67017
|
-
|
|
67045
|
+
// The window the visible band asks for, decided from geometry: the band's
|
|
67046
|
+
// two edges in row units, from where the scroller's viewport cuts the list
|
|
67047
|
+
// and the row size. What has to be drawn is everything between the edges,
|
|
67048
|
+
// plus what the scroll is about to bring — so the window is judged on the
|
|
67049
|
+
// rows it keeps AHEAD of the band, in the direction the user goes, and moves
|
|
67050
|
+
// once those fall under half a screen. Re-framed, it puts three quarters of
|
|
67051
|
+
// its spare rows ahead and one quarter behind: a window centred on the band
|
|
67052
|
+
// is due to move again as soon as the band has crossed the few rows it kept
|
|
67053
|
+
// ahead, and each move is rows drawn while the user waits for them.
|
|
67054
|
+
// Without a row size there is no geometry to reason on; the row under a
|
|
67055
|
+
// probe stands in for the band, framed the old way.
|
|
67056
|
+
const scrollDirectionRef = useRef(1);
|
|
67057
|
+
const windowSlidRef = useRef(false);
|
|
67058
|
+
useRef(false);
|
|
67059
|
+
const evaluateWindow = reason => {
|
|
67060
|
+
const total = virtual.totalSignal.peek();
|
|
67061
|
+
if (total <= renderBudget) {
|
|
67062
|
+
return;
|
|
67018
67063
|
}
|
|
67019
67064
|
const scrollerEl = getScroller();
|
|
67020
67065
|
const listEl = getListEl();
|
|
67021
|
-
|
|
67022
|
-
|
|
67023
|
-
|
|
67024
|
-
|
|
67025
|
-
|
|
67026
|
-
|
|
67027
|
-
|
|
67028
|
-
|
|
67029
|
-
|
|
67030
|
-
reportPosition();
|
|
67031
|
-
return;
|
|
67032
|
-
}
|
|
67033
|
-
reportPosition();
|
|
67034
|
-
const total = virtual.totalSignal.peek();
|
|
67035
|
-
if (total <= renderBudget) {
|
|
67036
|
-
return;
|
|
67037
|
-
}
|
|
67066
|
+
if (!scrollerEl || !listEl) {
|
|
67067
|
+
return;
|
|
67068
|
+
}
|
|
67069
|
+
const {
|
|
67070
|
+
start,
|
|
67071
|
+
end
|
|
67072
|
+
} = renderWindowRef.current;
|
|
67073
|
+
const virtualItemSize = virtualItemSizeSignal.peek();
|
|
67074
|
+
if (virtualItemSize === 0) {
|
|
67038
67075
|
const scrollInfo = getScrollInfo({
|
|
67039
67076
|
scrollValues: {
|
|
67040
67077
|
left: scrollerEl.scrollLeft,
|
|
@@ -67051,17 +67088,8 @@ const useListScrollSync = ({
|
|
|
67051
67088
|
return;
|
|
67052
67089
|
}
|
|
67053
67090
|
const {
|
|
67054
|
-
index
|
|
67055
|
-
reason
|
|
67091
|
+
index
|
|
67056
67092
|
} = scrollInfo;
|
|
67057
|
-
// Recentering on every row crossed would rebuild the whole window a few
|
|
67058
|
-
// times a second, and a window rebuilt is every row of it rendered
|
|
67059
|
-
// again. It only moves once what is on screen comes near one of its
|
|
67060
|
-
// edges — until then, it already holds what has to be drawn.
|
|
67061
|
-
const {
|
|
67062
|
-
start,
|
|
67063
|
-
end
|
|
67064
|
-
} = renderWindowRef.current;
|
|
67065
67093
|
const margin = Math.floor(renderBudget / 4);
|
|
67066
67094
|
const farFromStart = index - start >= margin || start === 0;
|
|
67067
67095
|
const farFromEnd = end - index > margin || end === total;
|
|
@@ -67069,12 +67097,113 @@ const useListScrollSync = ({
|
|
|
67069
67097
|
return;
|
|
67070
67098
|
}
|
|
67071
67099
|
const half = Math.floor(renderBudget / 2);
|
|
67072
|
-
let newStart =
|
|
67073
|
-
let newEnd =
|
|
67074
|
-
if (newEnd
|
|
67075
|
-
|
|
67100
|
+
let newStart = index - half < 0 ? 0 : index - half;
|
|
67101
|
+
let newEnd = newStart + renderBudget;
|
|
67102
|
+
if (newEnd > total) {
|
|
67103
|
+
newEnd = total;
|
|
67104
|
+
newStart = total - renderBudget < 0 ? 0 : total - renderBudget;
|
|
67105
|
+
}
|
|
67106
|
+
windowSlidRef.current = true;
|
|
67107
|
+
updateRenderWindow(newStart, newEnd, `${reason}: ${scrollInfo.reason}`);
|
|
67108
|
+
return;
|
|
67109
|
+
}
|
|
67110
|
+
const viewportRect = getScrollerViewportRect(scrollerEl);
|
|
67111
|
+
const listRect = listEl.getBoundingClientRect();
|
|
67112
|
+
const viewportFrom = horizontal ? viewportRect.left : viewportRect.top;
|
|
67113
|
+
const viewportTo = horizontal ? viewportRect.right : viewportRect.bottom;
|
|
67114
|
+
const listFrom = horizontal ? listRect.left : listRect.top;
|
|
67115
|
+
let bandStart = Math.floor((viewportFrom - listFrom) / virtualItemSize);
|
|
67116
|
+
// Exclusive, like the window's end.
|
|
67117
|
+
let bandEnd = Math.ceil((viewportTo - listFrom) / virtualItemSize);
|
|
67118
|
+
if (bandStart < 0) {
|
|
67119
|
+
bandStart = 0;
|
|
67120
|
+
}
|
|
67121
|
+
if (bandEnd > total) {
|
|
67122
|
+
bandEnd = total;
|
|
67123
|
+
}
|
|
67124
|
+
if (bandEnd <= bandStart) {
|
|
67125
|
+
return;
|
|
67126
|
+
}
|
|
67127
|
+
const visibleCount = bandEnd - bandStart;
|
|
67128
|
+
const spare = renderBudget - visibleCount;
|
|
67129
|
+
const behind = spare > 0 ? Math.floor(spare / 4) : 0;
|
|
67130
|
+
const ahead = spare > 0 ? spare - behind : 0;
|
|
67131
|
+
const halfScreen = Math.ceil(visibleCount / 2);
|
|
67132
|
+
const aheadNeeded = halfScreen < ahead / 2 ? halfScreen : Math.floor(ahead / 2);
|
|
67133
|
+
const behindNeeded = halfScreen < behind / 2 ? halfScreen : Math.floor(behind / 2);
|
|
67134
|
+
const forward = scrollDirectionRef.current > 0;
|
|
67135
|
+
const rowsAheadOfBand = forward ? end - bandEnd : bandStart - start;
|
|
67136
|
+
const rowsBehindBand = forward ? bandStart - start : end - bandEnd;
|
|
67137
|
+
const atEdgeAhead = forward ? end === total : start === 0;
|
|
67138
|
+
const atEdgeBehind = forward ? start === 0 : end === total;
|
|
67139
|
+
const coversBand = start <= bandStart && end >= bandEnd;
|
|
67140
|
+
const coversAhead = atEdgeAhead || rowsAheadOfBand >= aheadNeeded;
|
|
67141
|
+
const coversBehind = atEdgeBehind || rowsBehindBand >= behindNeeded;
|
|
67142
|
+
if (coversBand && coversAhead && coversBehind) {
|
|
67143
|
+
return;
|
|
67144
|
+
}
|
|
67145
|
+
let newStart = forward ? bandStart - behind : bandEnd + behind - renderBudget;
|
|
67146
|
+
if (newStart < 0) {
|
|
67147
|
+
newStart = 0;
|
|
67148
|
+
}
|
|
67149
|
+
let newEnd = newStart + renderBudget;
|
|
67150
|
+
if (newEnd > total) {
|
|
67151
|
+
newEnd = total;
|
|
67152
|
+
newStart = total - renderBudget < 0 ? 0 : total - renderBudget;
|
|
67153
|
+
}
|
|
67154
|
+
windowSlidRef.current = true;
|
|
67155
|
+
updateRenderWindow(newStart, newEnd);
|
|
67156
|
+
};
|
|
67157
|
+
// A slide is judged again once its rows are laid out: the scroll had moved
|
|
67158
|
+
// on while the rows were being drawn, or the anchoring of this very commit
|
|
67159
|
+
// moved it, and a window that stops short of the screen's edge with no
|
|
67160
|
+
// scroll event to come would stay short for good. On the next frame, after
|
|
67161
|
+
// the layout this commit's anchoring settles on.
|
|
67162
|
+
useLayoutEffect(() => {
|
|
67163
|
+
if (!windowSlidRef.current) {
|
|
67164
|
+
return undefined;
|
|
67165
|
+
}
|
|
67166
|
+
windowSlidRef.current = false;
|
|
67167
|
+
const frameId = requestAnimationFrame(() => {
|
|
67168
|
+
evaluateWindow("after slide");
|
|
67169
|
+
});
|
|
67170
|
+
return () => {
|
|
67171
|
+
cancelAnimationFrame(frameId);
|
|
67172
|
+
};
|
|
67173
|
+
}, [renderWindow]);
|
|
67174
|
+
|
|
67175
|
+
// Scroll listener — slides the window as the user scrolls.
|
|
67176
|
+
useLayoutEffect(() => {
|
|
67177
|
+
const listContainerEl = ref.current;
|
|
67178
|
+
if (!listContainerEl) {
|
|
67179
|
+
return undefined;
|
|
67180
|
+
}
|
|
67181
|
+
const scrollerEl = getScroller();
|
|
67182
|
+
const onScroll = () => {
|
|
67183
|
+
// Which way the user is going, read before the position is stored: what
|
|
67184
|
+
// the window keeps ahead is ahead of this.
|
|
67185
|
+
const previousScroll = currentScrollRef.current;
|
|
67186
|
+
if (previousScroll) {
|
|
67187
|
+
const delta = horizontal ? scrollerEl.scrollLeft - previousScroll.left : scrollerEl.scrollTop - previousScroll.top;
|
|
67188
|
+
if (delta > 0) {
|
|
67189
|
+
scrollDirectionRef.current = 1;
|
|
67190
|
+
} else if (delta < 0) {
|
|
67191
|
+
scrollDirectionRef.current = -1;
|
|
67192
|
+
}
|
|
67193
|
+
}
|
|
67194
|
+
updateCurrentScroll();
|
|
67195
|
+
// Where the user is now is where things must be held from now on.
|
|
67196
|
+
anchorRef.current = null;
|
|
67197
|
+
if (scrolledByListRef.current) {
|
|
67198
|
+
// The window stays where it is — the position it would be re-derived
|
|
67199
|
+
// from was chosen to keep the rows still — but where the list is has
|
|
67200
|
+
// genuinely changed, and whoever keeps that position must hear it.
|
|
67201
|
+
scrolledByListRef.current = false;
|
|
67202
|
+
reportPosition();
|
|
67203
|
+
return;
|
|
67076
67204
|
}
|
|
67077
|
-
|
|
67205
|
+
reportPosition();
|
|
67206
|
+
evaluateWindow("scroll");
|
|
67078
67207
|
};
|
|
67079
67208
|
// A page-level scroller does not emit "scroll" on the element itself
|
|
67080
67209
|
// (document.scrollingElement); the document does.
|
|
@@ -70008,8 +70137,11 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
|
|
|
70008
70137
|
* window, which slides as the user scrolls while fillers hold the room of
|
|
70009
70138
|
* the rows outside it. Rows declared one by one as `<List.Item>` children
|
|
70010
70139
|
* are all drawn, whatever this says — a list with more than a few dozen rows
|
|
70011
|
-
* gives them to a run (see docs/scroll.md, "Many rows").
|
|
70012
|
-
*
|
|
70140
|
+
* gives them to a run (see docs/scroll.md, "Many rows"). The window keeps
|
|
70141
|
+
* most of its spare rows ahead of the scroll, and moves once those fall
|
|
70142
|
+
* under half a screen; it has to be larger than what the scroller shows at
|
|
70143
|
+
* once, with room for that lookahead — the list warns below 30, and when a
|
|
70144
|
+
* budget leaves fewer than two rows beyond the screen.
|
|
70013
70145
|
*
|
|
70014
70146
|
* `{ initial, after }` for a list drawn in the click that opens it (a popup):
|
|
70015
70147
|
* `initial` rows in the commit the browser paints first — about what a phone
|
|
@@ -84043,7 +84175,7 @@ const css = /* css */`.navi_side_panel {
|
|
|
84043
84175
|
* @param {object} props
|
|
84044
84176
|
* @param {boolean} [props.open] - Controlled open state, forwarded as-is to
|
|
84045
84177
|
* `Popup`'s own `open`.
|
|
84046
|
-
* @param {import("@preact/signals").Signal<
|
|
84178
|
+
* @param {import("@preact/signals").Signal<any>} [props.signal] - The open
|
|
84047
84179
|
* state said the way every navi control says it: the panel opens and closes
|
|
84048
84180
|
* to match the signal, and writes into it whenever it opens or closes on its
|
|
84049
84181
|
* own (Escape, swipe, a --navi-close command) — one binding to both drive
|