@jsenv/navi 0.29.8 → 0.29.10
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/jsenv_navi.js +620 -262
- package/dist/jsenv_navi.js.map +51 -30
- package/package.json +1 -1
package/dist/jsenv_navi.js
CHANGED
|
@@ -1663,6 +1663,11 @@ const generateSignalId = () => {
|
|
|
1663
1663
|
* @param {"string" | "number" | "boolean" | "object"} [options.type="string"] - Type for localStorage serialization/deserialization
|
|
1664
1664
|
* @param {number} [options.step] - For number type: step size for precision. Values will be rounded to nearest multiple of step.
|
|
1665
1665
|
* @param {Array} [options.oneOf] - Array of valid values for validation. Signal will be marked invalid if value is not in this array
|
|
1666
|
+
* @param {boolean} [options.weak=false] - The param qualifies one visit, not the screen: it is written into a
|
|
1667
|
+
* url only when explicitly named (`routeParams={{ edit: id }}`), never inherited from the signal's current
|
|
1668
|
+
* value, and it goes back to the default value when the route stops matching. Use it for params like an
|
|
1669
|
+
* "edit this one" id, where every other link to the screen must lead to the plain screen.
|
|
1670
|
+
* Incompatible with `persists` (throws).
|
|
1666
1671
|
* @param {boolean} [options.debug=false] - Enable debug logging for this signal's operations
|
|
1667
1672
|
* @returns {import("@preact/signals").Signal} A signal that can be synchronized with a source signal and/or persisted in localStorage. The signal includes a `validity` property for validation state.
|
|
1668
1673
|
*
|
|
@@ -1735,8 +1740,15 @@ const stateSignal = (defaultValue, options = {}) => {
|
|
|
1735
1740
|
default: staticFallback,
|
|
1736
1741
|
ignoreArrayOrder,
|
|
1737
1742
|
autoFix,
|
|
1743
|
+
weak = false,
|
|
1738
1744
|
} = options;
|
|
1739
1745
|
|
|
1746
|
+
if (weak && persists) {
|
|
1747
|
+
throw new TypeError(
|
|
1748
|
+
`stateSignal "${id}": weak and persists are contradictory — a weak param qualifies one visit, it cannot be restored from a previous session.`,
|
|
1749
|
+
);
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1740
1752
|
// Check if defaultValue is a signal (dynamic default) or static value
|
|
1741
1753
|
const isDynamicDefault =
|
|
1742
1754
|
defaultValue &&
|
|
@@ -2099,6 +2111,23 @@ setBaseUrl(
|
|
|
2099
2111
|
: window.location.origin,
|
|
2100
2112
|
);
|
|
2101
2113
|
|
|
2114
|
+
/**
|
|
2115
|
+
* The single place where "this param was not provided, take the signal value"
|
|
2116
|
+
* happens while building a url. A weak param qualifies one visit, not the
|
|
2117
|
+
* screen: it enters a url only when the caller names it (or when it is already
|
|
2118
|
+
* in the url being preserved), so here it always reads as absent.
|
|
2119
|
+
* Not reading the signal also keeps the built url from depending on it.
|
|
2120
|
+
*/
|
|
2121
|
+
const readSignalForUrlBuild = (connection) => {
|
|
2122
|
+
if (!connection || !connection.signal) {
|
|
2123
|
+
return undefined;
|
|
2124
|
+
}
|
|
2125
|
+
if (connection.weak) {
|
|
2126
|
+
return undefined;
|
|
2127
|
+
}
|
|
2128
|
+
return connection.signal.value;
|
|
2129
|
+
};
|
|
2130
|
+
|
|
2102
2131
|
/**
|
|
2103
2132
|
* Creates a custom route pattern matcher
|
|
2104
2133
|
*/
|
|
@@ -2173,7 +2202,7 @@ const createRoutePattern = (pattern, { searchParams = {} } = {}) => {
|
|
|
2173
2202
|
// Parameter was explicitly provided - always respect explicit parameters
|
|
2174
2203
|
continue;
|
|
2175
2204
|
}
|
|
2176
|
-
const signalValue = connection
|
|
2205
|
+
const signalValue = readSignalForUrlBuild(connection);
|
|
2177
2206
|
if (signalValue !== undefined) {
|
|
2178
2207
|
// Parameter was not provided, check signal value
|
|
2179
2208
|
resolvedParams[paramName] = signalValue;
|
|
@@ -2186,7 +2215,7 @@ const createRoutePattern = (pattern, { searchParams = {} } = {}) => {
|
|
|
2186
2215
|
// Parameter was explicitly provided - always respect explicit parameters
|
|
2187
2216
|
continue;
|
|
2188
2217
|
}
|
|
2189
|
-
const signalValue = connection
|
|
2218
|
+
const signalValue = readSignalForUrlBuild(connection);
|
|
2190
2219
|
if (signalValue !== undefined) {
|
|
2191
2220
|
// Parameter was not provided, check signal value
|
|
2192
2221
|
resolvedParams[paramName] = signalValue;
|
|
@@ -2229,7 +2258,7 @@ const createRoutePattern = (pattern, { searchParams = {} } = {}) => {
|
|
|
2229
2258
|
continue;
|
|
2230
2259
|
}
|
|
2231
2260
|
|
|
2232
|
-
const ancestorSignalValue = ancestorConnection
|
|
2261
|
+
const ancestorSignalValue = readSignalForUrlBuild(ancestorConnection);
|
|
2233
2262
|
if (
|
|
2234
2263
|
ancestorSignalValue !== undefined &&
|
|
2235
2264
|
ancestorSignalValue !== ancestorConnection.getDefaultValue()
|
|
@@ -2307,7 +2336,7 @@ const createRoutePattern = (pattern, { searchParams = {} } = {}) => {
|
|
|
2307
2336
|
if (childParam in resolvedParams) {
|
|
2308
2337
|
continue;
|
|
2309
2338
|
}
|
|
2310
|
-
const childSignalValue = childConnection
|
|
2339
|
+
const childSignalValue = readSignalForUrlBuild(childConnection);
|
|
2311
2340
|
// Only include if not already resolved and is non-default
|
|
2312
2341
|
if (
|
|
2313
2342
|
childSignalValue !== undefined &&
|
|
@@ -2349,7 +2378,7 @@ const createRoutePattern = (pattern, { searchParams = {} } = {}) => {
|
|
|
2349
2378
|
}
|
|
2350
2379
|
} else {
|
|
2351
2380
|
// Parameter not provided but signal has a value
|
|
2352
|
-
const signalValue = connection
|
|
2381
|
+
const signalValue = readSignalForUrlBuild(connection);
|
|
2353
2382
|
if (connection.isCustomValue(signalValue)) {
|
|
2354
2383
|
// Only include custom values
|
|
2355
2384
|
filtered[paramName] = signalValue;
|
|
@@ -2368,7 +2397,7 @@ const createRoutePattern = (pattern, { searchParams = {} } = {}) => {
|
|
|
2368
2397
|
}
|
|
2369
2398
|
} else {
|
|
2370
2399
|
// Parameter not provided but signal has a value
|
|
2371
|
-
const signalValue = connection
|
|
2400
|
+
const signalValue = readSignalForUrlBuild(connection);
|
|
2372
2401
|
if (connection.isCustomValue(signalValue)) {
|
|
2373
2402
|
// Only include custom values
|
|
2374
2403
|
filtered[paramName] = signalValue;
|
|
@@ -2385,7 +2414,7 @@ const createRoutePattern = (pattern, { searchParams = {} } = {}) => {
|
|
|
2385
2414
|
const canReachLiteralValue = (literalValue, params, literalPosition) => {
|
|
2386
2415
|
// Check parent's own parameters (signals and user params)
|
|
2387
2416
|
const parentCanProvide = connections.some((conn) => {
|
|
2388
|
-
const signalValue = conn
|
|
2417
|
+
const signalValue = readSignalForUrlBuild(conn);
|
|
2389
2418
|
const userValue = params[conn.paramName];
|
|
2390
2419
|
const effectiveValue = userValue !== undefined ? userValue : signalValue;
|
|
2391
2420
|
return (
|
|
@@ -2414,7 +2443,7 @@ const createRoutePattern = (pattern, { searchParams = {} } = {}) => {
|
|
|
2414
2443
|
return false;
|
|
2415
2444
|
}
|
|
2416
2445
|
return connsAtPosition.some((conn) => {
|
|
2417
|
-
const signalValue = conn
|
|
2446
|
+
const signalValue = readSignalForUrlBuild(conn);
|
|
2418
2447
|
return signalValue === literalValue && conn.isCustomValue(signalValue);
|
|
2419
2448
|
});
|
|
2420
2449
|
};
|
|
@@ -2469,7 +2498,7 @@ const createRoutePattern = (pattern, { searchParams = {} } = {}) => {
|
|
|
2469
2498
|
pathConnectionMap.get(paramName) ||
|
|
2470
2499
|
queryConnectionMap.get(paramName);
|
|
2471
2500
|
if (parentConnection) {
|
|
2472
|
-
parentParamValue = parentConnection
|
|
2501
|
+
parentParamValue = readSignalForUrlBuild(parentConnection);
|
|
2473
2502
|
}
|
|
2474
2503
|
}
|
|
2475
2504
|
|
|
@@ -2554,7 +2583,7 @@ const createRoutePattern = (pattern, { searchParams = {} } = {}) => {
|
|
|
2554
2583
|
paramValue = item.userValue;
|
|
2555
2584
|
} else {
|
|
2556
2585
|
paramName = item.paramName;
|
|
2557
|
-
paramValue = item
|
|
2586
|
+
paramValue = readSignalForUrlBuild(item);
|
|
2558
2587
|
// Only include custom parent signal values (not using defaults)
|
|
2559
2588
|
if (paramValue === undefined || !item.isCustomValue(paramValue)) {
|
|
2560
2589
|
return { isCompatible: true, shouldInclude: false };
|
|
@@ -2661,7 +2690,7 @@ const createRoutePattern = (pattern, { searchParams = {} } = {}) => {
|
|
|
2661
2690
|
if (!siblingConnection) {
|
|
2662
2691
|
continue;
|
|
2663
2692
|
}
|
|
2664
|
-
const siblingSignalValue = siblingConnection
|
|
2693
|
+
const siblingSignalValue = readSignalForUrlBuild(siblingConnection);
|
|
2665
2694
|
if (siblingSignalValue === undefined) {
|
|
2666
2695
|
continue;
|
|
2667
2696
|
}
|
|
@@ -2705,7 +2734,7 @@ const createRoutePattern = (pattern, { searchParams = {} } = {}) => {
|
|
|
2705
2734
|
const explicitValue = params[paramName];
|
|
2706
2735
|
const connection =
|
|
2707
2736
|
pathConnectionMap.get(paramName) || queryConnectionMap.get(paramName);
|
|
2708
|
-
const signalValue = connection
|
|
2737
|
+
const signalValue = readSignalForUrlBuild(connection);
|
|
2709
2738
|
|
|
2710
2739
|
// Check if the parameter has the required value
|
|
2711
2740
|
if (
|
|
@@ -2759,7 +2788,7 @@ const createRoutePattern = (pattern, { searchParams = {} } = {}) => {
|
|
|
2759
2788
|
hasActiveParams = true;
|
|
2760
2789
|
}
|
|
2761
2790
|
} else {
|
|
2762
|
-
const signalValue = connection
|
|
2791
|
+
const signalValue = readSignalForUrlBuild(connection);
|
|
2763
2792
|
if (signalValue !== undefined) {
|
|
2764
2793
|
// No explicit override - use signal value
|
|
2765
2794
|
childParams[paramName] = signalValue;
|
|
@@ -2976,7 +3005,7 @@ const createRoutePattern = (pattern, { searchParams = {} } = {}) => {
|
|
|
2976
3005
|
}
|
|
2977
3006
|
// If explicitly undefined, don't include it (which means don't use child route)
|
|
2978
3007
|
} else {
|
|
2979
|
-
const signalValue = connection
|
|
3008
|
+
const signalValue = readSignalForUrlBuild(connection);
|
|
2980
3009
|
if (
|
|
2981
3010
|
signalValue !== undefined &&
|
|
2982
3011
|
connection.isCustomValue(signalValue)
|
|
@@ -3016,7 +3045,7 @@ const createRoutePattern = (pattern, { searchParams = {} } = {}) => {
|
|
|
3016
3045
|
continue; // Already have this parameter
|
|
3017
3046
|
}
|
|
3018
3047
|
|
|
3019
|
-
const signalValue = connection
|
|
3048
|
+
const signalValue = readSignalForUrlBuild(connection);
|
|
3020
3049
|
// Only include custom signal values (not using defaults)
|
|
3021
3050
|
if (
|
|
3022
3051
|
signalValue !== undefined &&
|
|
@@ -3198,7 +3227,7 @@ const createRoutePattern = (pattern, { searchParams = {} } = {}) => {
|
|
|
3198
3227
|
paramName,
|
|
3199
3228
|
connection,
|
|
3200
3229
|
] of childPatternObj.pathConnectionMap) {
|
|
3201
|
-
const signalValue = connection
|
|
3230
|
+
const signalValue = readSignalForUrlBuild(connection);
|
|
3202
3231
|
if (
|
|
3203
3232
|
signalValue !== undefined &&
|
|
3204
3233
|
connection.isCustomValue(signalValue)
|
|
@@ -3220,7 +3249,7 @@ const createRoutePattern = (pattern, { searchParams = {} } = {}) => {
|
|
|
3220
3249
|
paramName,
|
|
3221
3250
|
connection,
|
|
3222
3251
|
] of childPatternObj.queryConnectionMap) {
|
|
3223
|
-
const signalValue = connection
|
|
3252
|
+
const signalValue = readSignalForUrlBuild(connection);
|
|
3224
3253
|
if (
|
|
3225
3254
|
signalValue !== undefined &&
|
|
3226
3255
|
connection.isCustomValue(signalValue)
|
|
@@ -3840,7 +3869,7 @@ const createRoutePattern = (pattern, { searchParams = {} } = {}) => {
|
|
|
3840
3869
|
);
|
|
3841
3870
|
for (const conn of targetAncestor.connections) {
|
|
3842
3871
|
console.debug(
|
|
3843
|
-
`[${pattern}] tryDirectOptimization: Target connection ${conn.paramName}: value=${conn
|
|
3872
|
+
`[${pattern}] tryDirectOptimization: Target connection ${conn.paramName}: value=${readSignalForUrlBuild(conn)}, isCustom=${conn.isCustomValue(readSignalForUrlBuild(conn))}`,
|
|
3844
3873
|
);
|
|
3845
3874
|
}
|
|
3846
3875
|
}
|
|
@@ -3857,7 +3886,7 @@ const createRoutePattern = (pattern, { searchParams = {} } = {}) => {
|
|
|
3857
3886
|
}
|
|
3858
3887
|
|
|
3859
3888
|
// Only include if not already processed and has custom value (not default)
|
|
3860
|
-
const signalValue = connection
|
|
3889
|
+
const signalValue = readSignalForUrlBuild(connection);
|
|
3861
3890
|
if (signalValue !== undefined) {
|
|
3862
3891
|
// Don't include path parameters that correspond to literal segments we're optimizing away
|
|
3863
3892
|
const targetParam = targetParams.find((p) => p.name === paramName);
|
|
@@ -3903,7 +3932,7 @@ const createRoutePattern = (pattern, { searchParams = {} } = {}) => {
|
|
|
3903
3932
|
}
|
|
3904
3933
|
|
|
3905
3934
|
// Only inherit custom values (not defaults) that we don't already have
|
|
3906
|
-
const signalValue = connection
|
|
3935
|
+
const signalValue = readSignalForUrlBuild(connection);
|
|
3907
3936
|
if (
|
|
3908
3937
|
signalValue !== undefined &&
|
|
3909
3938
|
connection.isCustomValue(signalValue)
|
|
@@ -3989,7 +4018,7 @@ const createRoutePattern = (pattern, { searchParams = {} } = {}) => {
|
|
|
3989
4018
|
}
|
|
3990
4019
|
|
|
3991
4020
|
// Only inherit if we don't have this param and parent has custom value (not default)
|
|
3992
|
-
const parentSignalValue = parentConnection
|
|
4021
|
+
const parentSignalValue = readSignalForUrlBuild(parentConnection);
|
|
3993
4022
|
if (
|
|
3994
4023
|
parentSignalValue !== undefined &&
|
|
3995
4024
|
parentConnection.isCustomValue(parentSignalValue)
|
|
@@ -4075,7 +4104,36 @@ const createRoutePattern = (pattern, { searchParams = {} } = {}) => {
|
|
|
4075
4104
|
// search params are updated. When buildMostPreciseUrl performs an ancestor
|
|
4076
4105
|
// optimisation (e.g. "/map/isochrone/compare" → "/map/isochrone") it is trusted
|
|
4077
4106
|
// as-is because the built pathname will differ from the route's own base pathname.
|
|
4107
|
+
// Weak params are never inherited from their signal, but a url that already
|
|
4108
|
+
// carries one keeps it: staying on the same screen while another param
|
|
4109
|
+
// changes must not end the visit this param qualifies.
|
|
4110
|
+
const carryOverWeakParams = (currentUrl, params) => {
|
|
4111
|
+
let paramsWithWeak = params;
|
|
4112
|
+
for (const [paramName, connection] of [
|
|
4113
|
+
...pathConnectionMap,
|
|
4114
|
+
...queryConnectionMap,
|
|
4115
|
+
]) {
|
|
4116
|
+
if (!connection.weak || paramName in paramsWithWeak) {
|
|
4117
|
+
continue;
|
|
4118
|
+
}
|
|
4119
|
+
const currentValue = connection.signal.peek();
|
|
4120
|
+
if (currentValue === undefined) {
|
|
4121
|
+
continue;
|
|
4122
|
+
}
|
|
4123
|
+
const currentParams = applyOn(currentUrl);
|
|
4124
|
+
if (!currentParams || currentParams[paramName] === undefined) {
|
|
4125
|
+
continue;
|
|
4126
|
+
}
|
|
4127
|
+
if (paramsWithWeak === params) {
|
|
4128
|
+
paramsWithWeak = { ...params };
|
|
4129
|
+
}
|
|
4130
|
+
paramsWithWeak[paramName] = currentParams[paramName];
|
|
4131
|
+
}
|
|
4132
|
+
return paramsWithWeak;
|
|
4133
|
+
};
|
|
4134
|
+
|
|
4078
4135
|
const buildUrlPreservingPath = (currentUrl, params = {}) => {
|
|
4136
|
+
params = carryOverWeakParams(currentUrl, params);
|
|
4079
4137
|
const relativeBuiltUrl = buildMostPreciseUrl(params);
|
|
4080
4138
|
if (!currentUrl) {
|
|
4081
4139
|
return resolveRouteUrl(relativeBuiltUrl);
|
|
@@ -4332,7 +4390,7 @@ const parsePattern = (pattern, { pathConnectionMap, queryConnectionMap }) => {
|
|
|
4332
4390
|
if (
|
|
4333
4391
|
connection &&
|
|
4334
4392
|
connection.signal &&
|
|
4335
|
-
connection
|
|
4393
|
+
readSignalForUrlBuild(connection) === undefined &&
|
|
4336
4394
|
!hasDefault
|
|
4337
4395
|
) {
|
|
4338
4396
|
isOptional = true;
|
|
@@ -5516,7 +5574,10 @@ const route = (pattern, { searchParams } = {}) => {
|
|
|
5516
5574
|
});
|
|
5517
5575
|
};
|
|
5518
5576
|
route.matchesParams = (providedParams) => {
|
|
5519
|
-
|
|
5577
|
+
// paramsSignal (not route.params) so a component calling this during render
|
|
5578
|
+
// subscribes to param changes: a navigation from /games/me/a to /games/me/b
|
|
5579
|
+
// leaves matchingSignal untouched, only the params change.
|
|
5580
|
+
const currentParams = route.paramsSignal.value;
|
|
5520
5581
|
const resolvedParams = routePattern.resolveParams({
|
|
5521
5582
|
...currentParams,
|
|
5522
5583
|
...providedParams,
|
|
@@ -5883,6 +5944,22 @@ This prevents cross-test pollution and ensures clean state.`,
|
|
|
5883
5944
|
}
|
|
5884
5945
|
}
|
|
5885
5946
|
|
|
5947
|
+
// A weak param qualifies one visit: leaving the route ends it,
|
|
5948
|
+
// whatever the family and whatever the default. Coming back by a
|
|
5949
|
+
// url that does not carry the param comes back to a blank screen.
|
|
5950
|
+
if (connection.weak) {
|
|
5951
|
+
if (!parameterExtractedByMatchingRoute) {
|
|
5952
|
+
const defaultValue = connection.getDefaultValue();
|
|
5953
|
+
if (debug) {
|
|
5954
|
+
console.debug(
|
|
5955
|
+
`[route] weak param ${paramName}: route no longer matching, back to ${defaultValue}`,
|
|
5956
|
+
);
|
|
5957
|
+
}
|
|
5958
|
+
paramSignal.value = defaultValue;
|
|
5959
|
+
}
|
|
5960
|
+
continue;
|
|
5961
|
+
}
|
|
5962
|
+
|
|
5886
5963
|
// Only reset signal if:
|
|
5887
5964
|
// 1. We're navigating within the same route family (not to completely unrelated routes)
|
|
5888
5965
|
// 2. AND no matching route extracts this parameter from URL
|
|
@@ -31360,6 +31437,15 @@ const useActionAsyncData = (action, {
|
|
|
31360
31437
|
return [staleData, true, undefined];
|
|
31361
31438
|
}
|
|
31362
31439
|
|
|
31440
|
+
// An action without params has nothing to run and no one to start it: this is
|
|
31441
|
+
// where a route action lands when its params getter returns false. It is not a
|
|
31442
|
+
// load in progress, so there is nothing to wait for — suspending here would
|
|
31443
|
+
// throw a promise that never settles, and the whole <Loading> subtree would
|
|
31444
|
+
// stay hidden for good, silently.
|
|
31445
|
+
if (runningState !== RUNNING && action.paramsSignal.peek() === undefined) {
|
|
31446
|
+
return [action.dataSignal.peek(), false, undefined];
|
|
31447
|
+
}
|
|
31448
|
+
|
|
31363
31449
|
// IDLE or RUNNING with loadingEffect: "delegate" — suspend
|
|
31364
31450
|
const reason = runningState === RUNNING ? "loading" : "idle";
|
|
31365
31451
|
loadingRef.current = {
|
|
@@ -33534,11 +33620,12 @@ const collectBranches = children => {
|
|
|
33534
33620
|
type: "container",
|
|
33535
33621
|
node: child
|
|
33536
33622
|
};
|
|
33623
|
+
const guardMatching = route ? route.matchingSignal.value : false;
|
|
33537
33624
|
if (!matchingBranch) {
|
|
33538
33625
|
if (matchingChild) {
|
|
33539
33626
|
// Real leaf match inside — always select this container
|
|
33540
33627
|
matchingBranch = branch;
|
|
33541
|
-
} else if (
|
|
33628
|
+
} else if (guardMatching) {
|
|
33542
33629
|
// No leaf match but an explicit route guard matches — select this
|
|
33543
33630
|
// container so it can render its own fallback inside its layout
|
|
33544
33631
|
matchingBranch = branch;
|
|
@@ -33556,7 +33643,12 @@ const collectBranches = children => {
|
|
|
33556
33643
|
type: "leaf",
|
|
33557
33644
|
node: child
|
|
33558
33645
|
};
|
|
33559
|
-
|
|
33646
|
+
// every signal is read even once a match is found: reading is what
|
|
33647
|
+
// subscribes the container to it, and a branch that is skipped today is
|
|
33648
|
+
// the one that must wake the container up tomorrow
|
|
33649
|
+
const matching = route.matchingSignal.value;
|
|
33650
|
+
const paramsMatching = routeParams ? route.matchesParams(routeParams) : true;
|
|
33651
|
+
if (!matchingBranch && matching && paramsMatching) {
|
|
33560
33652
|
matchingBranch = branch;
|
|
33561
33653
|
}
|
|
33562
33654
|
}
|
|
@@ -51959,25 +52051,30 @@ installImportMetaCssBuild(import.meta);/**
|
|
|
51959
52051
|
* A value one steps through, one press at a time: what is chosen sits between
|
|
51960
52052
|
* the way back and the way on, and the two of them are the whole control.
|
|
51961
52053
|
*
|
|
51962
|
-
*
|
|
51963
|
-
* one
|
|
51964
|
-
*
|
|
51965
|
-
*
|
|
52054
|
+
* `Spin` knows nothing about what it steps through. It is handed how to move
|
|
52055
|
+
* one step (`valueAtStep`), how to tell two values apart (`compareValues`) and
|
|
52056
|
+
* what to write for one (`renderValue`); everything else — the frame, the two
|
|
52057
|
+
* ways out, the ends one cannot go past, the value a form carries — is the
|
|
52058
|
+
* same whatever the value is. `DaySpin` and `NumberSpin` at the bottom are two
|
|
52059
|
+
* of those answers, and a caller can write a third.
|
|
52060
|
+
*
|
|
52061
|
+
* The middle is one of two things, and that is the only real fork here:
|
|
52062
|
+
*
|
|
52063
|
+
* - a value one PICKS (`DaySpin`): a headless picker holds it, pressing the
|
|
52064
|
+
* middle opens it, and the three slides of a looping container show the one
|
|
52065
|
+
* before, this one and the one after — a window over a row with no end.
|
|
52066
|
+
* - a value one TYPES (`editable`, `NumberSpin`): the middle IS the field.
|
|
52067
|
+
* Nothing travels — one cannot slide what is being typed into — and the
|
|
52068
|
+
* arrows step the value where the caret already is.
|
|
51966
52069
|
*
|
|
51967
52070
|
* A picker, so it lives here: what one presses in the middle IS a picker, and
|
|
51968
52071
|
* the stepping is a way of showing it. It is headless and behind the three
|
|
51969
52072
|
* slides — there is one value being chosen, so there is one picker for it.
|
|
51970
52073
|
*
|
|
51971
|
-
*
|
|
51972
|
-
*
|
|
51973
|
-
*
|
|
51974
|
-
*
|
|
51975
|
-
*
|
|
51976
|
-
* Two things take the keyboard and no more: the container (arrows step,
|
|
51977
|
-
* Enter/Space open the picker) and the two chevrons. What is in the middle is
|
|
51978
|
-
* not a control — a click on the container opens the picker by command — which
|
|
51979
|
-
* is what keeps the focus where the travel happens instead of moving it into a
|
|
51980
|
-
* slide that is about to leave.
|
|
52074
|
+
* Two things take the keyboard and no more: the middle (the container in the
|
|
52075
|
+
* picking case, the field in the typing one) and nothing else — the chevrons
|
|
52076
|
+
* refuse it on purpose, which is what keeps the focus where the travel happens
|
|
52077
|
+
* instead of moving it into a slide that is about to leave.
|
|
51981
52078
|
*/
|
|
51982
52079
|
const css$p = /* css */`
|
|
51983
52080
|
@layer navi {
|
|
@@ -51991,7 +52088,7 @@ const css$p = /* css */`
|
|
|
51991
52088
|
|
|
51992
52089
|
.navi_picker_spin {
|
|
51993
52090
|
/* The padding is written on what is inside the box rather than on the box
|
|
51994
|
-
— the
|
|
52091
|
+
— the value takes all four sides, the two chevrons only the vertical ones
|
|
51995
52092
|
— so the four sides are resolved once here, in the side-then-axis-then
|
|
51996
52093
|
-shorthand order a Box resolves them in. What writes them: the padding
|
|
51997
52094
|
props, through PICKER_SPIN_STYLE_CSS_VARS below. */
|
|
@@ -52025,7 +52122,7 @@ const css$p = /* css */`
|
|
|
52025
52122
|
);
|
|
52026
52123
|
/* What the loading outline is drawn around. */
|
|
52027
52124
|
position: relative;
|
|
52028
|
-
/* Written in the control font, like the picker it wraps: the
|
|
52125
|
+
/* Written in the control font, like the picker it wraps: the value and its
|
|
52029
52126
|
two chevrons are a control, not running text. */
|
|
52030
52127
|
font-size: var(--navi-control-font-size);
|
|
52031
52128
|
font-family: var(--navi-control-font-family);
|
|
@@ -52042,17 +52139,24 @@ const css$p = /* css */`
|
|
|
52042
52139
|
keyboard here (see navi-focus-delegate below). */
|
|
52043
52140
|
outline-color: var(--navi-focus-outline-color);
|
|
52044
52141
|
outline-offset: 0px;
|
|
52142
|
+
/* No grey flash under a finger: what a press does is said by the chevron's
|
|
52143
|
+
own background, and the browser's rectangle is drawn square over corners
|
|
52144
|
+
that are round. Inherited, so the three pieces inside get it too. */
|
|
52145
|
+
-webkit-tap-highlight-color: var(--navi-control-tap-highlight-color);
|
|
52045
52146
|
}
|
|
52046
|
-
/* The
|
|
52047
|
-
fills it, so its own
|
|
52048
|
-
two rings that close together read as a mistake. Same offer a
|
|
52049
|
-
popover answer (data-focus-outline-delegate, see
|
|
52050
|
-
the same reply — the delegate stands down
|
|
52147
|
+
/* The middle holds the keyboard, and this box wears its ring: whatever is in
|
|
52148
|
+
there fills it, so a ring of its own would be drawn a pixel inside this
|
|
52149
|
+
border and two rings that close together read as a mistake. Same offer a
|
|
52150
|
+
dialog and a popover answer (data-focus-outline-delegate, see
|
|
52151
|
+
slide_container.jsx), and the same reply — the delegate stands down; a
|
|
52152
|
+
field says it with an outline of zero width instead (see Spin's own
|
|
52153
|
+
outlineWidth below).
|
|
52051
52154
|
Said on this box too (the first selector): nothing focuses it for real —
|
|
52052
|
-
it is the
|
|
52155
|
+
it is the middle that takes the keyboard — but it is where the ring is
|
|
52053
52156
|
drawn, so a demo can hold it there and show what it looks like. */
|
|
52054
52157
|
.navi_picker_spin[data-focus-visible],
|
|
52055
|
-
.navi_picker_spin:has([data-focus-outline-delegate][data-focus-visible])
|
|
52158
|
+
.navi_picker_spin:has([data-focus-outline-delegate][data-focus-visible]),
|
|
52159
|
+
.navi_picker_spin:has(.navi_input[data-focus-visible]) {
|
|
52056
52160
|
outline-style: solid;
|
|
52057
52161
|
}
|
|
52058
52162
|
.navi_picker_spin [data-focus-outline-delegate] {
|
|
@@ -52103,6 +52207,14 @@ const css$p = /* css */`
|
|
|
52103
52207
|
width: min(100%, var(--picker-spin-picker-width, 12ch));
|
|
52104
52208
|
translate: -50% 0;
|
|
52105
52209
|
}
|
|
52210
|
+
/* A middle one types into is the field itself: it takes the whole room
|
|
52211
|
+
between the chevrons rather than sitting in the centre of it, so the
|
|
52212
|
+
caret is where the value is and a click anywhere in the middle lands in
|
|
52213
|
+
the field. */
|
|
52214
|
+
.navi_picker_spin_middle > .navi_input {
|
|
52215
|
+
min-width: 0;
|
|
52216
|
+
flex: 1 1 auto;
|
|
52217
|
+
}
|
|
52106
52218
|
/* Where the padding lands: all four sides on the value, the two vertical
|
|
52107
52219
|
ones on the chevrons below — the same number above and below is what makes
|
|
52108
52220
|
the three one line rather than three boxes, while sideways it is the room
|
|
@@ -52128,9 +52240,9 @@ const css$p = /* css */`
|
|
|
52128
52240
|
.navi_picker_spin[data-disabled] [data-slide-container] {
|
|
52129
52241
|
cursor: default;
|
|
52130
52242
|
}
|
|
52131
|
-
/* Kept inside its own slide: the three
|
|
52132
|
-
sticking out would be written across the two beside it. A
|
|
52133
|
-
the box simply wraps — the box grows, and the words are all there; say
|
|
52243
|
+
/* Kept inside its own slide: the three values share one cell, so anything
|
|
52244
|
+
sticking out would be written across the two beside it. A value too long
|
|
52245
|
+
for the box simply wraps — the box grows, and the words are all there; say
|
|
52134
52246
|
maxLines to cut it instead, which the text itself knows how to do. */
|
|
52135
52247
|
.navi_picker_spin [data-slide] > * {
|
|
52136
52248
|
max-width: 100%;
|
|
@@ -52157,7 +52269,12 @@ const css$p = /* css */`
|
|
|
52157
52269
|
border-radius: 0;
|
|
52158
52270
|
cursor: pointer;
|
|
52159
52271
|
}
|
|
52160
|
-
|
|
52272
|
+
/* Said as data-hover rather than :hover: a touch browser synthesizes the
|
|
52273
|
+
enter and never the leave, so a CSS :hover would stay grey under the last
|
|
52274
|
+
chevron pressed until something else was touched. What tracks it (see
|
|
52275
|
+
pseudo_styles.js) knows there is no hover on such a device and simply does
|
|
52276
|
+
not set the attribute. */
|
|
52277
|
+
.navi_picker_spin > .navi_picker_spin_way_out[data-hover] {
|
|
52161
52278
|
background: color-mix(in srgb, currentColor 8%, transparent);
|
|
52162
52279
|
}
|
|
52163
52280
|
/* Nothing that way: still there, still pressable — pressing it is how one
|
|
@@ -52183,22 +52300,28 @@ const css$p = /* css */`
|
|
|
52183
52300
|
of a rounded spin is rounded there too, and nowhere else — the two corners
|
|
52184
52301
|
it does not own stay at the 0 above. Said with inherit rather than clipped
|
|
52185
52302
|
away with overflow, which would cut the focus ring of the very button it
|
|
52186
|
-
rounds.
|
|
52303
|
+
rounds.
|
|
52304
|
+
Which chevron is which is asked of the chevron itself (data-way-out) rather
|
|
52305
|
+
than of its place among its siblings: the loading outline is a <span> too
|
|
52306
|
+
and it is written first, so :first-of-type named IT and the chevron at the
|
|
52307
|
+
start went unrounded. */
|
|
52187
52308
|
.navi_picker_spin:not([data-vertical])
|
|
52188
|
-
> .navi_picker_spin_way_out
|
|
52309
|
+
> .navi_picker_spin_way_out[data-way-out="start"] {
|
|
52189
52310
|
border-start-start-radius: inherit;
|
|
52190
52311
|
border-end-start-radius: inherit;
|
|
52191
52312
|
}
|
|
52192
52313
|
.navi_picker_spin:not([data-vertical])
|
|
52193
|
-
> .navi_picker_spin_way_out
|
|
52314
|
+
> .navi_picker_spin_way_out[data-way-out="end"] {
|
|
52194
52315
|
border-start-end-radius: inherit;
|
|
52195
52316
|
border-end-end-radius: inherit;
|
|
52196
52317
|
}
|
|
52197
|
-
.navi_picker_spin[data-vertical]
|
|
52318
|
+
.navi_picker_spin[data-vertical]
|
|
52319
|
+
> .navi_picker_spin_way_out[data-way-out="start"] {
|
|
52198
52320
|
border-start-start-radius: inherit;
|
|
52199
52321
|
border-start-end-radius: inherit;
|
|
52200
52322
|
}
|
|
52201
|
-
.navi_picker_spin[data-vertical]
|
|
52323
|
+
.navi_picker_spin[data-vertical]
|
|
52324
|
+
> .navi_picker_spin_way_out[data-way-out="end"] {
|
|
52202
52325
|
border-end-end-radius: inherit;
|
|
52203
52326
|
border-end-start-radius: inherit;
|
|
52204
52327
|
}
|
|
@@ -52206,53 +52329,73 @@ const css$p = /* css */`
|
|
|
52206
52329
|
|
|
52207
52330
|
/**
|
|
52208
52331
|
* @type {import("ignore:preact").FunctionComponent<{
|
|
52209
|
-
* value?:
|
|
52210
|
-
* defaultValue?:
|
|
52211
|
-
* signal?: import("@preact/signals").Signal<
|
|
52332
|
+
* value?: any,
|
|
52333
|
+
* defaultValue?: any,
|
|
52334
|
+
* signal?: import("@preact/signals").Signal<any>,
|
|
52212
52335
|
* name?: string,
|
|
52213
|
-
* min?:
|
|
52214
|
-
* max?:
|
|
52336
|
+
* min?: any,
|
|
52337
|
+
* max?: any,
|
|
52215
52338
|
* step?: number,
|
|
52339
|
+
* type?: string,
|
|
52340
|
+
* editable?: boolean,
|
|
52341
|
+
* growsUpward?: boolean,
|
|
52342
|
+
* fallbackValue?: any,
|
|
52343
|
+
* valueAtStep: (value: any, count: number) => any,
|
|
52344
|
+
* compareValues?: (a: any, b: any) => number,
|
|
52345
|
+
* renderValue?: (value: any) => import("ignore:preact").ComponentChildren,
|
|
52346
|
+
* controlProps?: object,
|
|
52216
52347
|
* duration?: number,
|
|
52217
|
-
*
|
|
52218
|
-
* renderDay?: (day: string) => import("ignore:preact").ComponentChildren,
|
|
52348
|
+
* vertical?: boolean,
|
|
52219
52349
|
* previousLabel?: string,
|
|
52220
52350
|
* nextLabel?: string,
|
|
52221
52351
|
* [key: string]: any,
|
|
52222
52352
|
* }>}
|
|
52223
|
-
* @param {
|
|
52224
|
-
*
|
|
52225
|
-
*
|
|
52226
|
-
* @param {
|
|
52227
|
-
*
|
|
52353
|
+
* @param {any} [value] The value shown. Held from above: `uiAction` says when
|
|
52354
|
+
* it should move. Say `signal` for a two-way binding instead, or
|
|
52355
|
+
* `defaultValue` to let the spin hold the value itself.
|
|
52356
|
+
* @param {(value: any, count: number) => any} valueAtStep What is `count` steps
|
|
52357
|
+
* away from `value` — the whole of what a spin has to know about what it
|
|
52358
|
+
* steps through. Called with -1 and +1 (times `step`) for the two ways out.
|
|
52359
|
+
* @param {(a: any, b: any) => number} [compareValues] Which of two values comes
|
|
52360
|
+
* first, the way a sort comparator answers. Only `min`/`max` need it, and the
|
|
52361
|
+
* default compares them as they compare with `<` — which is what an ISO date
|
|
52362
|
+
* wants and what a number does not.
|
|
52363
|
+
* @param {any} [fallbackValue] What is shown when nobody said anything: a spin
|
|
52364
|
+
* always shows a value, so there is always one to fall back on.
|
|
52365
|
+
* @param {string} [type="text"] What kind of value it is, handed to the control
|
|
52366
|
+
* holding it — a picker's `type` ("date", "time"…) or an input's.
|
|
52367
|
+
* @param {boolean} [editable] The middle is typed into rather than pressed:
|
|
52368
|
+
* the field IS the middle, and nothing travels. Without it the value is
|
|
52369
|
+
* picked — a headless picker behind three slides that travel one step at a
|
|
52370
|
+
* press.
|
|
52371
|
+
* @param {object} [controlProps] Anything else the control in the middle takes
|
|
52372
|
+
* (`inputMode`, `maxLength`, `placeholder`…).
|
|
52373
|
+
* @param {number} [step=1] How many steps a press covers.
|
|
52228
52374
|
* @param {number} [duration=250] How long a travel takes, in milliseconds.
|
|
52229
|
-
* @param {
|
|
52230
|
-
*
|
|
52231
|
-
* that way says so.
|
|
52232
|
-
* @param {"long"|"short"|"numeric"} [format="long"] How the date is written.
|
|
52233
|
-
* Long by default, since this is a control one reads rather than a column
|
|
52234
|
-
* one scans; `short` where the room is not there.
|
|
52375
|
+
* @param {any} [min] The first value one can reach; `max` is the last. Beyond
|
|
52376
|
+
* them the travel simply does not happen and the chevron that way says so.
|
|
52235
52377
|
* @param {string} [padding] The room around the value, `paddingX`/`paddingY`
|
|
52236
52378
|
* and the four sides included. It goes on what is inside the box rather than
|
|
52237
|
-
* on the box: above and below it is taken by the
|
|
52238
|
-
* which is what makes the three the same height; sideways it is the
|
|
52239
|
-
* between the
|
|
52240
|
-
* every picker takes (`--navi-picker-padding-x-default` and its `-y`
|
|
52241
|
-
* so a theme that spaces its fields spaces this one with them.
|
|
52242
|
-
* @param {number} [maxLines] How many lines the
|
|
52243
|
-
* with an ellipsis — `maxLines={1}` keeps it on one line. Without it a
|
|
52379
|
+
* on the box: above and below it is taken by the value AND by the two
|
|
52380
|
+
* chevrons, which is what makes the three the same height; sideways it is the
|
|
52381
|
+
* room between the value and the chevron beside it. Left unsaid it is the
|
|
52382
|
+
* padding every picker takes (`--navi-picker-padding-x-default` and its `-y`
|
|
52383
|
+
* twin), so a theme that spaces its fields spaces this one with them.
|
|
52384
|
+
* @param {number} [maxLines] How many lines the value may take before it is cut
|
|
52385
|
+
* with an ellipsis — `maxLines={1}` keeps it on one line. Without it a value
|
|
52244
52386
|
* too long for the box wraps, and the box grows.
|
|
52245
52387
|
* @param {boolean} [vertical] The same control standing up: the ways out above
|
|
52246
|
-
* and below rather than left and right
|
|
52388
|
+
* and below rather than left and right.
|
|
52389
|
+
* @param {boolean} [growsUpward] Which end of a standing spin holds the value
|
|
52390
|
+
* one steps up to. Off by default — what one walks through comes from below,
|
|
52391
|
+
* the way the next line of a list does. On for a quantity: pressing ▲ has to
|
|
52392
|
+
* mean a bigger number. Nothing to say about a spin lying down, where the way
|
|
52393
|
+
* on is always to the right.
|
|
52247
52394
|
* Everything a box takes is taken here too — `width`, `borderWidth`,
|
|
52248
52395
|
* `borderRadius`, `backgroundColor`: this IS a box, and its corners are passed
|
|
52249
52396
|
* on to the chevrons sitting in them.
|
|
52250
|
-
* @param {(day: string) => import("ignore:preact").ComponentChildren} [renderDay] What
|
|
52251
|
-
* to write for a day. Defaults to the date plus what it is to today when
|
|
52252
|
-
* there is a word for it ("samedi 8 août (demain)"), since a day near now is
|
|
52253
|
-
* read as a distance from now before it is read as a date.
|
|
52254
52397
|
*/
|
|
52255
|
-
const
|
|
52398
|
+
const Spin = ({
|
|
52256
52399
|
value,
|
|
52257
52400
|
defaultValue,
|
|
52258
52401
|
uiAction,
|
|
@@ -52262,76 +52405,77 @@ const DaySpin = ({
|
|
|
52262
52405
|
max,
|
|
52263
52406
|
step = 1,
|
|
52264
52407
|
duration = 250,
|
|
52265
|
-
|
|
52266
|
-
|
|
52408
|
+
type = "text",
|
|
52409
|
+
editable,
|
|
52410
|
+
growsUpward,
|
|
52411
|
+
fallbackValue,
|
|
52412
|
+
valueAtStep,
|
|
52413
|
+
compareValues = compareValuesDefault,
|
|
52414
|
+
renderValue = renderValueDefault,
|
|
52415
|
+
controlProps,
|
|
52267
52416
|
vertical,
|
|
52268
52417
|
readOnly,
|
|
52269
52418
|
disabled,
|
|
52270
52419
|
loading,
|
|
52271
52420
|
maxLines,
|
|
52272
|
-
renderDay = renderDayDefault,
|
|
52273
52421
|
previousLabel,
|
|
52274
52422
|
nextLabel,
|
|
52275
52423
|
...rest
|
|
52276
52424
|
}) => {
|
|
52277
52425
|
import.meta.css = [css$p, "@jsenv/navi/src/control/picker/picker_spin.jsx"];
|
|
52278
52426
|
const id = useId();
|
|
52279
|
-
const containerId = `${id}
|
|
52280
|
-
const
|
|
52281
|
-
// The
|
|
52282
|
-
// `defaultValue` and `signal` are handed to it untouched (see
|
|
52283
|
-
// settles which of them wins — and what a form makes of each —
|
|
52284
|
-
// the answer back. Nothing of that story is told twice.
|
|
52285
|
-
const
|
|
52286
|
-
const
|
|
52287
|
-
|
|
52288
|
-
max,
|
|
52289
|
-
step
|
|
52290
|
-
});
|
|
52291
|
-
const day = useControlUIState(pickerRef, value ?? defaultValue ?? signalProp?.peek()) ?? dayFallback;
|
|
52427
|
+
const containerId = `${id}_values`;
|
|
52428
|
+
const controlId = `${id}_control`;
|
|
52429
|
+
// The control holds the value, and it is asked rather than shadowed:
|
|
52430
|
+
// `value`, `defaultValue` and `signal` are handed to it untouched (see
|
|
52431
|
+
// below), it settles which of them wins — and what a form makes of each —
|
|
52432
|
+
// and this reads the answer back. Nothing of that story is told twice.
|
|
52433
|
+
const controlRef = useRef();
|
|
52434
|
+
const middleRef = useRef();
|
|
52435
|
+
const valueShown = useControlUIState(controlRef, value ?? defaultValue ?? signalProp?.peek()) ?? fallbackValue;
|
|
52292
52436
|
|
|
52293
52437
|
// …and the other way round when a signal was handed over: a bound signal is
|
|
52294
|
-
// the
|
|
52295
|
-
// elsewhere on the page — and the control follows it. A
|
|
52438
|
+
// the value, wherever it is moved from — the url, a back/forward, a button
|
|
52439
|
+
// elsewhere on the page — and the control follows it. A control seeded from a
|
|
52296
52440
|
// signal only writes back into it (see resolveInputProps), which is enough
|
|
52297
|
-
// for a field one only ever types into and not for a
|
|
52298
|
-
// from outside. Undefined is not a
|
|
52299
|
-
// control goes back to what it started on.
|
|
52300
|
-
const
|
|
52301
|
-
// The
|
|
52441
|
+
// for a field one only ever types into and not for a value that is also moved
|
|
52442
|
+
// from outside. Undefined is not a value: the signal has nothing to say, so
|
|
52443
|
+
// the control goes back to what it started on.
|
|
52444
|
+
const signalValue = signalProp ? signalProp.value : undefined;
|
|
52445
|
+
// The value as of the render this effect belongs to, and not one the closure
|
|
52302
52446
|
// captured a while ago: a step writes the signal, the signal brings us back
|
|
52303
|
-
// here, and comparing against a stale
|
|
52447
|
+
// here, and comparing against a stale value would set it a second time — one
|
|
52304
52448
|
// uiAction per step becoming two.
|
|
52305
|
-
const
|
|
52306
|
-
|
|
52449
|
+
const valueRef = useRef(valueShown);
|
|
52450
|
+
valueRef.current = valueShown;
|
|
52307
52451
|
useLayoutEffect(() => {
|
|
52308
|
-
const
|
|
52309
|
-
if (!signalProp || !
|
|
52452
|
+
const controlEl = controlRef.current;
|
|
52453
|
+
if (!signalProp || !controlEl) {
|
|
52310
52454
|
return;
|
|
52311
52455
|
}
|
|
52312
|
-
if (
|
|
52313
|
-
dispatchRequestResetUIState(
|
|
52456
|
+
if (signalValue === undefined) {
|
|
52457
|
+
dispatchRequestResetUIState(controlEl);
|
|
52314
52458
|
return;
|
|
52315
52459
|
}
|
|
52316
|
-
if (
|
|
52317
|
-
dispatchRequestSetUIState(
|
|
52460
|
+
if (signalValue !== valueRef.current) {
|
|
52461
|
+
dispatchRequestSetUIState(controlEl, signalValue, {});
|
|
52318
52462
|
}
|
|
52319
|
-
}, [
|
|
52463
|
+
}, [signalValue]);
|
|
52320
52464
|
|
|
52321
|
-
// A step is a change made to the
|
|
52465
|
+
// A step is a change made to the control, not beside it: it goes in the way a
|
|
52322
52466
|
// paste or a pick from the calendar goes in, so the signal, the form and
|
|
52323
52467
|
// `uiAction` all learn about it from the same place — and the event that
|
|
52324
52468
|
// asked for it travels with it, which is how `uiAction` can tell a chevron
|
|
52325
52469
|
// from the calendar.
|
|
52326
|
-
// What asked for the
|
|
52470
|
+
// What asked for the value being set, while it is being set: the control
|
|
52327
52471
|
// announces the change as its own input event, which says nothing of what
|
|
52328
52472
|
// started it — so it is held here for the length of the dispatch and handed
|
|
52329
|
-
// to uiAction below.
|
|
52473
|
+
// to uiAction below.
|
|
52330
52474
|
const stepEventRef = useRef(null);
|
|
52331
|
-
const
|
|
52475
|
+
const setValue = (valueNext, event) => {
|
|
52332
52476
|
stepEventRef.current = event;
|
|
52333
52477
|
try {
|
|
52334
|
-
dispatchRequestSetUIState(
|
|
52478
|
+
dispatchRequestSetUIState(controlRef.current, valueNext, {
|
|
52335
52479
|
event
|
|
52336
52480
|
});
|
|
52337
52481
|
} finally {
|
|
@@ -52340,29 +52484,29 @@ const DaySpin = ({
|
|
|
52340
52484
|
};
|
|
52341
52485
|
|
|
52342
52486
|
// Passed through rather than defaulted here: a prop nobody wrote must not
|
|
52343
|
-
// reach the
|
|
52487
|
+
// reach the control at all (it reads the presence of `value`, not its
|
|
52344
52488
|
// content), so each is added only if it was given.
|
|
52345
|
-
const
|
|
52489
|
+
const valueProps = {};
|
|
52346
52490
|
if (value !== undefined) {
|
|
52347
|
-
|
|
52491
|
+
valueProps.value = value;
|
|
52348
52492
|
} else {
|
|
52349
52493
|
if (signalProp) {
|
|
52350
|
-
|
|
52494
|
+
valueProps.signal = signalProp;
|
|
52351
52495
|
}
|
|
52352
|
-
// A
|
|
52353
|
-
// what the signal starts on, or
|
|
52354
|
-
//
|
|
52355
|
-
//
|
|
52356
|
-
//
|
|
52357
|
-
//
|
|
52358
|
-
//
|
|
52359
|
-
|
|
52496
|
+
// A value is always shown, so the control always HOLDS one: what was named,
|
|
52497
|
+
// or what the signal starts on, or the fallback. Said as a default rather
|
|
52498
|
+
// than as a value, so a form reads what is shown as an answer one can send
|
|
52499
|
+
// rather than as something it already holds — and said even when a signal
|
|
52500
|
+
// is bound, because a signal with nothing in it would otherwise leave the
|
|
52501
|
+
// control empty while the spin shows a value, and a form has nothing to
|
|
52502
|
+
// send about an empty field.
|
|
52503
|
+
valueProps.defaultValue = defaultValue ?? signalProp?.options?.getDefaultValue?.(false) ?? fallbackValue;
|
|
52360
52504
|
}
|
|
52361
52505
|
|
|
52362
52506
|
// Told from above as often as said here: a form running its action puts
|
|
52363
52507
|
// every control inside it out of service (that is how the chevrons grey out
|
|
52364
|
-
// by themselves), and the
|
|
52365
|
-
// control, not a box with three moods.
|
|
52508
|
+
// by themselves), and the value they sit around must fade with them — it is
|
|
52509
|
+
// one control, not a box with three moods.
|
|
52366
52510
|
const readOnlyFromAbove = useContext(ReadOnlyContext);
|
|
52367
52511
|
const loadingFromAbove = useContext(LoadingContext$1);
|
|
52368
52512
|
const disabledFromAbove = useContext(DisabledContext);
|
|
@@ -52388,15 +52532,55 @@ const DaySpin = ({
|
|
|
52388
52532
|
}
|
|
52389
52533
|
return undefined;
|
|
52390
52534
|
};
|
|
52391
|
-
const
|
|
52392
|
-
|
|
52393
|
-
|
|
52394
|
-
|
|
52535
|
+
const valuePrevious = valueAtStep(valueShown, -step);
|
|
52536
|
+
const valueNext = valueAtStep(valueShown, step);
|
|
52537
|
+
const hasMin = min !== undefined && min !== "";
|
|
52538
|
+
const hasMax = max !== undefined && max !== "";
|
|
52539
|
+
const previousAllowed = !hasMin || compareValues(valuePrevious, min) >= 0;
|
|
52540
|
+
const nextAllowed = !hasMax || compareValues(valueNext, max) <= 0;
|
|
52541
|
+
|
|
52542
|
+
// Where the keyboard is put back after a press on a chevron: whatever holds
|
|
52543
|
+
// it in this spin, so one can keep going with the keys where one was. Found
|
|
52544
|
+
// in the middle rather than by id — a field's id lands on the box around it,
|
|
52545
|
+
// and what takes the keyboard is the input inside.
|
|
52546
|
+
const focusMiddle = () => {
|
|
52547
|
+
const target = editable ? middleRef.current?.querySelector(".navi_control_input") : document.getElementById(containerId);
|
|
52548
|
+
target?.focus({
|
|
52549
|
+
preventScroll: true
|
|
52550
|
+
});
|
|
52551
|
+
};
|
|
52552
|
+
|
|
52553
|
+
// Which end of a standing spin holds the value one steps UP to. A quantity
|
|
52554
|
+
// grows upwards — pressing ▲ on a number means a bigger number, and anything
|
|
52555
|
+
// else is read as a bug. What one walks through does the opposite: the day
|
|
52556
|
+
// after today arrives from below, the way the next line of a list does.
|
|
52557
|
+
const startIsNext = vertical && growsUpward;
|
|
52558
|
+
const valueAtStart = startIsNext ? valueNext : valuePrevious;
|
|
52559
|
+
const valueAtEnd = startIsNext ? valuePrevious : valueNext;
|
|
52560
|
+
const startAllowed = startIsNext ? nextAllowed : previousAllowed;
|
|
52561
|
+
const endAllowed = startIsNext ? previousAllowed : nextAllowed;
|
|
52562
|
+
const wayOut = atStart => {
|
|
52563
|
+
const isNext = atStart ? startIsNext : !startIsNext;
|
|
52564
|
+
return jsx(WayOut, {
|
|
52565
|
+
atStart: atStart,
|
|
52566
|
+
unavailableMessage: wayOutMessage(atStart ? startAllowed : endAllowed, isNext ? "spin.nothing_after" : "spin.nothing_before"),
|
|
52567
|
+
label: isNext ? nextLabel ?? naviI18n("spin.next") : previousLabel ?? naviI18n("spin.previous"),
|
|
52568
|
+
onPress: e => {
|
|
52569
|
+
focusMiddle();
|
|
52570
|
+
if (editable) {
|
|
52571
|
+
setValue(atStart ? valueAtStart : valueAtEnd, e);
|
|
52572
|
+
return;
|
|
52573
|
+
}
|
|
52574
|
+
// A direction on the map, not a value: the slides are laid out from
|
|
52575
|
+
// start to end (see below), so the way out at the start walks
|
|
52576
|
+
// backwards through them whichever value sits there.
|
|
52577
|
+
const command = atStart ? vertical ? "--navi-up" : "--navi-left" : vertical ? "--navi-down" : "--navi-right";
|
|
52578
|
+
triggerNaviCommand(e.currentTarget, command, e);
|
|
52579
|
+
},
|
|
52580
|
+
commandFor: editable ? undefined : containerId,
|
|
52581
|
+
children: atStart ? vertical ? jsx(ChevronUpSvg, {}) : jsx(ChevronLeftSvg, {}) : vertical ? jsx(ChevronDownSvg, {}) : jsx(ChevronRightSvg, {})
|
|
52582
|
+
});
|
|
52395
52583
|
};
|
|
52396
|
-
const dayPrevious = addDays(day, -step);
|
|
52397
|
-
const dayNext = addDays(day, step);
|
|
52398
|
-
const previousAllowed = !min || dayPrevious >= min;
|
|
52399
|
-
const nextAllowed = !max || dayNext <= max;
|
|
52400
52584
|
return jsxs(Box, {
|
|
52401
52585
|
...rest,
|
|
52402
52586
|
baseClassName: "navi_picker_spin",
|
|
@@ -52415,132 +52599,167 @@ const DaySpin = ({
|
|
|
52415
52599
|
loading: loading,
|
|
52416
52600
|
color: "var(--navi-loader-color)",
|
|
52417
52601
|
inset: -2
|
|
52418
|
-
}), jsx(
|
|
52419
|
-
command: vertical ? "--navi-up" : "--navi-left",
|
|
52420
|
-
containerId: containerId,
|
|
52421
|
-
unavailableMessage: wayOutMessage(previousAllowed, "spin.nothing_before"),
|
|
52422
|
-
label: previousLabel ?? naviI18n("spin.previous"),
|
|
52423
|
-
children: vertical ? jsx(ChevronUpSvg, {}) : jsx(ChevronLeftSvg, {})
|
|
52424
|
-
}), jsxs("div", {
|
|
52602
|
+
}), wayOut(true), jsx("div", {
|
|
52425
52603
|
className: "navi_picker_spin_middle",
|
|
52426
|
-
|
|
52427
|
-
|
|
52428
|
-
|
|
52429
|
-
type:
|
|
52430
|
-
|
|
52431
|
-
|
|
52432
|
-
|
|
52433
|
-
// holds, a `signal` it follows, a `defaultValue` it merely starts on —
|
|
52434
|
-
// including what a form makes of the difference (it HOLDS a value and
|
|
52435
|
-
// has nothing to send back, where a default is a suggestion and
|
|
52436
|
-
// confirming it is an answer). A day is always shown, so there is
|
|
52437
|
-
// always a default: today, when nobody named one.
|
|
52438
|
-
,
|
|
52439
|
-
|
|
52440
|
-
...dayProps,
|
|
52604
|
+
ref: middleRef,
|
|
52605
|
+
children: editable ? jsx(Input, {
|
|
52606
|
+
ref: controlRef,
|
|
52607
|
+
type: type,
|
|
52608
|
+
name: name,
|
|
52609
|
+
...controlProps,
|
|
52610
|
+
...valueProps,
|
|
52441
52611
|
min: min,
|
|
52442
52612
|
max: max,
|
|
52613
|
+
step: step,
|
|
52443
52614
|
readOnly: readOnly,
|
|
52444
52615
|
disabled: disabled,
|
|
52445
|
-
loading: loading
|
|
52446
|
-
|
|
52447
|
-
|
|
52448
|
-
|
|
52449
|
-
|
|
52450
|
-
id: containerId,
|
|
52451
|
-
layout: vertical ? "column" : "row"
|
|
52452
|
-
// What is left beside the two chevrons, whatever the days it holds are
|
|
52453
|
-
// long: a control that resizes as one steps through it is a control one
|
|
52454
|
-
// has to aim at twice.
|
|
52616
|
+
loading: loading
|
|
52617
|
+
// No frame of its own inside a frame, and no ring of its own
|
|
52618
|
+
// either: the spin draws both (see the CSS above), and an outline
|
|
52619
|
+
// of zero width is how a field stands down without its focus
|
|
52620
|
+
// state being touched.
|
|
52455
52621
|
,
|
|
52456
52622
|
|
|
52623
|
+
variant: "discrete",
|
|
52624
|
+
outlineWidth: "0",
|
|
52625
|
+
textAlign: "center",
|
|
52457
52626
|
expandX: true,
|
|
52458
|
-
|
|
52459
|
-
|
|
52460
|
-
|
|
52461
|
-
|
|
52462
|
-
|
|
52463
|
-
|
|
52627
|
+
uiAction: (valueNext, event) => {
|
|
52628
|
+
uiAction?.(valueNext, stepEventRef.current ?? event);
|
|
52629
|
+
}
|
|
52630
|
+
}) : jsxs(Fragment$1, {
|
|
52631
|
+
children: [jsx(Picker, {
|
|
52632
|
+
ref: controlRef,
|
|
52633
|
+
id: controlId,
|
|
52634
|
+
type: type,
|
|
52635
|
+
variant: "headless",
|
|
52636
|
+
name: name,
|
|
52637
|
+
...controlProps,
|
|
52638
|
+
...valueProps,
|
|
52639
|
+
min: min,
|
|
52640
|
+
max: max,
|
|
52641
|
+
readOnly: readOnly,
|
|
52642
|
+
disabled: disabled,
|
|
52643
|
+
loading: loading,
|
|
52644
|
+
uiAction: (valueNext, event) => {
|
|
52645
|
+
uiAction?.(valueNext, stepEventRef.current ?? event);
|
|
52646
|
+
}
|
|
52647
|
+
}), jsxs(SlideContainer, {
|
|
52648
|
+
id: containerId,
|
|
52649
|
+
layout: vertical ? "column" : "row"
|
|
52650
|
+
// What is left beside the two chevrons, whatever the values it
|
|
52651
|
+
// holds are long: a control that resizes as one steps through it
|
|
52652
|
+
// is a control one has to aim at twice.
|
|
52653
|
+
,
|
|
52464
52654
|
|
|
52465
|
-
|
|
52466
|
-
|
|
52467
|
-
|
|
52468
|
-
|
|
52469
|
-
|
|
52470
|
-
|
|
52471
|
-
|
|
52472
|
-
// one of the two is ever anything but zero. The event goes with it —
|
|
52473
|
-
// it is what says a chevron (or an arrow key) asked for this day.
|
|
52474
|
-
setDay(addDays(day, (dx || dy) * step), event);
|
|
52475
|
-
}
|
|
52476
|
-
// The whole middle opens the calendar — a command, like the chevrons
|
|
52477
|
-
// send one, and no button of its own: the day would then be one more
|
|
52478
|
-
// Tab stop, and the focus would follow it out of the box as it travels.
|
|
52479
|
-
,
|
|
52655
|
+
expandX: true,
|
|
52656
|
+
defaultCurrent: "current",
|
|
52657
|
+
duration: `${duration}ms`
|
|
52658
|
+
// The three values are a window over an endless row: the
|
|
52659
|
+
// container plays the travel and comes back to the middle, and
|
|
52660
|
+
// the value moves one step here, in onLoop, as it lands.
|
|
52661
|
+
,
|
|
52480
52662
|
|
|
52481
|
-
|
|
52482
|
-
|
|
52483
|
-
|
|
52484
|
-
|
|
52485
|
-
|
|
52486
|
-
|
|
52487
|
-
|
|
52488
|
-
|
|
52489
|
-
|
|
52663
|
+
loop: true,
|
|
52664
|
+
onLoop: ({
|
|
52665
|
+
dx,
|
|
52666
|
+
dy,
|
|
52667
|
+
event
|
|
52668
|
+
}) => {
|
|
52669
|
+
// One step, whichever axis it came from: the map is a line, so
|
|
52670
|
+
// only one of the two is ever anything but zero. Towards the
|
|
52671
|
+
// end of the line is a step forward, unless the line was laid
|
|
52672
|
+
// out the other way round (startIsNext). The event goes with
|
|
52673
|
+
// it — it is what says a chevron (or an arrow key) asked for
|
|
52674
|
+
// this value.
|
|
52675
|
+
const towardsEnd = dx || dy;
|
|
52676
|
+
setValue(valueAtStep(valueShown, (startIsNext ? -towardsEnd : towardsEnd) * step), event);
|
|
52677
|
+
}
|
|
52678
|
+
// The whole middle opens the picker — a command, like the
|
|
52679
|
+
// chevrons send one, and no button of its own: the value would
|
|
52680
|
+
// then be one more Tab stop, and the focus would follow it out of
|
|
52681
|
+
// the box as it travels.
|
|
52682
|
+
,
|
|
52490
52683
|
|
|
52491
|
-
|
|
52492
|
-
|
|
52493
|
-
|
|
52494
|
-
|
|
52495
|
-
|
|
52496
|
-
|
|
52497
|
-
|
|
52498
|
-
|
|
52499
|
-
|
|
52500
|
-
}), jsx(Slide, {
|
|
52501
|
-
area: "current",
|
|
52502
|
-
flex: true,
|
|
52503
|
-
align: "center"
|
|
52504
|
-
// The days a min/max leaves out are simply not reachable: the way out
|
|
52505
|
-
// is closed on the slide being left, so a key, a chevron and a
|
|
52506
|
-
// command are all stopped by the same thing.
|
|
52684
|
+
commandFor: controlId
|
|
52685
|
+
// Sent whatever state the control is in: the picker is the one
|
|
52686
|
+
// that knows it cannot be opened right now, and refusing there is
|
|
52687
|
+
// what says so out loud (read-only, busy). Refusing here would be
|
|
52688
|
+
// a press that does nothing and explains nothing.
|
|
52689
|
+
// preventDefault, because a <Label> around the whole control
|
|
52690
|
+
// forwards a click to what it labels — the picker — and that
|
|
52691
|
+
// would open the calendar a second time, right after this command
|
|
52692
|
+
// did.
|
|
52507
52693
|
,
|
|
52508
52694
|
|
|
52509
|
-
|
|
52510
|
-
|
|
52511
|
-
|
|
52512
|
-
|
|
52513
|
-
|
|
52514
|
-
|
|
52515
|
-
|
|
52516
|
-
|
|
52695
|
+
onClick: e => {
|
|
52696
|
+
e.preventDefault();
|
|
52697
|
+
triggerNaviCommand(e.currentTarget, "--navi-open", e);
|
|
52698
|
+
},
|
|
52699
|
+
children: [jsx(Slide, {
|
|
52700
|
+
area: "start",
|
|
52701
|
+
flex: true,
|
|
52702
|
+
align: "center",
|
|
52703
|
+
children: renderValue(valueAtStart, {
|
|
52704
|
+
maxLines
|
|
52705
|
+
})
|
|
52706
|
+
}), jsx(Slide, {
|
|
52707
|
+
area: "current",
|
|
52708
|
+
flex: true,
|
|
52709
|
+
align: "center"
|
|
52710
|
+
// The values a min/max leaves out are simply not reachable: the
|
|
52711
|
+
// way out is closed on the slide being left, so a key, a
|
|
52712
|
+
// chevron and a command are all stopped by the same thing.
|
|
52713
|
+
,
|
|
52714
|
+
|
|
52715
|
+
preventNavPrevious: !startAllowed,
|
|
52716
|
+
preventNavNext: !endAllowed,
|
|
52717
|
+
children: renderValue(valueShown, {
|
|
52718
|
+
maxLines
|
|
52719
|
+
})
|
|
52720
|
+
}), jsx(Slide, {
|
|
52721
|
+
area: "end",
|
|
52722
|
+
flex: true,
|
|
52723
|
+
align: "center",
|
|
52724
|
+
children: renderValue(valueAtEnd, {
|
|
52725
|
+
maxLines
|
|
52726
|
+
})
|
|
52727
|
+
})]
|
|
52517
52728
|
})]
|
|
52518
|
-
})
|
|
52519
|
-
}),
|
|
52520
|
-
command: vertical ? "--navi-down" : "--navi-right",
|
|
52521
|
-
containerId: containerId,
|
|
52522
|
-
unavailableMessage: wayOutMessage(nextAllowed, "spin.nothing_after"),
|
|
52523
|
-
label: nextLabel ?? naviI18n("spin.next"),
|
|
52524
|
-
children: vertical ? jsx(ChevronDownSvg, {}) : jsx(ChevronRightSvg, {})
|
|
52525
|
-
})]
|
|
52729
|
+
})
|
|
52730
|
+
}), wayOut(false)]
|
|
52526
52731
|
});
|
|
52527
52732
|
};
|
|
52528
52733
|
|
|
52529
52734
|
// A way out is a place one presses, not a control: no <button>, on purpose.
|
|
52530
52735
|
// A <button> is labelable, so a <Label> wrapping the whole spin would bind
|
|
52531
|
-
// to the first chevron instead of to the
|
|
52532
|
-
// the value — and a form would carry two more controls that answer for
|
|
52533
|
-
//
|
|
52534
|
-
//
|
|
52736
|
+
// to the first chevron instead of to the control — the thing that actually
|
|
52737
|
+
// holds the value — and a form would carry two more controls that answer for
|
|
52738
|
+
// nothing.
|
|
52739
|
+
// It is not focusable either: the keyboard walks the values on the middle (see
|
|
52740
|
+
// the container's own tabIndex, or the field's), where the arrows already mean
|
|
52741
|
+
// this.
|
|
52535
52742
|
const WayOut = ({
|
|
52536
|
-
|
|
52537
|
-
|
|
52743
|
+
atStart,
|
|
52744
|
+
commandFor,
|
|
52538
52745
|
unavailableMessage,
|
|
52539
52746
|
label,
|
|
52747
|
+
onPress,
|
|
52540
52748
|
children
|
|
52541
52749
|
}) => jsx(Box, {
|
|
52542
52750
|
as: "span",
|
|
52543
52751
|
baseClassName: "navi_picker_spin_way_out"
|
|
52752
|
+
// Which end of the box it sits in, said by the chevron rather than read
|
|
52753
|
+
// from its place among its siblings: that is what the corners it is
|
|
52754
|
+
// rounded by are keyed on (see the CSS above).
|
|
52755
|
+
,
|
|
52756
|
+
|
|
52757
|
+
"data-way-out": atStart ? "start" : "end"
|
|
52758
|
+
// Tracked rather than left to CSS :hover, which stays on after a tap on a
|
|
52759
|
+
// touch device (see the CSS above).
|
|
52760
|
+
,
|
|
52761
|
+
|
|
52762
|
+
pseudoClasses: WAY_OUT_PSEUDO_CLASSES
|
|
52544
52763
|
// Announced as a button because that is what it is to whoever cannot see
|
|
52545
52764
|
// the chevron — and marked unavailable rather than removed when there is
|
|
52546
52765
|
// nothing that way, so it keeps its place.
|
|
@@ -52560,49 +52779,44 @@ const WayOut = ({
|
|
|
52560
52779
|
// Read by triggerNaviCommand below the same way it reads a button's own.
|
|
52561
52780
|
,
|
|
52562
52781
|
|
|
52563
|
-
commandfor:
|
|
52782
|
+
commandfor: commandFor,
|
|
52564
52783
|
flex: true,
|
|
52565
52784
|
align: "center"
|
|
52566
52785
|
// A press, answered where it starts: mousedown rather than click, which is
|
|
52567
52786
|
// what makes holding one feel immediate — and the click after it is stopped
|
|
52568
52787
|
// below, so a <Label> wrapping the whole control does not forward it to the
|
|
52569
|
-
//
|
|
52788
|
+
// control and open the calendar on the way past.
|
|
52570
52789
|
,
|
|
52571
52790
|
|
|
52572
52791
|
onClick: e => {
|
|
52573
52792
|
e.preventDefault();
|
|
52574
52793
|
},
|
|
52575
52794
|
onMouseDown: e => {
|
|
52576
|
-
// No focus, no text selection: the keyboard is put on the
|
|
52795
|
+
// No focus, no text selection: the keyboard is put on the middle below.
|
|
52577
52796
|
e.preventDefault();
|
|
52578
|
-
const wayOutElement = e.currentTarget;
|
|
52579
52797
|
if (unavailableMessage) {
|
|
52580
52798
|
// Why it does nothing, said where one pressed: a control would have
|
|
52581
52799
|
// done this through its own interaction gate, and this one has none.
|
|
52582
52800
|
openCallout(unavailableMessage, {
|
|
52583
|
-
anchorElement:
|
|
52801
|
+
anchorElement: e.currentTarget,
|
|
52584
52802
|
status: "info",
|
|
52585
52803
|
openingEvent: e
|
|
52586
52804
|
});
|
|
52587
52805
|
return;
|
|
52588
52806
|
}
|
|
52589
|
-
|
|
52590
|
-
// pressing a chevron leaves one able to keep going with the keys.
|
|
52591
|
-
document.getElementById(containerId)?.focus({
|
|
52592
|
-
preventScroll: true
|
|
52593
|
-
});
|
|
52594
|
-
triggerNaviCommand(wayOutElement, command, e);
|
|
52807
|
+
onPress(e);
|
|
52595
52808
|
},
|
|
52596
52809
|
children: jsx(Icon, {
|
|
52597
52810
|
children: children
|
|
52598
52811
|
})
|
|
52599
52812
|
});
|
|
52600
52813
|
const PICKER_SPIN_PSEUDO_CLASSES = [":hover", ":focus-visible"];
|
|
52814
|
+
const WAY_OUT_PSEUDO_CLASSES = [":hover"];
|
|
52601
52815
|
|
|
52602
52816
|
// A padding written on this box would sit between its border and the chevrons,
|
|
52603
52817
|
// which are meant to reach the corners they are rounded by — so each padding
|
|
52604
|
-
// prop becomes a variable instead, and the CSS above hands it to the
|
|
52605
|
-
// the chevrons themselves. Same trick, and same var chain, as Picker's.
|
|
52818
|
+
// prop becomes a variable instead, and the CSS above hands it to the value and
|
|
52819
|
+
// to the chevrons themselves. Same trick, and same var chain, as Picker's.
|
|
52606
52820
|
const PICKER_SPIN_STYLE_CSS_VARS = {
|
|
52607
52821
|
padding: "--picker-spin-padding",
|
|
52608
52822
|
paddingX: "--picker-spin-padding-x",
|
|
@@ -52613,6 +52827,138 @@ const PICKER_SPIN_STYLE_CSS_VARS = {
|
|
|
52613
52827
|
paddingLeft: "--picker-spin-padding-left"
|
|
52614
52828
|
};
|
|
52615
52829
|
|
|
52830
|
+
// Two values compared the way `<` compares them: right for an ISO day, a time
|
|
52831
|
+
// or anything else written so that its order IS its alphabetical order. A
|
|
52832
|
+
// number is the exception, and NumberSpin below says so.
|
|
52833
|
+
const compareValuesDefault = (a, b) => {
|
|
52834
|
+
if (a < b) {
|
|
52835
|
+
return -1;
|
|
52836
|
+
}
|
|
52837
|
+
if (a > b) {
|
|
52838
|
+
return 1;
|
|
52839
|
+
}
|
|
52840
|
+
return 0;
|
|
52841
|
+
};
|
|
52842
|
+
const renderValueDefault = value => String(value ?? "");
|
|
52843
|
+
|
|
52844
|
+
/**
|
|
52845
|
+
* A whole number one steps through and types into: the field IS the middle, so
|
|
52846
|
+
* the value can be typed as readily as stepped, and the two chevrons stand
|
|
52847
|
+
* above and below it by default — sideways they would be where the caret
|
|
52848
|
+
* moves — with the bigger number up top.
|
|
52849
|
+
*
|
|
52850
|
+
* @type {import("ignore:preact").FunctionComponent<{
|
|
52851
|
+
* value?: number|string,
|
|
52852
|
+
* defaultValue?: number|string,
|
|
52853
|
+
* min?: number,
|
|
52854
|
+
* max?: number,
|
|
52855
|
+
* step?: number,
|
|
52856
|
+
* [key: string]: any,
|
|
52857
|
+
* }>}
|
|
52858
|
+
* @param {number} [min=0] The lowest number one can reach; `max` is the
|
|
52859
|
+
* highest. They also bound what typing can produce, and how wide the field
|
|
52860
|
+
* is asked to be (see `maxLength`).
|
|
52861
|
+
*/
|
|
52862
|
+
const NumberSpin = ({
|
|
52863
|
+
min = 0,
|
|
52864
|
+
max,
|
|
52865
|
+
step = 1,
|
|
52866
|
+
vertical = true,
|
|
52867
|
+
growsUpward = true,
|
|
52868
|
+
controlProps,
|
|
52869
|
+
...rest
|
|
52870
|
+
}) => jsx(Spin, {
|
|
52871
|
+
type: "navi_number",
|
|
52872
|
+
editable: true,
|
|
52873
|
+
growsUpward: growsUpward,
|
|
52874
|
+
min: min,
|
|
52875
|
+
max: max,
|
|
52876
|
+
step: step,
|
|
52877
|
+
vertical: vertical,
|
|
52878
|
+
fallbackValue: min,
|
|
52879
|
+
valueAtStep: (value, count) => numberAtStep(value, count, min),
|
|
52880
|
+
compareValues: (a, b) => Number(a) - Number(b),
|
|
52881
|
+
controlProps: {
|
|
52882
|
+
// The numeric keypad on a phone, and — through
|
|
52883
|
+
// input_resolver_mode — the "this field is full" event a group of
|
|
52884
|
+
// fields moves along on (see useInputGroup).
|
|
52885
|
+
inputMode: "numeric",
|
|
52886
|
+
maxLength: max === undefined ? undefined : String(max).length,
|
|
52887
|
+
...controlProps
|
|
52888
|
+
},
|
|
52889
|
+
...rest
|
|
52890
|
+
});
|
|
52891
|
+
|
|
52892
|
+
// One step away, bounds included: a step past `max` is a real number that
|
|
52893
|
+
// simply is not allowed, and Spin is the one that reads it as "nothing that
|
|
52894
|
+
// way" — clamping here would answer the chevron before it had a chance to say
|
|
52895
|
+
// so. A field mid-edit holding nothing (or nothing numeric) starts from `min`.
|
|
52896
|
+
const numberAtStep = (value, count, min) => {
|
|
52897
|
+
const number = Number(value);
|
|
52898
|
+
if (value === "" || value === undefined || Number.isNaN(number)) {
|
|
52899
|
+
return min;
|
|
52900
|
+
}
|
|
52901
|
+
return number + count;
|
|
52902
|
+
};
|
|
52903
|
+
|
|
52904
|
+
/**
|
|
52905
|
+
* A day one steps through: the date plus what it is to today when there is a
|
|
52906
|
+
* word for it, and a calendar behind it for the day that is far from here.
|
|
52907
|
+
*
|
|
52908
|
+
* @type {import("ignore:preact").FunctionComponent<{
|
|
52909
|
+
* value?: string,
|
|
52910
|
+
* defaultValue?: string,
|
|
52911
|
+
* signal?: import("@preact/signals").Signal<string>,
|
|
52912
|
+
* name?: string,
|
|
52913
|
+
* min?: string,
|
|
52914
|
+
* max?: string,
|
|
52915
|
+
* step?: number,
|
|
52916
|
+
* lang?: string,
|
|
52917
|
+
* renderDay?: (day: string) => import("ignore:preact").ComponentChildren,
|
|
52918
|
+
* [key: string]: any,
|
|
52919
|
+
* }>}
|
|
52920
|
+
* @param {string} [value] The day shown, as "YYYY-MM-DD".
|
|
52921
|
+
* @param {number} [step=1] How many days a press covers — 7 for a week at a
|
|
52922
|
+
* time, and the label then names the day one lands on, as it always does.
|
|
52923
|
+
* @param {string} [min] The first day one can reach, as "YYYY-MM-DD"; `max` is
|
|
52924
|
+
* the last.
|
|
52925
|
+
* @param {"long"|"short"|"numeric"} [format="long"] How the date is written.
|
|
52926
|
+
* Long by default, since this is a control one reads rather than a column
|
|
52927
|
+
* one scans; `short` where the room is not there.
|
|
52928
|
+
* @param {(day: string) => import("ignore:preact").ComponentChildren} [renderDay] What
|
|
52929
|
+
* to write for a day. Defaults to the date plus what it is to today when
|
|
52930
|
+
* there is a word for it ("samedi 8 août (demain)"), since a day near now is
|
|
52931
|
+
* read as a distance from now before it is read as a date.
|
|
52932
|
+
*/
|
|
52933
|
+
const DaySpin = ({
|
|
52934
|
+
min,
|
|
52935
|
+
max,
|
|
52936
|
+
step = 1,
|
|
52937
|
+
lang,
|
|
52938
|
+
format = "long",
|
|
52939
|
+
renderDay = renderDayDefault,
|
|
52940
|
+
...rest
|
|
52941
|
+
}) => jsx(Spin, {
|
|
52942
|
+
type: "date",
|
|
52943
|
+
min: min,
|
|
52944
|
+
max: max,
|
|
52945
|
+
step: step,
|
|
52946
|
+
fallbackValue: firstDayAllowed({
|
|
52947
|
+
min,
|
|
52948
|
+
max,
|
|
52949
|
+
step
|
|
52950
|
+
}),
|
|
52951
|
+
valueAtStep: addDays,
|
|
52952
|
+
renderValue: (day, {
|
|
52953
|
+
maxLines
|
|
52954
|
+
}) => renderDay(day, {
|
|
52955
|
+
lang,
|
|
52956
|
+
format,
|
|
52957
|
+
maxLines
|
|
52958
|
+
}),
|
|
52959
|
+
...rest
|
|
52960
|
+
});
|
|
52961
|
+
|
|
52616
52962
|
// The date, and what it is to today when that is something one has a word for:
|
|
52617
52963
|
// "samedi 8 août (demain)" says both where one is and how far that is, and only
|
|
52618
52964
|
// the second is read at a glance.
|
|
@@ -54565,6 +54911,15 @@ const css$k = /* css */`
|
|
|
54565
54911
|
--wheel-window-inset: calc((100% - var(--wheel-item-height)) / 2);
|
|
54566
54912
|
--wheel-band-inset: calc((100% - var(--wheel-emphasis-size)) / 2);
|
|
54567
54913
|
|
|
54914
|
+
/* As wide as the rows ON SCREEN, which is not the same as "as wide as
|
|
54915
|
+
the widest value": the rows are a handful of recycled slots, so a wheel
|
|
54916
|
+
whose labels are not all the same width resizes as one scrolls — "9"
|
|
54917
|
+
becoming "10" widens it mid-glide, and whatever holds it (a popup)
|
|
54918
|
+
moves under the finger. Give such a wheel a width and the question is
|
|
54919
|
+
settled once; the per-row padding stays where it is, inside it, and
|
|
54920
|
+
stays scrollable. Digits are the case where this does not arise on its
|
|
54921
|
+
own: data-wheel-type="integer" makes them tabular above, so they all
|
|
54922
|
+
advance the same — until one of them takes a digit more. */
|
|
54568
54923
|
width: fit-content;
|
|
54569
54924
|
|
|
54570
54925
|
.navi_wheel_viewport {
|
|
@@ -54649,6 +55004,9 @@ const css$k = /* css */`
|
|
|
54649
55004
|
--wheel-window-inset: calc((100% - var(--wheel-item-width)) / 2);
|
|
54650
55005
|
--wheel-band-inset: calc((100% - var(--wheel-emphasis-size)) / 2);
|
|
54651
55006
|
|
|
55007
|
+
/* The cross axis follows the rows on screen — same story as the vertical
|
|
55008
|
+
branch above, and the same answer: say a height when the rows are not
|
|
55009
|
+
all as tall as each other. */
|
|
54652
55010
|
height: fit-content;
|
|
54653
55011
|
|
|
54654
55012
|
.navi_wheel_viewport {
|
|
@@ -62425,5 +62783,5 @@ const UserSvg = () => jsx("svg", {
|
|
|
62425
62783
|
})
|
|
62426
62784
|
});
|
|
62427
62785
|
|
|
62428
|
-
export { ActionRenderer, ActiveKeyboardShortcuts, Address, Badge, BadgeCount, BadgeList, Binder, Box, Button, ButtonCopyToClipboard, Caption, CardLayout, CheckSvg, CheckboxGroup, CloseSvg, Code, Col, Colgroup, Color, ConstructionSvg, ControlGroup, DaySpin, Details, Dialog, Editable, ErrorBoundary, ErrorBoundaryContext, ExclamationSvg, EyeClosedSvg, EyeSvg, Field, FixedBar, Form, Group, Head, HeartSvg, HomeSvg, Icon, Image, Input, InputDuration, Interpolate, Label, Link, LinkAnchorSvg, LinkBlankTargetSvg, LinkCurrentSvg, List, ListItem, ListItemGroup, ListItems, Loading, LoadingDotsSvg, LoadingIndicator, LoadingIndicatorFluid, LoadingOutline, MessageBox, Meter, Nav, NaviDebug, Paragraph, Picker, Popover, Popup, Quantity, RadioGroup, Route, RowNumberCol, RowNumberTableCell, SVGMaskOverlay, SearchSvg, SelectableInput, SelectionContext, Separator, SettingsSvg, SidePanel, Slide, SlideContainer, StarSvg, SummaryMarker, Svg, Table, TableCell, Tbody, Text, TextBox, Textarea, TextareaCharCount, Thead, Time, Title, Tr, UITransition, Unit, UserSvg, ViewportLayout, Wheel, WheelGroup, WheelItem, actionRunEffect, anyMatchingRouteSignal, applySearch, arraySignalMembership, coarsePointerSignal, compareTwoJsValues, createAction, createAvailableConstraint, createRequestCanceller, createSearch, createSelectionKeyboardShortcuts, createSlot, defineNaviConfirmPopupOptions, enableDebugActions, enableDebugOnDocumentLoading, ensureDocumentStartViewTransition, filterTableSelection, formatDatetime, formatDay, formatDayRelative, formatMonth, formatNumber, formatTime, formatTimeRelative, getNowHours, getNowHoursRoundedToStep, interpolateText, isCellSelected, isColumnSelected, isRowSelected, isToday, languagesSignal, localStorageSignal, moveArrayItemByIndex, navBack, navForward, navIntegratedVia, navTo, naviI18n, openCallout, rawUrlPart, registerGlobalConstraint, reload, rerunActions, resource, route, routeAction, setBaseUrl, setPreferredLanguage, setSupportedLanguages, setupRoutes, stateSignal, stopLoad, stringifyTableSelectionValue, swapArrayItemByIndex, syncOwnedResourceToSignals, syncResourceToSignals, updateActions, useActionStatus, useArraySignalMembership, useAsyncData, useCalloutRequestClose, useCancelPrevious, useCellGridFromRows, useConstraintValidityState, useDependenciesDiff, useDisplayedLayoutEffect, useDocumentResource, useDocumentState, useDocumentUrl, useEditionController, useFocusGroup, useKeyboardShortcuts, useNavState, useOrderedColumns, usePopupMode, useRouteStatus, useRunOnMount, useSearchText, useSelectableElement, useSelectionController, useSignalSync, useSlideValue, useStateArray, useTitleLevel, useUrlSearchParam, valueInLocalStorage, windowWidthSignal };
|
|
62786
|
+
export { ActionRenderer, ActiveKeyboardShortcuts, Address, Badge, BadgeCount, BadgeList, Binder, Box, Button, ButtonCopyToClipboard, Caption, CardLayout, CheckSvg, CheckboxGroup, CloseSvg, Code, Col, Colgroup, Color, ConstructionSvg, ControlGroup, DaySpin, Details, Dialog, Editable, ErrorBoundary, ErrorBoundaryContext, ExclamationSvg, EyeClosedSvg, EyeSvg, Field, FixedBar, Form, Group, Head, HeartSvg, HomeSvg, Icon, Image, Input, InputDuration, Interpolate, Label, Link, LinkAnchorSvg, LinkBlankTargetSvg, LinkCurrentSvg, List, ListItem, ListItemGroup, ListItems, Loading, LoadingDotsSvg, LoadingIndicator, LoadingIndicatorFluid, LoadingOutline, MessageBox, Meter, Nav, NaviDebug, NumberSpin, Paragraph, Picker, Popover, Popup, Quantity, RadioGroup, Route, RowNumberCol, RowNumberTableCell, SVGMaskOverlay, SearchSvg, SelectableInput, SelectionContext, Separator, SettingsSvg, SidePanel, Slide, SlideContainer, Spin, StarSvg, SummaryMarker, Svg, Table, TableCell, Tbody, Text, TextBox, Textarea, TextareaCharCount, Thead, Time, Title, Tr, UITransition, Unit, UserSvg, ViewportLayout, Wheel, WheelGroup, WheelItem, actionRunEffect, anyMatchingRouteSignal, applySearch, arraySignalMembership, coarsePointerSignal, compareTwoJsValues, createAction, createAvailableConstraint, createRequestCanceller, createSearch, createSelectionKeyboardShortcuts, createSlot, defineNaviConfirmPopupOptions, enableDebugActions, enableDebugOnDocumentLoading, ensureDocumentStartViewTransition, filterTableSelection, formatDatetime, formatDay, formatDayRelative, formatMonth, formatNumber, formatTime, formatTimeRelative, getNowHours, getNowHoursRoundedToStep, interpolateText, isCellSelected, isColumnSelected, isRowSelected, isToday, languagesSignal, localStorageSignal, moveArrayItemByIndex, navBack, navForward, navIntegratedVia, navTo, naviI18n, openCallout, rawUrlPart, registerGlobalConstraint, reload, rerunActions, resource, route, routeAction, setBaseUrl, setPreferredLanguage, setSupportedLanguages, setupRoutes, stateSignal, stopLoad, stringifyTableSelectionValue, swapArrayItemByIndex, syncOwnedResourceToSignals, syncResourceToSignals, updateActions, useActionStatus, useArraySignalMembership, useAsyncData, useCalloutRequestClose, useCancelPrevious, useCellGridFromRows, useConstraintValidityState, useDependenciesDiff, useDisplayedLayoutEffect, useDocumentResource, useDocumentState, useDocumentUrl, useEditionController, useFocusGroup, useInputGroup, useKeyboardShortcuts, useNavState, useOrderedColumns, usePopupMode, useRouteStatus, useRunOnMount, useSearchText, useSelectableElement, useSelectionController, useSignalSync, useSlideValue, useStateArray, useTitleLevel, useUrlSearchParam, valueInLocalStorage, windowWidthSignal };
|
|
62429
62787
|
//# sourceMappingURL=jsenv_navi.js.map
|