@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/dev/jsenv_navi.js
CHANGED
|
@@ -13145,12 +13145,13 @@ const useBoundAction = (action, actionParamsSignal) => {
|
|
|
13145
13145
|
if (existingNoopAction) {
|
|
13146
13146
|
return existingNoopAction;
|
|
13147
13147
|
}
|
|
13148
|
+
// Not bound to the params signal: nothing reads what a noop runs with, and
|
|
13149
|
+
// a proxy following the signal would retarget (a new child action for each
|
|
13150
|
+
// new value) at every change — for every button of a form, at every change
|
|
13151
|
+
// of any of its fields.
|
|
13148
13152
|
const noopAction = createAction(() => {}, { params: undefined });
|
|
13149
|
-
|
|
13150
|
-
|
|
13151
|
-
: noopAction;
|
|
13152
|
-
noopActionRef.current = noopActionBound;
|
|
13153
|
-
return noopActionBound;
|
|
13153
|
+
noopActionRef.current = noopAction;
|
|
13154
|
+
return noopAction;
|
|
13154
13155
|
}
|
|
13155
13156
|
const isFunction = typeof action === "function";
|
|
13156
13157
|
if (!isFunction) {
|
|
@@ -13919,7 +13920,9 @@ const createControlValidation = (
|
|
|
13919
13920
|
|
|
13920
13921
|
let newConstraintValidityState = { valid: true };
|
|
13921
13922
|
const constraintSet = getConstraintSet();
|
|
13922
|
-
|
|
13923
|
+
// Only for the debug lines of a failure or a change: computed when one of
|
|
13924
|
+
// them is written, not at every check (every notch of a wheel).
|
|
13925
|
+
const elementSig = () => getElementSignature(controller.ref.current);
|
|
13923
13926
|
// Not logged: every control checks its constraints on every interaction and
|
|
13924
13927
|
// almost always passes, so this line alone was most of the debug output —
|
|
13925
13928
|
// and with devtools open, formatting it costs a frame. What matters is a
|
|
@@ -13945,7 +13948,7 @@ const createControlValidation = (
|
|
|
13945
13948
|
constraintValidityInfo.message,
|
|
13946
13949
|
);
|
|
13947
13950
|
debugUIState(
|
|
13948
|
-
`${elementSig} constraint "${constraint.name}" failed -> ${constraintValidityInfo.message}`,
|
|
13951
|
+
`${elementSig()} constraint "${constraint.name}" failed -> ${constraintValidityInfo.message}`,
|
|
13949
13952
|
);
|
|
13950
13953
|
const thisConstraintFailureInfo = {
|
|
13951
13954
|
name: constraint.name,
|
|
@@ -14017,7 +14020,7 @@ const createControlValidation = (
|
|
|
14017
14020
|
if (element) {
|
|
14018
14021
|
debugUIState(
|
|
14019
14022
|
event,
|
|
14020
|
-
`${elementSig} constraint validity changed -> dispatch ${NAVI_VALIDITY_CHANGE_CUSTOM_EVENT}`,
|
|
14023
|
+
`${elementSig()} constraint validity changed -> dispatch ${NAVI_VALIDITY_CHANGE_CUSTOM_EVENT}`,
|
|
14021
14024
|
);
|
|
14022
14025
|
dispatchPublicCustomEvent(element, NAVI_VALIDITY_CHANGE_CUSTOM_EVENT);
|
|
14023
14026
|
}
|
|
@@ -14063,7 +14066,7 @@ const createControlValidation = (
|
|
|
14063
14066
|
event,
|
|
14064
14067
|
{ report = false, fromRequestAction = false } = {},
|
|
14065
14068
|
) => {
|
|
14066
|
-
const elementSig = getElementSignature(controller.ref.current);
|
|
14069
|
+
const elementSig = () => getElementSignature(controller.ref.current);
|
|
14067
14070
|
const isValid = checkValidity({ event, fromRequestAction });
|
|
14068
14071
|
if (failingManagedControlValidity) {
|
|
14069
14072
|
// Group/form case: find the actual failing leaf and report on it.
|
|
@@ -14085,7 +14088,7 @@ const createControlValidation = (
|
|
|
14085
14088
|
if (report) {
|
|
14086
14089
|
debugUIState(
|
|
14087
14090
|
event,
|
|
14088
|
-
`syncValidity ${elementSig}: has failing constraint and report=true -> reportValidity`,
|
|
14091
|
+
`syncValidity ${elementSig()}: has failing constraint and report=true -> reportValidity`,
|
|
14089
14092
|
);
|
|
14090
14093
|
reportValidity({ event });
|
|
14091
14094
|
} else if (failedConstraintInfo.status === "error") {
|
|
@@ -14095,12 +14098,12 @@ const createControlValidation = (
|
|
|
14095
14098
|
// on the next action attempt via autoResetOnAction.
|
|
14096
14099
|
debugUIState(
|
|
14097
14100
|
event,
|
|
14098
|
-
`syncValidity ${elementSig}: has error constraint and report=false -> keep callout open`,
|
|
14101
|
+
`syncValidity ${elementSig()}: has error constraint and report=false -> keep callout open`,
|
|
14099
14102
|
);
|
|
14100
14103
|
} else {
|
|
14101
14104
|
debugUIState(
|
|
14102
14105
|
event,
|
|
14103
|
-
`syncValidity ${elementSig}: has failing constraint but report=false -> close callout if any`,
|
|
14106
|
+
`syncValidity ${elementSig()}: has failing constraint but report=false -> close callout if any`,
|
|
14104
14107
|
);
|
|
14105
14108
|
callout.removeOpenToken(VALIDATION_TOKEN, event);
|
|
14106
14109
|
}
|
|
@@ -28087,6 +28090,7 @@ This prevents cross-test pollution and ensures clean state.`,
|
|
|
28087
28090
|
|
|
28088
28091
|
// Store previous route states to detect changes
|
|
28089
28092
|
const routePreviousStateMap = new WeakMap();
|
|
28093
|
+
let previousPath = null;
|
|
28090
28094
|
const updateRoutes = (
|
|
28091
28095
|
url,
|
|
28092
28096
|
{
|
|
@@ -28095,6 +28099,14 @@ This prevents cross-test pollution and ensures clean state.`,
|
|
|
28095
28099
|
} = {},
|
|
28096
28100
|
) => {
|
|
28097
28101
|
currentUrl = url;
|
|
28102
|
+
// Whether a route matches is decided by the path alone: the search string
|
|
28103
|
+
// only fills the params (see matchUrl). An address that keeps its path — a
|
|
28104
|
+
// search param written, at every notch of a wheel bound to it — cannot make
|
|
28105
|
+
// a route start or stop matching, so only the routes already matching are
|
|
28106
|
+
// read again.
|
|
28107
|
+
const path = urlWithoutSearchAndHash(url);
|
|
28108
|
+
const pathKept = path === previousPath;
|
|
28109
|
+
previousPath = path;
|
|
28098
28110
|
const returnValue = {};
|
|
28099
28111
|
const routeMatchInfoSet = new Set();
|
|
28100
28112
|
for (const route of routeSet) {
|
|
@@ -28110,7 +28122,8 @@ This prevents cross-test pollution and ensures clean state.`,
|
|
|
28110
28122
|
const oldParams = previousState.params;
|
|
28111
28123
|
const oldActionParams = previousState.actionParams;
|
|
28112
28124
|
|
|
28113
|
-
let extractedParams =
|
|
28125
|
+
let extractedParams =
|
|
28126
|
+
pathKept && !oldMatching ? null : routePattern.applyOn(url);
|
|
28114
28127
|
let newMatching = Boolean(extractedParams);
|
|
28115
28128
|
let newParams;
|
|
28116
28129
|
if (extractedParams) {
|
|
@@ -28179,6 +28192,25 @@ This prevents cross-test pollution and ensures clean state.`,
|
|
|
28179
28192
|
}
|
|
28180
28193
|
}
|
|
28181
28194
|
|
|
28195
|
+
// What the matching routes say, read once for every connection of every
|
|
28196
|
+
// route that does not match (below), rather than by walking all the
|
|
28197
|
+
// routes again for each of those connections.
|
|
28198
|
+
const matchingRawParamsArray = [];
|
|
28199
|
+
const paramNameDeclaredByMatchingRouteSet = new Set();
|
|
28200
|
+
const matchingFamilyRootSet = new Set();
|
|
28201
|
+
for (const matchingRoute of matchingRouteSet) {
|
|
28202
|
+
matchingRawParamsArray.push(matchingRoute.rawParamsSignal.value);
|
|
28203
|
+
const matchingPattern =
|
|
28204
|
+
getRoutePrivateProperties(matchingRoute).routePattern;
|
|
28205
|
+
for (const paramName of matchingPattern.queryConnectionMap.keys()) {
|
|
28206
|
+
paramNameDeclaredByMatchingRouteSet.add(paramName);
|
|
28207
|
+
}
|
|
28208
|
+
for (const paramName of matchingPattern.pathConnectionMap.keys()) {
|
|
28209
|
+
paramNameDeclaredByMatchingRouteSet.add(paramName);
|
|
28210
|
+
}
|
|
28211
|
+
matchingFamilyRootSet.add(matchingPattern.familyRoot);
|
|
28212
|
+
}
|
|
28213
|
+
|
|
28182
28214
|
for (const {
|
|
28183
28215
|
route,
|
|
28184
28216
|
routePrivateProperties,
|
|
@@ -28194,40 +28226,21 @@ This prevents cross-test pollution and ensures clean state.`,
|
|
|
28194
28226
|
const urlParamValue = rawParams[paramName];
|
|
28195
28227
|
|
|
28196
28228
|
if (!newMatching) {
|
|
28197
|
-
//
|
|
28198
|
-
|
|
28199
|
-
|
|
28200
|
-
|
|
28201
|
-
|
|
28202
|
-
|
|
28203
|
-
|
|
28204
|
-
|
|
28205
|
-
|
|
28206
|
-
|
|
28207
|
-
|
|
28208
|
-
|
|
28209
|
-
|
|
28210
|
-
|
|
28211
|
-
|
|
28212
|
-
|
|
28213
|
-
// Same param, on the page one is arriving at: what it is worth
|
|
28214
|
-
// there is that page's business, url or no url — it may open on
|
|
28215
|
-
// a default of its own (see searchParams' `{ signal, default }`).
|
|
28216
|
-
const otherPattern =
|
|
28217
|
-
getRoutePrivateProperties(otherRoute).routePattern;
|
|
28218
|
-
if (
|
|
28219
|
-
otherPattern.queryConnectionMap.has(paramName) ||
|
|
28220
|
-
otherPattern.pathConnectionMap.has(paramName)
|
|
28221
|
-
) {
|
|
28222
|
-
parameterDeclaredByMatchingRoute = true;
|
|
28223
|
-
}
|
|
28224
|
-
|
|
28225
|
-
// Same family = same topmost ancestor
|
|
28226
|
-
// (familyRoot, computed in setupRoutePatterns)
|
|
28227
|
-
if (otherPattern.familyRoot === routePattern.familyRoot) {
|
|
28228
|
-
matchingRouteInSameFamily = true;
|
|
28229
|
-
}
|
|
28230
|
-
}
|
|
28229
|
+
// Whether a matching route extracts this parameter from the url
|
|
28230
|
+
const parameterExtractedByMatchingRoute =
|
|
28231
|
+
matchingRawParamsArray.some(
|
|
28232
|
+
(matchingRawParams) => paramName in matchingRawParams,
|
|
28233
|
+
);
|
|
28234
|
+
// Same param, on the page one is arriving at: what it is worth
|
|
28235
|
+
// there is that page's business, url or no url — it may open on
|
|
28236
|
+
// a default of its own (see searchParams' `{ signal, default }`).
|
|
28237
|
+
const parameterDeclaredByMatchingRoute =
|
|
28238
|
+
paramNameDeclaredByMatchingRouteSet.has(paramName);
|
|
28239
|
+
// Same family = same topmost ancestor
|
|
28240
|
+
// (familyRoot, computed in setupRoutePatterns)
|
|
28241
|
+
const matchingRouteInSameFamily = matchingFamilyRootSet.has(
|
|
28242
|
+
routePattern.familyRoot,
|
|
28243
|
+
);
|
|
28231
28244
|
|
|
28232
28245
|
// A weak param qualifies one visit: leaving the route ends it,
|
|
28233
28246
|
// whatever the family and whatever the default. Coming back by a
|
|
@@ -28479,6 +28492,11 @@ const getParamsCacheKey = (params) => {
|
|
|
28479
28492
|
return key;
|
|
28480
28493
|
};
|
|
28481
28494
|
|
|
28495
|
+
const urlWithoutSearchAndHash = (url) => {
|
|
28496
|
+
const urlObject = new URL(url);
|
|
28497
|
+
return `${urlObject.origin}${urlObject.pathname}`;
|
|
28498
|
+
};
|
|
28499
|
+
|
|
28482
28500
|
/**
|
|
28483
28501
|
* Is there an entry of THIS document behind the current one? And ahead of it?
|
|
28484
28502
|
*
|
|
@@ -28922,7 +28940,13 @@ const forgetScrollerUnlessPageLeft = (name) => {
|
|
|
28922
28940
|
}
|
|
28923
28941
|
};
|
|
28924
28942
|
|
|
28925
|
-
|
|
28943
|
+
// `currentUrlString`: the address the href is compared with — the document's by
|
|
28944
|
+
// default. A caller subscribed to documentUrlSignal passes the value it read,
|
|
28945
|
+
// which is ahead of window.location while a debounced write is pending.
|
|
28946
|
+
const getHrefTargetInfo = (
|
|
28947
|
+
href,
|
|
28948
|
+
currentUrlString = window.location.href,
|
|
28949
|
+
) => {
|
|
28926
28950
|
href = String(href);
|
|
28927
28951
|
|
|
28928
28952
|
if (!href || href.trim() === "") {
|
|
@@ -28935,8 +28959,8 @@ const getHrefTargetInfo = (href) => {
|
|
|
28935
28959
|
};
|
|
28936
28960
|
}
|
|
28937
28961
|
|
|
28938
|
-
const currentUrl = new URL(
|
|
28939
|
-
const targetUrl = new URL(href,
|
|
28962
|
+
const currentUrl = new URL(currentUrlString);
|
|
28963
|
+
const targetUrl = new URL(href, currentUrlString);
|
|
28940
28964
|
|
|
28941
28965
|
let isCurrent = false;
|
|
28942
28966
|
{
|
|
@@ -28978,6 +29002,21 @@ const getHrefTargetInfo = (href) => {
|
|
|
28978
29002
|
};
|
|
28979
29003
|
};
|
|
28980
29004
|
|
|
29005
|
+
// A field of getHrefTargetInfo that depends on the page one is on (isCurrent,
|
|
29006
|
+
// isAnchor), read through a computed of that one boolean: the caller re-renders
|
|
29007
|
+
// when it flips, not at every address write — a search param bound to a wheel
|
|
29008
|
+
// writes the address at every notch.
|
|
29009
|
+
const useHrefTargetFlag = (href, flagName) => {
|
|
29010
|
+
const flagSignal = useMemo(
|
|
29011
|
+
() =>
|
|
29012
|
+
computed(
|
|
29013
|
+
() => getHrefTargetInfo(href, documentUrlSignal.value)[flagName],
|
|
29014
|
+
),
|
|
29015
|
+
[href, flagName],
|
|
29016
|
+
);
|
|
29017
|
+
return flagSignal.value;
|
|
29018
|
+
};
|
|
29019
|
+
|
|
28981
29020
|
/*
|
|
28982
29021
|
* An app can ship more than one document — an admin panel, a status page, an
|
|
28983
29022
|
* entry point of its own — and a link to one of them is not a route, it is a
|
|
@@ -47869,19 +47908,28 @@ const usePreloadOpeningOnPress = (ref, command, prefetch = true) => {
|
|
|
47869
47908
|
|
|
47870
47909
|
/**
|
|
47871
47910
|
* Hook that reactively checks if a URL is visited.
|
|
47872
|
-
* Re-renders when
|
|
47911
|
+
* Re-renders when that answer changes — not at every url added to the visited
|
|
47912
|
+
* set, which grows at every new address (a search param bound to a wheel
|
|
47913
|
+
* writes one at every notch).
|
|
47873
47914
|
*
|
|
47874
47915
|
* @param {string} url - The URL to check
|
|
47875
47916
|
* @returns {boolean} Whether the URL has been visited
|
|
47876
47917
|
*/
|
|
47877
47918
|
const useIsVisited = (url) => {
|
|
47878
|
-
|
|
47879
|
-
|
|
47880
|
-
|
|
47881
|
-
|
|
47882
|
-
|
|
47883
|
-
|
|
47884
|
-
|
|
47919
|
+
const visitedSignal = useMemo(
|
|
47920
|
+
() =>
|
|
47921
|
+
computed(() => {
|
|
47922
|
+
// Both are what the answer depends on: the set, and the address a
|
|
47923
|
+
// relative url is resolved against (see isVisited).
|
|
47924
|
+
// eslint-disable-next-line no-unused-expressions
|
|
47925
|
+
visitedUrlsSignal.value;
|
|
47926
|
+
// eslint-disable-next-line no-unused-expressions
|
|
47927
|
+
documentUrlSignal.value;
|
|
47928
|
+
return isVisited(url);
|
|
47929
|
+
}),
|
|
47930
|
+
[url],
|
|
47931
|
+
);
|
|
47932
|
+
return visitedSignal.value;
|
|
47885
47933
|
};
|
|
47886
47934
|
|
|
47887
47935
|
/**
|
|
@@ -48550,13 +48598,14 @@ const LinkPlain = props => {
|
|
|
48550
48598
|
const loading = basePseudoState[":-navi-loading"];
|
|
48551
48599
|
const shouldDimColor = readOnly || disabled;
|
|
48552
48600
|
useDimColorWhen(props.ref, shouldDimColor);
|
|
48553
|
-
//
|
|
48554
|
-
|
|
48601
|
+
// Re-rendered when one of these flips, not at every address write (see
|
|
48602
|
+
// useHrefTargetFlag). Whether the href leaves the site does not depend on
|
|
48603
|
+
// the address.
|
|
48604
|
+
const isCurrent = useHrefTargetFlag(href, "isCurrent");
|
|
48605
|
+
const isAnchor = useHrefTargetFlag(href, "isAnchor");
|
|
48555
48606
|
const {
|
|
48556
|
-
isSameSite
|
|
48557
|
-
|
|
48558
|
-
isCurrent
|
|
48559
|
-
} = getHrefTargetInfo(href);
|
|
48607
|
+
isSameSite
|
|
48608
|
+
} = getHrefTargetInfo(href, documentUrlSignal.peek());
|
|
48560
48609
|
// A tab that is a SLIDE is current when the container is on it — which the
|
|
48561
48610
|
// <Nav> around reads off that container, so nothing here has to be told.
|
|
48562
48611
|
const innerCurrent = current || (slide ? nav?.currentSlideArea === slide : isCurrent);
|
|
@@ -53522,10 +53571,9 @@ const ButtonHrefResolver = props => {
|
|
|
53522
53571
|
});
|
|
53523
53572
|
};
|
|
53524
53573
|
|
|
53525
|
-
//
|
|
53526
|
-
//
|
|
53527
|
-
//
|
|
53528
|
-
// would otherwise pay for each write.
|
|
53574
|
+
// Whether the href is the page one is on depends on the address, and re-renders
|
|
53575
|
+
// the button only when it flips (see useHrefTargetFlag). Whether it leaves the
|
|
53576
|
+
// site does not depend on the address.
|
|
53529
53577
|
const ButtonWithHref = props => {
|
|
53530
53578
|
const Next = useNextResolver();
|
|
53531
53579
|
const {
|
|
@@ -53534,12 +53582,10 @@ const ButtonWithHref = props => {
|
|
|
53534
53582
|
rel,
|
|
53535
53583
|
pseudoState
|
|
53536
53584
|
} = props;
|
|
53537
|
-
|
|
53538
|
-
useDocumentUrl();
|
|
53585
|
+
const isCurrent = useHrefTargetFlag(href, "isCurrent");
|
|
53539
53586
|
const {
|
|
53540
|
-
isSameSite
|
|
53541
|
-
|
|
53542
|
-
} = getHrefTargetInfo(href);
|
|
53587
|
+
isSameSite
|
|
53588
|
+
} = getHrefTargetInfo(href, documentUrlSignal.peek());
|
|
53543
53589
|
return jsx(Next, {
|
|
53544
53590
|
...props,
|
|
53545
53591
|
target: target === undefined ? isSameSite ? undefined : "_blank" : target,
|
|
@@ -83110,7 +83156,6 @@ const useWheelInteractions = ({
|
|
|
83110
83156
|
attemptInteraction,
|
|
83111
83157
|
cancelAnim,
|
|
83112
83158
|
setPos,
|
|
83113
|
-
releaseScrub,
|
|
83114
83159
|
snapPosToRow,
|
|
83115
83160
|
getItemSize,
|
|
83116
83161
|
viewportMain,
|
|
@@ -83519,8 +83564,6 @@ const useWheelInteractions = ({
|
|
|
83519
83564
|
clearTimeout(settleTimer);
|
|
83520
83565
|
stopClaimingGesture();
|
|
83521
83566
|
cancelAnim();
|
|
83522
|
-
// Nothing is left to settle the scrub in progress.
|
|
83523
|
-
releaseScrub();
|
|
83524
83567
|
document.removeEventListener("pointerdown", onDocumentPointerDown, {
|
|
83525
83568
|
capture: true
|
|
83526
83569
|
});
|
|
@@ -83686,11 +83729,11 @@ function WheelUI(props) {
|
|
|
83686
83729
|
...props,
|
|
83687
83730
|
ref: inputRef,
|
|
83688
83731
|
type: "navi_js",
|
|
83689
|
-
//
|
|
83690
|
-
//
|
|
83691
|
-
// stops setUIState from auto-firing the action on every value change
|
|
83692
|
-
//
|
|
83693
|
-
// override actionEvent.
|
|
83732
|
+
// uiAction fires live (every center-crossing, via setPos live reports);
|
|
83733
|
+
// the committed action must fire only when the wheel stabilizes. "custom"
|
|
83734
|
+
// stops setUIState from auto-firing the action on every value change, so
|
|
83735
|
+
// the live reports are uiAction-only; commitSelection then dispatches the
|
|
83736
|
+
// action explicitly on settle. A caller can still override actionEvent.
|
|
83694
83737
|
actionEvent: props.actionEvent ?? "custom"
|
|
83695
83738
|
}, {
|
|
83696
83739
|
controlType: "input"
|
|
@@ -83786,11 +83829,6 @@ function WheelUI(props) {
|
|
|
83786
83829
|
// while our own scroll-driven selection does not scroll a second time. Holds the
|
|
83787
83830
|
// TARGET so rapid inputs step from it and accumulate. null = nothing pending.
|
|
83788
83831
|
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);
|
|
83794
83832
|
// The wheel does NOT use native scroll. `posRef` is our own virtual scroll
|
|
83795
83833
|
// position (px, main axis); the list track is translated by -pos. This is the
|
|
83796
83834
|
// single source of truth, wrapped modulo the real-list extent in loop mode, so
|
|
@@ -83979,14 +84017,12 @@ function WheelUI(props) {
|
|
|
83979
84017
|
posRef.current = clampNumber(posRef.current, 0, (count - 1) * size);
|
|
83980
84018
|
};
|
|
83981
84019
|
|
|
83982
|
-
// `live`
|
|
83983
|
-
//
|
|
83984
|
-
//
|
|
83985
|
-
//
|
|
83986
|
-
//
|
|
83987
|
-
//
|
|
83988
|
-
// (navigation.md, "A value written per frame"). A programmatic move
|
|
83989
|
-
// (centerOnIndex) omits `live`: it places the value the wheel already has.
|
|
84020
|
+
// `live` (user scrubs: drag, momentum, wheel scroll) reports the value as each
|
|
84021
|
+
// item crosses the center, so uiAction tracks the wheel in real time — like
|
|
84022
|
+
// every other control. The committed action still fires only once it settles
|
|
84023
|
+
// (commitSelection). A programmatic move (centerOnIndex on an external value
|
|
84024
|
+
// sync) omits it, so scrolling the wheel into place doesn't echo the value
|
|
84025
|
+
// back out.
|
|
83990
84026
|
const setPos = (vp, pos, {
|
|
83991
84027
|
live = false
|
|
83992
84028
|
} = {}) => {
|
|
@@ -83997,25 +84033,18 @@ function WheelUI(props) {
|
|
|
83997
84033
|
renderPos(vp);
|
|
83998
84034
|
if (live && interactive) {
|
|
83999
84035
|
const index = centeredIndex(vp);
|
|
84000
|
-
if (index !== centeredIndexRef.current
|
|
84001
|
-
|
|
84002
|
-
|
|
84003
|
-
|
|
84004
|
-
|
|
84036
|
+
if (index !== centeredIndexRef.current) {
|
|
84037
|
+
const item = trackedItemsRef.current[index];
|
|
84038
|
+
if (item) {
|
|
84039
|
+
// Keep centeredIndexRef in step so the every-render
|
|
84040
|
+
// syncCenterToSelection doesn't read this as an external change and
|
|
84041
|
+
// fight the in-progress motion.
|
|
84042
|
+
centeredIndexRef.current = index;
|
|
84043
|
+
requestSelectValue(item.value, new CustomEvent("navi_wheel_scrub"));
|
|
84044
|
+
}
|
|
84005
84045
|
}
|
|
84006
84046
|
}
|
|
84007
84047
|
};
|
|
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
|
-
};
|
|
84019
84048
|
|
|
84020
84049
|
// Returns whether it actually interrupted a motion in flight — lets the caller
|
|
84021
84050
|
// tell a fresh gesture from one that "caught" a spinning/gliding wheel.
|
|
@@ -84051,7 +84080,6 @@ function WheelUI(props) {
|
|
|
84051
84080
|
renderPos(vp);
|
|
84052
84081
|
}
|
|
84053
84082
|
centeredIndexRef.current = index;
|
|
84054
|
-
scrubHoldsRowRef.current = false;
|
|
84055
84083
|
if (!interactive || glideFromOutsideRef.current) {
|
|
84056
84084
|
return;
|
|
84057
84085
|
}
|
|
@@ -84256,7 +84284,6 @@ function WheelUI(props) {
|
|
|
84256
84284
|
// inputs accumulate.
|
|
84257
84285
|
const glideToIndex = (vp, index, event) => {
|
|
84258
84286
|
centeredIndexRef.current = index;
|
|
84259
|
-
scrubHoldsRowRef.current = false;
|
|
84260
84287
|
requestSelectValue(trackedItemsRef.current[index].value, event);
|
|
84261
84288
|
glideTo(vp, glideTargetFor(vp, index));
|
|
84262
84289
|
};
|
|
@@ -84287,12 +84314,12 @@ function WheelUI(props) {
|
|
|
84287
84314
|
// Sync the center with the current value — used on first display and whenever
|
|
84288
84315
|
// the controlled value changes from outside.
|
|
84289
84316
|
const syncCenterToSelection = (viewportEl, behavior) => {
|
|
84290
|
-
// A fling in flight
|
|
84291
|
-
//
|
|
84292
|
-
//
|
|
84293
|
-
// below (glideTo never restarts the
|
|
84294
|
-
// mid-glide is where the wheel ends up.
|
|
84295
|
-
if (momentumRef.current !== null
|
|
84317
|
+
// A fling in flight reports its own value at every row crossing and commits
|
|
84318
|
+
// on settle: the finger that threw it owns the wheel until then, and a value
|
|
84319
|
+
// arriving meanwhile is re-stated by the next crossing anyway. A glide is
|
|
84320
|
+
// different: its target is re-aimed below (glideTo never restarts the
|
|
84321
|
+
// loop), so a value set from outside mid-glide is where the wheel ends up.
|
|
84322
|
+
if (momentumRef.current !== null) {
|
|
84296
84323
|
return;
|
|
84297
84324
|
}
|
|
84298
84325
|
if (trackedItemsRef.current.length === 0) {
|
|
@@ -84375,7 +84402,6 @@ function WheelUI(props) {
|
|
|
84375
84402
|
attemptInteraction,
|
|
84376
84403
|
cancelAnim,
|
|
84377
84404
|
setPos,
|
|
84378
|
-
releaseScrub,
|
|
84379
84405
|
snapPosToRow,
|
|
84380
84406
|
getItemSize,
|
|
84381
84407
|
viewportMain,
|