@jsenv/navi 0.29.408 → 0.29.410
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 +145 -119
- package/dist/dev/jsenv_navi.js.map +9 -9
- package/dist/jsenv_navi.js +145 -119
- package/dist/jsenv_navi.js.map +9 -9
- package/docs/control_value.md +6 -7
- package/package.json +2 -2
package/dist/jsenv_navi.js
CHANGED
|
@@ -12662,12 +12662,13 @@ const useBoundAction = (action, actionParamsSignal) => {
|
|
|
12662
12662
|
if (existingNoopAction) {
|
|
12663
12663
|
return existingNoopAction;
|
|
12664
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.
|
|
12665
12669
|
const noopAction = createAction(() => {}, { params: undefined });
|
|
12666
|
-
|
|
12667
|
-
|
|
12668
|
-
: noopAction;
|
|
12669
|
-
noopActionRef.current = noopActionBound;
|
|
12670
|
-
return noopActionBound;
|
|
12670
|
+
noopActionRef.current = noopAction;
|
|
12671
|
+
return noopAction;
|
|
12671
12672
|
}
|
|
12672
12673
|
const isFunction = typeof action === "function";
|
|
12673
12674
|
if (!isFunction) {
|
|
@@ -13436,7 +13437,9 @@ const createControlValidation = (
|
|
|
13436
13437
|
|
|
13437
13438
|
let newConstraintValidityState = { valid: true };
|
|
13438
13439
|
const constraintSet = getConstraintSet();
|
|
13439
|
-
|
|
13440
|
+
// Only for the debug lines of a failure or a change: computed when one of
|
|
13441
|
+
// them is written, not at every check (every notch of a wheel).
|
|
13442
|
+
const elementSig = () => getElementSignature(controller.ref.current);
|
|
13440
13443
|
// Not logged: every control checks its constraints on every interaction and
|
|
13441
13444
|
// almost always passes, so this line alone was most of the debug output —
|
|
13442
13445
|
// and with devtools open, formatting it costs a frame. What matters is a
|
|
@@ -13462,7 +13465,7 @@ const createControlValidation = (
|
|
|
13462
13465
|
constraintValidityInfo.message,
|
|
13463
13466
|
);
|
|
13464
13467
|
debugUIState(
|
|
13465
|
-
`${elementSig} constraint "${constraint.name}" failed -> ${constraintValidityInfo.message}`,
|
|
13468
|
+
`${elementSig()} constraint "${constraint.name}" failed -> ${constraintValidityInfo.message}`,
|
|
13466
13469
|
);
|
|
13467
13470
|
const thisConstraintFailureInfo = {
|
|
13468
13471
|
name: constraint.name,
|
|
@@ -13534,7 +13537,7 @@ const createControlValidation = (
|
|
|
13534
13537
|
if (element) {
|
|
13535
13538
|
debugUIState(
|
|
13536
13539
|
event,
|
|
13537
|
-
`${elementSig} constraint validity changed -> dispatch ${NAVI_VALIDITY_CHANGE_CUSTOM_EVENT}`,
|
|
13540
|
+
`${elementSig()} constraint validity changed -> dispatch ${NAVI_VALIDITY_CHANGE_CUSTOM_EVENT}`,
|
|
13538
13541
|
);
|
|
13539
13542
|
dispatchPublicCustomEvent(element, NAVI_VALIDITY_CHANGE_CUSTOM_EVENT);
|
|
13540
13543
|
}
|
|
@@ -13580,7 +13583,7 @@ const createControlValidation = (
|
|
|
13580
13583
|
event,
|
|
13581
13584
|
{ report = false, fromRequestAction = false } = {},
|
|
13582
13585
|
) => {
|
|
13583
|
-
const elementSig = getElementSignature(controller.ref.current);
|
|
13586
|
+
const elementSig = () => getElementSignature(controller.ref.current);
|
|
13584
13587
|
const isValid = checkValidity({ event, fromRequestAction });
|
|
13585
13588
|
if (failingManagedControlValidity) {
|
|
13586
13589
|
// Group/form case: find the actual failing leaf and report on it.
|
|
@@ -13602,7 +13605,7 @@ const createControlValidation = (
|
|
|
13602
13605
|
if (report) {
|
|
13603
13606
|
debugUIState(
|
|
13604
13607
|
event,
|
|
13605
|
-
`syncValidity ${elementSig}: has failing constraint and report=true -> reportValidity`,
|
|
13608
|
+
`syncValidity ${elementSig()}: has failing constraint and report=true -> reportValidity`,
|
|
13606
13609
|
);
|
|
13607
13610
|
reportValidity({ event });
|
|
13608
13611
|
} else if (failedConstraintInfo.status === "error") {
|
|
@@ -13612,12 +13615,12 @@ const createControlValidation = (
|
|
|
13612
13615
|
// on the next action attempt via autoResetOnAction.
|
|
13613
13616
|
debugUIState(
|
|
13614
13617
|
event,
|
|
13615
|
-
`syncValidity ${elementSig}: has error constraint and report=false -> keep callout open`,
|
|
13618
|
+
`syncValidity ${elementSig()}: has error constraint and report=false -> keep callout open`,
|
|
13616
13619
|
);
|
|
13617
13620
|
} else {
|
|
13618
13621
|
debugUIState(
|
|
13619
13622
|
event,
|
|
13620
|
-
`syncValidity ${elementSig}: has failing constraint but report=false -> close callout if any`,
|
|
13623
|
+
`syncValidity ${elementSig()}: has failing constraint but report=false -> close callout if any`,
|
|
13621
13624
|
);
|
|
13622
13625
|
callout.removeOpenToken(VALIDATION_TOKEN, event);
|
|
13623
13626
|
}
|
|
@@ -27351,6 +27354,7 @@ This prevents cross-test pollution and ensures clean state.`,
|
|
|
27351
27354
|
|
|
27352
27355
|
// Store previous route states to detect changes
|
|
27353
27356
|
const routePreviousStateMap = new WeakMap();
|
|
27357
|
+
let previousPath = null;
|
|
27354
27358
|
const updateRoutes = (
|
|
27355
27359
|
url,
|
|
27356
27360
|
{
|
|
@@ -27359,6 +27363,14 @@ This prevents cross-test pollution and ensures clean state.`,
|
|
|
27359
27363
|
} = {},
|
|
27360
27364
|
) => {
|
|
27361
27365
|
currentUrl = url;
|
|
27366
|
+
// Whether a route matches is decided by the path alone: the search string
|
|
27367
|
+
// only fills the params (see matchUrl). An address that keeps its path — a
|
|
27368
|
+
// search param written, at every notch of a wheel bound to it — cannot make
|
|
27369
|
+
// a route start or stop matching, so only the routes already matching are
|
|
27370
|
+
// read again.
|
|
27371
|
+
const path = urlWithoutSearchAndHash(url);
|
|
27372
|
+
const pathKept = path === previousPath;
|
|
27373
|
+
previousPath = path;
|
|
27362
27374
|
const returnValue = {};
|
|
27363
27375
|
const routeMatchInfoSet = new Set();
|
|
27364
27376
|
for (const route of routeSet) {
|
|
@@ -27374,7 +27386,8 @@ This prevents cross-test pollution and ensures clean state.`,
|
|
|
27374
27386
|
const oldParams = previousState.params;
|
|
27375
27387
|
const oldActionParams = previousState.actionParams;
|
|
27376
27388
|
|
|
27377
|
-
let extractedParams =
|
|
27389
|
+
let extractedParams =
|
|
27390
|
+
pathKept && !oldMatching ? null : routePattern.applyOn(url);
|
|
27378
27391
|
let newMatching = Boolean(extractedParams);
|
|
27379
27392
|
let newParams;
|
|
27380
27393
|
if (extractedParams) {
|
|
@@ -27443,6 +27456,25 @@ This prevents cross-test pollution and ensures clean state.`,
|
|
|
27443
27456
|
}
|
|
27444
27457
|
}
|
|
27445
27458
|
|
|
27459
|
+
// What the matching routes say, read once for every connection of every
|
|
27460
|
+
// route that does not match (below), rather than by walking all the
|
|
27461
|
+
// routes again for each of those connections.
|
|
27462
|
+
const matchingRawParamsArray = [];
|
|
27463
|
+
const paramNameDeclaredByMatchingRouteSet = new Set();
|
|
27464
|
+
const matchingFamilyRootSet = new Set();
|
|
27465
|
+
for (const matchingRoute of matchingRouteSet) {
|
|
27466
|
+
matchingRawParamsArray.push(matchingRoute.rawParamsSignal.value);
|
|
27467
|
+
const matchingPattern =
|
|
27468
|
+
getRoutePrivateProperties(matchingRoute).routePattern;
|
|
27469
|
+
for (const paramName of matchingPattern.queryConnectionMap.keys()) {
|
|
27470
|
+
paramNameDeclaredByMatchingRouteSet.add(paramName);
|
|
27471
|
+
}
|
|
27472
|
+
for (const paramName of matchingPattern.pathConnectionMap.keys()) {
|
|
27473
|
+
paramNameDeclaredByMatchingRouteSet.add(paramName);
|
|
27474
|
+
}
|
|
27475
|
+
matchingFamilyRootSet.add(matchingPattern.familyRoot);
|
|
27476
|
+
}
|
|
27477
|
+
|
|
27446
27478
|
for (const {
|
|
27447
27479
|
route,
|
|
27448
27480
|
routePrivateProperties,
|
|
@@ -27458,40 +27490,21 @@ This prevents cross-test pollution and ensures clean state.`,
|
|
|
27458
27490
|
const urlParamValue = rawParams[paramName];
|
|
27459
27491
|
|
|
27460
27492
|
if (!newMatching) {
|
|
27461
|
-
//
|
|
27462
|
-
|
|
27463
|
-
|
|
27464
|
-
|
|
27465
|
-
|
|
27466
|
-
|
|
27467
|
-
|
|
27468
|
-
|
|
27469
|
-
|
|
27470
|
-
|
|
27471
|
-
|
|
27472
|
-
|
|
27473
|
-
|
|
27474
|
-
|
|
27475
|
-
|
|
27476
|
-
|
|
27477
|
-
// Same param, on the page one is arriving at: what it is worth
|
|
27478
|
-
// there is that page's business, url or no url — it may open on
|
|
27479
|
-
// a default of its own (see searchParams' `{ signal, default }`).
|
|
27480
|
-
const otherPattern =
|
|
27481
|
-
getRoutePrivateProperties(otherRoute).routePattern;
|
|
27482
|
-
if (
|
|
27483
|
-
otherPattern.queryConnectionMap.has(paramName) ||
|
|
27484
|
-
otherPattern.pathConnectionMap.has(paramName)
|
|
27485
|
-
) {
|
|
27486
|
-
parameterDeclaredByMatchingRoute = true;
|
|
27487
|
-
}
|
|
27488
|
-
|
|
27489
|
-
// Same family = same topmost ancestor
|
|
27490
|
-
// (familyRoot, computed in setupRoutePatterns)
|
|
27491
|
-
if (otherPattern.familyRoot === routePattern.familyRoot) {
|
|
27492
|
-
matchingRouteInSameFamily = true;
|
|
27493
|
-
}
|
|
27494
|
-
}
|
|
27493
|
+
// Whether a matching route extracts this parameter from the url
|
|
27494
|
+
const parameterExtractedByMatchingRoute =
|
|
27495
|
+
matchingRawParamsArray.some(
|
|
27496
|
+
(matchingRawParams) => paramName in matchingRawParams,
|
|
27497
|
+
);
|
|
27498
|
+
// Same param, on the page one is arriving at: what it is worth
|
|
27499
|
+
// there is that page's business, url or no url — it may open on
|
|
27500
|
+
// a default of its own (see searchParams' `{ signal, default }`).
|
|
27501
|
+
const parameterDeclaredByMatchingRoute =
|
|
27502
|
+
paramNameDeclaredByMatchingRouteSet.has(paramName);
|
|
27503
|
+
// Same family = same topmost ancestor
|
|
27504
|
+
// (familyRoot, computed in setupRoutePatterns)
|
|
27505
|
+
const matchingRouteInSameFamily = matchingFamilyRootSet.has(
|
|
27506
|
+
routePattern.familyRoot,
|
|
27507
|
+
);
|
|
27495
27508
|
|
|
27496
27509
|
// A weak param qualifies one visit: leaving the route ends it,
|
|
27497
27510
|
// whatever the family and whatever the default. Coming back by a
|
|
@@ -27724,6 +27737,11 @@ const getParamsCacheKey = (params) => {
|
|
|
27724
27737
|
return key;
|
|
27725
27738
|
};
|
|
27726
27739
|
|
|
27740
|
+
const urlWithoutSearchAndHash = (url) => {
|
|
27741
|
+
const urlObject = new URL(url);
|
|
27742
|
+
return `${urlObject.origin}${urlObject.pathname}`;
|
|
27743
|
+
};
|
|
27744
|
+
|
|
27727
27745
|
/**
|
|
27728
27746
|
* Is there an entry of THIS document behind the current one? And ahead of it?
|
|
27729
27747
|
*
|
|
@@ -28167,7 +28185,13 @@ const forgetScrollerUnlessPageLeft = (name) => {
|
|
|
28167
28185
|
}
|
|
28168
28186
|
};
|
|
28169
28187
|
|
|
28170
|
-
|
|
28188
|
+
// `currentUrlString`: the address the href is compared with — the document's by
|
|
28189
|
+
// default. A caller subscribed to documentUrlSignal passes the value it read,
|
|
28190
|
+
// which is ahead of window.location while a debounced write is pending.
|
|
28191
|
+
const getHrefTargetInfo = (
|
|
28192
|
+
href,
|
|
28193
|
+
currentUrlString = window.location.href,
|
|
28194
|
+
) => {
|
|
28171
28195
|
href = String(href);
|
|
28172
28196
|
|
|
28173
28197
|
if (!href || href.trim() === "") {
|
|
@@ -28180,8 +28204,8 @@ const getHrefTargetInfo = (href) => {
|
|
|
28180
28204
|
};
|
|
28181
28205
|
}
|
|
28182
28206
|
|
|
28183
|
-
const currentUrl = new URL(
|
|
28184
|
-
const targetUrl = new URL(href,
|
|
28207
|
+
const currentUrl = new URL(currentUrlString);
|
|
28208
|
+
const targetUrl = new URL(href, currentUrlString);
|
|
28185
28209
|
|
|
28186
28210
|
let isCurrent = false;
|
|
28187
28211
|
{
|
|
@@ -28223,6 +28247,21 @@ const getHrefTargetInfo = (href) => {
|
|
|
28223
28247
|
};
|
|
28224
28248
|
};
|
|
28225
28249
|
|
|
28250
|
+
// A field of getHrefTargetInfo that depends on the page one is on (isCurrent,
|
|
28251
|
+
// isAnchor), read through a computed of that one boolean: the caller re-renders
|
|
28252
|
+
// when it flips, not at every address write — a search param bound to a wheel
|
|
28253
|
+
// writes the address at every notch.
|
|
28254
|
+
const useHrefTargetFlag = (href, flagName) => {
|
|
28255
|
+
const flagSignal = useMemo(
|
|
28256
|
+
() =>
|
|
28257
|
+
computed(
|
|
28258
|
+
() => getHrefTargetInfo(href, documentUrlSignal.value)[flagName],
|
|
28259
|
+
),
|
|
28260
|
+
[href, flagName],
|
|
28261
|
+
);
|
|
28262
|
+
return flagSignal.value;
|
|
28263
|
+
};
|
|
28264
|
+
|
|
28226
28265
|
/*
|
|
28227
28266
|
* An app can ship more than one document — an admin panel, a status page, an
|
|
28228
28267
|
* entry point of its own — and a link to one of them is not a route, it is a
|
|
@@ -46728,19 +46767,28 @@ const usePreloadOpeningOnPress = (ref, command, prefetch = true) => {
|
|
|
46728
46767
|
|
|
46729
46768
|
/**
|
|
46730
46769
|
* Hook that reactively checks if a URL is visited.
|
|
46731
|
-
* Re-renders when
|
|
46770
|
+
* Re-renders when that answer changes — not at every url added to the visited
|
|
46771
|
+
* set, which grows at every new address (a search param bound to a wheel
|
|
46772
|
+
* writes one at every notch).
|
|
46732
46773
|
*
|
|
46733
46774
|
* @param {string} url - The URL to check
|
|
46734
46775
|
* @returns {boolean} Whether the URL has been visited
|
|
46735
46776
|
*/
|
|
46736
46777
|
const useIsVisited = (url) => {
|
|
46737
|
-
|
|
46738
|
-
|
|
46739
|
-
|
|
46740
|
-
|
|
46741
|
-
|
|
46742
|
-
|
|
46743
|
-
|
|
46778
|
+
const visitedSignal = useMemo(
|
|
46779
|
+
() =>
|
|
46780
|
+
computed(() => {
|
|
46781
|
+
// Both are what the answer depends on: the set, and the address a
|
|
46782
|
+
// relative url is resolved against (see isVisited).
|
|
46783
|
+
// eslint-disable-next-line no-unused-expressions
|
|
46784
|
+
visitedUrlsSignal.value;
|
|
46785
|
+
// eslint-disable-next-line no-unused-expressions
|
|
46786
|
+
documentUrlSignal.value;
|
|
46787
|
+
return isVisited(url);
|
|
46788
|
+
}),
|
|
46789
|
+
[url],
|
|
46790
|
+
);
|
|
46791
|
+
return visitedSignal.value;
|
|
46744
46792
|
};
|
|
46745
46793
|
|
|
46746
46794
|
/**
|
|
@@ -47400,13 +47448,14 @@ const LinkPlain = props => {
|
|
|
47400
47448
|
const loading = basePseudoState[":-navi-loading"];
|
|
47401
47449
|
const shouldDimColor = readOnly || disabled;
|
|
47402
47450
|
useDimColorWhen(props.ref, shouldDimColor);
|
|
47403
|
-
//
|
|
47404
|
-
|
|
47451
|
+
// Re-rendered when one of these flips, not at every address write (see
|
|
47452
|
+
// useHrefTargetFlag). Whether the href leaves the site does not depend on
|
|
47453
|
+
// the address.
|
|
47454
|
+
const isCurrent = useHrefTargetFlag(href, "isCurrent");
|
|
47455
|
+
const isAnchor = useHrefTargetFlag(href, "isAnchor");
|
|
47405
47456
|
const {
|
|
47406
|
-
isSameSite
|
|
47407
|
-
|
|
47408
|
-
isCurrent
|
|
47409
|
-
} = getHrefTargetInfo(href);
|
|
47457
|
+
isSameSite
|
|
47458
|
+
} = getHrefTargetInfo(href, documentUrlSignal.peek());
|
|
47410
47459
|
// A tab that is a SLIDE is current when the container is on it — which the
|
|
47411
47460
|
// <Nav> around reads off that container, so nothing here has to be told.
|
|
47412
47461
|
const innerCurrent = current || (slide ? nav?.currentSlideArea === slide : isCurrent);
|
|
@@ -52372,10 +52421,9 @@ const ButtonHrefResolver = props => {
|
|
|
52372
52421
|
});
|
|
52373
52422
|
};
|
|
52374
52423
|
|
|
52375
|
-
//
|
|
52376
|
-
//
|
|
52377
|
-
//
|
|
52378
|
-
// would otherwise pay for each write.
|
|
52424
|
+
// Whether the href is the page one is on depends on the address, and re-renders
|
|
52425
|
+
// the button only when it flips (see useHrefTargetFlag). Whether it leaves the
|
|
52426
|
+
// site does not depend on the address.
|
|
52379
52427
|
const ButtonWithHref = props => {
|
|
52380
52428
|
const Next = useNextResolver();
|
|
52381
52429
|
const {
|
|
@@ -52384,12 +52432,10 @@ const ButtonWithHref = props => {
|
|
|
52384
52432
|
rel,
|
|
52385
52433
|
pseudoState
|
|
52386
52434
|
} = props;
|
|
52387
|
-
|
|
52388
|
-
useDocumentUrl();
|
|
52435
|
+
const isCurrent = useHrefTargetFlag(href, "isCurrent");
|
|
52389
52436
|
const {
|
|
52390
|
-
isSameSite
|
|
52391
|
-
|
|
52392
|
-
} = getHrefTargetInfo(href);
|
|
52437
|
+
isSameSite
|
|
52438
|
+
} = getHrefTargetInfo(href, documentUrlSignal.peek());
|
|
52393
52439
|
return jsx(Next, {
|
|
52394
52440
|
...props,
|
|
52395
52441
|
target: target === undefined ? isSameSite ? undefined : "_blank" : target,
|
|
@@ -80989,7 +81035,6 @@ const useWheelInteractions = ({
|
|
|
80989
81035
|
attemptInteraction,
|
|
80990
81036
|
cancelAnim,
|
|
80991
81037
|
setPos,
|
|
80992
|
-
releaseScrub,
|
|
80993
81038
|
snapPosToRow,
|
|
80994
81039
|
getItemSize,
|
|
80995
81040
|
viewportMain,
|
|
@@ -81397,8 +81442,6 @@ const useWheelInteractions = ({
|
|
|
81397
81442
|
clearTimeout(settleTimer);
|
|
81398
81443
|
stopClaimingGesture();
|
|
81399
81444
|
cancelAnim();
|
|
81400
|
-
// Nothing is left to settle the scrub in progress.
|
|
81401
|
-
releaseScrub();
|
|
81402
81445
|
document.removeEventListener("pointerdown", onDocumentPointerDown, {
|
|
81403
81446
|
capture: true
|
|
81404
81447
|
});
|
|
@@ -81564,11 +81607,11 @@ function WheelUI(props) {
|
|
|
81564
81607
|
...props,
|
|
81565
81608
|
ref: inputRef,
|
|
81566
81609
|
type: "navi_js",
|
|
81567
|
-
//
|
|
81568
|
-
//
|
|
81569
|
-
// stops setUIState from auto-firing the action on every value change
|
|
81570
|
-
//
|
|
81571
|
-
// override actionEvent.
|
|
81610
|
+
// uiAction fires live (every center-crossing, via setPos live reports);
|
|
81611
|
+
// the committed action must fire only when the wheel stabilizes. "custom"
|
|
81612
|
+
// stops setUIState from auto-firing the action on every value change, so
|
|
81613
|
+
// the live reports are uiAction-only; commitSelection then dispatches the
|
|
81614
|
+
// action explicitly on settle. A caller can still override actionEvent.
|
|
81572
81615
|
actionEvent: props.actionEvent ?? "custom"
|
|
81573
81616
|
}, {
|
|
81574
81617
|
controlType: "input"
|
|
@@ -81664,11 +81707,6 @@ function WheelUI(props) {
|
|
|
81664
81707
|
// while our own scroll-driven selection does not scroll a second time. Holds the
|
|
81665
81708
|
// TARGET so rapid inputs step from it and accumulate. null = nothing pending.
|
|
81666
81709
|
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);
|
|
81672
81710
|
// The wheel does NOT use native scroll. `posRef` is our own virtual scroll
|
|
81673
81711
|
// position (px, main axis); the list track is translated by -pos. This is the
|
|
81674
81712
|
// single source of truth, wrapped modulo the real-list extent in loop mode, so
|
|
@@ -81857,14 +81895,12 @@ function WheelUI(props) {
|
|
|
81857
81895
|
posRef.current = clampNumber(posRef.current, 0, (count - 1) * size);
|
|
81858
81896
|
};
|
|
81859
81897
|
|
|
81860
|
-
// `live`
|
|
81861
|
-
//
|
|
81862
|
-
//
|
|
81863
|
-
//
|
|
81864
|
-
//
|
|
81865
|
-
//
|
|
81866
|
-
// (navigation.md, "A value written per frame"). A programmatic move
|
|
81867
|
-
// (centerOnIndex) omits `live`: it places the value the wheel already has.
|
|
81898
|
+
// `live` (user scrubs: drag, momentum, wheel scroll) reports the value as each
|
|
81899
|
+
// item crosses the center, so uiAction tracks the wheel in real time — like
|
|
81900
|
+
// every other control. The committed action still fires only once it settles
|
|
81901
|
+
// (commitSelection). A programmatic move (centerOnIndex on an external value
|
|
81902
|
+
// sync) omits it, so scrolling the wheel into place doesn't echo the value
|
|
81903
|
+
// back out.
|
|
81868
81904
|
const setPos = (vp, pos, {
|
|
81869
81905
|
live = false
|
|
81870
81906
|
} = {}) => {
|
|
@@ -81875,25 +81911,18 @@ function WheelUI(props) {
|
|
|
81875
81911
|
renderPos(vp);
|
|
81876
81912
|
if (live && interactive) {
|
|
81877
81913
|
const index = centeredIndex(vp);
|
|
81878
|
-
if (index !== centeredIndexRef.current
|
|
81879
|
-
|
|
81880
|
-
|
|
81881
|
-
|
|
81882
|
-
|
|
81914
|
+
if (index !== centeredIndexRef.current) {
|
|
81915
|
+
const item = trackedItemsRef.current[index];
|
|
81916
|
+
if (item) {
|
|
81917
|
+
// Keep centeredIndexRef in step so the every-render
|
|
81918
|
+
// syncCenterToSelection doesn't read this as an external change and
|
|
81919
|
+
// fight the in-progress motion.
|
|
81920
|
+
centeredIndexRef.current = index;
|
|
81921
|
+
requestSelectValue(item.value, new CustomEvent("navi_wheel_scrub"));
|
|
81922
|
+
}
|
|
81883
81923
|
}
|
|
81884
81924
|
}
|
|
81885
81925
|
};
|
|
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
|
-
};
|
|
81897
81926
|
|
|
81898
81927
|
// Returns whether it actually interrupted a motion in flight — lets the caller
|
|
81899
81928
|
// tell a fresh gesture from one that "caught" a spinning/gliding wheel.
|
|
@@ -81929,7 +81958,6 @@ function WheelUI(props) {
|
|
|
81929
81958
|
renderPos(vp);
|
|
81930
81959
|
}
|
|
81931
81960
|
centeredIndexRef.current = index;
|
|
81932
|
-
scrubHoldsRowRef.current = false;
|
|
81933
81961
|
if (!interactive || glideFromOutsideRef.current) {
|
|
81934
81962
|
return;
|
|
81935
81963
|
}
|
|
@@ -82127,7 +82155,6 @@ function WheelUI(props) {
|
|
|
82127
82155
|
// inputs accumulate.
|
|
82128
82156
|
const glideToIndex = (vp, index, event) => {
|
|
82129
82157
|
centeredIndexRef.current = index;
|
|
82130
|
-
scrubHoldsRowRef.current = false;
|
|
82131
82158
|
requestSelectValue(trackedItemsRef.current[index].value, event);
|
|
82132
82159
|
glideTo(vp, glideTargetFor(vp, index));
|
|
82133
82160
|
};
|
|
@@ -82158,12 +82185,12 @@ function WheelUI(props) {
|
|
|
82158
82185
|
// Sync the center with the current value — used on first display and whenever
|
|
82159
82186
|
// the controlled value changes from outside.
|
|
82160
82187
|
const syncCenterToSelection = (viewportEl, behavior) => {
|
|
82161
|
-
// A fling in flight
|
|
82162
|
-
//
|
|
82163
|
-
//
|
|
82164
|
-
// below (glideTo never restarts the
|
|
82165
|
-
// mid-glide is where the wheel ends up.
|
|
82166
|
-
if (momentumRef.current !== null
|
|
82188
|
+
// A fling in flight reports its own value at every row crossing and commits
|
|
82189
|
+
// on settle: the finger that threw it owns the wheel until then, and a value
|
|
82190
|
+
// arriving meanwhile is re-stated by the next crossing anyway. A glide is
|
|
82191
|
+
// different: its target is re-aimed below (glideTo never restarts the
|
|
82192
|
+
// loop), so a value set from outside mid-glide is where the wheel ends up.
|
|
82193
|
+
if (momentumRef.current !== null) {
|
|
82167
82194
|
return;
|
|
82168
82195
|
}
|
|
82169
82196
|
if (trackedItemsRef.current.length === 0) {
|
|
@@ -82246,7 +82273,6 @@ function WheelUI(props) {
|
|
|
82246
82273
|
attemptInteraction,
|
|
82247
82274
|
cancelAnim,
|
|
82248
82275
|
setPos,
|
|
82249
|
-
releaseScrub,
|
|
82250
82276
|
snapPosToRow,
|
|
82251
82277
|
getItemSize,
|
|
82252
82278
|
viewportMain,
|