@jsenv/navi 0.29.407 → 0.29.409
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 +199 -47
- package/dist/dev/jsenv_navi.js.map +10 -8
- package/dist/jsenv_navi.js +199 -47
- package/dist/jsenv_navi.js.map +10 -8
- package/docs/AI_INSTRUCTIONS.md +3 -2
- package/docs/scroll.md +52 -1
- package/package.json +2 -2
package/dist/jsenv_navi.js
CHANGED
|
@@ -12639,7 +12639,8 @@ const useActionBoundToOneParam = (action, paramsSignal) => {
|
|
|
12639
12639
|
}, []);
|
|
12640
12640
|
return [boundAction, getValue(), setValue];
|
|
12641
12641
|
};
|
|
12642
|
-
//
|
|
12642
|
+
// The action bound to the signal, without reading it: the caller is not
|
|
12643
|
+
// re-rendered by the params (a control, a <details>).
|
|
12643
12644
|
const useAction = (action, paramsSignal) => {
|
|
12644
12645
|
return useBoundAction(action, paramsSignal);
|
|
12645
12646
|
};
|
|
@@ -12661,12 +12662,13 @@ const useBoundAction = (action, actionParamsSignal) => {
|
|
|
12661
12662
|
if (existingNoopAction) {
|
|
12662
12663
|
return existingNoopAction;
|
|
12663
12664
|
}
|
|
12665
|
+
// Not bound to the params signal: nothing reads what a noop runs with, and
|
|
12666
|
+
// a proxy following the signal would retarget (a new child action for each
|
|
12667
|
+
// new value) at every change — for every button of a form, at every change
|
|
12668
|
+
// of any of its fields.
|
|
12664
12669
|
const noopAction = createAction(() => {}, { params: undefined });
|
|
12665
|
-
|
|
12666
|
-
|
|
12667
|
-
: noopAction;
|
|
12668
|
-
noopActionRef.current = noopActionBound;
|
|
12669
|
-
return noopActionBound;
|
|
12670
|
+
noopActionRef.current = noopAction;
|
|
12671
|
+
return noopAction;
|
|
12670
12672
|
}
|
|
12671
12673
|
const isFunction = typeof action === "function";
|
|
12672
12674
|
if (!isFunction) {
|
|
@@ -22377,7 +22379,7 @@ import.meta.css = /* css */[`[data-scrollable] > .navi_loading_outline_wrapper,
|
|
|
22377
22379
|
}
|
|
22378
22380
|
}
|
|
22379
22381
|
|
|
22380
|
-
[hidden] {
|
|
22382
|
+
[hidden]:not([hidden="until-found" i]) {
|
|
22381
22383
|
display: none !important;
|
|
22382
22384
|
}
|
|
22383
22385
|
`, "@jsenv/navi/src/box/box.jsx"];
|
|
@@ -41540,7 +41542,14 @@ const useControlProps = (props, {
|
|
|
41540
41542
|
persists,
|
|
41541
41543
|
uiActionInternal
|
|
41542
41544
|
});
|
|
41543
|
-
|
|
41545
|
+
// What the control draws is what it holds by itself, and this read is what
|
|
41546
|
+
// re-renders it when that changes. A button inheriting the value of the
|
|
41547
|
+
// control around it (a submit in a form, see ownUIStateSignal) draws none of
|
|
41548
|
+
// that value, it only hands it to its action: subscribed to it, every button
|
|
41549
|
+
// of a form would re-render at each change of any of its fields.
|
|
41550
|
+
// eslint-disable-next-line no-unused-expressions
|
|
41551
|
+
uiStateController.ownUIStateSignal.value;
|
|
41552
|
+
const boundAction = useAction(props.action, uiStateController.uiStateSignal);
|
|
41544
41553
|
const [controlRootProps, controlHostProps] = useInteractiveProps(props, {
|
|
41545
41554
|
uiStateController,
|
|
41546
41555
|
boundAction,
|
|
@@ -42305,7 +42314,10 @@ const useControlProps = (props, {
|
|
|
42305
42314
|
};
|
|
42306
42315
|
}
|
|
42307
42316
|
}
|
|
42308
|
-
|
|
42317
|
+
|
|
42318
|
+
// Its own, like syncDomState: a button's DOM value is not the value it
|
|
42319
|
+
// inherits from the control around it.
|
|
42320
|
+
const uiState = uiStateController.ownUIStateSignal.peek();
|
|
42309
42321
|
const domProps = toDomProps(uiState);
|
|
42310
42322
|
{
|
|
42311
42323
|
// Same as syncDomState: a field being typed into keeps its own text, so a
|
|
@@ -42833,7 +42845,9 @@ const useInteractiveProps = (props, {
|
|
|
42833
42845
|
// stop waiting on a run that is still going. What it waits on is the run
|
|
42834
42846
|
// it started, from navi_action_start to its settlement.
|
|
42835
42847
|
const runningAction = uiStateController.runningActionSignal.value;
|
|
42836
|
-
|
|
42848
|
+
// Only whether it runs: the whole status (useActionStatus) reads the params
|
|
42849
|
+
// too, which for a button inheriting its form's value are the form.
|
|
42850
|
+
const actionRunning = (runningAction || boundAction).runningStateSignal.value === RUNNING;
|
|
42837
42851
|
const networkPolicyReason = useNetworkPolicyReason();
|
|
42838
42852
|
const {
|
|
42839
42853
|
disabled,
|
|
@@ -42881,7 +42895,7 @@ const useInteractiveProps = (props, {
|
|
|
42881
42895
|
// just set stays visible and interactive while the action runs — no
|
|
42882
42896
|
// loading, no readonly. On failure resetOnError rolls the state back and
|
|
42883
42897
|
// the error callout says why.
|
|
42884
|
-
const actionLoading = optimistic ? false :
|
|
42898
|
+
const actionLoading = optimistic ? false : actionRunning;
|
|
42885
42899
|
const loadingResolved = loadingBase || actionLoading;
|
|
42886
42900
|
const readOnlyResolved = readOnlyBase || actionLoading;
|
|
42887
42901
|
// Read-only, and what this control opens still opens: reading what is in
|
|
@@ -43076,7 +43090,11 @@ const useInteractiveProps = (props, {
|
|
|
43076
43090
|
errorEffect: actionErrorEffect,
|
|
43077
43091
|
errorMapping
|
|
43078
43092
|
});
|
|
43079
|
-
|
|
43093
|
+
// What the attribute says is that an action is there (see
|
|
43094
|
+
// findClosestControlWithAction); its text is a label. Peeked (toString),
|
|
43095
|
+
// because `.callSource` follows the params — for a button inheriting its
|
|
43096
|
+
// form's value, a re-render at every change of any field.
|
|
43097
|
+
const dataAction = action === undefined ? undefined : String(boundAction);
|
|
43080
43098
|
Object.assign(controlHostProps, {
|
|
43081
43099
|
"data-action": dataAction
|
|
43082
43100
|
});
|
|
@@ -51993,30 +52011,9 @@ const ButtonUI = props => {
|
|
|
51993
52011
|
} = buttonControlHostProps;
|
|
51994
52012
|
const loading = basePseudoState[":-navi-loading"];
|
|
51995
52013
|
|
|
51996
|
-
//
|
|
51997
|
-
|
|
51998
|
-
const
|
|
51999
|
-
let as = "button";
|
|
52000
|
-
let innerTarget;
|
|
52001
|
-
let innerRel;
|
|
52002
|
-
let innerCurrent;
|
|
52003
|
-
if (isLink) {
|
|
52004
|
-
as = "a";
|
|
52005
|
-
const {
|
|
52006
|
-
isSameSite,
|
|
52007
|
-
isCurrent
|
|
52008
|
-
} = getHrefTargetInfo(href);
|
|
52009
|
-
innerTarget = target === undefined ? isSameSite ? undefined : "_blank" : target;
|
|
52010
|
-
innerRel = rel === undefined ? isSameSite ? undefined : "noopener noreferrer" : rel;
|
|
52011
|
-
innerCurrent = isCurrent;
|
|
52012
|
-
}
|
|
52013
|
-
// For a button that has only an href: nothing else knows it points at the
|
|
52014
|
-
// page one is on. A route says so through `pseudoState`, which Box lays over
|
|
52015
|
-
// this one.
|
|
52016
|
-
buttonControlHostProps.basePseudoState = {
|
|
52017
|
-
...basePseudoState,
|
|
52018
|
-
":-navi-href-current": innerCurrent
|
|
52019
|
-
};
|
|
52014
|
+
// Where the href leads next to the page one is on (the target, the rel, the
|
|
52015
|
+
// current state) is resolved above, by ButtonHrefResolver.
|
|
52016
|
+
const as = href === undefined ? "button" : "a";
|
|
52020
52017
|
|
|
52021
52018
|
// Worn as an attribute, like a link's (see link_replace.js): read off the
|
|
52022
52019
|
// anchor by the click handler, off the source by --navi-nav-to.
|
|
@@ -52065,8 +52062,8 @@ const ButtonUI = props => {
|
|
|
52065
52062
|
ref: ref,
|
|
52066
52063
|
as: as,
|
|
52067
52064
|
href: href,
|
|
52068
|
-
target:
|
|
52069
|
-
rel:
|
|
52065
|
+
target: target,
|
|
52066
|
+
rel: rel,
|
|
52070
52067
|
replace: undefined,
|
|
52071
52068
|
...replaceRequest,
|
|
52072
52069
|
document: undefined,
|
|
@@ -52366,6 +52363,49 @@ const ButtonConfirm = ({
|
|
|
52366
52363
|
});
|
|
52367
52364
|
};
|
|
52368
52365
|
|
|
52366
|
+
const ButtonHrefResolver = props => {
|
|
52367
|
+
const Next = useNextResolver();
|
|
52368
|
+
if (props.href !== undefined) {
|
|
52369
|
+
return renderResolver(ButtonWithHref, props);
|
|
52370
|
+
}
|
|
52371
|
+
return jsx(Next, {
|
|
52372
|
+
...props
|
|
52373
|
+
});
|
|
52374
|
+
};
|
|
52375
|
+
|
|
52376
|
+
// What the href means next to the page one is on depends on the document url,
|
|
52377
|
+
// so a button with an href re-renders at every address write. One without
|
|
52378
|
+
// never reaches here: it has nothing to re-compute, and every button on screen
|
|
52379
|
+
// would otherwise pay for each write.
|
|
52380
|
+
const ButtonWithHref = props => {
|
|
52381
|
+
const Next = useNextResolver();
|
|
52382
|
+
const {
|
|
52383
|
+
href,
|
|
52384
|
+
target,
|
|
52385
|
+
rel,
|
|
52386
|
+
pseudoState
|
|
52387
|
+
} = props;
|
|
52388
|
+
// Read for the subscription: getHrefTargetInfo reads window.location.
|
|
52389
|
+
useDocumentUrl();
|
|
52390
|
+
const {
|
|
52391
|
+
isSameSite,
|
|
52392
|
+
isCurrent
|
|
52393
|
+
} = getHrefTargetInfo(href);
|
|
52394
|
+
return jsx(Next, {
|
|
52395
|
+
...props,
|
|
52396
|
+
target: target === undefined ? isSameSite ? undefined : "_blank" : target,
|
|
52397
|
+
rel: rel === undefined ? isSameSite ? undefined : "noopener noreferrer" : rel
|
|
52398
|
+
// Under whatever the caller already says: a route knows better whether
|
|
52399
|
+
// it is the page one is on (see button_route.jsx), and a demo can force
|
|
52400
|
+
// the state.
|
|
52401
|
+
,
|
|
52402
|
+
pseudoState: {
|
|
52403
|
+
":-navi-href-current": isCurrent,
|
|
52404
|
+
...pseudoState
|
|
52405
|
+
}
|
|
52406
|
+
});
|
|
52407
|
+
};
|
|
52408
|
+
|
|
52369
52409
|
const ButtonRouteResolver = props => {
|
|
52370
52410
|
const Next = useNextResolver();
|
|
52371
52411
|
if (props.route) {
|
|
@@ -52620,7 +52660,7 @@ const COMMAND_DEFAULT_PROPS_FACTORIES = {
|
|
|
52620
52660
|
* (it wears it itself) and loses the shrink under the finger, which has
|
|
52621
52661
|
* nothing left to scale but the interactive area itself.
|
|
52622
52662
|
*/
|
|
52623
|
-
const Button = /*#__PURE__*/createComponentResolver([ButtonFirstResolver, ButtonRouteResolver, ButtonCommandPropResolver, ButtonConfirmResolver, ButtonUI]);
|
|
52663
|
+
const Button = /*#__PURE__*/createComponentResolver([ButtonFirstResolver, ButtonRouteResolver, ButtonCommandPropResolver, ButtonConfirmResolver, ButtonHrefResolver, ButtonUI]);
|
|
52624
52664
|
|
|
52625
52665
|
installImportMetaCssBuild(import.meta);
|
|
52626
52666
|
const css$O = /* css */`.navi_control_swap {
|
|
@@ -69896,12 +69936,28 @@ const css$x = /* css */`@layer navi {
|
|
|
69896
69936
|
display: inline-block;
|
|
69897
69937
|
}
|
|
69898
69938
|
|
|
69939
|
+
.navi_list_find_stand_in {
|
|
69940
|
+
height: calc(var(--x-find-line-count) * var(--x-find-line-size));
|
|
69941
|
+
color: #0000;
|
|
69942
|
+
line-height: var(--x-find-line-size);
|
|
69943
|
+
white-space: pre;
|
|
69944
|
+
overflow: clip;
|
|
69945
|
+
}
|
|
69946
|
+
|
|
69899
69947
|
.navi_list_container[data-horizontal] {
|
|
69900
69948
|
--list-max-height: none;
|
|
69901
69949
|
|
|
69902
69950
|
& > .navi_list_scroll_container > .navi_list > .navi_list_virtual_filler {
|
|
69903
69951
|
width: var(--size-to-fill, 0px);
|
|
69904
69952
|
height: 100%;
|
|
69953
|
+
display: flex;
|
|
69954
|
+
|
|
69955
|
+
& > .navi_list_find_stand_in {
|
|
69956
|
+
width: calc(var(--x-find-line-count) * var(--x-find-line-size));
|
|
69957
|
+
writing-mode: vertical-lr;
|
|
69958
|
+
flex-shrink: 0;
|
|
69959
|
+
height: auto;
|
|
69960
|
+
}
|
|
69905
69961
|
}
|
|
69906
69962
|
}
|
|
69907
69963
|
|
|
@@ -72399,10 +72455,12 @@ const Fallback = ({
|
|
|
72399
72455
|
// settled after the first commit.
|
|
72400
72456
|
const VirtualFiller = ({
|
|
72401
72457
|
edge,
|
|
72402
|
-
itemCount
|
|
72458
|
+
itemCount,
|
|
72459
|
+
findChunks
|
|
72403
72460
|
}) => {
|
|
72404
72461
|
const listRows = useContext(ListRowsContext);
|
|
72405
|
-
const
|
|
72462
|
+
const virtualItemSize = listRows.virtualItemSizeSignal.value;
|
|
72463
|
+
const sizeToFill = itemCount * virtualItemSize;
|
|
72406
72464
|
if (!sizeToFill) {
|
|
72407
72465
|
return null;
|
|
72408
72466
|
}
|
|
@@ -72413,8 +72471,17 @@ const VirtualFiller = ({
|
|
|
72413
72471
|
"navi-virtual-filler": edge,
|
|
72414
72472
|
"aria-hidden": true,
|
|
72415
72473
|
style: {
|
|
72416
|
-
"--size-to-fill": `${sizeToFill}px
|
|
72417
|
-
|
|
72474
|
+
"--size-to-fill": `${sizeToFill}px`,
|
|
72475
|
+
"--x-find-line-size": findChunks ? `${virtualItemSize}px` : undefined
|
|
72476
|
+
},
|
|
72477
|
+
children: findChunks && findChunks.map(chunk => jsx("div", {
|
|
72478
|
+
className: "navi_list_find_stand_in",
|
|
72479
|
+
hidden: "until-found",
|
|
72480
|
+
style: {
|
|
72481
|
+
"--x-find-line-count": chunk.to - chunk.from
|
|
72482
|
+
},
|
|
72483
|
+
children: chunk.text
|
|
72484
|
+
}, `${chunk.from}_${chunk.to}`))
|
|
72418
72485
|
});
|
|
72419
72486
|
};
|
|
72420
72487
|
|
|
@@ -73047,6 +73114,7 @@ const VISIBILITY_HIDDEN_STYLE = {
|
|
|
73047
73114
|
*
|
|
73048
73115
|
* @type {import("ignore:preact").FunctionComponent<{
|
|
73049
73116
|
* renderItem: (item: any, index: number, state: {refreshing: boolean}) => import("ignore:preact").ComponentChildren,
|
|
73117
|
+
* findText?: (item: any, index: number) => string,
|
|
73050
73118
|
* items?: any[],
|
|
73051
73119
|
* itemsAction?: (range: {start: number, end: number, limit: number, before?: string, after?: string, around?: string, count?: number, signal: AbortSignal}) => any,
|
|
73052
73120
|
* count?: number,
|
|
@@ -73065,6 +73133,13 @@ const VISIBILITY_HIDDEN_STYLE = {
|
|
|
73065
73133
|
* the run. `state.refreshing` says the rows drawn are the ones from before
|
|
73066
73134
|
* while the run reads the collection again — the list carries
|
|
73067
73135
|
* `navi-refreshing` for the same reason.
|
|
73136
|
+
* @param {(item: any, index: number) => string} [props.findText]
|
|
73137
|
+
* The text of a row as the browser's find in page (Cmd/Ctrl + F) sees it
|
|
73138
|
+
* while the row is not drawn — without it, find reaches only the rows in the
|
|
73139
|
+
* render window. The rows held off screen then carry that text where they
|
|
73140
|
+
* stand, hidden until the browser finds it there: it scrolls to the place,
|
|
73141
|
+
* and the list draws the row. One line per row. A row the run does not hold
|
|
73142
|
+
* yet has nothing to be found by.
|
|
73068
73143
|
* @param {any[]} [props.items]
|
|
73069
73144
|
* The collection, when it is held in memory: all of it, in order. Nothing is
|
|
73070
73145
|
* ever asked for — `itemsAction`, `count`, `pageSize` and `memoryBudget` have
|
|
@@ -73161,12 +73236,19 @@ const createStoreHoldingNothing = rowCount => {
|
|
|
73161
73236
|
};
|
|
73162
73237
|
};
|
|
73163
73238
|
|
|
73239
|
+
// How many rows share one element of the text a filler carries for find in
|
|
73240
|
+
// page (see findText). One element per row would put back the DOM nodes the
|
|
73241
|
+
// window saves; one per filler would be revealed whole — every row of it laid
|
|
73242
|
+
// out — by the first match found in it.
|
|
73243
|
+
const FIND_CHUNK_ROW_COUNT = 64;
|
|
73244
|
+
|
|
73164
73245
|
// What a run draws: the rows its window frames, a skeleton for each one the
|
|
73165
73246
|
// store does not hold, and fillers holding the room of the rows outside it.
|
|
73166
73247
|
// Which rows are held, and fetching the others, is the store's (see
|
|
73167
73248
|
// useItemStore).
|
|
73168
73249
|
const useRunRows = (store, {
|
|
73169
73250
|
renderItem,
|
|
73251
|
+
findText,
|
|
73170
73252
|
pageSize,
|
|
73171
73253
|
groupBy,
|
|
73172
73254
|
renderGroupLabel,
|
|
@@ -73374,6 +73456,37 @@ const useRunRows = (store, {
|
|
|
73374
73456
|
}
|
|
73375
73457
|
group.children.push(rowNode);
|
|
73376
73458
|
};
|
|
73459
|
+
// The text of the rows a filler stands for, cut in chunks aligned on the
|
|
73460
|
+
// run's own ranks: the window sliding changes the chunk at its edge and
|
|
73461
|
+
// leaves the others as they are.
|
|
73462
|
+
const getFindChunks = (from, to) => {
|
|
73463
|
+
if (!findText) {
|
|
73464
|
+
return null;
|
|
73465
|
+
}
|
|
73466
|
+
const chunks = [];
|
|
73467
|
+
let chunkFrom = from;
|
|
73468
|
+
while (chunkFrom < to) {
|
|
73469
|
+
const chunkIndex = Math.floor(rankOf(chunkFrom) / FIND_CHUNK_ROW_COUNT);
|
|
73470
|
+
const alignedTo = rowOf((chunkIndex + 1) * FIND_CHUNK_ROW_COUNT);
|
|
73471
|
+
const chunkTo = alignedTo < to ? alignedTo : to;
|
|
73472
|
+
const lines = [];
|
|
73473
|
+
let lineRowIndex = chunkFrom;
|
|
73474
|
+
while (lineRowIndex < chunkTo) {
|
|
73475
|
+
const item = getItemAt(lineRowIndex);
|
|
73476
|
+
// A row not held has nothing to find, but keeps its line: the rows
|
|
73477
|
+
// after it stay at their place.
|
|
73478
|
+
lines.push(item === undefined ? "" : toFindLine(findText(item, lineRowIndex)));
|
|
73479
|
+
lineRowIndex++;
|
|
73480
|
+
}
|
|
73481
|
+
chunks.push({
|
|
73482
|
+
from: chunkFrom,
|
|
73483
|
+
to: chunkTo,
|
|
73484
|
+
text: lines.join("\n")
|
|
73485
|
+
});
|
|
73486
|
+
chunkFrom = chunkTo;
|
|
73487
|
+
}
|
|
73488
|
+
return chunks;
|
|
73489
|
+
};
|
|
73377
73490
|
// The room held for this run's own rows that the window leaves out. It
|
|
73378
73491
|
// belongs to the run and not to the list: a list is not necessarily made of
|
|
73379
73492
|
// one run, and what sits before or after it (a header, rows given one by
|
|
@@ -73381,7 +73494,8 @@ const useRunRows = (store, {
|
|
|
73381
73494
|
if (windowFrom > runStart) {
|
|
73382
73495
|
rows.push(jsx(VirtualFiller, {
|
|
73383
73496
|
edge: "before",
|
|
73384
|
-
itemCount: windowFrom - runStart
|
|
73497
|
+
itemCount: windowFrom - runStart,
|
|
73498
|
+
findChunks: getFindChunks(runStart, windowFrom)
|
|
73385
73499
|
}, "navi-list-filler-before"));
|
|
73386
73500
|
}
|
|
73387
73501
|
const renderItemState = {
|
|
@@ -73483,11 +73597,20 @@ const useRunRows = (store, {
|
|
|
73483
73597
|
if (runEnd > windowTo) {
|
|
73484
73598
|
rows.push(jsx(VirtualFiller, {
|
|
73485
73599
|
edge: "after",
|
|
73486
|
-
itemCount: runEnd - windowTo
|
|
73600
|
+
itemCount: runEnd - windowTo,
|
|
73601
|
+
findChunks: getFindChunks(windowTo, runEnd)
|
|
73487
73602
|
}, "navi-list-filler-after"));
|
|
73488
73603
|
}
|
|
73489
73604
|
return rows;
|
|
73490
73605
|
};
|
|
73606
|
+
// One row, one line: a line break inside it would push every row below off
|
|
73607
|
+
// its place.
|
|
73608
|
+
const toFindLine = text => {
|
|
73609
|
+
if (text === undefined || text === null) {
|
|
73610
|
+
return "";
|
|
73611
|
+
}
|
|
73612
|
+
return String(text).replace(/[\r\n]+/g, " ");
|
|
73613
|
+
};
|
|
73491
73614
|
|
|
73492
73615
|
// A run's row that has not arrived, standing where the real one will. It never
|
|
73493
73616
|
// reaches ListItemUI (see ListItemSkeletonResolver), so it is drawn among the
|
|
@@ -81588,8 +81711,22 @@ function WheelUI(props) {
|
|
|
81588
81711
|
...style
|
|
81589
81712
|
};
|
|
81590
81713
|
|
|
81591
|
-
// Main-axis size of the viewport, from the orientation.
|
|
81592
|
-
|
|
81714
|
+
// Main-axis size of the viewport, from the orientation. Cached like the row
|
|
81715
|
+
// size: applyOffset runs right after each window commit, where reading the
|
|
81716
|
+
// box would force a layout at every row crossing. The observer below keeps it
|
|
81717
|
+
// current; 0 (no box yet) is never kept, so the next read measures again.
|
|
81718
|
+
const viewportSizeRef = useRef(0);
|
|
81719
|
+
const measureViewportMain = vp => isHorizontal ? vp.clientWidth : vp.clientHeight;
|
|
81720
|
+
const viewportMain = vp => {
|
|
81721
|
+
if (viewportSizeRef.current > 0) {
|
|
81722
|
+
return viewportSizeRef.current;
|
|
81723
|
+
}
|
|
81724
|
+
const size = measureViewportMain(vp);
|
|
81725
|
+
if (size > 0) {
|
|
81726
|
+
viewportSizeRef.current = size;
|
|
81727
|
+
}
|
|
81728
|
+
return size;
|
|
81729
|
+
};
|
|
81593
81730
|
|
|
81594
81731
|
// Row size (px). Rows are uniform, so one measurement (cached) drives all the
|
|
81595
81732
|
// geometry — positions are then computed from value indices, not read off the
|
|
@@ -81660,6 +81797,21 @@ function WheelUI(props) {
|
|
|
81660
81797
|
applyOffset(vp);
|
|
81661
81798
|
}
|
|
81662
81799
|
};
|
|
81800
|
+
// Refreshes the cached viewport size (see viewportMain). Read in the observer
|
|
81801
|
+
// callback, where the layout is already clean. A new size moves the center
|
|
81802
|
+
// window, so the offset is re-applied with it.
|
|
81803
|
+
useLayoutEffect(() => {
|
|
81804
|
+
const vp = getViewport();
|
|
81805
|
+
viewportSizeRef.current = 0;
|
|
81806
|
+
const resizeObserver = new ResizeObserver(() => {
|
|
81807
|
+
viewportSizeRef.current = measureViewportMain(vp);
|
|
81808
|
+
applyOffset(vp);
|
|
81809
|
+
});
|
|
81810
|
+
resizeObserver.observe(vp);
|
|
81811
|
+
return () => {
|
|
81812
|
+
resizeObserver.disconnect();
|
|
81813
|
+
};
|
|
81814
|
+
}, [isHorizontal]);
|
|
81663
81815
|
|
|
81664
81816
|
// The value index at the center for the current position.
|
|
81665
81817
|
const centeredIndex = vp => {
|