@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/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
|
};
|
|
@@ -22377,7 +22378,7 @@ import.meta.css = /* css */[`[data-scrollable] > .navi_loading_outline_wrapper,
|
|
|
22377
22378
|
}
|
|
22378
22379
|
}
|
|
22379
22380
|
|
|
22380
|
-
[hidden] {
|
|
22381
|
+
[hidden]:not([hidden="until-found" i]) {
|
|
22381
22382
|
display: none !important;
|
|
22382
22383
|
}
|
|
22383
22384
|
`, "@jsenv/navi/src/box/box.jsx"];
|
|
@@ -41540,7 +41541,14 @@ const useControlProps = (props, {
|
|
|
41540
41541
|
persists,
|
|
41541
41542
|
uiActionInternal
|
|
41542
41543
|
});
|
|
41543
|
-
|
|
41544
|
+
// What the control draws is what it holds by itself, and this read is what
|
|
41545
|
+
// re-renders it when that changes. A button inheriting the value of the
|
|
41546
|
+
// control around it (a submit in a form, see ownUIStateSignal) draws none of
|
|
41547
|
+
// that value, it only hands it to its action: subscribed to it, every button
|
|
41548
|
+
// of a form would re-render at each change of any of its fields.
|
|
41549
|
+
// eslint-disable-next-line no-unused-expressions
|
|
41550
|
+
uiStateController.ownUIStateSignal.value;
|
|
41551
|
+
const boundAction = useAction(props.action, uiStateController.uiStateSignal);
|
|
41544
41552
|
const [controlRootProps, controlHostProps] = useInteractiveProps(props, {
|
|
41545
41553
|
uiStateController,
|
|
41546
41554
|
boundAction,
|
|
@@ -42305,7 +42313,10 @@ const useControlProps = (props, {
|
|
|
42305
42313
|
};
|
|
42306
42314
|
}
|
|
42307
42315
|
}
|
|
42308
|
-
|
|
42316
|
+
|
|
42317
|
+
// Its own, like syncDomState: a button's DOM value is not the value it
|
|
42318
|
+
// inherits from the control around it.
|
|
42319
|
+
const uiState = uiStateController.ownUIStateSignal.peek();
|
|
42309
42320
|
const domProps = toDomProps(uiState);
|
|
42310
42321
|
{
|
|
42311
42322
|
// Same as syncDomState: a field being typed into keeps its own text, so a
|
|
@@ -42833,7 +42844,9 @@ const useInteractiveProps = (props, {
|
|
|
42833
42844
|
// stop waiting on a run that is still going. What it waits on is the run
|
|
42834
42845
|
// it started, from navi_action_start to its settlement.
|
|
42835
42846
|
const runningAction = uiStateController.runningActionSignal.value;
|
|
42836
|
-
|
|
42847
|
+
// Only whether it runs: the whole status (useActionStatus) reads the params
|
|
42848
|
+
// too, which for a button inheriting its form's value are the form.
|
|
42849
|
+
const actionRunning = (runningAction || boundAction).runningStateSignal.value === RUNNING;
|
|
42837
42850
|
const networkPolicyReason = useNetworkPolicyReason();
|
|
42838
42851
|
const {
|
|
42839
42852
|
disabled,
|
|
@@ -42881,7 +42894,7 @@ const useInteractiveProps = (props, {
|
|
|
42881
42894
|
// just set stays visible and interactive while the action runs — no
|
|
42882
42895
|
// loading, no readonly. On failure resetOnError rolls the state back and
|
|
42883
42896
|
// the error callout says why.
|
|
42884
|
-
const actionLoading = optimistic ? false :
|
|
42897
|
+
const actionLoading = optimistic ? false : actionRunning;
|
|
42885
42898
|
const loadingResolved = loadingBase || actionLoading;
|
|
42886
42899
|
const readOnlyResolved = readOnlyBase || actionLoading;
|
|
42887
42900
|
// Read-only, and what this control opens still opens: reading what is in
|
|
@@ -43076,7 +43089,11 @@ const useInteractiveProps = (props, {
|
|
|
43076
43089
|
errorEffect: actionErrorEffect,
|
|
43077
43090
|
errorMapping
|
|
43078
43091
|
});
|
|
43079
|
-
|
|
43092
|
+
// What the attribute says is that an action is there (see
|
|
43093
|
+
// findClosestControlWithAction); its text is a label. Peeked (toString),
|
|
43094
|
+
// because `.callSource` follows the params — for a button inheriting its
|
|
43095
|
+
// form's value, a re-render at every change of any field.
|
|
43096
|
+
const dataAction = action === undefined ? undefined : String(boundAction);
|
|
43080
43097
|
Object.assign(controlHostProps, {
|
|
43081
43098
|
"data-action": dataAction
|
|
43082
43099
|
});
|
|
@@ -51993,30 +52010,9 @@ const ButtonUI = props => {
|
|
|
51993
52010
|
} = buttonControlHostProps;
|
|
51994
52011
|
const loading = basePseudoState[":-navi-loading"];
|
|
51995
52012
|
|
|
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
|
-
};
|
|
52013
|
+
// Where the href leads next to the page one is on (the target, the rel, the
|
|
52014
|
+
// current state) is resolved above, by ButtonHrefResolver.
|
|
52015
|
+
const as = href === undefined ? "button" : "a";
|
|
52020
52016
|
|
|
52021
52017
|
// Worn as an attribute, like a link's (see link_replace.js): read off the
|
|
52022
52018
|
// anchor by the click handler, off the source by --navi-nav-to.
|
|
@@ -52065,8 +52061,8 @@ const ButtonUI = props => {
|
|
|
52065
52061
|
ref: ref,
|
|
52066
52062
|
as: as,
|
|
52067
52063
|
href: href,
|
|
52068
|
-
target:
|
|
52069
|
-
rel:
|
|
52064
|
+
target: target,
|
|
52065
|
+
rel: rel,
|
|
52070
52066
|
replace: undefined,
|
|
52071
52067
|
...replaceRequest,
|
|
52072
52068
|
document: undefined,
|
|
@@ -52366,6 +52362,49 @@ const ButtonConfirm = ({
|
|
|
52366
52362
|
});
|
|
52367
52363
|
};
|
|
52368
52364
|
|
|
52365
|
+
const ButtonHrefResolver = props => {
|
|
52366
|
+
const Next = useNextResolver();
|
|
52367
|
+
if (props.href !== undefined) {
|
|
52368
|
+
return renderResolver(ButtonWithHref, props);
|
|
52369
|
+
}
|
|
52370
|
+
return jsx(Next, {
|
|
52371
|
+
...props
|
|
52372
|
+
});
|
|
52373
|
+
};
|
|
52374
|
+
|
|
52375
|
+
// What the href means next to the page one is on depends on the document url,
|
|
52376
|
+
// so a button with an href re-renders at every address write. One without
|
|
52377
|
+
// never reaches here: it has nothing to re-compute, and every button on screen
|
|
52378
|
+
// would otherwise pay for each write.
|
|
52379
|
+
const ButtonWithHref = props => {
|
|
52380
|
+
const Next = useNextResolver();
|
|
52381
|
+
const {
|
|
52382
|
+
href,
|
|
52383
|
+
target,
|
|
52384
|
+
rel,
|
|
52385
|
+
pseudoState
|
|
52386
|
+
} = props;
|
|
52387
|
+
// Read for the subscription: getHrefTargetInfo reads window.location.
|
|
52388
|
+
useDocumentUrl();
|
|
52389
|
+
const {
|
|
52390
|
+
isSameSite,
|
|
52391
|
+
isCurrent
|
|
52392
|
+
} = getHrefTargetInfo(href);
|
|
52393
|
+
return jsx(Next, {
|
|
52394
|
+
...props,
|
|
52395
|
+
target: target === undefined ? isSameSite ? undefined : "_blank" : target,
|
|
52396
|
+
rel: rel === undefined ? isSameSite ? undefined : "noopener noreferrer" : rel
|
|
52397
|
+
// Under whatever the caller already says: a route knows better whether
|
|
52398
|
+
// it is the page one is on (see button_route.jsx), and a demo can force
|
|
52399
|
+
// the state.
|
|
52400
|
+
,
|
|
52401
|
+
pseudoState: {
|
|
52402
|
+
":-navi-href-current": isCurrent,
|
|
52403
|
+
...pseudoState
|
|
52404
|
+
}
|
|
52405
|
+
});
|
|
52406
|
+
};
|
|
52407
|
+
|
|
52369
52408
|
const ButtonRouteResolver = props => {
|
|
52370
52409
|
const Next = useNextResolver();
|
|
52371
52410
|
if (props.route) {
|
|
@@ -52620,7 +52659,7 @@ const COMMAND_DEFAULT_PROPS_FACTORIES = {
|
|
|
52620
52659
|
* (it wears it itself) and loses the shrink under the finger, which has
|
|
52621
52660
|
* nothing left to scale but the interactive area itself.
|
|
52622
52661
|
*/
|
|
52623
|
-
const Button = /*#__PURE__*/createComponentResolver([ButtonFirstResolver, ButtonRouteResolver, ButtonCommandPropResolver, ButtonConfirmResolver, ButtonUI]);
|
|
52662
|
+
const Button = /*#__PURE__*/createComponentResolver([ButtonFirstResolver, ButtonRouteResolver, ButtonCommandPropResolver, ButtonConfirmResolver, ButtonHrefResolver, ButtonUI]);
|
|
52624
52663
|
|
|
52625
52664
|
installImportMetaCssBuild(import.meta);
|
|
52626
52665
|
const css$O = /* css */`.navi_control_swap {
|
|
@@ -69896,12 +69935,28 @@ const css$x = /* css */`@layer navi {
|
|
|
69896
69935
|
display: inline-block;
|
|
69897
69936
|
}
|
|
69898
69937
|
|
|
69938
|
+
.navi_list_find_stand_in {
|
|
69939
|
+
height: calc(var(--x-find-line-count) * var(--x-find-line-size));
|
|
69940
|
+
color: #0000;
|
|
69941
|
+
line-height: var(--x-find-line-size);
|
|
69942
|
+
white-space: pre;
|
|
69943
|
+
overflow: clip;
|
|
69944
|
+
}
|
|
69945
|
+
|
|
69899
69946
|
.navi_list_container[data-horizontal] {
|
|
69900
69947
|
--list-max-height: none;
|
|
69901
69948
|
|
|
69902
69949
|
& > .navi_list_scroll_container > .navi_list > .navi_list_virtual_filler {
|
|
69903
69950
|
width: var(--size-to-fill, 0px);
|
|
69904
69951
|
height: 100%;
|
|
69952
|
+
display: flex;
|
|
69953
|
+
|
|
69954
|
+
& > .navi_list_find_stand_in {
|
|
69955
|
+
width: calc(var(--x-find-line-count) * var(--x-find-line-size));
|
|
69956
|
+
writing-mode: vertical-lr;
|
|
69957
|
+
flex-shrink: 0;
|
|
69958
|
+
height: auto;
|
|
69959
|
+
}
|
|
69905
69960
|
}
|
|
69906
69961
|
}
|
|
69907
69962
|
|
|
@@ -72399,10 +72454,12 @@ const Fallback = ({
|
|
|
72399
72454
|
// settled after the first commit.
|
|
72400
72455
|
const VirtualFiller = ({
|
|
72401
72456
|
edge,
|
|
72402
|
-
itemCount
|
|
72457
|
+
itemCount,
|
|
72458
|
+
findChunks
|
|
72403
72459
|
}) => {
|
|
72404
72460
|
const listRows = useContext(ListRowsContext);
|
|
72405
|
-
const
|
|
72461
|
+
const virtualItemSize = listRows.virtualItemSizeSignal.value;
|
|
72462
|
+
const sizeToFill = itemCount * virtualItemSize;
|
|
72406
72463
|
if (!sizeToFill) {
|
|
72407
72464
|
return null;
|
|
72408
72465
|
}
|
|
@@ -72413,8 +72470,17 @@ const VirtualFiller = ({
|
|
|
72413
72470
|
"navi-virtual-filler": edge,
|
|
72414
72471
|
"aria-hidden": true,
|
|
72415
72472
|
style: {
|
|
72416
|
-
"--size-to-fill": `${sizeToFill}px
|
|
72417
|
-
|
|
72473
|
+
"--size-to-fill": `${sizeToFill}px`,
|
|
72474
|
+
"--x-find-line-size": findChunks ? `${virtualItemSize}px` : undefined
|
|
72475
|
+
},
|
|
72476
|
+
children: findChunks && findChunks.map(chunk => jsx("div", {
|
|
72477
|
+
className: "navi_list_find_stand_in",
|
|
72478
|
+
hidden: "until-found",
|
|
72479
|
+
style: {
|
|
72480
|
+
"--x-find-line-count": chunk.to - chunk.from
|
|
72481
|
+
},
|
|
72482
|
+
children: chunk.text
|
|
72483
|
+
}, `${chunk.from}_${chunk.to}`))
|
|
72418
72484
|
});
|
|
72419
72485
|
};
|
|
72420
72486
|
|
|
@@ -73047,6 +73113,7 @@ const VISIBILITY_HIDDEN_STYLE = {
|
|
|
73047
73113
|
*
|
|
73048
73114
|
* @type {import("ignore:preact").FunctionComponent<{
|
|
73049
73115
|
* renderItem: (item: any, index: number, state: {refreshing: boolean}) => import("ignore:preact").ComponentChildren,
|
|
73116
|
+
* findText?: (item: any, index: number) => string,
|
|
73050
73117
|
* items?: any[],
|
|
73051
73118
|
* itemsAction?: (range: {start: number, end: number, limit: number, before?: string, after?: string, around?: string, count?: number, signal: AbortSignal}) => any,
|
|
73052
73119
|
* count?: number,
|
|
@@ -73065,6 +73132,13 @@ const VISIBILITY_HIDDEN_STYLE = {
|
|
|
73065
73132
|
* the run. `state.refreshing` says the rows drawn are the ones from before
|
|
73066
73133
|
* while the run reads the collection again — the list carries
|
|
73067
73134
|
* `navi-refreshing` for the same reason.
|
|
73135
|
+
* @param {(item: any, index: number) => string} [props.findText]
|
|
73136
|
+
* The text of a row as the browser's find in page (Cmd/Ctrl + F) sees it
|
|
73137
|
+
* while the row is not drawn — without it, find reaches only the rows in the
|
|
73138
|
+
* render window. The rows held off screen then carry that text where they
|
|
73139
|
+
* stand, hidden until the browser finds it there: it scrolls to the place,
|
|
73140
|
+
* and the list draws the row. One line per row. A row the run does not hold
|
|
73141
|
+
* yet has nothing to be found by.
|
|
73068
73142
|
* @param {any[]} [props.items]
|
|
73069
73143
|
* The collection, when it is held in memory: all of it, in order. Nothing is
|
|
73070
73144
|
* ever asked for — `itemsAction`, `count`, `pageSize` and `memoryBudget` have
|
|
@@ -73161,12 +73235,19 @@ const createStoreHoldingNothing = rowCount => {
|
|
|
73161
73235
|
};
|
|
73162
73236
|
};
|
|
73163
73237
|
|
|
73238
|
+
// How many rows share one element of the text a filler carries for find in
|
|
73239
|
+
// page (see findText). One element per row would put back the DOM nodes the
|
|
73240
|
+
// window saves; one per filler would be revealed whole — every row of it laid
|
|
73241
|
+
// out — by the first match found in it.
|
|
73242
|
+
const FIND_CHUNK_ROW_COUNT = 64;
|
|
73243
|
+
|
|
73164
73244
|
// What a run draws: the rows its window frames, a skeleton for each one the
|
|
73165
73245
|
// store does not hold, and fillers holding the room of the rows outside it.
|
|
73166
73246
|
// Which rows are held, and fetching the others, is the store's (see
|
|
73167
73247
|
// useItemStore).
|
|
73168
73248
|
const useRunRows = (store, {
|
|
73169
73249
|
renderItem,
|
|
73250
|
+
findText,
|
|
73170
73251
|
pageSize,
|
|
73171
73252
|
groupBy,
|
|
73172
73253
|
renderGroupLabel,
|
|
@@ -73374,6 +73455,37 @@ const useRunRows = (store, {
|
|
|
73374
73455
|
}
|
|
73375
73456
|
group.children.push(rowNode);
|
|
73376
73457
|
};
|
|
73458
|
+
// The text of the rows a filler stands for, cut in chunks aligned on the
|
|
73459
|
+
// run's own ranks: the window sliding changes the chunk at its edge and
|
|
73460
|
+
// leaves the others as they are.
|
|
73461
|
+
const getFindChunks = (from, to) => {
|
|
73462
|
+
if (!findText) {
|
|
73463
|
+
return null;
|
|
73464
|
+
}
|
|
73465
|
+
const chunks = [];
|
|
73466
|
+
let chunkFrom = from;
|
|
73467
|
+
while (chunkFrom < to) {
|
|
73468
|
+
const chunkIndex = Math.floor(rankOf(chunkFrom) / FIND_CHUNK_ROW_COUNT);
|
|
73469
|
+
const alignedTo = rowOf((chunkIndex + 1) * FIND_CHUNK_ROW_COUNT);
|
|
73470
|
+
const chunkTo = alignedTo < to ? alignedTo : to;
|
|
73471
|
+
const lines = [];
|
|
73472
|
+
let lineRowIndex = chunkFrom;
|
|
73473
|
+
while (lineRowIndex < chunkTo) {
|
|
73474
|
+
const item = getItemAt(lineRowIndex);
|
|
73475
|
+
// A row not held has nothing to find, but keeps its line: the rows
|
|
73476
|
+
// after it stay at their place.
|
|
73477
|
+
lines.push(item === undefined ? "" : toFindLine(findText(item, lineRowIndex)));
|
|
73478
|
+
lineRowIndex++;
|
|
73479
|
+
}
|
|
73480
|
+
chunks.push({
|
|
73481
|
+
from: chunkFrom,
|
|
73482
|
+
to: chunkTo,
|
|
73483
|
+
text: lines.join("\n")
|
|
73484
|
+
});
|
|
73485
|
+
chunkFrom = chunkTo;
|
|
73486
|
+
}
|
|
73487
|
+
return chunks;
|
|
73488
|
+
};
|
|
73377
73489
|
// The room held for this run's own rows that the window leaves out. It
|
|
73378
73490
|
// belongs to the run and not to the list: a list is not necessarily made of
|
|
73379
73491
|
// one run, and what sits before or after it (a header, rows given one by
|
|
@@ -73381,7 +73493,8 @@ const useRunRows = (store, {
|
|
|
73381
73493
|
if (windowFrom > runStart) {
|
|
73382
73494
|
rows.push(jsx(VirtualFiller, {
|
|
73383
73495
|
edge: "before",
|
|
73384
|
-
itemCount: windowFrom - runStart
|
|
73496
|
+
itemCount: windowFrom - runStart,
|
|
73497
|
+
findChunks: getFindChunks(runStart, windowFrom)
|
|
73385
73498
|
}, "navi-list-filler-before"));
|
|
73386
73499
|
}
|
|
73387
73500
|
const renderItemState = {
|
|
@@ -73483,11 +73596,20 @@ const useRunRows = (store, {
|
|
|
73483
73596
|
if (runEnd > windowTo) {
|
|
73484
73597
|
rows.push(jsx(VirtualFiller, {
|
|
73485
73598
|
edge: "after",
|
|
73486
|
-
itemCount: runEnd - windowTo
|
|
73599
|
+
itemCount: runEnd - windowTo,
|
|
73600
|
+
findChunks: getFindChunks(windowTo, runEnd)
|
|
73487
73601
|
}, "navi-list-filler-after"));
|
|
73488
73602
|
}
|
|
73489
73603
|
return rows;
|
|
73490
73604
|
};
|
|
73605
|
+
// One row, one line: a line break inside it would push every row below off
|
|
73606
|
+
// its place.
|
|
73607
|
+
const toFindLine = text => {
|
|
73608
|
+
if (text === undefined || text === null) {
|
|
73609
|
+
return "";
|
|
73610
|
+
}
|
|
73611
|
+
return String(text).replace(/[\r\n]+/g, " ");
|
|
73612
|
+
};
|
|
73491
73613
|
|
|
73492
73614
|
// A run's row that has not arrived, standing where the real one will. It never
|
|
73493
73615
|
// reaches ListItemUI (see ListItemSkeletonResolver), so it is drawn among the
|
|
@@ -80867,6 +80989,7 @@ const useWheelInteractions = ({
|
|
|
80867
80989
|
attemptInteraction,
|
|
80868
80990
|
cancelAnim,
|
|
80869
80991
|
setPos,
|
|
80992
|
+
releaseScrub,
|
|
80870
80993
|
snapPosToRow,
|
|
80871
80994
|
getItemSize,
|
|
80872
80995
|
viewportMain,
|
|
@@ -81274,6 +81397,8 @@ const useWheelInteractions = ({
|
|
|
81274
81397
|
clearTimeout(settleTimer);
|
|
81275
81398
|
stopClaimingGesture();
|
|
81276
81399
|
cancelAnim();
|
|
81400
|
+
// Nothing is left to settle the scrub in progress.
|
|
81401
|
+
releaseScrub();
|
|
81277
81402
|
document.removeEventListener("pointerdown", onDocumentPointerDown, {
|
|
81278
81403
|
capture: true
|
|
81279
81404
|
});
|
|
@@ -81439,11 +81564,11 @@ function WheelUI(props) {
|
|
|
81439
81564
|
...props,
|
|
81440
81565
|
ref: inputRef,
|
|
81441
81566
|
type: "navi_js",
|
|
81442
|
-
//
|
|
81443
|
-
// the
|
|
81444
|
-
// stops setUIState from auto-firing the action on every value change
|
|
81445
|
-
//
|
|
81446
|
-
//
|
|
81567
|
+
// An arrow or a tap changes the value at once while the row glides into
|
|
81568
|
+
// place, and the action must wait for the wheel to come to rest. "custom"
|
|
81569
|
+
// stops setUIState from auto-firing the action on every value change;
|
|
81570
|
+
// commitSelection dispatches it explicitly on settle. A caller can still
|
|
81571
|
+
// override actionEvent.
|
|
81447
81572
|
actionEvent: props.actionEvent ?? "custom"
|
|
81448
81573
|
}, {
|
|
81449
81574
|
controlType: "input"
|
|
@@ -81539,6 +81664,11 @@ function WheelUI(props) {
|
|
|
81539
81664
|
// while our own scroll-driven selection does not scroll a second time. Holds the
|
|
81540
81665
|
// TARGET so rapid inputs step from it and accumulate. null = nothing pending.
|
|
81541
81666
|
const centeredIndexRef = useRef(null);
|
|
81667
|
+
// A user scrub moved the wheel onto a row that is not the value yet: set at
|
|
81668
|
+
// the first row it crosses, cleared when it settles (commitSelection), when
|
|
81669
|
+
// an arrow or a tap sets a value at once (glideToIndex), or when it is
|
|
81670
|
+
// dropped (releaseScrub). See setPos.
|
|
81671
|
+
const scrubHoldsRowRef = useRef(false);
|
|
81542
81672
|
// The wheel does NOT use native scroll. `posRef` is our own virtual scroll
|
|
81543
81673
|
// position (px, main axis); the list track is translated by -pos. This is the
|
|
81544
81674
|
// single source of truth, wrapped modulo the real-list extent in loop mode, so
|
|
@@ -81588,8 +81718,22 @@ function WheelUI(props) {
|
|
|
81588
81718
|
...style
|
|
81589
81719
|
};
|
|
81590
81720
|
|
|
81591
|
-
// Main-axis size of the viewport, from the orientation.
|
|
81592
|
-
|
|
81721
|
+
// Main-axis size of the viewport, from the orientation. Cached like the row
|
|
81722
|
+
// size: applyOffset runs right after each window commit, where reading the
|
|
81723
|
+
// box would force a layout at every row crossing. The observer below keeps it
|
|
81724
|
+
// current; 0 (no box yet) is never kept, so the next read measures again.
|
|
81725
|
+
const viewportSizeRef = useRef(0);
|
|
81726
|
+
const measureViewportMain = vp => isHorizontal ? vp.clientWidth : vp.clientHeight;
|
|
81727
|
+
const viewportMain = vp => {
|
|
81728
|
+
if (viewportSizeRef.current > 0) {
|
|
81729
|
+
return viewportSizeRef.current;
|
|
81730
|
+
}
|
|
81731
|
+
const size = measureViewportMain(vp);
|
|
81732
|
+
if (size > 0) {
|
|
81733
|
+
viewportSizeRef.current = size;
|
|
81734
|
+
}
|
|
81735
|
+
return size;
|
|
81736
|
+
};
|
|
81593
81737
|
|
|
81594
81738
|
// Row size (px). Rows are uniform, so one measurement (cached) drives all the
|
|
81595
81739
|
// geometry — positions are then computed from value indices, not read off the
|
|
@@ -81660,6 +81804,21 @@ function WheelUI(props) {
|
|
|
81660
81804
|
applyOffset(vp);
|
|
81661
81805
|
}
|
|
81662
81806
|
};
|
|
81807
|
+
// Refreshes the cached viewport size (see viewportMain). Read in the observer
|
|
81808
|
+
// callback, where the layout is already clean. A new size moves the center
|
|
81809
|
+
// window, so the offset is re-applied with it.
|
|
81810
|
+
useLayoutEffect(() => {
|
|
81811
|
+
const vp = getViewport();
|
|
81812
|
+
viewportSizeRef.current = 0;
|
|
81813
|
+
const resizeObserver = new ResizeObserver(() => {
|
|
81814
|
+
viewportSizeRef.current = measureViewportMain(vp);
|
|
81815
|
+
applyOffset(vp);
|
|
81816
|
+
});
|
|
81817
|
+
resizeObserver.observe(vp);
|
|
81818
|
+
return () => {
|
|
81819
|
+
resizeObserver.disconnect();
|
|
81820
|
+
};
|
|
81821
|
+
}, [isHorizontal]);
|
|
81663
81822
|
|
|
81664
81823
|
// The value index at the center for the current position.
|
|
81665
81824
|
const centeredIndex = vp => {
|
|
@@ -81698,12 +81857,14 @@ function WheelUI(props) {
|
|
|
81698
81857
|
posRef.current = clampNumber(posRef.current, 0, (count - 1) * size);
|
|
81699
81858
|
};
|
|
81700
81859
|
|
|
81701
|
-
// `live`
|
|
81702
|
-
//
|
|
81703
|
-
//
|
|
81704
|
-
//
|
|
81705
|
-
//
|
|
81706
|
-
//
|
|
81860
|
+
// `live` marks a user scrub (drag, momentum, wheel scroll). A scrub moves the
|
|
81861
|
+
// drawing, not the value: the row it reaches is held by the wheel, and only
|
|
81862
|
+
// commitSelection turns it into the value, once the motion settles. Reported
|
|
81863
|
+
// at each crossing, every notch would be a value change for all that is
|
|
81864
|
+
// bound to the wheel — its group, its form, a signal, the address, which
|
|
81865
|
+
// records where a value was put down, not the path the finger took
|
|
81866
|
+
// (navigation.md, "A value written per frame"). A programmatic move
|
|
81867
|
+
// (centerOnIndex) omits `live`: it places the value the wheel already has.
|
|
81707
81868
|
const setPos = (vp, pos, {
|
|
81708
81869
|
live = false
|
|
81709
81870
|
} = {}) => {
|
|
@@ -81714,18 +81875,25 @@ function WheelUI(props) {
|
|
|
81714
81875
|
renderPos(vp);
|
|
81715
81876
|
if (live && interactive) {
|
|
81716
81877
|
const index = centeredIndex(vp);
|
|
81717
|
-
if (index !== centeredIndexRef.current) {
|
|
81718
|
-
|
|
81719
|
-
|
|
81720
|
-
|
|
81721
|
-
|
|
81722
|
-
// fight the in-progress motion.
|
|
81723
|
-
centeredIndexRef.current = index;
|
|
81724
|
-
requestSelectValue(item.value, new CustomEvent("navi_wheel_scrub"));
|
|
81725
|
-
}
|
|
81878
|
+
if (index !== centeredIndexRef.current && trackedItemsRef.current[index]) {
|
|
81879
|
+
// In step with the scrub so the next arrow or tap steps from the row
|
|
81880
|
+
// the wheel is on (see currentTargetIndex).
|
|
81881
|
+
centeredIndexRef.current = index;
|
|
81882
|
+
scrubHoldsRowRef.current = true;
|
|
81726
81883
|
}
|
|
81727
81884
|
}
|
|
81728
81885
|
};
|
|
81886
|
+
// The scrub was dropped before it settled (the wheel stopped being
|
|
81887
|
+
// interactive, or its input bindings were torn down): the row it held never
|
|
81888
|
+
// became the value. Forgetting it lets the next sync put the wheel back on
|
|
81889
|
+
// the value.
|
|
81890
|
+
const releaseScrub = () => {
|
|
81891
|
+
if (!scrubHoldsRowRef.current) {
|
|
81892
|
+
return;
|
|
81893
|
+
}
|
|
81894
|
+
scrubHoldsRowRef.current = false;
|
|
81895
|
+
centeredIndexRef.current = null;
|
|
81896
|
+
};
|
|
81729
81897
|
|
|
81730
81898
|
// Returns whether it actually interrupted a motion in flight — lets the caller
|
|
81731
81899
|
// tell a fresh gesture from one that "caught" a spinning/gliding wheel.
|
|
@@ -81761,6 +81929,7 @@ function WheelUI(props) {
|
|
|
81761
81929
|
renderPos(vp);
|
|
81762
81930
|
}
|
|
81763
81931
|
centeredIndexRef.current = index;
|
|
81932
|
+
scrubHoldsRowRef.current = false;
|
|
81764
81933
|
if (!interactive || glideFromOutsideRef.current) {
|
|
81765
81934
|
return;
|
|
81766
81935
|
}
|
|
@@ -81958,6 +82127,7 @@ function WheelUI(props) {
|
|
|
81958
82127
|
// inputs accumulate.
|
|
81959
82128
|
const glideToIndex = (vp, index, event) => {
|
|
81960
82129
|
centeredIndexRef.current = index;
|
|
82130
|
+
scrubHoldsRowRef.current = false;
|
|
81961
82131
|
requestSelectValue(trackedItemsRef.current[index].value, event);
|
|
81962
82132
|
glideTo(vp, glideTargetFor(vp, index));
|
|
81963
82133
|
};
|
|
@@ -81988,12 +82158,12 @@ function WheelUI(props) {
|
|
|
81988
82158
|
// Sync the center with the current value — used on first display and whenever
|
|
81989
82159
|
// the controlled value changes from outside.
|
|
81990
82160
|
const syncCenterToSelection = (viewportEl, behavior) => {
|
|
81991
|
-
// A fling in flight
|
|
81992
|
-
//
|
|
81993
|
-
// arriving meanwhile
|
|
81994
|
-
//
|
|
81995
|
-
//
|
|
81996
|
-
if (momentumRef.current !== null) {
|
|
82161
|
+
// A fling in flight, or a scrub holding a row, commits on settle: the finger
|
|
82162
|
+
// owns the wheel until then, and the row it leaves the wheel on overrules a
|
|
82163
|
+
// value arriving meanwhile. A glide is different: its target is re-aimed
|
|
82164
|
+
// below (glideTo never restarts the loop), so a value set from outside
|
|
82165
|
+
// mid-glide is where the wheel ends up.
|
|
82166
|
+
if (momentumRef.current !== null || scrubHoldsRowRef.current) {
|
|
81997
82167
|
return;
|
|
81998
82168
|
}
|
|
81999
82169
|
if (trackedItemsRef.current.length === 0) {
|
|
@@ -82076,6 +82246,7 @@ function WheelUI(props) {
|
|
|
82076
82246
|
attemptInteraction,
|
|
82077
82247
|
cancelAnim,
|
|
82078
82248
|
setPos,
|
|
82249
|
+
releaseScrub,
|
|
82079
82250
|
snapPosToRow,
|
|
82080
82251
|
getItemSize,
|
|
82081
82252
|
viewportMain,
|