@jsenv/navi 0.29.407 → 0.29.408
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 +239 -68
- package/dist/dev/jsenv_navi.js.map +10 -8
- package/dist/jsenv_navi.js +239 -68
- package/dist/jsenv_navi.js.map +10 -8
- package/docs/AI_INSTRUCTIONS.md +3 -2
- package/docs/control_value.md +7 -6
- package/docs/scroll.md +52 -1
- package/package.json +2 -2
package/dist/dev/jsenv_navi.js
CHANGED
|
@@ -13122,7 +13122,8 @@ const useActionBoundToOneParam = (action, paramsSignal) => {
|
|
|
13122
13122
|
}, []);
|
|
13123
13123
|
return [boundAction, getValue(), setValue];
|
|
13124
13124
|
};
|
|
13125
|
-
//
|
|
13125
|
+
// The action bound to the signal, without reading it: the caller is not
|
|
13126
|
+
// re-rendered by the params (a control, a <details>).
|
|
13126
13127
|
const useAction = (action, paramsSignal) => {
|
|
13127
13128
|
return useBoundAction(action, paramsSignal);
|
|
13128
13129
|
};
|
|
@@ -23009,7 +23010,7 @@ import.meta.css = /* css */[`[data-scrollable] > .navi_loading_outline_wrapper,
|
|
|
23009
23010
|
}
|
|
23010
23011
|
}
|
|
23011
23012
|
|
|
23012
|
-
[hidden] {
|
|
23013
|
+
[hidden]:not([hidden="until-found" i]) {
|
|
23013
23014
|
display: none !important;
|
|
23014
23015
|
}
|
|
23015
23016
|
`, "@jsenv/navi/src/box/box.jsx"];
|
|
@@ -42656,7 +42657,14 @@ const useControlProps = (props, {
|
|
|
42656
42657
|
persists,
|
|
42657
42658
|
uiActionInternal
|
|
42658
42659
|
});
|
|
42659
|
-
|
|
42660
|
+
// What the control draws is what it holds by itself, and this read is what
|
|
42661
|
+
// re-renders it when that changes. A button inheriting the value of the
|
|
42662
|
+
// control around it (a submit in a form, see ownUIStateSignal) draws none of
|
|
42663
|
+
// that value, it only hands it to its action: subscribed to it, every button
|
|
42664
|
+
// of a form would re-render at each change of any of its fields.
|
|
42665
|
+
// eslint-disable-next-line no-unused-expressions
|
|
42666
|
+
uiStateController.ownUIStateSignal.value;
|
|
42667
|
+
const boundAction = useAction(props.action, uiStateController.uiStateSignal);
|
|
42660
42668
|
const [controlRootProps, controlHostProps] = useInteractiveProps(props, {
|
|
42661
42669
|
uiStateController,
|
|
42662
42670
|
boundAction,
|
|
@@ -43423,7 +43431,10 @@ const useControlProps = (props, {
|
|
|
43423
43431
|
};
|
|
43424
43432
|
}
|
|
43425
43433
|
}
|
|
43426
|
-
|
|
43434
|
+
|
|
43435
|
+
// Its own, like syncDomState: a button's DOM value is not the value it
|
|
43436
|
+
// inherits from the control around it.
|
|
43437
|
+
const uiState = uiStateController.ownUIStateSignal.peek();
|
|
43427
43438
|
const domProps = toDomProps(uiState);
|
|
43428
43439
|
{
|
|
43429
43440
|
// Same as syncDomState: a field being typed into keeps its own text, so a
|
|
@@ -43972,7 +43983,9 @@ const useInteractiveProps = (props, {
|
|
|
43972
43983
|
// stop waiting on a run that is still going. What it waits on is the run
|
|
43973
43984
|
// it started, from navi_action_start to its settlement.
|
|
43974
43985
|
const runningAction = uiStateController.runningActionSignal.value;
|
|
43975
|
-
|
|
43986
|
+
// Only whether it runs: the whole status (useActionStatus) reads the params
|
|
43987
|
+
// too, which for a button inheriting its form's value are the form.
|
|
43988
|
+
const actionRunning = (runningAction || boundAction).runningStateSignal.value === RUNNING;
|
|
43976
43989
|
const networkPolicyReason = useNetworkPolicyReason();
|
|
43977
43990
|
const {
|
|
43978
43991
|
disabled,
|
|
@@ -44020,7 +44033,7 @@ const useInteractiveProps = (props, {
|
|
|
44020
44033
|
// just set stays visible and interactive while the action runs — no
|
|
44021
44034
|
// loading, no readonly. On failure resetOnError rolls the state back and
|
|
44022
44035
|
// the error callout says why.
|
|
44023
|
-
const actionLoading = optimistic ? false :
|
|
44036
|
+
const actionLoading = optimistic ? false : actionRunning;
|
|
44024
44037
|
const loadingResolved = loadingBase || actionLoading;
|
|
44025
44038
|
const readOnlyResolved = readOnlyBase || actionLoading;
|
|
44026
44039
|
// Read-only, and what this control opens still opens: reading what is in
|
|
@@ -44215,7 +44228,11 @@ const useInteractiveProps = (props, {
|
|
|
44215
44228
|
errorEffect: actionErrorEffect,
|
|
44216
44229
|
errorMapping
|
|
44217
44230
|
});
|
|
44218
|
-
|
|
44231
|
+
// What the attribute says is that an action is there (see
|
|
44232
|
+
// findClosestControlWithAction); its text is a label. Peeked (toString),
|
|
44233
|
+
// because `.callSource` follows the params — for a button inheriting its
|
|
44234
|
+
// form's value, a re-render at every change of any field.
|
|
44235
|
+
const dataAction = action === undefined ? undefined : String(boundAction);
|
|
44219
44236
|
Object.assign(controlHostProps, {
|
|
44220
44237
|
"data-action": dataAction
|
|
44221
44238
|
});
|
|
@@ -53143,30 +53160,9 @@ const ButtonUI = props => {
|
|
|
53143
53160
|
} = buttonControlHostProps;
|
|
53144
53161
|
const loading = basePseudoState[":-navi-loading"];
|
|
53145
53162
|
|
|
53146
|
-
//
|
|
53147
|
-
|
|
53148
|
-
const
|
|
53149
|
-
let as = "button";
|
|
53150
|
-
let innerTarget;
|
|
53151
|
-
let innerRel;
|
|
53152
|
-
let innerCurrent;
|
|
53153
|
-
if (isLink) {
|
|
53154
|
-
as = "a";
|
|
53155
|
-
const {
|
|
53156
|
-
isSameSite,
|
|
53157
|
-
isCurrent
|
|
53158
|
-
} = getHrefTargetInfo(href);
|
|
53159
|
-
innerTarget = target === undefined ? isSameSite ? undefined : "_blank" : target;
|
|
53160
|
-
innerRel = rel === undefined ? isSameSite ? undefined : "noopener noreferrer" : rel;
|
|
53161
|
-
innerCurrent = isCurrent;
|
|
53162
|
-
}
|
|
53163
|
-
// For a button that has only an href: nothing else knows it points at the
|
|
53164
|
-
// page one is on. A route says so through `pseudoState`, which Box lays over
|
|
53165
|
-
// this one.
|
|
53166
|
-
buttonControlHostProps.basePseudoState = {
|
|
53167
|
-
...basePseudoState,
|
|
53168
|
-
":-navi-href-current": innerCurrent
|
|
53169
|
-
};
|
|
53163
|
+
// Where the href leads next to the page one is on (the target, the rel, the
|
|
53164
|
+
// current state) is resolved above, by ButtonHrefResolver.
|
|
53165
|
+
const as = href === undefined ? "button" : "a";
|
|
53170
53166
|
|
|
53171
53167
|
// Worn as an attribute, like a link's (see link_replace.js): read off the
|
|
53172
53168
|
// anchor by the click handler, off the source by --navi-nav-to.
|
|
@@ -53215,8 +53211,8 @@ const ButtonUI = props => {
|
|
|
53215
53211
|
ref: ref,
|
|
53216
53212
|
as: as,
|
|
53217
53213
|
href: href,
|
|
53218
|
-
target:
|
|
53219
|
-
rel:
|
|
53214
|
+
target: target,
|
|
53215
|
+
rel: rel,
|
|
53220
53216
|
replace: undefined,
|
|
53221
53217
|
...replaceRequest,
|
|
53222
53218
|
document: undefined,
|
|
@@ -53516,6 +53512,49 @@ const ButtonConfirm = ({
|
|
|
53516
53512
|
});
|
|
53517
53513
|
};
|
|
53518
53514
|
|
|
53515
|
+
const ButtonHrefResolver = props => {
|
|
53516
|
+
const Next = useNextResolver();
|
|
53517
|
+
if (props.href !== undefined) {
|
|
53518
|
+
return renderResolver(ButtonWithHref, props);
|
|
53519
|
+
}
|
|
53520
|
+
return jsx(Next, {
|
|
53521
|
+
...props
|
|
53522
|
+
});
|
|
53523
|
+
};
|
|
53524
|
+
|
|
53525
|
+
// What the href means next to the page one is on depends on the document url,
|
|
53526
|
+
// so a button with an href re-renders at every address write. One without
|
|
53527
|
+
// never reaches here: it has nothing to re-compute, and every button on screen
|
|
53528
|
+
// would otherwise pay for each write.
|
|
53529
|
+
const ButtonWithHref = props => {
|
|
53530
|
+
const Next = useNextResolver();
|
|
53531
|
+
const {
|
|
53532
|
+
href,
|
|
53533
|
+
target,
|
|
53534
|
+
rel,
|
|
53535
|
+
pseudoState
|
|
53536
|
+
} = props;
|
|
53537
|
+
// Read for the subscription: getHrefTargetInfo reads window.location.
|
|
53538
|
+
useDocumentUrl();
|
|
53539
|
+
const {
|
|
53540
|
+
isSameSite,
|
|
53541
|
+
isCurrent
|
|
53542
|
+
} = getHrefTargetInfo(href);
|
|
53543
|
+
return jsx(Next, {
|
|
53544
|
+
...props,
|
|
53545
|
+
target: target === undefined ? isSameSite ? undefined : "_blank" : target,
|
|
53546
|
+
rel: rel === undefined ? isSameSite ? undefined : "noopener noreferrer" : rel
|
|
53547
|
+
// Under whatever the caller already says: a route knows better whether
|
|
53548
|
+
// it is the page one is on (see button_route.jsx), and a demo can force
|
|
53549
|
+
// the state.
|
|
53550
|
+
,
|
|
53551
|
+
pseudoState: {
|
|
53552
|
+
":-navi-href-current": isCurrent,
|
|
53553
|
+
...pseudoState
|
|
53554
|
+
}
|
|
53555
|
+
});
|
|
53556
|
+
};
|
|
53557
|
+
|
|
53519
53558
|
const ButtonRouteResolver = props => {
|
|
53520
53559
|
const Next = useNextResolver();
|
|
53521
53560
|
if (props.route) {
|
|
@@ -53773,7 +53812,7 @@ const COMMAND_DEFAULT_PROPS_FACTORIES = {
|
|
|
53773
53812
|
* (it wears it itself) and loses the shrink under the finger, which has
|
|
53774
53813
|
* nothing left to scale but the interactive area itself.
|
|
53775
53814
|
*/
|
|
53776
|
-
const Button = /*#__PURE__*/createComponentResolver([ButtonFirstResolver, ButtonRouteResolver, ButtonCommandPropResolver, ButtonConfirmResolver, ButtonUI]);
|
|
53815
|
+
const Button = /*#__PURE__*/createComponentResolver([ButtonFirstResolver, ButtonRouteResolver, ButtonCommandPropResolver, ButtonConfirmResolver, ButtonHrefResolver, ButtonUI]);
|
|
53777
53816
|
|
|
53778
53817
|
installImportMetaCssBuild(import.meta);
|
|
53779
53818
|
const css$O = /* css */`.navi_control_swap {
|
|
@@ -71269,12 +71308,28 @@ const css$x = /* css */`@layer navi {
|
|
|
71269
71308
|
display: inline-block;
|
|
71270
71309
|
}
|
|
71271
71310
|
|
|
71311
|
+
.navi_list_find_stand_in {
|
|
71312
|
+
height: calc(var(--x-find-line-count) * var(--x-find-line-size));
|
|
71313
|
+
color: #0000;
|
|
71314
|
+
line-height: var(--x-find-line-size);
|
|
71315
|
+
white-space: pre;
|
|
71316
|
+
overflow: clip;
|
|
71317
|
+
}
|
|
71318
|
+
|
|
71272
71319
|
.navi_list_container[data-horizontal] {
|
|
71273
71320
|
--list-max-height: none;
|
|
71274
71321
|
|
|
71275
71322
|
& > .navi_list_scroll_container > .navi_list > .navi_list_virtual_filler {
|
|
71276
71323
|
width: var(--size-to-fill, 0px);
|
|
71277
71324
|
height: 100%;
|
|
71325
|
+
display: flex;
|
|
71326
|
+
|
|
71327
|
+
& > .navi_list_find_stand_in {
|
|
71328
|
+
width: calc(var(--x-find-line-count) * var(--x-find-line-size));
|
|
71329
|
+
writing-mode: vertical-lr;
|
|
71330
|
+
flex-shrink: 0;
|
|
71331
|
+
height: auto;
|
|
71332
|
+
}
|
|
71278
71333
|
}
|
|
71279
71334
|
}
|
|
71280
71335
|
|
|
@@ -73871,10 +73926,12 @@ const Fallback = ({
|
|
|
73871
73926
|
// settled after the first commit.
|
|
73872
73927
|
const VirtualFiller = ({
|
|
73873
73928
|
edge,
|
|
73874
|
-
itemCount
|
|
73929
|
+
itemCount,
|
|
73930
|
+
findChunks
|
|
73875
73931
|
}) => {
|
|
73876
73932
|
const listRows = useContext(ListRowsContext);
|
|
73877
|
-
const
|
|
73933
|
+
const virtualItemSize = listRows.virtualItemSizeSignal.value;
|
|
73934
|
+
const sizeToFill = itemCount * virtualItemSize;
|
|
73878
73935
|
if (!sizeToFill) {
|
|
73879
73936
|
return null;
|
|
73880
73937
|
}
|
|
@@ -73885,8 +73942,17 @@ const VirtualFiller = ({
|
|
|
73885
73942
|
"navi-virtual-filler": edge,
|
|
73886
73943
|
"aria-hidden": true,
|
|
73887
73944
|
style: {
|
|
73888
|
-
"--size-to-fill": `${sizeToFill}px
|
|
73889
|
-
|
|
73945
|
+
"--size-to-fill": `${sizeToFill}px`,
|
|
73946
|
+
"--x-find-line-size": findChunks ? `${virtualItemSize}px` : undefined
|
|
73947
|
+
},
|
|
73948
|
+
children: findChunks && findChunks.map(chunk => jsx("div", {
|
|
73949
|
+
className: "navi_list_find_stand_in",
|
|
73950
|
+
hidden: "until-found",
|
|
73951
|
+
style: {
|
|
73952
|
+
"--x-find-line-count": chunk.to - chunk.from
|
|
73953
|
+
},
|
|
73954
|
+
children: chunk.text
|
|
73955
|
+
}, `${chunk.from}_${chunk.to}`))
|
|
73890
73956
|
});
|
|
73891
73957
|
};
|
|
73892
73958
|
|
|
@@ -74522,6 +74588,7 @@ const VISIBILITY_HIDDEN_STYLE = {
|
|
|
74522
74588
|
*
|
|
74523
74589
|
* @type {import("ignore:preact").FunctionComponent<{
|
|
74524
74590
|
* renderItem: (item: any, index: number, state: {refreshing: boolean}) => import("ignore:preact").ComponentChildren,
|
|
74591
|
+
* findText?: (item: any, index: number) => string,
|
|
74525
74592
|
* items?: any[],
|
|
74526
74593
|
* itemsAction?: (range: {start: number, end: number, limit: number, before?: string, after?: string, around?: string, count?: number, signal: AbortSignal}) => any,
|
|
74527
74594
|
* count?: number,
|
|
@@ -74540,6 +74607,13 @@ const VISIBILITY_HIDDEN_STYLE = {
|
|
|
74540
74607
|
* the run. `state.refreshing` says the rows drawn are the ones from before
|
|
74541
74608
|
* while the run reads the collection again — the list carries
|
|
74542
74609
|
* `navi-refreshing` for the same reason.
|
|
74610
|
+
* @param {(item: any, index: number) => string} [props.findText]
|
|
74611
|
+
* The text of a row as the browser's find in page (Cmd/Ctrl + F) sees it
|
|
74612
|
+
* while the row is not drawn — without it, find reaches only the rows in the
|
|
74613
|
+
* render window. The rows held off screen then carry that text where they
|
|
74614
|
+
* stand, hidden until the browser finds it there: it scrolls to the place,
|
|
74615
|
+
* and the list draws the row. One line per row. A row the run does not hold
|
|
74616
|
+
* yet has nothing to be found by.
|
|
74543
74617
|
* @param {any[]} [props.items]
|
|
74544
74618
|
* The collection, when it is held in memory: all of it, in order. Nothing is
|
|
74545
74619
|
* ever asked for — `itemsAction`, `count`, `pageSize` and `memoryBudget` have
|
|
@@ -74636,12 +74710,19 @@ const createStoreHoldingNothing = rowCount => {
|
|
|
74636
74710
|
};
|
|
74637
74711
|
};
|
|
74638
74712
|
|
|
74713
|
+
// How many rows share one element of the text a filler carries for find in
|
|
74714
|
+
// page (see findText). One element per row would put back the DOM nodes the
|
|
74715
|
+
// window saves; one per filler would be revealed whole — every row of it laid
|
|
74716
|
+
// out — by the first match found in it.
|
|
74717
|
+
const FIND_CHUNK_ROW_COUNT = 64;
|
|
74718
|
+
|
|
74639
74719
|
// What a run draws: the rows its window frames, a skeleton for each one the
|
|
74640
74720
|
// store does not hold, and fillers holding the room of the rows outside it.
|
|
74641
74721
|
// Which rows are held, and fetching the others, is the store's (see
|
|
74642
74722
|
// useItemStore).
|
|
74643
74723
|
const useRunRows = (store, {
|
|
74644
74724
|
renderItem,
|
|
74725
|
+
findText,
|
|
74645
74726
|
pageSize,
|
|
74646
74727
|
groupBy,
|
|
74647
74728
|
renderGroupLabel,
|
|
@@ -74849,6 +74930,37 @@ const useRunRows = (store, {
|
|
|
74849
74930
|
}
|
|
74850
74931
|
group.children.push(rowNode);
|
|
74851
74932
|
};
|
|
74933
|
+
// The text of the rows a filler stands for, cut in chunks aligned on the
|
|
74934
|
+
// run's own ranks: the window sliding changes the chunk at its edge and
|
|
74935
|
+
// leaves the others as they are.
|
|
74936
|
+
const getFindChunks = (from, to) => {
|
|
74937
|
+
if (!findText) {
|
|
74938
|
+
return null;
|
|
74939
|
+
}
|
|
74940
|
+
const chunks = [];
|
|
74941
|
+
let chunkFrom = from;
|
|
74942
|
+
while (chunkFrom < to) {
|
|
74943
|
+
const chunkIndex = Math.floor(rankOf(chunkFrom) / FIND_CHUNK_ROW_COUNT);
|
|
74944
|
+
const alignedTo = rowOf((chunkIndex + 1) * FIND_CHUNK_ROW_COUNT);
|
|
74945
|
+
const chunkTo = alignedTo < to ? alignedTo : to;
|
|
74946
|
+
const lines = [];
|
|
74947
|
+
let lineRowIndex = chunkFrom;
|
|
74948
|
+
while (lineRowIndex < chunkTo) {
|
|
74949
|
+
const item = getItemAt(lineRowIndex);
|
|
74950
|
+
// A row not held has nothing to find, but keeps its line: the rows
|
|
74951
|
+
// after it stay at their place.
|
|
74952
|
+
lines.push(item === undefined ? "" : toFindLine(findText(item, lineRowIndex)));
|
|
74953
|
+
lineRowIndex++;
|
|
74954
|
+
}
|
|
74955
|
+
chunks.push({
|
|
74956
|
+
from: chunkFrom,
|
|
74957
|
+
to: chunkTo,
|
|
74958
|
+
text: lines.join("\n")
|
|
74959
|
+
});
|
|
74960
|
+
chunkFrom = chunkTo;
|
|
74961
|
+
}
|
|
74962
|
+
return chunks;
|
|
74963
|
+
};
|
|
74852
74964
|
// The room held for this run's own rows that the window leaves out. It
|
|
74853
74965
|
// belongs to the run and not to the list: a list is not necessarily made of
|
|
74854
74966
|
// one run, and what sits before or after it (a header, rows given one by
|
|
@@ -74856,7 +74968,8 @@ const useRunRows = (store, {
|
|
|
74856
74968
|
if (windowFrom > runStart) {
|
|
74857
74969
|
rows.push(jsx(VirtualFiller, {
|
|
74858
74970
|
edge: "before",
|
|
74859
|
-
itemCount: windowFrom - runStart
|
|
74971
|
+
itemCount: windowFrom - runStart,
|
|
74972
|
+
findChunks: getFindChunks(runStart, windowFrom)
|
|
74860
74973
|
}, "navi-list-filler-before"));
|
|
74861
74974
|
}
|
|
74862
74975
|
const renderItemState = {
|
|
@@ -74958,11 +75071,20 @@ const useRunRows = (store, {
|
|
|
74958
75071
|
if (runEnd > windowTo) {
|
|
74959
75072
|
rows.push(jsx(VirtualFiller, {
|
|
74960
75073
|
edge: "after",
|
|
74961
|
-
itemCount: runEnd - windowTo
|
|
75074
|
+
itemCount: runEnd - windowTo,
|
|
75075
|
+
findChunks: getFindChunks(windowTo, runEnd)
|
|
74962
75076
|
}, "navi-list-filler-after"));
|
|
74963
75077
|
}
|
|
74964
75078
|
return rows;
|
|
74965
75079
|
};
|
|
75080
|
+
// One row, one line: a line break inside it would push every row below off
|
|
75081
|
+
// its place.
|
|
75082
|
+
const toFindLine = text => {
|
|
75083
|
+
if (text === undefined || text === null) {
|
|
75084
|
+
return "";
|
|
75085
|
+
}
|
|
75086
|
+
return String(text).replace(/[\r\n]+/g, " ");
|
|
75087
|
+
};
|
|
74966
75088
|
|
|
74967
75089
|
// A run's row that has not arrived, standing where the real one will. It never
|
|
74968
75090
|
// reaches ListItemUI (see ListItemSkeletonResolver), so it is drawn among the
|
|
@@ -82988,6 +83110,7 @@ const useWheelInteractions = ({
|
|
|
82988
83110
|
attemptInteraction,
|
|
82989
83111
|
cancelAnim,
|
|
82990
83112
|
setPos,
|
|
83113
|
+
releaseScrub,
|
|
82991
83114
|
snapPosToRow,
|
|
82992
83115
|
getItemSize,
|
|
82993
83116
|
viewportMain,
|
|
@@ -83396,6 +83519,8 @@ const useWheelInteractions = ({
|
|
|
83396
83519
|
clearTimeout(settleTimer);
|
|
83397
83520
|
stopClaimingGesture();
|
|
83398
83521
|
cancelAnim();
|
|
83522
|
+
// Nothing is left to settle the scrub in progress.
|
|
83523
|
+
releaseScrub();
|
|
83399
83524
|
document.removeEventListener("pointerdown", onDocumentPointerDown, {
|
|
83400
83525
|
capture: true
|
|
83401
83526
|
});
|
|
@@ -83561,11 +83686,11 @@ function WheelUI(props) {
|
|
|
83561
83686
|
...props,
|
|
83562
83687
|
ref: inputRef,
|
|
83563
83688
|
type: "navi_js",
|
|
83564
|
-
//
|
|
83565
|
-
// the
|
|
83566
|
-
// stops setUIState from auto-firing the action on every value change
|
|
83567
|
-
//
|
|
83568
|
-
//
|
|
83689
|
+
// An arrow or a tap changes the value at once while the row glides into
|
|
83690
|
+
// place, and the action must wait for the wheel to come to rest. "custom"
|
|
83691
|
+
// stops setUIState from auto-firing the action on every value change;
|
|
83692
|
+
// commitSelection dispatches it explicitly on settle. A caller can still
|
|
83693
|
+
// override actionEvent.
|
|
83569
83694
|
actionEvent: props.actionEvent ?? "custom"
|
|
83570
83695
|
}, {
|
|
83571
83696
|
controlType: "input"
|
|
@@ -83661,6 +83786,11 @@ function WheelUI(props) {
|
|
|
83661
83786
|
// while our own scroll-driven selection does not scroll a second time. Holds the
|
|
83662
83787
|
// TARGET so rapid inputs step from it and accumulate. null = nothing pending.
|
|
83663
83788
|
const centeredIndexRef = useRef(null);
|
|
83789
|
+
// A user scrub moved the wheel onto a row that is not the value yet: set at
|
|
83790
|
+
// the first row it crosses, cleared when it settles (commitSelection), when
|
|
83791
|
+
// an arrow or a tap sets a value at once (glideToIndex), or when it is
|
|
83792
|
+
// dropped (releaseScrub). See setPos.
|
|
83793
|
+
const scrubHoldsRowRef = useRef(false);
|
|
83664
83794
|
// The wheel does NOT use native scroll. `posRef` is our own virtual scroll
|
|
83665
83795
|
// position (px, main axis); the list track is translated by -pos. This is the
|
|
83666
83796
|
// single source of truth, wrapped modulo the real-list extent in loop mode, so
|
|
@@ -83710,8 +83840,22 @@ function WheelUI(props) {
|
|
|
83710
83840
|
...style
|
|
83711
83841
|
};
|
|
83712
83842
|
|
|
83713
|
-
// Main-axis size of the viewport, from the orientation.
|
|
83714
|
-
|
|
83843
|
+
// Main-axis size of the viewport, from the orientation. Cached like the row
|
|
83844
|
+
// size: applyOffset runs right after each window commit, where reading the
|
|
83845
|
+
// box would force a layout at every row crossing. The observer below keeps it
|
|
83846
|
+
// current; 0 (no box yet) is never kept, so the next read measures again.
|
|
83847
|
+
const viewportSizeRef = useRef(0);
|
|
83848
|
+
const measureViewportMain = vp => isHorizontal ? vp.clientWidth : vp.clientHeight;
|
|
83849
|
+
const viewportMain = vp => {
|
|
83850
|
+
if (viewportSizeRef.current > 0) {
|
|
83851
|
+
return viewportSizeRef.current;
|
|
83852
|
+
}
|
|
83853
|
+
const size = measureViewportMain(vp);
|
|
83854
|
+
if (size > 0) {
|
|
83855
|
+
viewportSizeRef.current = size;
|
|
83856
|
+
}
|
|
83857
|
+
return size;
|
|
83858
|
+
};
|
|
83715
83859
|
|
|
83716
83860
|
// Row size (px). Rows are uniform, so one measurement (cached) drives all the
|
|
83717
83861
|
// geometry — positions are then computed from value indices, not read off the
|
|
@@ -83782,6 +83926,21 @@ function WheelUI(props) {
|
|
|
83782
83926
|
applyOffset(vp);
|
|
83783
83927
|
}
|
|
83784
83928
|
};
|
|
83929
|
+
// Refreshes the cached viewport size (see viewportMain). Read in the observer
|
|
83930
|
+
// callback, where the layout is already clean. A new size moves the center
|
|
83931
|
+
// window, so the offset is re-applied with it.
|
|
83932
|
+
useLayoutEffect(() => {
|
|
83933
|
+
const vp = getViewport();
|
|
83934
|
+
viewportSizeRef.current = 0;
|
|
83935
|
+
const resizeObserver = new ResizeObserver(() => {
|
|
83936
|
+
viewportSizeRef.current = measureViewportMain(vp);
|
|
83937
|
+
applyOffset(vp);
|
|
83938
|
+
});
|
|
83939
|
+
resizeObserver.observe(vp);
|
|
83940
|
+
return () => {
|
|
83941
|
+
resizeObserver.disconnect();
|
|
83942
|
+
};
|
|
83943
|
+
}, [isHorizontal]);
|
|
83785
83944
|
|
|
83786
83945
|
// The value index at the center for the current position.
|
|
83787
83946
|
const centeredIndex = vp => {
|
|
@@ -83820,12 +83979,14 @@ function WheelUI(props) {
|
|
|
83820
83979
|
posRef.current = clampNumber(posRef.current, 0, (count - 1) * size);
|
|
83821
83980
|
};
|
|
83822
83981
|
|
|
83823
|
-
// `live`
|
|
83824
|
-
//
|
|
83825
|
-
//
|
|
83826
|
-
//
|
|
83827
|
-
//
|
|
83828
|
-
//
|
|
83982
|
+
// `live` marks a user scrub (drag, momentum, wheel scroll). A scrub moves the
|
|
83983
|
+
// drawing, not the value: the row it reaches is held by the wheel, and only
|
|
83984
|
+
// commitSelection turns it into the value, once the motion settles. Reported
|
|
83985
|
+
// at each crossing, every notch would be a value change for all that is
|
|
83986
|
+
// bound to the wheel — its group, its form, a signal, the address, which
|
|
83987
|
+
// records where a value was put down, not the path the finger took
|
|
83988
|
+
// (navigation.md, "A value written per frame"). A programmatic move
|
|
83989
|
+
// (centerOnIndex) omits `live`: it places the value the wheel already has.
|
|
83829
83990
|
const setPos = (vp, pos, {
|
|
83830
83991
|
live = false
|
|
83831
83992
|
} = {}) => {
|
|
@@ -83836,18 +83997,25 @@ function WheelUI(props) {
|
|
|
83836
83997
|
renderPos(vp);
|
|
83837
83998
|
if (live && interactive) {
|
|
83838
83999
|
const index = centeredIndex(vp);
|
|
83839
|
-
if (index !== centeredIndexRef.current) {
|
|
83840
|
-
|
|
83841
|
-
|
|
83842
|
-
|
|
83843
|
-
|
|
83844
|
-
// fight the in-progress motion.
|
|
83845
|
-
centeredIndexRef.current = index;
|
|
83846
|
-
requestSelectValue(item.value, new CustomEvent("navi_wheel_scrub"));
|
|
83847
|
-
}
|
|
84000
|
+
if (index !== centeredIndexRef.current && trackedItemsRef.current[index]) {
|
|
84001
|
+
// In step with the scrub so the next arrow or tap steps from the row
|
|
84002
|
+
// the wheel is on (see currentTargetIndex).
|
|
84003
|
+
centeredIndexRef.current = index;
|
|
84004
|
+
scrubHoldsRowRef.current = true;
|
|
83848
84005
|
}
|
|
83849
84006
|
}
|
|
83850
84007
|
};
|
|
84008
|
+
// The scrub was dropped before it settled (the wheel stopped being
|
|
84009
|
+
// interactive, or its input bindings were torn down): the row it held never
|
|
84010
|
+
// became the value. Forgetting it lets the next sync put the wheel back on
|
|
84011
|
+
// the value.
|
|
84012
|
+
const releaseScrub = () => {
|
|
84013
|
+
if (!scrubHoldsRowRef.current) {
|
|
84014
|
+
return;
|
|
84015
|
+
}
|
|
84016
|
+
scrubHoldsRowRef.current = false;
|
|
84017
|
+
centeredIndexRef.current = null;
|
|
84018
|
+
};
|
|
83851
84019
|
|
|
83852
84020
|
// Returns whether it actually interrupted a motion in flight — lets the caller
|
|
83853
84021
|
// tell a fresh gesture from one that "caught" a spinning/gliding wheel.
|
|
@@ -83883,6 +84051,7 @@ function WheelUI(props) {
|
|
|
83883
84051
|
renderPos(vp);
|
|
83884
84052
|
}
|
|
83885
84053
|
centeredIndexRef.current = index;
|
|
84054
|
+
scrubHoldsRowRef.current = false;
|
|
83886
84055
|
if (!interactive || glideFromOutsideRef.current) {
|
|
83887
84056
|
return;
|
|
83888
84057
|
}
|
|
@@ -84087,6 +84256,7 @@ function WheelUI(props) {
|
|
|
84087
84256
|
// inputs accumulate.
|
|
84088
84257
|
const glideToIndex = (vp, index, event) => {
|
|
84089
84258
|
centeredIndexRef.current = index;
|
|
84259
|
+
scrubHoldsRowRef.current = false;
|
|
84090
84260
|
requestSelectValue(trackedItemsRef.current[index].value, event);
|
|
84091
84261
|
glideTo(vp, glideTargetFor(vp, index));
|
|
84092
84262
|
};
|
|
@@ -84117,12 +84287,12 @@ function WheelUI(props) {
|
|
|
84117
84287
|
// Sync the center with the current value — used on first display and whenever
|
|
84118
84288
|
// the controlled value changes from outside.
|
|
84119
84289
|
const syncCenterToSelection = (viewportEl, behavior) => {
|
|
84120
|
-
// A fling in flight
|
|
84121
|
-
//
|
|
84122
|
-
// arriving meanwhile
|
|
84123
|
-
//
|
|
84124
|
-
//
|
|
84125
|
-
if (momentumRef.current !== null) {
|
|
84290
|
+
// A fling in flight, or a scrub holding a row, commits on settle: the finger
|
|
84291
|
+
// owns the wheel until then, and the row it leaves the wheel on overrules a
|
|
84292
|
+
// value arriving meanwhile. A glide is different: its target is re-aimed
|
|
84293
|
+
// below (glideTo never restarts the loop), so a value set from outside
|
|
84294
|
+
// mid-glide is where the wheel ends up.
|
|
84295
|
+
if (momentumRef.current !== null || scrubHoldsRowRef.current) {
|
|
84126
84296
|
return;
|
|
84127
84297
|
}
|
|
84128
84298
|
if (trackedItemsRef.current.length === 0) {
|
|
@@ -84205,6 +84375,7 @@ function WheelUI(props) {
|
|
|
84205
84375
|
attemptInteraction,
|
|
84206
84376
|
cancelAnim,
|
|
84207
84377
|
setPos,
|
|
84378
|
+
releaseScrub,
|
|
84208
84379
|
snapPosToRow,
|
|
84209
84380
|
getItemSize,
|
|
84210
84381
|
viewportMain,
|