@jsenv/navi 0.29.442 → 0.29.444
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 +350 -101
- package/dist/dev/jsenv_navi.js.map +9 -9
- package/dist/jsenv_navi.js +328 -100
- package/dist/jsenv_navi.js.map +9 -9
- package/docs/AI_INSTRUCTIONS.md +11 -8
- package/docs/actions.md +6 -2
- package/docs/control_value.md +28 -8
- package/docs/mobile_touch.md +19 -6
- package/docs/scroll.md +39 -9
- package/package.json +1 -1
package/dist/jsenv_navi.js
CHANGED
|
@@ -6281,6 +6281,10 @@ const CONTROL_PROP_SET = new Set([
|
|
|
6281
6281
|
"charGuard",
|
|
6282
6282
|
"maxLengthGuard",
|
|
6283
6283
|
|
|
6284
|
+
// A checkbox that is a yes/no: `true` checked, `false` unchecked, instead of
|
|
6285
|
+
// HTML's value-or-nothing (see resolveEmptyUIState).
|
|
6286
|
+
"boolean",
|
|
6287
|
+
|
|
6284
6288
|
// This control answers for itself: it registers into no group, no form and no
|
|
6285
6289
|
// picker around it (see useUIStateController). It belongs here rather than
|
|
6286
6290
|
// being deleted off `props` where it is read: props is the vnode's own object
|
|
@@ -42149,6 +42153,14 @@ const useUIStateController = (
|
|
|
42149
42153
|
return ownUIState || parentUIState;
|
|
42150
42154
|
})
|
|
42151
42155
|
: ownUIStateSignal;
|
|
42156
|
+
// What the control is worth, which its action is bound to: what it
|
|
42157
|
+
// holds, or its own empty when it holds nothing. Apart from
|
|
42158
|
+
// uiStateSignal because unchecked is `undefined` there (the DOM reads any
|
|
42159
|
+
// other value as checked), while a yes/no unchecked is worth `false`.
|
|
42160
|
+
const emptyUIStateSignal = signal(emptyUIState);
|
|
42161
|
+
const valueSignal = computed(() =>
|
|
42162
|
+
uiStateOrEmpty(uiStateSignal.value, emptyUIStateSignal.value),
|
|
42163
|
+
);
|
|
42152
42164
|
|
|
42153
42165
|
// The two-way half of a bound `signal` prop: setting it re-renders and
|
|
42154
42166
|
// re-syncs via state_prop_change, but with the same value → guarded as a
|
|
@@ -42197,6 +42209,7 @@ const useUIStateController = (
|
|
|
42197
42209
|
// travel command ended up carrying a picker's selection: see
|
|
42198
42210
|
// resolveCommandValue in commands.js.
|
|
42199
42211
|
ownUIStateSignal,
|
|
42212
|
+
valueSignal,
|
|
42200
42213
|
value: controlInfo.value,
|
|
42201
42214
|
// The suggestion this control started on — what tells a field showing
|
|
42202
42215
|
// its default from one carrying an answer (see isUIStateHeld).
|
|
@@ -42256,7 +42269,11 @@ const useUIStateController = (
|
|
|
42256
42269
|
}
|
|
42257
42270
|
s.uiActionInternal?.(currentUIState, e);
|
|
42258
42271
|
if (s.uiAction) {
|
|
42259
|
-
|
|
42272
|
+
const value = uiStateOrEmpty(
|
|
42273
|
+
currentUIState,
|
|
42274
|
+
controller.emptyUIState,
|
|
42275
|
+
);
|
|
42276
|
+
s.uiAction(value, e);
|
|
42260
42277
|
}
|
|
42261
42278
|
if (skipCommand) ; else {
|
|
42262
42279
|
const command = controller.controlHostProps.command;
|
|
@@ -42743,6 +42760,7 @@ const useUIStateController = (
|
|
|
42743
42760
|
return {
|
|
42744
42761
|
controller,
|
|
42745
42762
|
parentUiStateSignalHolder,
|
|
42763
|
+
emptyUIStateSignal,
|
|
42746
42764
|
...liveValues(),
|
|
42747
42765
|
};
|
|
42748
42766
|
},
|
|
@@ -42864,6 +42882,7 @@ const useUIStateController = (
|
|
|
42864
42882
|
);
|
|
42865
42883
|
scope.parentUiStateSignalHolder.value =
|
|
42866
42884
|
parentUIStateController?.uiStateSignal ?? null;
|
|
42885
|
+
scope.emptyUIStateSignal.value = emptyUIState;
|
|
42867
42886
|
|
|
42868
42887
|
const { controller } = scope;
|
|
42869
42888
|
const controllerRef = controller.ref;
|
|
@@ -43074,14 +43093,11 @@ const keptAndNamedChildUIStates = (children, fallbackState, kept) => {
|
|
|
43074
43093
|
const readNamedChildUIStates = (children, { warnNameless }) => {
|
|
43075
43094
|
const values = {};
|
|
43076
43095
|
for (const child of children) {
|
|
43077
|
-
const { name
|
|
43096
|
+
const { name } = child;
|
|
43078
43097
|
// A control holding nothing writes its own empty, not a hole: the key is in
|
|
43079
43098
|
// the object either way, and what is read from it keeps the shape the
|
|
43080
43099
|
// reader was promised (see resolveEmptyUIState).
|
|
43081
|
-
const uiState =
|
|
43082
|
-
child.uiState === undefined && emptyUIState !== undefined
|
|
43083
|
-
? emptyUIState
|
|
43084
|
-
: child.uiState;
|
|
43100
|
+
const uiState = uiStateOrEmpty(child.uiState, child.emptyUIState);
|
|
43085
43101
|
if (!name) {
|
|
43086
43102
|
if (isNamelessGrouping(child, uiState)) {
|
|
43087
43103
|
Object.assign(
|
|
@@ -44413,11 +44429,10 @@ const useUIFacadeStateController = (props, realUIStateController) => {
|
|
|
44413
44429
|
// whose last item was unselected holds [], the same thing clearing it
|
|
44414
44430
|
// leaves — not a value that changes type on its owner the moment it
|
|
44415
44431
|
// empties.
|
|
44416
|
-
const
|
|
44417
|
-
|
|
44418
|
-
|
|
44419
|
-
|
|
44420
|
-
: child.uiState;
|
|
44432
|
+
const uiStateToAdopt = uiStateOrEmpty(
|
|
44433
|
+
child.uiState,
|
|
44434
|
+
s.realUIStateController.emptyUIState,
|
|
44435
|
+
);
|
|
44421
44436
|
s.realUIStateController.setUIState(uiStateToAdopt, propagateUpEvent);
|
|
44422
44437
|
updatingRef.current = false;
|
|
44423
44438
|
},
|
|
@@ -44657,8 +44672,9 @@ const resolveEmptyUIState = (props, controlType) => {
|
|
|
44657
44672
|
if (controlType === "input" && props.type === "checkbox") {
|
|
44658
44673
|
// A checkbox is a member of a set, the way HTML has it: checked it sends
|
|
44659
44674
|
// its value ("on" by default), unchecked it sends nothing at all. Only one
|
|
44660
|
-
//
|
|
44661
|
-
|
|
44675
|
+
// declared `boolean` is a yes/no, and a yes/no nobody said yes to is
|
|
44676
|
+
// `false`.
|
|
44677
|
+
return props.boolean ? false : undefined;
|
|
44662
44678
|
}
|
|
44663
44679
|
const stateShape = props["navi-state-shape"];
|
|
44664
44680
|
if (stateShape === "array") {
|
|
@@ -44670,6 +44686,15 @@ const resolveEmptyUIState = (props, controlType) => {
|
|
|
44670
44686
|
return undefined;
|
|
44671
44687
|
};
|
|
44672
44688
|
|
|
44689
|
+
// What a control holding `uiState` is worth: its own empty when it holds
|
|
44690
|
+
// nothing, so the reader gets the shape it was promised.
|
|
44691
|
+
const uiStateOrEmpty = (uiState, emptyUIState) => {
|
|
44692
|
+
if (uiState === undefined && emptyUIState !== undefined) {
|
|
44693
|
+
return emptyUIState;
|
|
44694
|
+
}
|
|
44695
|
+
return uiState;
|
|
44696
|
+
};
|
|
44697
|
+
|
|
44673
44698
|
// What a cleared control shows: its own empty, kept in the shape it was holding.
|
|
44674
44699
|
const resolveClearedUIState = (controller) => {
|
|
44675
44700
|
const { controlType, props } = controller;
|
|
@@ -44904,7 +44929,7 @@ const useControlProps = (props, {
|
|
|
44904
44929
|
// of a form would re-render at each change of any of its fields.
|
|
44905
44930
|
// eslint-disable-next-line no-unused-expressions
|
|
44906
44931
|
uiStateController.ownUIStateSignal.value;
|
|
44907
|
-
const boundAction = useAction(props.action, uiStateController.
|
|
44932
|
+
const boundAction = useAction(props.action, uiStateController.valueSignal);
|
|
44908
44933
|
const [controlRootProps, controlHostProps] = useInteractiveProps(props, {
|
|
44909
44934
|
uiStateController,
|
|
44910
44935
|
boundAction,
|
|
@@ -56981,7 +57006,12 @@ const css$M = /* css */`@layer navi {
|
|
|
56981
57006
|
const InputCheckbox = props => {
|
|
56982
57007
|
const defaultRef = useRef();
|
|
56983
57008
|
props.ref = props.ref || defaultRef;
|
|
56984
|
-
|
|
57009
|
+
useRef(false);
|
|
57010
|
+
if (props.boolean) {
|
|
57011
|
+
props.value = true;
|
|
57012
|
+
} else if (props.value === undefined) {
|
|
57013
|
+
props.value = "on";
|
|
57014
|
+
}
|
|
56985
57015
|
if (props.headless) {
|
|
56986
57016
|
return jsx(InputCheckboxHeadless, {
|
|
56987
57017
|
...props,
|
|
@@ -71419,15 +71449,16 @@ document.addEventListener(
|
|
|
71419
71449
|
);
|
|
71420
71450
|
|
|
71421
71451
|
/*
|
|
71422
|
-
*
|
|
71423
|
-
* progress. A write is an instant scroll, and
|
|
71424
|
-
* scroll it is playing on its own, short of
|
|
71425
|
-
* (Home, PageDown, Space), a smooth `scrollTo`
|
|
71426
|
-
* bar tap on iOS. A scroll following a hand is
|
|
71427
|
-
* turn carries on, and on most engines a
|
|
71428
|
-
* the write left it, and the fling it
|
|
71429
|
-
*
|
|
71430
|
-
* every move of a list's
|
|
71452
|
+
* Moves a scroll by what the content moved under the user, unless the write
|
|
71453
|
+
* would cost them the scroll in progress. A write is an instant scroll, and
|
|
71454
|
+
* the browser abandons for it any scroll it is playing on its own, short of
|
|
71455
|
+
* where that one was going: a key (Home, PageDown, Space), a smooth `scrollTo`
|
|
71456
|
+
* or `scrollIntoView`, the status bar tap on iOS. A scroll following a hand is
|
|
71457
|
+
* not abandoned: the wheel's next turn carries on, and on most engines a
|
|
71458
|
+
* finger keeps dragging from wherever the write left it, and the fling it
|
|
71459
|
+
* leaves behind once it lifts carries on from there too (Chrome under
|
|
71460
|
+
* DevTools' touch emulation: flings written to at every move of a list's
|
|
71461
|
+
* window went as far as flings never written to).
|
|
71431
71462
|
*
|
|
71432
71463
|
* A fling must not be taken for a scroll the browser plays on its own. A list
|
|
71433
71464
|
* then leaves uncorrected, for the whole fling, the room its fillers lose above
|
|
@@ -71454,6 +71485,18 @@ document.addEventListener(
|
|
|
71454
71485
|
* while the finger is still on one of them, and the touchend of a removed
|
|
71455
71486
|
* element never reaches the document.
|
|
71456
71487
|
*
|
|
71488
|
+
* Chrome ends a scroll at every write, a finger's fling included: "scrollend"
|
|
71489
|
+
* a frame after the write, and the fling goes on a frame later as a scroll of
|
|
71490
|
+
* its own, as far as it was going (a bare page written to every 120ms of a
|
|
71491
|
+
* fling: 9 "scrollend" for 8 writes, the fling as long). Taken for a new
|
|
71492
|
+
* scroll, it is nobody's once it starts far enough from the finger's last
|
|
71493
|
+
* move, and the list's corrections are refused for the rest of the fling: in
|
|
71494
|
+
* wematch, cards drawn above the screen pushing it down at every window slide.
|
|
71495
|
+
* So a scroll starting right after the "scrollend" of a scroll written to here
|
|
71496
|
+
* goes on as that scroll: same start, same hand. Only a written one: a scroll
|
|
71497
|
+
* starting right after one that ended on its own is a new one, a key or a
|
|
71498
|
+
* smooth `scrollTo` must not pass for the fling before it.
|
|
71499
|
+
*
|
|
71457
71500
|
* In a browser without "scrollend", no write is said to interrupt anything: a
|
|
71458
71501
|
* scroll would never be seen ending.
|
|
71459
71502
|
*/
|
|
@@ -71463,8 +71506,8 @@ document.addEventListener(
|
|
|
71463
71506
|
// hand on a scroll that came from elsewhere.
|
|
71464
71507
|
const WHEEL_REACH = 100;
|
|
71465
71508
|
// The fling Chrome plays as a scroll of its own starts a frame or two after the
|
|
71466
|
-
// drag
|
|
71467
|
-
//
|
|
71509
|
+
// "scrollend" of the drag, or of a write (DevTools' touch emulation, 4× CPU:
|
|
71510
|
+
// 12ms after the drag's, 24 to 40ms after a write's in wematch).
|
|
71468
71511
|
const FLING_REACH = 150;
|
|
71469
71512
|
// iOS's touch handling, the one engine whose finger drops writes (see above).
|
|
71470
71513
|
const FINGER_KEEPS_WRITES = !window.CSS.supports(
|
|
@@ -71473,14 +71516,41 @@ const FINGER_KEEPS_WRITES = !window.CSS.supports(
|
|
|
71473
71516
|
);
|
|
71474
71517
|
|
|
71475
71518
|
const flightMap = new WeakMap();
|
|
71519
|
+
// Scrollers written to since their last "scrollend": that "scrollend" may be
|
|
71520
|
+
// the write's.
|
|
71521
|
+
const writtenScrollerSet = new WeakSet();
|
|
71522
|
+
// The flight of a written scroller, from its "scrollend" until a scroll goes on
|
|
71523
|
+
// with it.
|
|
71524
|
+
const writtenFlightEndMap = new WeakMap();
|
|
71476
71525
|
let fingersDown = 0;
|
|
71477
71526
|
let wheelTimeStamp = -Infinity;
|
|
71478
71527
|
let fingerScrollTimeStamp = -Infinity;
|
|
71479
71528
|
|
|
71480
71529
|
/**
|
|
71530
|
+
* Answers whether it wrote.
|
|
71531
|
+
*
|
|
71481
71532
|
* @param {Element} scrollerEl
|
|
71533
|
+
* @param {number} delta
|
|
71534
|
+
* @param {{ horizontal?: boolean }} [options]
|
|
71482
71535
|
* @returns {boolean}
|
|
71483
71536
|
*/
|
|
71537
|
+
const scrollByUnlessInterrupting = (
|
|
71538
|
+
scrollerEl,
|
|
71539
|
+
delta,
|
|
71540
|
+
{ horizontal } = {},
|
|
71541
|
+
) => {
|
|
71542
|
+
if (wouldInterruptScroll(scrollerEl)) {
|
|
71543
|
+
return false;
|
|
71544
|
+
}
|
|
71545
|
+
writtenScrollerSet.add(scrollerEl);
|
|
71546
|
+
if (horizontal) {
|
|
71547
|
+
scrollerEl.scrollLeft += delta;
|
|
71548
|
+
} else {
|
|
71549
|
+
scrollerEl.scrollTop += delta;
|
|
71550
|
+
}
|
|
71551
|
+
return true;
|
|
71552
|
+
};
|
|
71553
|
+
|
|
71484
71554
|
const wouldInterruptScroll = (scrollerEl) => {
|
|
71485
71555
|
const flight = flightMap.get(scrollerEl);
|
|
71486
71556
|
if (!flight) {
|
|
@@ -71523,6 +71593,15 @@ if ("onscrollend" in window) {
|
|
|
71523
71593
|
if (flightMap.has(scroller)) {
|
|
71524
71594
|
return;
|
|
71525
71595
|
}
|
|
71596
|
+
const writtenFlightEnd = writtenFlightEndMap.get(scroller);
|
|
71597
|
+
writtenFlightEndMap.delete(scroller);
|
|
71598
|
+
if (
|
|
71599
|
+
writtenFlightEnd &&
|
|
71600
|
+
e.timeStamp - writtenFlightEnd.timeStamp < FLING_REACH
|
|
71601
|
+
) {
|
|
71602
|
+
flightMap.set(scroller, writtenFlightEnd.flight);
|
|
71603
|
+
return;
|
|
71604
|
+
}
|
|
71526
71605
|
flightMap.set(scroller, { timeStamp: e.timeStamp });
|
|
71527
71606
|
},
|
|
71528
71607
|
{ capture: true, passive: true },
|
|
@@ -71530,7 +71609,13 @@ if ("onscrollend" in window) {
|
|
|
71530
71609
|
document.addEventListener(
|
|
71531
71610
|
"scrollend",
|
|
71532
71611
|
(e) => {
|
|
71533
|
-
|
|
71612
|
+
const scroller = asScroller(e.target);
|
|
71613
|
+
const flight = flightMap.get(scroller);
|
|
71614
|
+
flightMap.delete(scroller);
|
|
71615
|
+
if (flight && writtenScrollerSet.has(scroller)) {
|
|
71616
|
+
writtenFlightEndMap.set(scroller, { flight, timeStamp: e.timeStamp });
|
|
71617
|
+
}
|
|
71618
|
+
writtenScrollerSet.delete(scroller);
|
|
71534
71619
|
},
|
|
71535
71620
|
{ capture: true, passive: true },
|
|
71536
71621
|
);
|
|
@@ -72252,13 +72337,24 @@ const createListItems = () => {
|
|
|
72252
72337
|
}
|
|
72253
72338
|
return null;
|
|
72254
72339
|
},
|
|
72255
|
-
// The room the item at that index took when it was
|
|
72256
|
-
//
|
|
72340
|
+
// The room the item at that index takes: the room it took when it was
|
|
72341
|
+
// last drawn, else the room its run is given for it (see List.Items'
|
|
72342
|
+
// itemSize), else undefined.
|
|
72257
72343
|
roomAt: (index) => {
|
|
72258
72344
|
for (const locator of locatorByOwner.values()) {
|
|
72259
|
-
const
|
|
72260
|
-
if (
|
|
72261
|
-
return
|
|
72345
|
+
const room = locator.roomAt(index);
|
|
72346
|
+
if (room !== undefined) {
|
|
72347
|
+
return room;
|
|
72348
|
+
}
|
|
72349
|
+
}
|
|
72350
|
+
return undefined;
|
|
72351
|
+
},
|
|
72352
|
+
// The room the run of that index is given for it, whatever was measured.
|
|
72353
|
+
givenRoomAt: (index) => {
|
|
72354
|
+
for (const locator of locatorByOwner.values()) {
|
|
72355
|
+
const room = locator.givenRoomAt(index);
|
|
72356
|
+
if (room !== undefined) {
|
|
72357
|
+
return room;
|
|
72262
72358
|
}
|
|
72263
72359
|
}
|
|
72264
72360
|
return undefined;
|
|
@@ -73747,6 +73843,23 @@ const css$x = /* css */`@property --list-group-label-height {
|
|
|
73747
73843
|
&[data-hidden-while-empty]:not(:has([navi-list-item-real])) {
|
|
73748
73844
|
display: none;
|
|
73749
73845
|
}
|
|
73846
|
+
|
|
73847
|
+
&[data-continued] {
|
|
73848
|
+
margin-top: calc(-1 * var(--x-list-gap, 0px));
|
|
73849
|
+
display: grid;
|
|
73850
|
+
|
|
73851
|
+
& > .navi_list_item_group_label, & > .navi_list_item_group_list {
|
|
73852
|
+
grid-area: 1 / 1;
|
|
73853
|
+
}
|
|
73854
|
+
|
|
73855
|
+
& > .navi_list_item_group_label {
|
|
73856
|
+
align-self: start;
|
|
73857
|
+
}
|
|
73858
|
+
}
|
|
73859
|
+
|
|
73860
|
+
&[data-continued="hole"] > .navi_list_item_group_label {
|
|
73861
|
+
visibility: hidden;
|
|
73862
|
+
}
|
|
73750
73863
|
}
|
|
73751
73864
|
|
|
73752
73865
|
.navi_list_container[data-horizontal] > .navi_list_scroll_container > .navi_list > .navi_list_item_group {
|
|
@@ -74528,6 +74641,7 @@ const useListScrollSync = ({
|
|
|
74528
74641
|
// that changed since is measured again when drawn again, and the anchoring
|
|
74529
74642
|
// holds the view across the difference. Items laid side by side in a grid
|
|
74530
74643
|
// share a line, which the fillers hold at the one size.
|
|
74644
|
+
useRef(false);
|
|
74531
74645
|
const rememberItemRooms = () => {
|
|
74532
74646
|
if (!ref.current || itemsPerLineRef.current !== 1) {
|
|
74533
74647
|
return;
|
|
@@ -74536,7 +74650,12 @@ const useListScrollSync = ({
|
|
|
74536
74650
|
if (!listEl) {
|
|
74537
74651
|
return;
|
|
74538
74652
|
}
|
|
74539
|
-
readItemRooms(listEl, horizontal,
|
|
74653
|
+
readItemRooms(listEl, horizontal, (item, room, complete) => {
|
|
74654
|
+
listItems.roomById.set(item.id, room);
|
|
74655
|
+
{
|
|
74656
|
+
return;
|
|
74657
|
+
}
|
|
74658
|
+
});
|
|
74540
74659
|
};
|
|
74541
74660
|
const updateRenderWindow = (newStart, newEnd, reason) => {
|
|
74542
74661
|
const {
|
|
@@ -75294,18 +75413,14 @@ const useListScrollSync = ({
|
|
|
75294
75413
|
if (delta > -0.5 && delta < 0.5) {
|
|
75295
75414
|
return;
|
|
75296
75415
|
}
|
|
75297
|
-
|
|
75298
|
-
|
|
75299
|
-
|
|
75300
|
-
|
|
75301
|
-
|
|
75302
|
-
|
|
75303
|
-
}
|
|
75304
|
-
|
|
75305
|
-
if (horizontal) {
|
|
75306
|
-
scrollerEl.scrollLeft += delta;
|
|
75307
|
-
} else {
|
|
75308
|
-
scrollerEl.scrollTop += delta;
|
|
75416
|
+
// Not into a scroll it would cut short, as the anchoring's write (see
|
|
75417
|
+
// holdAnchorStill). The document's box is the scroller of a list
|
|
75418
|
+
// scrolling the page, and it resizes with every item drawn taller than
|
|
75419
|
+
// its filler: while the page scrolls, that is at every window move.
|
|
75420
|
+
if (scrollByUnlessInterrupting(scrollerEl, delta, {
|
|
75421
|
+
horizontal
|
|
75422
|
+
})) {
|
|
75423
|
+
anchorRef.current = null;
|
|
75309
75424
|
}
|
|
75310
75425
|
};
|
|
75311
75426
|
const observer = new ResizeObserver(entries => {
|
|
@@ -75398,19 +75513,15 @@ const useListScrollSync = ({
|
|
|
75398
75513
|
return;
|
|
75399
75514
|
}
|
|
75400
75515
|
anchorRef.current = null;
|
|
75401
|
-
|
|
75402
|
-
|
|
75403
|
-
|
|
75404
|
-
|
|
75405
|
-
|
|
75406
|
-
|
|
75407
|
-
|
|
75408
|
-
}
|
|
75409
|
-
|
|
75410
|
-
if (horizontal) {
|
|
75411
|
-
scrollerEl.scrollLeft += drift;
|
|
75412
|
-
} else {
|
|
75413
|
-
scrollerEl.scrollTop += drift;
|
|
75516
|
+
// Not written into a scroll nobody is looking at: its items are passing
|
|
75517
|
+
// by, on their way somewhere the browser takes them (a key, a smooth
|
|
75518
|
+
// scrollTo, the status bar, a fling on iOS). The write would stop that
|
|
75519
|
+
// scroll short of where it was going. Left alone, the items drawn above
|
|
75520
|
+
// push the rest by `drift` while all of it moves, and the scroll lands.
|
|
75521
|
+
if (scrollByUnlessInterrupting(scrollerEl, drift, {
|
|
75522
|
+
horizontal
|
|
75523
|
+
})) {
|
|
75524
|
+
scrolledByListRef.current = true;
|
|
75414
75525
|
}
|
|
75415
75526
|
};
|
|
75416
75527
|
useLayoutEffect(holdAnchorStill);
|
|
@@ -76451,18 +76562,17 @@ const readWindowGeometry = (scrollerEl, listEl, horizontal) => {
|
|
|
76451
76562
|
};
|
|
76452
76563
|
};
|
|
76453
76564
|
// The room each item drawn takes along the scroll axis (see
|
|
76454
|
-
// rememberItemRooms):
|
|
76455
|
-
// what stands between
|
|
76456
|
-
// opens — is counted
|
|
76457
|
-
//
|
|
76458
|
-
|
|
76565
|
+
// rememberItemRooms): from where the item before it ends to where it ends, so
|
|
76566
|
+
// that what stands between the two — a separator, the label of the group it
|
|
76567
|
+
// opens — is counted with the item it comes with. A group the window cuts
|
|
76568
|
+
// draws its label without room (see the data-continued rule above): that
|
|
76569
|
+
// label's room is held by the item that opens the group, in the filler. The
|
|
76570
|
+
// first item drawn is measured from the end of what stands before it in the
|
|
76571
|
+
// list (a filler, a header); a stand-in is not measured, it takes the room it
|
|
76572
|
+
// is given and not the room of its item, but an item after it ends where it
|
|
76573
|
+
// ends. `complete` says whether the room was read up to an end before it.
|
|
76574
|
+
const readItemRooms = (listEl, horizontal, onRoom) => {
|
|
76459
76575
|
let previous = null;
|
|
76460
|
-
const keepRoom = (item, next) => {
|
|
76461
|
-
if (item.id === null) {
|
|
76462
|
-
return;
|
|
76463
|
-
}
|
|
76464
|
-
roomById.set(item.id, next && next.index === item.index + 1 ? next.from - item.from : item.to - item.from);
|
|
76465
|
-
};
|
|
76466
76576
|
for (const itemEl of listEl.querySelectorAll(`[${LIST_ITEM_INDEX_ATTRIBUTE}]`)) {
|
|
76467
76577
|
if (!isOwnItem(itemEl, listEl)) {
|
|
76468
76578
|
continue;
|
|
@@ -76477,14 +76587,41 @@ const readItemRooms = (listEl, horizontal, roomById) => {
|
|
|
76477
76587
|
from: horizontal ? rect.left : rect.top,
|
|
76478
76588
|
to: horizontal ? rect.right : rect.bottom
|
|
76479
76589
|
};
|
|
76480
|
-
if (
|
|
76481
|
-
|
|
76590
|
+
if (item.id !== null) {
|
|
76591
|
+
const endBefore = previous && previous.index === item.index - 1 ? previous.to : readEndBefore(itemEl, listEl, horizontal);
|
|
76592
|
+
if (endBefore === null) {
|
|
76593
|
+
onRoom(item, item.to - item.from, false);
|
|
76594
|
+
} else {
|
|
76595
|
+
onRoom(item, item.to - endBefore, true);
|
|
76596
|
+
}
|
|
76482
76597
|
}
|
|
76483
76598
|
previous = item;
|
|
76484
76599
|
}
|
|
76485
|
-
|
|
76486
|
-
|
|
76600
|
+
};
|
|
76601
|
+
// Where what stands before an item in the list ends: the element before the
|
|
76602
|
+
// item, or before its group when it is the first of it — or the start of the
|
|
76603
|
+
// list's content when nothing does. Null when the element before is not laid
|
|
76604
|
+
// out.
|
|
76605
|
+
const readEndBefore = (itemEl, listEl, horizontal) => {
|
|
76606
|
+
let slotEl = itemEl;
|
|
76607
|
+
const groupListEl = itemEl.parentElement;
|
|
76608
|
+
if (groupListEl && groupListEl.classList.contains("navi_list_item_group_list") && groupListEl.firstElementChild === itemEl) {
|
|
76609
|
+
slotEl = groupListEl.closest(".navi_list_item_group") || itemEl;
|
|
76610
|
+
}
|
|
76611
|
+
const beforeEl = slotEl.previousElementSibling;
|
|
76612
|
+
if (beforeEl) {
|
|
76613
|
+
const rect = beforeEl.getBoundingClientRect();
|
|
76614
|
+
if (rect.width === 0 && rect.height === 0) {
|
|
76615
|
+
return null;
|
|
76616
|
+
}
|
|
76617
|
+
return horizontal ? rect.right : rect.bottom;
|
|
76487
76618
|
}
|
|
76619
|
+
if (slotEl.parentElement !== listEl) {
|
|
76620
|
+
return null;
|
|
76621
|
+
}
|
|
76622
|
+
const listRect = listEl.getBoundingClientRect();
|
|
76623
|
+
const listStyle = window.getComputedStyle(listEl);
|
|
76624
|
+
return horizontal ? listRect.left + parseFloat(listStyle.borderLeftWidth) + parseFloat(listStyle.paddingLeft) : listRect.top + parseFloat(listStyle.borderTopWidth) + parseFloat(listStyle.paddingTop);
|
|
76488
76625
|
};
|
|
76489
76626
|
// The first item the screen shows from `position` on, and the item after the
|
|
76490
76627
|
// last one it shows up to `position` (exclusive, like the window's end). An
|
|
@@ -77034,6 +77171,8 @@ const UnorderedList = ({
|
|
|
77034
77171
|
grid: trackColumns ? true : undefined,
|
|
77035
77172
|
gridTemplateColumns: trackColumns,
|
|
77036
77173
|
...rest,
|
|
77174
|
+
// The gap a group the window cuts takes back (see data-continued).
|
|
77175
|
+
style: withListGap(rest.style, spacing),
|
|
77037
77176
|
spacing: spacing,
|
|
77038
77177
|
baseClassName: "navi_list",
|
|
77039
77178
|
children: [!suppressFallback && searchFallbackShown && jsx(SearchFallback, {
|
|
@@ -77066,6 +77205,21 @@ const UnorderedList = ({
|
|
|
77066
77205
|
})]
|
|
77067
77206
|
});
|
|
77068
77207
|
};
|
|
77208
|
+
// The list's spacing as a variable, next to the style it is given (an object
|
|
77209
|
+
// or a css string, as Box takes either).
|
|
77210
|
+
const withListGap = (style, spacing) => {
|
|
77211
|
+
if (spacing === undefined) {
|
|
77212
|
+
return style;
|
|
77213
|
+
}
|
|
77214
|
+
const gap = stringifySpacingStyle(spacing, "gap");
|
|
77215
|
+
if (typeof style === "string") {
|
|
77216
|
+
return `${style};--x-list-gap:${gap}`;
|
|
77217
|
+
}
|
|
77218
|
+
return {
|
|
77219
|
+
...style,
|
|
77220
|
+
"--x-list-gap": gap
|
|
77221
|
+
};
|
|
77222
|
+
};
|
|
77069
77223
|
|
|
77070
77224
|
// The "no match" message. Whether it shows is decided by ListUI (see its
|
|
77071
77225
|
// searchFallbackShown): a search left nothing to display, and the caller did
|
|
@@ -77103,19 +77257,20 @@ const Fallback = ({
|
|
|
77103
77257
|
// every item it draws must not be redrawn — every item of it — because the size
|
|
77104
77258
|
// settled after the first commit.
|
|
77105
77259
|
// It holds lines: the items side by side on one take its room together, and a
|
|
77106
|
-
// last line left short takes it all the same. An item
|
|
77107
|
-
//
|
|
77260
|
+
// last line left short takes it all the same. An item whose room is known takes
|
|
77261
|
+
// that room instead: the one it took when the list drew it before (see
|
|
77262
|
+
// rememberItemRooms), or the one its run is given (see List.Items' itemSize).
|
|
77108
77263
|
const VirtualFiller = ({
|
|
77109
77264
|
edge,
|
|
77110
77265
|
itemCount,
|
|
77111
77266
|
itemsPerLine,
|
|
77112
|
-
|
|
77113
|
-
|
|
77267
|
+
itemsKnown,
|
|
77268
|
+
roomKnown,
|
|
77114
77269
|
findChunks
|
|
77115
77270
|
}) => {
|
|
77116
77271
|
const listItems = useContext(ListItemsContext);
|
|
77117
77272
|
const lineSize = listItems.virtualItemSizeSignal.value;
|
|
77118
|
-
const sizeToFill = Math.ceil(itemCount / itemsPerLine) * lineSize -
|
|
77273
|
+
const sizeToFill = Math.ceil(itemCount / itemsPerLine) * lineSize - itemsKnown * lineSize / itemsPerLine + roomKnown;
|
|
77119
77274
|
// A filler resizing moves what stands below it — the items on screen, when it
|
|
77120
77275
|
// holds the room of items above them — and it resizes in a commit of its own
|
|
77121
77276
|
// when the item size settles after the list has rendered: the list puts its
|
|
@@ -77829,6 +77984,7 @@ const VISIBILITY_HIDDEN_STYLE = {
|
|
|
77829
77984
|
* pageSize?: number,
|
|
77830
77985
|
* memoryBudget?: number,
|
|
77831
77986
|
* renderSkeleton?: false | ((index: number) => import("ignore:preact").ComponentChildren),
|
|
77987
|
+
* itemSize?: (index: number) => number,
|
|
77832
77988
|
* renderError?: (failure: {error: any, retry: () => void, start: number, end: number}) => import("ignore:preact").ComponentChildren,
|
|
77833
77989
|
* onRequestStateChange?: (state: {busy: boolean, refreshing: boolean, range: {start: number, end: number}|null}) => void,
|
|
77834
77990
|
* }>}
|
|
@@ -77861,6 +78017,10 @@ const VISIBILITY_HIDDEN_STYLE = {
|
|
|
77861
78017
|
* `<List.Group>` whose label (`renderGroupLabel`) stays on screen for as long
|
|
77862
78018
|
* as one of them is. The groups are found in the data as it arrives, which is
|
|
77863
78019
|
* the only way a list that discovers its items page by page can have any.
|
|
78020
|
+
* Style the label, not the group element: a group the render window cuts is
|
|
78021
|
+
* laid out by the list, its label over its first items, so that the label
|
|
78022
|
+
* and the gap before it — held in the room of the item opening the group —
|
|
78023
|
+
* take no room twice.
|
|
77864
78024
|
* @param {(item: any, index: number) => any} [props.renderGroupLabel]
|
|
77865
78025
|
* The label of the group an item opens, given that item.
|
|
77866
78026
|
* @param {(item: any, index: number) => object} [props.groupLabelProps]
|
|
@@ -77879,9 +78039,26 @@ const VISIBILITY_HIDDEN_STYLE = {
|
|
|
77879
78039
|
* further out. `Infinity` keeps every item the run ever received; `0` keeps
|
|
77880
78040
|
* only the ones around the window.
|
|
77881
78041
|
* @param {false|(index: number) => any} [props.renderSkeleton]
|
|
77882
|
-
* What to draw for an item the run does not hold
|
|
78042
|
+
* What to draw for an item the run does not hold, at the room that item will
|
|
78043
|
+
* take: a screen of skeletons alone has no item for the list to hold still,
|
|
78044
|
+
* and when the page lands, what is below a skeleton of the wrong size moves
|
|
78045
|
+
* by the difference (see docs/scroll.md). Defaults to List's own
|
|
77883
78046
|
* `renderSkeleton`, then to a bare `<List.Item skeleton>`; `false` leaves the
|
|
77884
78047
|
* item empty (its room is still held, or the list would jump as it loads).
|
|
78048
|
+
* @param {(index: number) => number} [props.itemSize]
|
|
78049
|
+
* The room the item at an index takes along the scroll axis, in px, known
|
|
78050
|
+
* before the item arrives: from where the item before it ends to where it
|
|
78051
|
+
* ends — what stands between the two is its own (the gap, a separator, the
|
|
78052
|
+
* label of the group it opens; the first item of the list has no gap before
|
|
78053
|
+
* it). The fillers then hold every item the list has not drawn at its own
|
|
78054
|
+
* room instead of List's `virtualItemSize`, and an item drawn above the
|
|
78055
|
+
* screen takes exactly the room held for it: nothing on screen moves. It is
|
|
78056
|
+
* the only way the screen holds still during a fling on iOS, where writing
|
|
78057
|
+
* the scroll to correct it would stop the fling. The same index
|
|
78058
|
+
* `renderSkeleton` is given — the two draw on the same knowledge. A room
|
|
78059
|
+
* measured on an item drawn wins over it, and in development one that
|
|
78060
|
+
* differs from it is reported. Items side by side in a grid (`columns`)
|
|
78061
|
+
* share a line, which keeps `virtualItemSize`.
|
|
77885
78062
|
* @param {(failure: {error: any, retry: () => void, start: number, end: number}) => any} [props.renderError]
|
|
77886
78063
|
* What to draw where items were asked for and never came: given the `error`,
|
|
77887
78064
|
* a `retry` to call, and the `start`/`end` of the range that failed — the
|
|
@@ -77954,6 +78131,7 @@ const useRunItems = (store, {
|
|
|
77954
78131
|
renderItem,
|
|
77955
78132
|
findText,
|
|
77956
78133
|
pageSize,
|
|
78134
|
+
itemSize,
|
|
77957
78135
|
groupBy,
|
|
77958
78136
|
renderGroupLabel,
|
|
77959
78137
|
groupLabelProps,
|
|
@@ -78037,6 +78215,22 @@ const useRunItems = (store, {
|
|
|
78037
78215
|
const item = getItemAt(itemIndex);
|
|
78038
78216
|
return item === undefined ? undefined : idOf(item, itemIndex);
|
|
78039
78217
|
};
|
|
78218
|
+
// The room the caller says an item takes (`itemSize`), drawn or not.
|
|
78219
|
+
const givenRoomOf = itemIndex => {
|
|
78220
|
+
if (!itemSize || itemsPerLine !== 1 || itemIndex < runStart || itemIndex >= runEnd) {
|
|
78221
|
+
return undefined;
|
|
78222
|
+
}
|
|
78223
|
+
const room = itemSize(itemIndex);
|
|
78224
|
+
return room > 0 ? room : undefined;
|
|
78225
|
+
};
|
|
78226
|
+
// The room an item took when the list last drew it (see rememberItemRooms),
|
|
78227
|
+
// else the room the caller gives it, else undefined: a filler holds an item
|
|
78228
|
+
// it knows nothing about at the item size.
|
|
78229
|
+
const roomOf = itemIndex => {
|
|
78230
|
+
const id = idAt(itemIndex);
|
|
78231
|
+
const remembered = id === undefined ? undefined : listItems.roomById.get(id);
|
|
78232
|
+
return remembered === undefined ? givenRoomOf(itemIndex) : remembered;
|
|
78233
|
+
};
|
|
78040
78234
|
listItems.setItemLocator(ownerId, {
|
|
78041
78235
|
indexOf: id => {
|
|
78042
78236
|
let found = null;
|
|
@@ -78048,32 +78242,27 @@ const useRunItems = (store, {
|
|
|
78048
78242
|
});
|
|
78049
78243
|
return found;
|
|
78050
78244
|
},
|
|
78051
|
-
|
|
78245
|
+
roomAt: roomOf,
|
|
78246
|
+
givenRoomAt: givenRoomOf
|
|
78052
78247
|
});
|
|
78053
|
-
|
|
78054
|
-
|
|
78055
|
-
const roomOf = itemIndex => {
|
|
78056
|
-
const id = idAt(itemIndex);
|
|
78057
|
-
return id === undefined ? undefined : listItems.roomById.get(id);
|
|
78058
|
-
};
|
|
78059
|
-
const readRemembered = (from, to) => {
|
|
78060
|
-
const remembered = {
|
|
78248
|
+
const readKnownRooms = (from, to) => {
|
|
78249
|
+
const known = {
|
|
78061
78250
|
room: 0,
|
|
78062
78251
|
count: 0
|
|
78063
78252
|
};
|
|
78064
|
-
if (listItems.roomById.size === 0) {
|
|
78065
|
-
return
|
|
78253
|
+
if (listItems.roomById.size === 0 && !itemSize) {
|
|
78254
|
+
return known;
|
|
78066
78255
|
}
|
|
78067
78256
|
let itemIndex = from;
|
|
78068
78257
|
while (itemIndex < to) {
|
|
78069
78258
|
const room = roomOf(itemIndex);
|
|
78070
78259
|
if (room !== undefined) {
|
|
78071
|
-
|
|
78072
|
-
|
|
78260
|
+
known.room += room;
|
|
78261
|
+
known.count++;
|
|
78073
78262
|
}
|
|
78074
78263
|
itemIndex++;
|
|
78075
78264
|
}
|
|
78076
|
-
return
|
|
78265
|
+
return known;
|
|
78077
78266
|
};
|
|
78078
78267
|
useLayoutEffect(() => {
|
|
78079
78268
|
return () => {
|
|
@@ -78194,6 +78383,7 @@ const useRunItems = (store, {
|
|
|
78194
78383
|
nodes.push(jsx(ListItemGroup, {
|
|
78195
78384
|
label: group.label,
|
|
78196
78385
|
labelProps: group.labelProps,
|
|
78386
|
+
continued: group.continued,
|
|
78197
78387
|
children: group.children
|
|
78198
78388
|
}, groupCount === 1 ? `${ownerId}_group_${group.key}` : `${ownerId}_group_${group.key}_${groupCount}`));
|
|
78199
78389
|
group = null;
|
|
@@ -78231,10 +78421,38 @@ const useRunItems = (store, {
|
|
|
78231
78421
|
}
|
|
78232
78422
|
return groupBy(item, itemIndex);
|
|
78233
78423
|
};
|
|
78424
|
+
// Where a group opened at that item began, when not there (see
|
|
78425
|
+
// data-continued): "above" the window, or in the "hole" of items not held
|
|
78426
|
+
// drawn right before it. Items not held belong to the group they lead to as
|
|
78427
|
+
// often as not — a page lands every hundred items, a group starts a few
|
|
78428
|
+
// times in a thread — so a group the window starts in is taken to go on
|
|
78429
|
+
// above it unless the item before, held, says otherwise.
|
|
78430
|
+
// Along y only, never built along x: there a group stands its label over
|
|
78431
|
+
// its items, so the label takes no room on the axis (unless it is wider than
|
|
78432
|
+
// the items drawn) and only the gap would be taken back — `margin-left`,
|
|
78433
|
+
// without the overlay of the data-continued rule. Nothing measured it yet.
|
|
78434
|
+
let afterHole = false;
|
|
78435
|
+
const whereGroupContinues = (groupKey, itemIndex) => {
|
|
78436
|
+
if (listItems.horizontal) {
|
|
78437
|
+
return undefined;
|
|
78438
|
+
}
|
|
78439
|
+
if (afterHole) {
|
|
78440
|
+
return "hole";
|
|
78441
|
+
}
|
|
78442
|
+
if (itemIndex !== windowFrom || windowFrom <= runStart) {
|
|
78443
|
+
return undefined;
|
|
78444
|
+
}
|
|
78445
|
+
const itemBefore = getItemAt(windowFrom - 1);
|
|
78446
|
+
if (itemBefore === undefined || groupBy(itemBefore, windowFrom - 1) === groupKey) {
|
|
78447
|
+
return "above";
|
|
78448
|
+
}
|
|
78449
|
+
return undefined;
|
|
78450
|
+
};
|
|
78234
78451
|
const pushItem = (itemNode, item, itemIndex, groupKey) => {
|
|
78235
78452
|
if (groupKey === undefined) {
|
|
78236
78453
|
closeGroup();
|
|
78237
78454
|
nodes.push(itemNode);
|
|
78455
|
+
afterHole = item === undefined;
|
|
78238
78456
|
return;
|
|
78239
78457
|
}
|
|
78240
78458
|
if (!group || group.key !== groupKey) {
|
|
@@ -78243,9 +78461,11 @@ const useRunItems = (store, {
|
|
|
78243
78461
|
key: groupKey,
|
|
78244
78462
|
label: renderGroupLabel ? renderGroupLabel(item, itemIndex) : groupKey,
|
|
78245
78463
|
labelProps: groupLabelProps ? groupLabelProps(item, itemIndex) : undefined,
|
|
78464
|
+
continued: whereGroupContinues(groupKey, itemIndex),
|
|
78246
78465
|
children: []
|
|
78247
78466
|
};
|
|
78248
78467
|
}
|
|
78468
|
+
afterHole = false;
|
|
78249
78469
|
group.children.push(itemNode);
|
|
78250
78470
|
};
|
|
78251
78471
|
// The text of the items a filler stands for, cut in chunks aligned on the
|
|
@@ -78288,13 +78508,13 @@ const useRunItems = (store, {
|
|
|
78288
78508
|
// one run, and what sits before or after it (a header, items given one by
|
|
78289
78509
|
// one) is not virtualized at all.
|
|
78290
78510
|
if (windowFrom > runStart) {
|
|
78291
|
-
const
|
|
78511
|
+
const known = readKnownRooms(runStart, windowFrom);
|
|
78292
78512
|
nodes.push(jsx(VirtualFiller, {
|
|
78293
78513
|
edge: "before",
|
|
78294
78514
|
itemCount: windowFrom - runStart,
|
|
78295
78515
|
itemsPerLine: itemsPerLine,
|
|
78296
|
-
|
|
78297
|
-
|
|
78516
|
+
itemsKnown: known.count,
|
|
78517
|
+
roomKnown: known.room,
|
|
78298
78518
|
findChunks: getFindChunks(runStart, windowFrom)
|
|
78299
78519
|
}, "navi-list-filler-before"));
|
|
78300
78520
|
}
|
|
@@ -78321,6 +78541,7 @@ const useRunItems = (store, {
|
|
|
78321
78541
|
retry: store.retry
|
|
78322
78542
|
})
|
|
78323
78543
|
}, `${ownerId}_failure_${failureFrom}`));
|
|
78544
|
+
afterHole = false;
|
|
78324
78545
|
itemIndex = failureTo + 1;
|
|
78325
78546
|
continue;
|
|
78326
78547
|
}
|
|
@@ -78396,13 +78617,13 @@ const useRunItems = (store, {
|
|
|
78396
78617
|
}
|
|
78397
78618
|
closeGroup();
|
|
78398
78619
|
if (runEnd > windowTo) {
|
|
78399
|
-
const
|
|
78620
|
+
const known = readKnownRooms(windowTo, runEnd);
|
|
78400
78621
|
nodes.push(jsx(VirtualFiller, {
|
|
78401
78622
|
edge: "after",
|
|
78402
78623
|
itemCount: runEnd - windowTo,
|
|
78403
78624
|
itemsPerLine: itemsPerLine,
|
|
78404
|
-
|
|
78405
|
-
|
|
78625
|
+
itemsKnown: known.count,
|
|
78626
|
+
roomKnown: known.room,
|
|
78406
78627
|
findChunks: getFindChunks(windowTo, runEnd)
|
|
78407
78628
|
}, "navi-list-filler-after"));
|
|
78408
78629
|
}
|
|
@@ -79047,12 +79268,17 @@ const useItemStore = ({
|
|
|
79047
79268
|
* hiddenWhileEmpty — the group leaves the flow (`display: none`) while it
|
|
79048
79269
|
* holds no real item — a search that emptied it, items not
|
|
79049
79270
|
* arrived yet
|
|
79271
|
+
* continued — set by a run of items, not by callers: the group began
|
|
79272
|
+
* before the first item drawn ("above" the render window,
|
|
79273
|
+
* or in a "hole" of items not held), and what stands
|
|
79274
|
+
* before that item takes no room (see data-continued)
|
|
79050
79275
|
* ...rest — forwarded to the outer <li role="presentation">
|
|
79051
79276
|
*/
|
|
79052
79277
|
const ListItemGroup = ({
|
|
79053
79278
|
label,
|
|
79054
79279
|
labelProps,
|
|
79055
79280
|
hiddenWhileEmpty,
|
|
79281
|
+
continued,
|
|
79056
79282
|
children,
|
|
79057
79283
|
...rest
|
|
79058
79284
|
}) => {
|
|
@@ -79095,6 +79321,7 @@ const ListItemGroup = ({
|
|
|
79095
79321
|
baseClassName: "navi_list_item_group",
|
|
79096
79322
|
role: "presentation",
|
|
79097
79323
|
"data-hidden-while-empty": hiddenWhileEmpty ? "" : undefined,
|
|
79324
|
+
"data-continued": continued,
|
|
79098
79325
|
children: [jsx("span", {
|
|
79099
79326
|
...labelRest,
|
|
79100
79327
|
ref: labelRef,
|
|
@@ -79341,7 +79568,8 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
|
|
|
79341
79568
|
* the fillers hold for it outside the render window, what a scroll position
|
|
79342
79569
|
* inside them is read with, and the least an item on its way takes (see
|
|
79343
79570
|
* `renderSkeleton`). An item the list has drawn is held at the room it took
|
|
79344
|
-
* then
|
|
79571
|
+
* then, and an item whose run says its room (`<List.Items itemSize>`) at
|
|
79572
|
+
* that room. Left out, it is the average of the items measured so far — once when
|
|
79345
79573
|
* the list mounts, again when a popup around it opens, and after each commit
|
|
79346
79574
|
* while items are held off screen. Given, it is the worst case: the size
|
|
79347
79575
|
* every item has, or, in a list whose items differ, the smallest an item can
|